Compare commits

..
1680 Commits
Author SHA1 Message Date
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
EtienneandGitHub f45205011e Fix compose.env (#415)
Flip "stacks" and "repos" paths
2025-04-15 12:11:48 -07:00
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 GoetteandGitHub 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
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 KossenandGitHub 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 BeckerandGitHub 9ef25e7575 Create FUNDING.yml 2025-02-13 12:07:48 -08:00
boomamandGitHub 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 KossenandGitHub 82b2e68cd3 Adding Resource Sync documentation. (#259) 2025-01-11 21:32:02 -08:00
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 BeckerandGitHub ab8777460d Simplify periphery aio.Dockerfile 2024-12-14 09:13:01 -08:00
Maxwell BeckerandGitHub 7e030e702f Simplify aio.Dockerfile 2024-12-14 09:12:32 -08:00
Maxwell BeckerandGitHub 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
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 BeckerandGitHub 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 BeckerandGitHub 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 BeckerandGitHub 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 BeckerandGitHub 1f79987c58 1.16.8 (#170)
* update configs

* bump to 1.16.8
2024-11-01 14:35:12 -07:00
Maxwell BeckerandGitHub 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
Maxwell BeckerandGitHub f5a59b0333 1.16.7 (#167)
* 1.16.7

* increase builder max poll to allow User Data more time to setup periphery

* rework to KOMODO_OIDC_REDIRECT_HOST
2024-10-31 21:06:01 -07:00
mbecker20 cacea235f9 replace networks empty with network_mode, replace container: network mode 2024-10-30 02:58:27 -04:00
mbecker20 54ba31dca9 gen ts types 2024-10-30 02:18:57 -04:00
Maxwell BeckerandGitHub 17d7ecb419 1.16.6 (#163)
* remove instrument from validate_cancel_build

* use type safe AllResources map - Action not showing omnisearch

* Stack support replicated services

* server docker nested tables

* fix container networks which use network of another container

* bump version

* add 'address' to ServerListItemInfo

* secrets list on variables page wraps

* fix user data script

* update default template user data

* improve sidebar layout styling

* fix network names shown on containers

* improve stack service / container page

* deleted resource log records Toml backup for later reference

* align all the tables

* add Url Builder type
2024-10-29 23:17:10 -07:00
mbecker20 38f3448790 add Procedures and Actions page 2024-10-28 00:57:42 -04:00
Maxwell BeckerandGitHub ec88a6fa5a 1.16.5 (#159)
* repo pull lock

* implement BatchRunAction

* other batch methods
2024-10-27 20:56:56 -07:00
mbecker20 3820cd0ca2 make Build Organization configurable with custom value 2024-10-27 14:42:06 -04:00
mbecker20 419aa87bbb update resource toml examples to latest standard 2024-10-26 16:22:12 -04:00
Maxwell BeckerandGitHub 7a9ad42203 1.16.4 (#151)
* rust client improvements and docs

* sync rust client

* version 1.16.4

* UI support YAML / TOML utils, typed Deno namespace

* add ResourcesToml to typeshare

* add YAML and TOML convenience

* make the types available globally

* preload container with @std/yaml and @std/toml, clean up genned files

* add deno setup to alpine dockerfile
2024-10-26 12:15:34 -07:00
mbecker20 3f1cfa9064 update docs. Add Variables / Secrets docs 2024-10-25 00:02:12 -04:00
Maxwell BeckerandGitHub d05c81864e 1.16.3 (#150)
* refactor listener api implementation for Gitlab integration

* version 1.16.3

* builder delete id link cleanup

* refactor and add "__ALL__" branch to avoid branch filtering

* frontend config the webhook url

* action webhook config

* clean up webhook url copy

* add __ALL__ branch switch for Actions / Procedures
2024-10-24 16:03:00 -07:00
mbecker20 f1a09f34ab tweak dev docs and runfile 2024-10-22 17:04:49 -04:00
mbecker20 23c6e6306d fix usage of runnables-cli in dev docs 2024-10-22 16:36:25 -04:00
mbecker20 800da90561 tweaks to dev docs 2024-10-22 15:25:33 -04:00
mbecker20 b24bf6ed89 fix docsite broken links 2024-10-22 15:17:10 -04:00
Matt FoxxandGitHub d66a781a13 docs: Add development docs (#136)
* docs: Add development POC

* docs: Flesh out full build/run steps

* feat: Add mergeable compose file to expose port and internal periphery url

* feat: Add .devcontainer and VSCode Tasks for developing Komodo

* Make cargo cache persistent in devcontainer

* Add deno to devcontainer

* Update tasks to include TS client copy to frontend before run

* Recommend extensions for used dependencies in vscode workspace

All extensions recommended are included in the devcontainer. This makes it easier for users not using devcontainer to get lang support.

* Update local `run` sequence for development docs
2024-10-22 12:09:26 -07:00
Maxwell BeckerandGitHub f9b2994d44 1.16.2 (#145)
* Env vars written using same quotes (single vs double) as the user passes

* fmt

* trim start matches '-'

* ts client version
2024-10-22 11:41:17 -07:00
mbecker20 c0d6d96b64 get username works for service users 2024-10-22 03:36:20 -04:00
mbecker20 34496b948a bump ts client to 1.16.1 2024-10-22 02:58:42 -04:00
mbecker20 90c6adf923 fix periphery installer force file recreation command 2024-10-22 02:55:39 -04:00
mbecker20 3b72dc65cc remove "Overviews" label from sidebar 2024-10-22 02:27:22 -04:00
mbecker20 05f38d02be bump version to 1.16.1 2024-10-22 02:21:16 -04:00
Maxwell BeckerandGitHub ea5506c202 1.16.1 (#143)
* ensure sync state cache is refreshed on sync create / copy

* clean up resources post_create

* show sidebar if element length > 1

* update `run_komodo_command` command

* rename all resources

* refresh repo cache after clone / pull

* improve rename repo log
2024-10-21 23:19:40 -07:00
mbecker20 64b0a5c9d2 delete unrelated caddy compose 2024-10-21 00:30:54 -04:00
mbecker20 93cc6a3a6e Add running Action to dashboard "Active" 2024-10-21 00:21:17 -04:00
mbecker20 7ae69cf33b ignore top level return linting 2024-10-20 05:10:28 -04:00
mbecker20 404e00cc64 move action info inline 2024-10-20 04:43:29 -04:00
mbecker20 6fe5bc7420 properly host client lib for deno importing (types working) 2024-10-20 03:17:58 -04:00
mbecker20 82324b00ee typescript komodo_client v 1.16.0 2024-10-20 02:35:43 -04:00
Maxwell BeckerandGitHub 5daba3a557 1.16.0 (#140)
* consolidate deserializers

* key value list doc

* use string list deserializers for all entity Vec<String>

* add additional env files support

* plumbing for Action resource

* js client readme indentation

* regen lock

* add action UI

* action backend

* start on action frontend

* update lock

* get up to speed

* get action started

* clean up default action file

* seems to work

* toml export include action

* action works

* action works part 2

* bump rust version to 1.82.0

* copy deno bin from bin image

* action use local dir

* update not having changes doesn't return error

* format with prettier

* support yaml formatting with prettier

* variable no change is Ok
2024-10-19 23:27:28 -07:00
mbecker20 020cdc06fd remove migrator link in readme 2024-10-18 21:23:02 -04:00
Maxwell BeckerandGitHub cb270f4dff 1.15.12 (#139)
* add containers link to mobile dropdown

* fix update / alert not showing permission issue

* prevent disk alert back and forth

* improve user group pending toml
2024-10-18 17:14:22 -07:00
Matt FoxxandGitHub 21666cf9b3 feat: Add docs link to topbar (#134) 2024-10-18 16:10:01 -07:00
mbecker20 a417926690 1.15.11 allow adding stack to user group 2024-10-16 23:09:06 -04:00
mbecker20 293b36fae4 Allow adding Stack to User Group 2024-10-16 23:08:44 -04:00
mbecker20 dca37e9ba8 1.15.10 connect with http using DOCKER_HOST 2024-10-16 22:16:07 -04:00
Morgan WyattandGitHub 1cc302fcbf Update docker.rs to allow http docker socket connection (#131)
* Update docker.rs to allow http docker socket connection

Add or_else to allow attempt to connect to docker socket proxy via http if local connection fails

* Update docker.rs

Change two part connection to use connect_with_defaults instead, per review on PR.
2024-10-16 19:13:19 -07:00
mbecker20 febcf739d0 Remove Comma from installer: thanks @PiotrBzdrega 2024-10-16 10:43:54 -04:00
mbecker20 cb79e00794 update systemd service file 2024-10-15 17:35:54 -04:00
mbecker20 869b397596 force service file recreation docs 2024-10-15 17:25:29 -04:00
Maxwell BeckerandGitHub 41d1ff9760 1.15.9 (#127)
* add close alert threshold to prevent Ok - Warning back and forth

* remove part about repo being deleted, no longer behavior

* resource sync share general common

* remove this changelog. use releases

* remove changelog from readme

* write commit file clean up path

* docs: supports any git provider repo

* fix docs: authorization

* multiline command supports escaped newlines

* move webhook to build config advanced

* parser comments with escaped newline

* improve parser

* save use Enter. escape monaco using escape

* improve logic when deployment / stack action buttons shown

* used_mem = total - available

* Fix unrecognized path have 404

* webhooks will 404 if misconfigured

* move update logger / alerter

* delete migrator

* update examples

* publish typescript client komodo_client
2024-10-14 23:04:49 -07:00
mbecker20 dfafadf57b demo / build username pw 2024-10-14 11:49:44 -04:00
mbecker20 538a79b8b5 fix upausing all container action state 2024-10-13 18:11:09 -04:00
Maxwell BeckerandGitHub 5088dc5c3c 1.15.8 (#124)
* fix all containers restart and unpause

* add CommitSync to Procedure

* validate resource query tags causes failure on non exist

* files on host init working. match tags fail if tag doesnt exist

* intelligent sync match tag selector

* fix linting

* Wait for user initialize file on host
2024-10-13 15:03:16 -07:00
mbecker20 581d7e0b2c fix Procedure sync log 2024-10-13 04:21:03 -04:00
mbecker20 657298041f remove unneeded syncs volume 2024-10-13 04:03:09 -04:00
mbecker20 d71e9dca11 fix version 2024-10-13 03:21:56 -04:00
Maxwell BeckerandGitHub 165131bdf8 1.15.7 (#119)
* 1.15.7-dev ensure git config set

* add username to commit msg
2024-10-13 00:01:14 -07:00
mbecker20 0a81d2a0d0 add labels to mongo compose 2024-10-13 00:57:13 -04:00
Maxwell BeckerandGitHub 44ab5eb804 1.15.6 (#117)
* add periphery.skip label, skip in StopAllContainers

* add core config sync directory

* deploy stack if changed

* fix stack env_file_path when git repo and using run_directory

* deploy stack if changed

* write sync contents

* commit to git based sync, managed git based sync

* can sync non UI defined resource syncs

* sync UI control

* clippy

* init new stack compose file in repo

* better error message when attached Server / Builder invalid

* specify multiple resource file paths (mixed files + folders)

* use react charts

* tweak stats charts

* add Containers page

* 1.15.6

* stack deploy check if deployes vs remote has changed

* improve ux with loading indicators

* sync diff accounts for deploy / after

* fix new chart time axes
2024-10-12 21:42:46 -07:00
Maxwell BeckerandGitHub e3d8e603ec 1.15.5 (#116)
* 1.15.5
- Update your user's username and password
- **Admin**: Delete Users

* update username / password / delete user backend

* bump version

* alerter default disabled

* delete users and update username / password

* set password "" after update
2024-10-11 19:42:43 -07:00
mbecker20 8b5c179473 account recover note 2024-10-11 19:16:01 -04:00
mbecker20 8582bc92da fix Destroy Before Deploy config 2024-10-10 04:17:17 -04:00
Maxwell BeckerandGitHub 8ee270d045 1.15.4 (#114)
* stack destroy before deploy option

* add timestamps. Fix log polling even when poll not selected

* Add build [[$VERSION]] support. VERSION build arg default

* fix clippy lint

* initialize `first_builder`

* run_komodo_command uses parse_multiline_command

* comment UI for $VERSION and new command feature

* bump some deps

* support multiline commands in pre_deploy / pre_build
2024-10-10 00:37:23 -07:00
Maxwell BeckerandGitHub 2cfae525e9 1.15.3 (#109)
* fix parser support single quote '

* add stack reclone toggle

* git clone with token uses token:<TOKEN> for gitlab compatability

* support stack pre deploy shell command

* rename compose down update log stage

* deployment configure registry login account

* local testing setup

* bump version to 1.15.3

* new resources auto assign server if only one

* better error log when try to create resource with duplicate name

* end description with .

* ConfirmUpdate multi language

* fix compose write to host logic

* improve instrumentation

* improve update diff when small array

improve 2

* fix compose env file passing when repo_dir is not absolute
2024-10-08 23:07:38 -07:00
mbecker20 80e5d2a972 frontend dev setup guide 2024-10-08 16:55:24 -04:00
mbecker20 6f22c011a6 builder / server template add correct additional line if empty params 2024-10-07 22:55:48 -04:00
mbecker20 401cccee79 config nav buttons secondary 2024-10-07 21:55:14 -04:00
mbecker20 654b923f98 fix broken link to periphery setup 2024-10-07 18:56:14 -04:00
mbecker20 61261be70f update docs, split connecting servers out of Core Setup 2024-10-07 18:54:00 -04:00
mbecker20 46418125e3 update docs for periphery systemd --user install 2024-10-07 18:53:43 -04:00
mbecker20 e029e94f0d 1.15.2 Pass KOMODO_OIDC_ADDITIONAL_AUDIENCES 2024-10-07 15:44:51 -04:00
mbecker20 3be2b5163b 1.15.1 do not add trailing slash OIDC provider 2024-10-07 13:23:40 -04:00
mbecker20 6a145f58ff pass provider as-is. Authentik users should add a trailing slash 2024-10-07 13:16:25 -04:00
mbecker20 f1cede2ebd update dark / light stack screenshot to have action buttons 2024-10-07 08:05:39 -04:00
mbecker20 a5cfa1d412 update screenshots 2024-10-07 07:30:18 -04:00
mbecker20 a0674654c1 update screenshots 2024-10-07 07:30:11 -04:00
mbecker20 3faa1c58c1 update screenshots 2024-10-07 07:30:05 -04:00
mbecker20 7e296f34af screenshots 2024-10-07 07:29:58 -04:00
mbecker20 9f8ced190c update screenshots 2024-10-07 07:29:02 -04:00
mbecker20 c194bb16d8 update screenshots 2024-10-07 07:28:45 -04:00
mbecker20 39fec9b55e update screenshots 2024-10-07 07:27:52 -04:00
mbecker20 e97ed9888d update screenshots 1 2024-10-07 07:27:16 -04:00
mbecker20 559102ffe3 update readme 2024-10-07 07:25:36 -04:00
mbecker20 6bf80ddcc7 update screenshots readme 2024-10-07 07:25:24 -04:00
mbecker20 89dbe1b4d9 stack file_contents editor respects readOnly / disabled 2024-10-07 06:58:00 -04:00
mbecker20 334e16d646 OIDC use preferred username 2024-10-07 06:35:46 -04:00
mbecker20 a7bbe519f4 add build server link 2024-10-07 06:15:53 -04:00
mbecker20 5827486c5a add redirect uri for OIDC 2024-10-07 06:15:00 -04:00
mbecker20 8ca8f7eddd add context to oidc init error 2024-10-07 06:10:12 -04:00
mbecker20 0600276b43 fix parse KOMODO_MONGO_ in envs 2024-10-07 05:43:09 -04:00
mbecker20 a77a1495c7 active resources mb-12 not always there 2024-10-07 05:14:54 -04:00
mbecker20 021ed5d15f ActiveResources margin bottom 2024-10-07 03:24:57 -04:00
7d4376f426 1.15.0 (#90)
* attach env_file to compose build and compose pull stages

* fmt and bump rust version

* bump dependencies

* ignored for Sqlite message

* fix Build secret args info

* improve secret arguments info

* improve environment, ports, volumes deserializers

* rename `mongo` to `database` in config

* support _FILE in secret env vars

* improve setup - simpler compose

* remove aws ecr container registry support, alpine dockerfiles

* log periphery config

* ssl_enabled mode

* log http vs https

* periphery client accept untrust ssl certs

* fix nav issue from links

* configurable ssl

* KOMODO_ENSURE_SERVER -> KOMODO_FIRST_SERVER

* mount proc and ssl volume

* managed sync

* validate files on host resource path

* remove sync repo not configured guards

* disable confirm dialog

* fix sync hash / message Option

* try dev dockerfile

* refresh sync resources after commit

* socket invalidate handling

* delete dev dockerfile

* Commit Changes

* Add Info tab to syncs

* fix new Info parsing issue with serde default

* refresh stack cache on create / update

* managed syncs can't sync themselves

* managed syncs seems to work

* bump thiserror

* use alpine as main dockerfile

* apt add --no-cache

* disable user write perms, super admin perms to manage admins

* manage admin user UI

* implement disable non admin create frontend

* disable create non admin

* Copy button shown based on permission

* warning message on managed sync

* implement monaco editor

* impl simple match tags config

* resource sync support match tags

* more match tag filtering

* improve config with better saving diffs

* export button use monaco

* deser Conversions with wrapping strings

* envs editing

* don't delete variables / user groups if match tags defined

* env from_str improve

* improve dashboards

* remove core ca stuff for now

* move periphery ssl gen to dedicated file

* default server address periphery:8120

* clean up ssl configs

* server dashboard

* nice test compose

* add discord alerter

* discord alerter

* stack hideInfo logic

* compose setup

* alert table

* improve config hover card style

* update min editor height and stack config

* Feat: Styling Updates (#94)

* sidebar takes full screen height

* add bg accent to navbar

* add aschild prop to topbar alerts trigger

* stylize resource rows

* internally scrollable data tables

* better hover color for outlined button

* always show scrollbar to prevent layout shift

* better hover color for navbar

* rearrange buttons

* fix table and resource row styles

* cleanup scrollbar css

* use page for dashboard instead of section

* fix padding

* resource sync refactor and env keep comments

* frontend build

* improve configs

* config nice

* Feat/UI (#95)

* stylize resource rows

* internally scrollable data tables

* fix table and resource row styles

* use page for dashboard instead of section

* fix padding

* add `ResourcePageHeader` to required components

* add generic resource page header component

* add resource page headers for all components

* add resource notificaitons component

* add `TextUpdateMenu2` for use in resource page

* cleanup resource notificaitons

* update resource page layout

* ui edits

* sync kind of work

* clean up unused import

* syncs seem to work

* new sync pending

* monaco diff hide unchanged regions

* update styling all in config  resource select links

* confirm update default strings

* move procedure Add Stage to left

* update colors / styles

* frontend build

* backend for write file contents to host

* compose reference ports comment out

* server config

* ensure parent directory created

* fix frontend build

* remove default stack run_directory

* fix periphery compose deploy response set

* update compose files

* move server stats under tabs

* fix deployment list item getting correct image when not deployed

* stack updates cache after file write

* edit files on host

* clean up unused imports

* top level config update assignment must be spread

* update deps, move alert module

* move stack module

* move sync module

* move to sync db_client usage after init

* support generic OIDC provider

* init builders / server templates specifying https

* special cases for server / deployment state

* improve alert details

* add builder template `use_https` config

* try downgrade aws sdk ec2 for x86 build

* update debian dockerfiles to rm lists/*

* optionally configure seperate KOMODO_OIDC_REDIRECT

* add defaults to compose.env

* keep tags / search right aligned when view only

* clean up configs

* remove unused migrator deps

* update roadmap support generic OIDC

* initialize sync use confirm button

* key_value syntax highlighting

* smaller debian dockerfiles

* clean up deps.sh

* debian dockerifle

* New config layout (#96)

* new config layout

* fix image config layout and components config

* fix dom nesting and cleanup components

* fix label, make switches flex row

* ensure smooth scroll on hash navigations

* width 180 on config sidebar

* slight edits to config

* log whether https builder

* DISABLED <switch> ENABLED

* fix some more config

* smaller checked component

* server config looking good

* auto initialize compose files when files on host

* stack files on host good

* stack config nice

* remove old config

* deployments looking good

* build looking good

* Repo good

* nice config for builders

* alerter good

* server template config

* syncs good

* tweak stack config

* use status badge for update tables

* unified update page using router params

* replace /updates with unified updates page

* redirect all resource updates to unified update page

* fix reset handling

* unmount legacy page

* try periphery rustls

* rm unused import

* fix broken deps

* add unified alerts apge

* mount new alerts, remove old alerts page

* reroute resource alerts to unified alerts page

* back to periphery openssl

* ssl_enabled defaults to false for backward compat

* reqwest need json feature

* back to og yaml monaco

* Uncomment config fields for clearer config

* clean up compose env

* implement pull or clone, avoid deleting repo directory

* refactor mongo configuration params

* all configs respect empty string null

* add back status to header

* build toml don't have version if not auto incrementing

* fix comile

* fix repo pull cd to correct dir

* fix core pull_or_clone directory

* improve statuses

* remove ' ' from kv list parser

* longer CSRF valid for, to give time to login / accept

* don't compute diff / execute if there are any file_errors

* PartialBuilderConfig enum user inner option

* move errors to top

* fix toml init serializer

* server template and bulder manually add config.params line

* better way to check builder / template params empty

* improve build configs

* merge links into network area deployment

* default periphery config

* improve SystemCommand editor

* better Repo server / builder Info

* improve Alerts / Updates with ResourceSelector

* fix unused frontend

* update ResourceSync description

* toml use [resource.config] syntax

* update toml syntax

* update Build.image_registry schema

* fix repo / stack resource link alias

* reorder image registry

* align toml / yaml parser style

* some config updates

---------

Co-authored-by: Karamvir Singh <67458484+karamvirsingh98@users.noreply.github.com>
Co-authored-by: kv <karamvir.singh98@gmail.com>
2024-10-06 23:54:23 -07:00
Brad LugoandGitHub 7e9b406a34 fix: change edit url for docsite (#91)
Changes the default edit url provided by the docusaurus template to the
correct komodo url.
2024-09-22 23:08:34 -07:00
Brad LugoandGitHub dcf78b05b3 fix: check if systemd is available for install (#89)
Adds a check at the beginning of setup-periphery.py to verify if
`systemctl` is executable by the current user and if systemd is the
init system in use. These changes will inform the user we've decided
systemd is unavailable and exits before we attempt to use any systemd
functionality, but modify this logic to configure other init systems in
the future.

Relates to (but doesn't close)
https://github.com/mbecker20/komodo/issues/66.
2024-09-22 19:34:56 -07:00
3236302d05 Reduce periphery image size (#82)
* Reduce periphery image size

* rename new alpine Dockerfile as slim.Dockerfile

* bump slim dockerfile rust version

---------

Co-authored-by: mbecker20 <becker.maxh@gmail.com>
2024-09-21 14:57:51 -07:00
mbecker20 fc41258d6c fix docs deploy command --env-file 2024-09-13 12:12:01 +03:00
mbecker20 ae8df90361 uncomment compose envs set by variables for .env edit only 2024-09-12 11:11:28 +03:00
mbecker20 7d05b2677f explicit yaml 2024-09-12 00:52:35 +03:00
mbecker20 2f55468a4c simple docs 2024-09-11 23:13:42 +03:00
mbecker20 a20bd2c23f docker compose doc 2024-09-11 23:08:56 +03:00
mbecker20 b3aa0ffa78 remove unnecessary docs 2024-09-11 22:47:13 +03:00
mbecker20 8e58a283cd fix caddy compose 2024-09-11 22:07:06 +03:00
mbecker20 9b2d9932ef add prune buildx 2024-09-11 21:18:56 +03:00
mbecker20 7cb093ade1 fix links 2 2024-09-11 21:04:48 +03:00
mbecker20 e2f73d8474 fix broken doc links 2024-09-11 21:01:46 +03:00
Maxwell BeckerandGitHub 12abd5a5bd 1.14.2 (#70)
* docker builders / buildx prune backend

* seems to work with ferret

* improve UI error messages

* compose files

* update compose variables comment

* update compose files

* update sqlite compose

* env vars and others support end of line comment starting with " #"

* aws and hetzner default user data for hands free setup

* move configs

* new core config

* smth

* implement disable user registration

* clean up compose files

* add DISABLE_USER_REGISTRATION

* 1.14.2

* final
2024-09-11 10:50:59 -07:00
Maxwell BeckerandGitHub f349cdf50d 1.14.1 (#69)
* 1.14.1

* 1.14.1 version

* repo pull use configured repo path

* don't show UI defined file if using Stack files on host mode

* Stack "run build" option

* note on bind mounts

* improve bind mount doc

* add links to schema

* add new stacks configs UI

* interp into stack build_extra_args

* add links UI
2024-09-10 08:17:53 -07:00
mbecker20 796bcac952 no business edition docs 2024-09-07 18:50:18 +03:00
mbecker20 fed05684aa no business edition 2024-09-07 18:48:58 +03:00
mbecker20 80a91584a8 version number link to releases 2024-09-07 12:54:54 +03:00
mbecker20 12d05e9a25 1.14.0 stable 2024-09-07 12:51:56 +03:00
mbecker20 f4d06c91ff add 5s log polling option 2024-09-07 12:42:35 +03:00
mbecker20 5d7449529f fix Builder schema link 2024-09-05 01:40:54 +03:00
mbecker20 a0021d1785 obfuscate secret variable value with * 2024-09-04 17:15:59 +03:00
mbecker20 bbd23e3f5f improve login failure feedback 2024-09-04 17:15:47 +03:00
mbecker20 71841a8e41 update komodo CLI readme 2024-09-04 16:48:27 +03:00
mbecker20 5228ffd9b8 fix changelog 2024-09-02 12:54:56 +03:00
mbecker20 a06f506e54 installer log 2024-09-02 03:13:58 +03:00
mbecker20 71d6a55e50 modified installer 2024-09-02 03:12:17 +03:00
mbecker20 d16c03dd2a fix force service file install 2024-09-02 03:09:14 +03:00
mbecker20 6abd9a6554 publish rc1 crates 2024-09-02 02:06:02 +03:00
mbecker20 5f04e881a5 fix doc link 2024-09-02 01:45:39 +03:00
Maxwell BeckerandGitHub 5fc0a87dea 1.14 - Rename to Komodo - Docker Management (#56)
* setup network page

* add Network, Image, Container

* Docker ListItems and Inspects

* frontend build

* dev0

* network info working

* fix cargo lock

* dev1

* pages for the things

* implement Active in dashboard

* RunBuild update trigger list refresh

* rename deployment executions to StartDeployment etc

* add server level container control

* dev2

* add Config field to Image

* can get image labels from Config.Labels

* mount container page

* server show resource count

* add GetContainerLog api

* add _AllContainers api

* dev3

* move ResourceTarget to entities mod

* GetResourceMatchingContainer api

* connect container to resource

* dev4 add volume names to container list items

* ts types

* volume / image / network unused management

* add image history to image page

* fix PruneContainers incorret Operation

* update cache for server for server after server actions

* dev5

* add singapore to Hetzner

* implement delete single network / image / volume api

* dev6

* include "in use" on Docker Lists

* add docker resource delete buttons

* is nice

* fix volume all in use

* remove google font dependency

* use host networking in test compose

* implement Secret Variables (hidden in logs)

* remove unneeded borrow

* interpolate variables / secrets into extra args / onclone / onpull / command etc

* validate empty strings before SelectItem

* rename everything to Komodo

* rename workspace to komodo

* rc1
2024-09-01 15:38:40 -07:00
mbecker20 2463ed3879 1.13.4 Fix periphery image registry login / pull ordering 2024-08-20 12:50:07 -04:00
mbecker20 a2758ce6f4 Stack: login to registry BEFORE pulling image 2024-08-20 12:49:19 -04:00
mbecker20 3f1788dbbb docsite: reindent ports in example 2024-08-19 11:36:17 -04:00
mbecker20 33a0560af6 frontend: Ignore Services correct content hidden logic 2024-08-19 00:44:48 -04:00
mbecker20 610a10c488 frontend: show state when using Stack files-on-host 2024-08-18 21:16:53 -04:00
mbecker20 39b217687d 1.13.3 filter Periphery disks 2024-08-18 18:12:47 -04:00
mbecker20 2f73461979 stack don't show Git Repo config is file contents defined in UI 2024-08-18 18:06:12 -04:00
mbecker20 aae9bb9e51 move image registry to the top of Build Image config 2024-08-18 18:06:12 -04:00
mbecker20 7d011d93fa Fix: Periphery should flter out "overlay" volumes, not include them 2024-08-18 18:05:56 -04:00
mbecker20 bffdea4357 Swarm roadmap 2024-08-18 16:05:58 -04:00
mbecker20 790566bf79 add note about podman support 2024-08-18 13:37:26 -04:00
mbecker20 b17db93f13 quick-fix: fix build version config 2024-08-18 04:57:25 -04:00
mbecker20 daa2ea9361 better build version management 2024-08-18 04:24:22 -04:00
mbecker20 176fb04707 add docs about running periphery in contianer 2024-08-18 04:04:03 -04:00
mbecker20 5ba1254cdb add docker compose docs 2024-08-18 03:30:53 -04:00
Maxwell BeckerandGitHub 43593162b0 1.13.2 local compose (#36)
* stack config files_on_host

* refresh stack cache not blocked when using files_on_host

* add remote errors status

* improve info tab

* store the full path in ComposeContents
2024-08-18 00:04:47 -07:00
Maxwell BeckerandGitHub 418f359492 1.13.2 (#35)
* 1.13.2 add periphery disk report whitelist / blacklist

* improve setup docs

* publish 1.13.2 client
2024-08-17 15:28:01 -07:00
mbecker20 3cded60166 close any open alerts on non-existant disk mounts 2024-08-17 18:04:11 -04:00
mbecker20 6f70f9acb0 no need to try to parse deploy stuff, only can cause failure 2024-08-17 17:38:14 -04:00
mbecker20 6e1064e58e auto enable ensured server 2024-08-17 14:28:38 -04:00
mbecker20 d96e5b4c46 comment out environment so parsing doesn't fail 2024-08-17 14:26:10 -04:00
mbecker20 5a8822c7d2 add note about arm periphery 2024-08-17 14:23:21 -04:00
Maxwell BeckerandGitHub 1f2d236228 Dockerized periphery (#34)
* Add webhooks page to docs

* supports

* supports

* periphery Dockerfile

* add comments. Remove unneeded default config

* add FILE SYSTEM log

* remove log

* filter disks included in periphery disk report, on periphery side

* dockerized periphery

* all in one compose file docs

* remove some unused deps
2024-08-17 00:25:42 -07:00
mbecker20 a89bd4a36d deleting ResourceSync cleans up open alerts
use .then in alert cleanup
2024-08-16 12:27:50 -04:00
mbecker20 0b40dff72b add images, volumes to 1.14 roadmap 2024-08-16 11:54:24 -04:00
mbecker20 59874f0a92 temp downgrade tower due to build issue 2024-08-16 02:35:23 -04:00
mbecker20 14e459b32e debug on RefreshCache tasks 2024-08-16 02:08:30 -04:00
mbecker20 f6c55b7be1 default log config include opentelemetry service name 2024-08-16 02:05:48 -04:00
mbecker20 460819a145 hetzner pass enable ipv4/v6 explicitly 2024-08-16 01:42:28 -04:00
mbecker20 91f4df8ac2 update logging deps 2024-08-16 01:15:53 -04:00
mbecker20 6a19e18539 pass disabled to stack TextInput area 2024-08-15 04:17:15 -04:00
mbecker20 30c5fa3569 add gap to commit hover 2024-08-15 03:56:03 -04:00
mbecker20 4b6aa1d73d stack services use new StateBadge 2024-08-15 03:32:30 -04:00
mbecker20 5dfd007580 fmt 2024-08-15 03:24:52 -04:00
mbecker20 955670d979 improve Stack ingore_services info tip 2024-08-15 03:24:49 -04:00
mbecker20 f70e359f14 avoid warn log on refresh repo task if no repo configured 2024-08-15 03:24:27 -04:00
mbecker20 a2b0981f76 filter out any disks with mount path at /var/lib/docker/volumes. Filter out user defined ones on server 2024-08-15 02:36:53 -04:00
mbecker20 49a8e581bf move Stack post_create stuff into trait for reusability 2024-08-15 02:32:38 -04:00
mbecker20 2d0c1724db fix too many recent cards 2024-08-12 18:38:44 -07:00
mbecker20 20ae1c22d7 config example links correctly 2024-08-12 15:03:02 -07:00
mbecker20 e8d75b2a3d improve docsite resource page 2024-08-12 14:59:10 -07:00
mbecker20 e23d68f86a update logs will convert ansi colors to html 2024-08-12 13:59:56 -07:00
mbecker20 2111976450 Join the Discord 2024-08-12 02:10:19 -07:00
mbecker20 8a0109522b fix remove recents on delete 2024-08-12 00:33:08 -07:00
mbecker20 8d75fa3f2f provide custom webhook secret to all resources which take webhooks 2024-08-11 22:05:26 -07:00
mbecker20 197e938346 stack ignore_services config hidden 2024-08-11 20:18:28 -07:00
mbecker20 6ba0184551 add ignore_services to stack 2024-08-11 20:00:20 -07:00
mbecker20 c456b67018 ListStackServices refetch interval 2024-08-11 19:28:05 -07:00
mbecker20 02e152af4d clean up config struct 2024-08-11 19:16:44 -07:00
mbecker20 392e691f92 add repo build webhook 2024-08-11 18:26:51 -07:00
mbecker20 495e208ccd add building in repo busy check 2024-08-11 17:47:10 -07:00
mbecker20 14474adb90 add building state to repo 2024-08-11 17:46:26 -07:00
mbecker20 896784e2e3 fix repo action UI responsiveness 2024-08-11 17:38:15 -07:00
mbecker20 2e690bce24 repo table just show repo and branch 2024-08-11 17:36:00 -07:00
mbecker20 7172d24512 add message if fail to remove_dir_all after compose deploy 2024-08-11 17:21:19 -07:00
mbecker20 b754c89118 validate config makes sure ids not empty 2024-08-11 17:09:53 -07:00
Maxwell BeckerandGitHub 31a23dfe2d v1.13.1 improve stack edge cases, and UI action responsiveness (#26)
* get stack state from project

* move custom image name / tag below image setting for build config

* services also trigger stack action state

* add status to stack page

* 1.13.1 patch
2024-08-11 17:01:09 -07:00
mbecker20 b0f80cafc3 improve action responsiveness by improving when update is sent out rel to action state set 2024-08-11 14:59:34 -07:00
mbecker20 85a16f6c6f ensure run directory is normalized before create dir all 2024-08-11 14:14:17 -07:00
mbecker20 29a7e4c27b add link to builder in build info 2024-08-11 13:24:29 -07:00
mbecker20 a73b572725 improve dashboard responsiveness 2024-08-11 12:07:14 -07:00
mbecker20 aa44bf04e8 validate repo builder id in diff (new field) 2024-08-11 05:06:17 -07:00
mbecker20 93348621c5 replace repo builder_id with name for toml export 2024-08-11 05:00:34 -07:00
mbecker20 4b2139ede2 docker compose ls --all 2024-08-11 04:56:28 -07:00
mbecker20 3251216be7 update server address config placeholder 2024-08-11 03:35:34 -07:00
mbecker20 1f980a45e8 fix compose example file to reference monitor-mongo 2024-08-11 03:34:19 -07:00
mbecker20 94da1dce99 fill out Procedure execute types 2024-08-11 02:38:15 -07:00
mbecker20 d4fc015494 cli don't panic of no HOME env var 2024-08-11 02:26:18 -07:00
mbecker20 5800fc91d2 repo don't show build button if builder not attached 2024-08-11 02:12:39 -07:00
mbecker20 91785e1e8f monitor_cli install instructions 2024-08-11 01:56:56 -07:00
mbecker20 41fccdb16e demo link in readme 2024-08-10 23:58:42 -07:00
mbecker20 78cf93da8a improve dashboard recents responsiveness 2024-08-10 23:52:24 -07:00
mbecker20 ea36549dbe fix stack service routing page - works with updates 2024-08-10 23:01:31 -07:00
mbecker20 a319095869 improve readme 2024-08-10 16:02:29 -07:00
mbecker20 a6d7a80cbc capitalize Monitor Docs 2024-08-10 15:33:17 -07:00
mbecker20 20f051c890 readme 2024-08-10 15:32:22 -07:00
mbecker20 2fef954ad5 add link to demo docs 2024-08-10 15:30:54 -07:00
mbecker20 e1b9367ee3 remove attempt at parsing json out of config 2024-08-10 14:27:58 -07:00
mbecker20 c7717fbfdf disable create variable for non admin 2024-08-10 13:59:42 -07:00
mbecker20 bf918042c3 point to right mongo 2024-08-10 13:33:09 -07:00
mbecker20 46ac16100d fix example compose "depends on" 2024-08-10 12:55:31 -07:00
mbecker20 eca0378c56 apply spellcheck={false} 2024-08-10 12:50:21 -07:00
mbecker20 bfd5c5390d fix use before init loop 2024-08-10 12:18:07 -07:00
mbecker20 db41878278 guard against running on_clone / on_pull on core 2024-08-10 11:56:18 -07:00
mbecker20 26468ed8ea add infos for stack repo branch commit 2024-08-10 11:49:23 -07:00
mbecker20 707751708d update to latest rust docker base 2024-08-10 11:31:52 -07:00
mbecker20 d28d3422a3 disable server builder cancel in UI 2024-08-10 11:04:51 -07:00
mbecker20 9e2b1ede93 tweak layout: items-center 2024-08-10 10:22:03 -07:00
mbecker20 37e37deb04 clarify what "sync resources" means 2024-08-10 09:42:42 -07:00
mbecker20 e73a6ca72c fix docsite link error 2024-08-10 09:39:54 -07:00
mbecker20 6082b7b1bd update client version in toml 2024-08-10 09:35:20 -07:00
678767c24b [v1.13 - Komodo] Docker compose support with the Stack resource (#24) Co-authored with @karamvirsingh98
* add some network stuff to container summary

* improve settings tables UI

* periphery build supports additional tags

* fix variable container sizing

* alert types newline wrap

* plumbing for Stack resource

* plumbing for Stack resource

* mount stack api

* stack resource sync

* get remote compose file

* support image_name and image_tag

* add server config placeholders. default server config address

* configure image name and image tag

* deployment work with build image_name and image_tag

* stack UI

* fe builds

* configure registry provider and account

* implement periphery stack api

* stack poll interval

* add UI provider management

* deploy stacks

* build push commit hash tag.

* Destroy stack

* update default core port to 9120

* remove git_account alias

* finish stack (and container) api

* frontend builds

* cant cancel server based builds

* fix

* use git pull -f

* 9120

* start UI updates (#15)

* fix  From<Stack> for CloneArgs

* remove unused imports

* UI Updates (#16)

* cleanup dashboard charts for resources

* bring back solid scrollbars

* enable sidebar scrolling

* remove alerts from all resources

* pass jwt secret

* stacks dont delete the target

* parse services from yaml

* stacks deploy

* close

* looking good

* closer

* destroy stack when file missing. onboard stacks

* figure out stack container name matching

* get stack state correct

* work with service views

* UI Updates - Sidebar, Topbar Alerts, and All Resources page (#17)

* move sidebar to use fixed positioning instead of sticky

* add alert details dialog to topbar alerts

* cleanup all resources page layout

* ensure resource links don't propagate clicks

* periphery support passing env with --env-file

* StackServicePage

* default run_directory to ./ for clarify

* add stack webhook listeners

* add default compose name of stack name

* stacks controlled with project name

* migrate to dotenvy

* add stack to dashboard

* remove deploying / destroying stack services

* update config files

* fix getting service logs

* git / docker provider management api

* implement passing git / registry token from db

* rename system user Github to Git Webhook

* seperate deployed and latest services on stack info

* add stack service level operations

* UI Updates - Update Shadcn/UI components, prevent navbar menu layout shift (#20)

* add dashboard pie for resource syncs

* dashboard items same height

* update shadcn components

* ensure centered following sheet update

* cleanup layout, prevent navbar menu layout shifts

* add manual filter, fix toast call

* guard webhooks

* remove deployed_message, latest_message from StackListItemInfo

* stop all containers on server correctly

* support multiple compose files

* cache all containers networks images projects

* remove project missing from db cache

* work on sync deploy stuff

* rework deployment sync deploy to support stacks. they can depend on each other.

* UI Updates - Remove topbar transparency, pretty status badges, tidy resource page layout with a 'back' button (#21)

* remove topbar transparency

* cleanup unused

* responsive dashboard

* better mobile header

* dont need to calc 64px less since header is using position fixed

* add status badge component

* update status badges

* further simplify layout

* allow undefined status as prop

* use new status badges for alerts

* update status badges for all resources

* undo layout change

* tidy up resource page layout, add back button

* no need for button wrapper

* remove unused

* build cancel log

* update ts types

* fix fe type changes

* fe tweaks

* remove on build logs

* core refresh cache immediately on startup

* jwt_ttl

* canonicalize run directory on host

* update canonicalize error message

* core use docker-compose

* fix incorrect project missing, add status string to stack info

* remove entries in "after" that aren't deploying

* fix dockerfiel

* build custom tag postfix

* sync fixes

* ensure UpdateGitProviderAccount doesn't change id

* ensure UpdateDockerRegistryAccount doesn't change id

*  configure providers in the UI

* add // comment support to env, conversions

* add updates for provider deletes

* improve sync pending deploy log

* add more deployment actions

* add backward compat with v1.12 for clone repo

* stack deploy format

* fe

* alert menus clone when click resource link

* rename stacks

* don't close on click

* snake case stack state, in line with deployment state

* sync redeploy stack if newer hash (optional behind resource field 'latest_hash')

* remove nav to tree

* RefreshStack/Sync debug instruments

* improve inline UI docs

* implement resource base_permission backend

* plumbing for Repo build

* build repos

* write env file repos

* add latest hash / message to build info

* add optional hash to update

* keep built_hash updated

* add backend for build / repo latest hash management

* remove unused resources

* clean up repo dirs after cache update

* fix repo info deser error

* add build / repo git status

* fix page layouts

* improve layout responsive

* most config incline docs

* add descriptions for all resource types

* default local auth false

* fix omnibar arrow keys issue

* add compose file to example config

* image registry

* dashboard display no resources messge

* update deps.

* show when no config

* resource sync use config git_provider

* fix networks

* fix deploy error due to after

* update lots of docs

* fix server stat charts not working

* update screenshots

* update changelog

* add a disclaimer

* remove file paths docs stuff

* build repo

* v1.13 - Komodo

* update docs for cli

* fill out the compose example more

---------

Co-authored-by: Karamvir Singh <67458484+karamvirsingh98@users.noreply.github.com>
2024-08-10 09:33:14 -07:00
mbecker20 59cb86d599 serde default on token re Issue 10 2024-08-02 11:13:31 -07:00
mbecker20 5f0a9ad652 remove env vars / conversions / labels # comment support 2024-07-31 13:04:57 -07:00
mbecker20 fc758121da note on login if no auth methods configured 2024-07-31 12:56:59 -07:00
mbecker20 95ccf1af0b reset version on copy 2024-07-31 05:03:33 -07:00
mbecker20 627f7ab585 detect aarch64 periphery install 2024-07-31 04:58:12 -07:00
mbecker20 4238abf61a fix resource sync delete operation 2024-07-31 02:19:24 -07:00
mbecker20 66bfe69983 add note about user periphery install 2024-07-31 00:14:19 -07:00
mbecker20 42b493ae10 host network in example 2024-07-30 15:56:01 -07:00
mbecker20 f4d6c50b67 ensure core config startup log redacted 2024-07-30 14:43:15 -07:00
mbecker20 17176a7d56 add note about upgrading periphery 2024-07-30 14:20:15 -07:00
mbecker20 140b95b70c skip secret interp respected for core secrets 2024-07-30 00:18:28 -07:00
mbecker20 3a2cb73088 improve git https config look 2024-07-29 20:51:55 -07:00
mbecker20 4585533bc5 migration optional env vars 2024-07-29 20:28:57 -07:00
mbecker20 83099f03a1 changelog 2024-07-29 19:36:48 -07:00
mbecker20 9e619c0250 add sync screenshots 2024-07-29 19:34:10 -07:00
mbecker20 edf49dc685 update resource syncs 2024-07-29 19:30:28 -07:00
mbecker20 beffc8c159 consistent dockerfile 2024-07-29 19:18:14 -07:00
mbecker20 d99cf87da0 update client to 1.12 2024-07-29 18:36:42 -07:00
mbecker20 8e19eb7b0f versions 2024-07-29 18:33:14 -07:00
mbecker20 78a0b56c73 migrator readme 2024-07-29 18:32:11 -07:00
mbecker20 bf5dc52237 fix upgrades docs 2024-07-29 18:29:49 -07:00
mbecker20 482ea59d4c add docsite upgrades 2024-07-29 18:28:12 -07:00
Maxwell BeckerandGitHub 7740d36f49 v1.12 Custom Git Providers / Docker Registries (#8)
* update deps

* remove patch when 0 for deployments using specific build version

* implement custom git provider and image registry support

* common providers api

* toml array alias

* username alias account

* get fe to build

* http or https

* fix frontend build

* improve registry / provider config

* frontend build

* rework deployment / builds image registry

* frontend builds

* update build config fe

* configure builder additional accounts / secrets

* guard against managing non-github repo webhooks

* fmt

* md size dashboard

* lowercase organization in image name

* update config docs

* update example env

* provider configuration

* distribute migrator

* fix casing mismatch

* docs
2024-07-29 18:23:58 -07:00
mbecker20 820754deda roadmap 2024-07-24 00:11:58 -07:00
mbecker20 4219884198 roadmapx 2024-07-24 00:11:08 -07:00
mbecker20 d9e24cc35a add roadmap 2024-07-24 00:10:32 -07:00
mbecker20 8d2ce884d9 1.11.1 updated hetzner instances 2024-07-20 02:49:38 -07:00
mbecker20 313b000e64 update hetzner server types 2024-07-20 01:16:52 -07:00
mbecker20 c2f9e29605 close failed procedure execution updates 2024-07-19 23:21:21 -07:00
Maxwell BeckerandGitHub 8c6f38cafb v1.11 Improve permission management (#6)
* add "all permissions" feature on user and user group schema

* prepare support for group all

* implement user.all and user_group.all for broad base permissioning

* clean up unused deps

* sync support user group permissions regex

* 1.11

* fix fe ? issue

* this doesn't work

* sync handle user group all set

* retain above non earlier

* remove permissions that already exist

* update docs

* add user group docs

* minimize user group permissions for execute

* sync toml

* add sync name to slack alert title

* add syncs to alerter white/blacklist

* use \\ instead of $reg

* share resource type base permissions api users and user groups

* manage user / group base permissions ui

* manage user / group base resource type permissions

* update api permission handling

* manage all resource permissions in table

* user show group membership

* update client to 1.11
2024-07-19 02:11:36 -07:00
mbecker20 4a03eba99a granular invalidations 2024-07-17 14:51:51 -07:00
mbecker20 79fe078e3b 1.10.5 cpu/mem only update alert if severity increases (or resolved) 2024-07-17 14:36:22 -07:00
mbecker20 6be032fcd4 update client to 1.10.4 2024-07-16 16:06:38 -07:00
mbecker20 d0c94278ec 1.10.4 fix EnvVar parsing when value contains '=' 2024-07-16 16:05:11 -07:00
mbecker20 03ae7268fd fix server table search when sorting by deployments 2024-07-10 12:09:42 -07:00
mbecker20 f443294818 add clear link to api docs 2024-07-10 02:33:14 -07:00
mbecker20 2202835d86 improve core setup docs 2024-07-10 02:26:58 -07:00
mbecker20 98fbc7a506 improve migrator and add Dockerfile 2024-07-10 02:25:44 -07:00
mbecker20 8ee89296e1 frontend only invalidate on update Complete 2024-07-09 13:50:03 -07:00
mbecker20 989c3d2d01 more compact webhook button labels 2024-07-09 02:26:50 -07:00
mbecker20 dc72883b90 update config example 2024-07-09 02:09:17 -07:00
mbecker20 e99364430f update local client version 2024-07-09 02:06:30 -07:00
mbecker20 e106e38cd9 1.10.3 support multiple github webhook app installations 2024-07-09 02:05:38 -07:00
mbecker20 e4d0c56e49 debug git logs 2024-07-09 00:50:24 -07:00
mbecker20 7427a158f4 full err too large for alert 2024-07-09 00:40:11 -07:00
mbecker20 b926f89954 log on build unsuccessful and alerting 2024-07-09 00:20:03 -07:00
mbecker20 e666a22f08 debug instrument git calls 2024-07-09 00:09:06 -07:00
mbecker20 4107f779a5 fix build increment major version 2024-07-08 13:15:52 -07:00
mbecker20 828d6cdfed improve responsive 2024-07-05 20:19:20 -07:00
mbecker20 fe82400a99 1.10.2 ResourceSync manage repo webhooks 2024-07-05 20:02:20 -07:00
mbecker20 e37fc6adde publish 1.10.1 2024-07-05 03:32:24 -07:00
mbecker20 c21c8f99ae manage webhooks working 2024-07-05 03:29:23 -07:00
mbecker20 78a63f92bb build repo webhook management 2024-07-05 03:17:29 -07:00
mbecker20 ce67655021 core info provide owners 2024-07-05 02:26:18 -07:00
mbecker20 2ccecf38f2 default pk path /github/private-key.pem 2024-07-05 02:15:35 -07:00
mbecker20 1ddae31aad update config example 2024-07-05 02:06:27 -07:00
mbecker20 097fbefa63 1.10.1 2024-07-05 02:02:59 -07:00
mbecker20 b51442a661 ts types 2024-07-05 02:02:25 -07:00
mbecker20 a21d49d224 build / repo webhook write api 2024-07-05 02:02:03 -07:00
mbecker20 c99a33880e Create / Delete webhook api 2024-07-05 01:31:15 -07:00
mbecker20 6ee55262ba webhook management api aware if repo can be managed 2024-07-05 01:18:21 -07:00
mbecker20 878b9b55bb see whether webhooks enabled 2024-07-05 01:05:27 -07:00
mbecker20 af6193f83a update async_timing_util 2024-07-04 21:15:38 -07:00
mbecker20 b8fefddd8b EC2 2024-07-04 19:13:49 -07:00
mbecker20 7f490f5bf2 tweak 2024-07-04 19:12:02 -07:00
mbecker20 efa7c13286 docs 2024-07-04 19:08:48 -07:00
mbecker20 f913be7a0b builder setup guide 2024-07-04 19:03:43 -07:00
mbecker20 35901ef7ea actions can wrap 2024-07-04 17:53:24 -07:00
mbecker20 5b938490fc response 2024-07-04 17:29:45 -07:00
mbecker20 a7326a0116 user group toml export replace target ids with names 2024-07-04 17:10:36 -07:00
mbecker20 877bda91d7 improve log responsiveness 2024-07-04 16:49:08 -07:00
mbecker20 439a091e50 improve resource responsive 2024-07-04 16:29:13 -07:00
mbecker20 b0e89f4963 fix dashboard 2024-07-04 15:46:43 -07:00
mbecker20 b1e4b55ba1 more responsive 2024-07-04 14:41:40 -07:00
mbecker20 d4a1891c70 delete user group 2024-07-04 14:17:03 -07:00
mbecker20 9db7592d7e all_resources tables use right search 2024-07-04 01:25:40 -07:00
mbecker20 84fb603951 1.10 2024-07-01 03:18:26 -07:00
mbecker20 55bac0dd13 check right thing for empty 2024-07-01 03:12:22 -07:00
mbecker20 b143f42363 update mungos 2024-07-01 02:47:06 -07:00
mbecker20 007efd136a 1.10.0 pre 2024-07-01 02:38:24 -07:00
mbecker20 b329767f9e 1.10.0-pre-0 2024-07-01 02:33:01 -07:00
mbecker20 b4231957d5 config for secret args 2024-07-01 02:31:53 -07:00
mbecker20 b4dc446f95 interpolate core variables / secrets into build secret_args 2024-07-01 02:27:03 -07:00
mbecker20 c92515cecc combine into router 2024-07-01 01:44:07 -07:00
mbecker20 f3712feea2 finish periphery clean 2024-07-01 01:39:03 -07:00
mbecker20 0e81d17860 shrink periphery implementation 2024-07-01 01:19:25 -07:00
mbecker20 c3f1557b83 fix mem alert 2024-06-30 00:27:37 -07:00
mbecker20 5f88e4b436 seperate webhook actions 2024-06-25 01:22:38 -07:00
mbecker20 473c6b3867 dont send failed build alert on build cancel 2024-06-24 16:59:34 -07:00
mbecker20 c10edaa5d1 fix builder toml export 2024-06-23 03:00:31 -07:00
mbecker20 9418a6d963 update client to 1.9.0 2024-06-23 02:30:50 -07:00
mbecker20 57646b750f clean up 2024-06-23 02:29:47 -07:00
mbecker20 0d57f9411c can deploy ecr 2024-06-23 02:27:19 -07:00
mbecker20 7d396dd539 clean up ecr 2024-06-23 02:22:14 -07:00
mbecker20 bfe762b71a install unzip 2024-06-23 01:37:12 -07:00
mbecker20 16ede84bac install aws cli core 2024-06-23 01:31:15 -07:00
mbecker20 4524db94db get ecr token using cli 2024-06-23 01:23:56 -07:00
mbecker20 580dab4acd improve error log formatting 2024-06-23 01:02:52 -07:00
mbecker20 645382856a update only flattens one level deep 2024-06-22 23:56:01 -07:00
mbecker20 5c4e6a6dbb select aws config 2024-06-22 23:33:35 -07:00
mbecker20 66810e1efb add method to get availabel aws ecr labels 2024-06-22 23:29:02 -07:00
mbecker20 69a84882f0 1.9.0 2024-06-22 23:06:53 -07:00
mbecker20 41648436a5 default periphery method fields 2024-06-22 22:59:51 -07:00
mbecker20 083a88aa7b implement aws ecr image registry 2024-06-22 22:57:26 -07:00
mbecker20 750f95c90d improve shortcut menu 2024-06-22 18:24:38 -07:00
mbecker20 129f3ecd82 add more kb shortcuts and shortcut menu 2024-06-22 02:56:57 -07:00
mbecker20 1b754f80ab fix double emojis 2024-06-22 01:54:45 -07:00
mbecker20 968a882012 fix alerter table 2024-06-22 01:29:31 -07:00
mbecker20 696ebdb26f label blacklist correctly 2024-06-22 01:25:38 -07:00
mbecker20 8fee04607d imporve slack alerting 2024-06-22 01:10:13 -07:00
mbecker20 6fe250244b add alerter blacklist 2024-06-22 00:30:43 -07:00
mbecker20 b530af0eec send_alerts for sync alert 2024-06-21 23:09:38 -07:00
mbecker20 21e9361079 remove unused 2024-06-21 02:28:35 -07:00
mbecker20 524d2d956b fix alerts usage 2024-06-21 02:23:42 -07:00
mbecker20 aca9633941 add links and errors to slack messages 2024-06-21 01:12:46 -07:00
mbecker20 32e1bd2dda add badges for tag filter shortcuts 2024-06-21 00:15:40 -07:00
mbecker20 cb363d1559 add shift + T and shift + C to manage tags 2024-06-20 23:51:12 -07:00
mbecker20 63eb74b9c8 Add and configure build alerts 2024-06-20 23:41:28 -07:00
mbecker20 bbcc27704f bump rust builder version 2024-06-16 16:00:57 -07:00
mbecker20 0aa9513dd0 1.8.0 2024-06-16 15:36:51 -07:00
mbecker20 26b216b478 add resources page 2024-06-16 15:33:31 -07:00
mbecker20 166299bb57 sync docs 2024-06-16 14:35:09 -07:00
mbecker20 03c47eb3dc remove cli sync 2024-06-16 01:41:54 -07:00
mbecker20 1fcb4ad085 move / update changelog 2024-06-16 01:41:15 -07:00
mbecker20 f51af8fbe1 docs 2024-06-16 01:34:08 -07:00
mbecker20 4a975e1b92 update resource sync docs 2024-06-16 01:33:05 -07:00
mbecker20 ba556e3284 fix doc link 2024-06-16 00:31:23 -07:00
mbecker20 299a326942 log build has new version 2024-06-16 00:20:22 -07:00
mbecker20 a5d4b9aefb add cached results reasons 2024-06-16 00:04:05 -07:00
mbecker20 40b820ae42 add reason to deploy logs 2024-06-15 22:01:14 -07:00
mbecker20 7028bf2996 remove termination_signal for tokio signal 2024-06-15 21:48:54 -07:00
mbecker20 75ebd0e6c0 fix fe cancel logic error 2024-06-15 21:36:26 -07:00
mbecker20 426153df66 try improve toml parse error message 2024-06-15 21:33:53 -07:00
mbecker20 5bd423a6a6 sync deploy new build 2024-06-15 21:15:17 -07:00
mbecker20 c24131d383 nested propogate read resources error 2024-06-15 20:37:29 -07:00
mbecker20 9f54b6c26a 1.8.0. improve env config UI, add sync deploy state management 2024-06-15 20:15:33 -07:00
mbecker20 ab8ae51ece slight more colors 2024-06-15 20:14:25 -07:00
mbecker20 ef2a83ff16 add colors to procedure logs 2024-06-15 20:06:34 -07:00
mbecker20 7872771aee clean up sync log 2024-06-15 19:45:53 -07:00
mbecker20 b12cf858d8 sync deploy logs need \n 2024-06-15 19:36:46 -07:00
mbecker20 38dba91c3a sync deploy accounts for any dependencies in 'after' need deploy 2024-06-15 19:20:45 -07:00
mbecker20 ea8136aa57 add sync deployment state log 2024-06-15 17:31:49 -07:00
mbecker20 f956e12e28 move formatting to shared lib 2024-06-15 17:15:05 -07:00
mbecker20 207ea52b95 add finished log 2024-06-15 17:12:02 -07:00
mbecker20 caf28d3a26 sync deploy 2024-06-15 17:03:16 -07:00
mbecker20 8fff45649d implement sync deployment get updates for view with deploy action 2024-06-15 15:50:10 -07:00
mbecker20 de5df70e11 invert search FE 2024-06-15 00:58:03 -07:00
mbecker20 3df010ac2a read req error debug 2024-06-15 00:54:11 -07:00
mbecker20 2d3beb708e invert logs 2024-06-15 00:28:04 -07:00
mbecker20 1dc22d01c4 improve execute instrumentation 2024-06-15 00:20:28 -07:00
mbecker20 eb029d0408 clone repo to specific directory on host 2024-06-14 23:43:47 -07:00
mbecker20 f926932181 build / deployment env variable / secret selectors 2024-06-14 23:28:08 -07:00
mbecker20 cc96d80c6a string deser filter empty lines 2024-06-14 22:20:39 -07:00
mbecker20 144b49495c string deser can handle empty string 2024-06-14 22:15:02 -07:00
mbecker20 de9354bdc7 frontend manage env with string 2024-06-14 22:10:07 -07:00
mbecker20 38bfee84d7 read resources propogate error 2024-06-14 21:53:13 -07:00
mbecker20 ec33d9fb9e trim incoming value env var string, conversion string, before deserialize 2024-06-14 21:42:59 -07:00
mbecker20 0a66937b1d fix unused liniting 2024-06-14 21:30:10 -07:00
mbecker20 43cc0c3bc1 remove @ in format date 2024-06-14 14:48:22 -07:00
mbecker20 c14b395c70 quick copy variable value 2024-06-12 12:15:29 -07:00
mbecker20 7b8529a7c6 tweak colors 2024-06-12 11:55:06 -07:00
mbecker20 547c089581 update colors 2024-06-12 11:53:39 -07:00
mbecker20 4fe5e461b3 use stroke for icons 2024-06-12 03:48:47 -07:00
mbecker20 edfb873f7c improve error logs 2024-06-12 03:22:51 -07:00
mbecker20 5ef5294c44 remove onkeydown causing redundant create 2024-06-12 03:15:07 -07:00
mbecker20 5d3c50e04f reorder procedure config table 2024-06-12 02:47:41 -07:00
mbecker20 f10efbb5ba add bg to body 2024-06-12 02:39:26 -07:00
mbecker20 39ce98161b add the colors, always plz 2024-06-12 02:21:49 -07:00
mbecker20 cff6e79eee fix omnibar all resource types 2024-06-12 01:46:30 -07:00
mbecker20 dedf22ede8 continue on disabled stage 2024-06-12 01:25:10 -07:00
mbecker20 6955b92a99 add same colors in update 2024-06-12 01:15:39 -07:00
mbecker20 5c63eeab02 better sync coloring 2024-06-12 01:13:33 -07:00
mbecker20 4c14a4ae20 create variable log skip description line if it's empty 2024-06-12 00:39:23 -07:00
mbecker20 29fd856a2d deal with deployment build version 2024-06-11 03:07:56 -07:00
mbecker20 195bdbd94a fix " to \" 2024-06-11 02:14:57 -07:00
mbecker20 298ccd945c improve export dialog sizing 2024-06-11 01:42:06 -07:00
mbecker20 436e4e79e9 toml include ResourceSync 2024-06-11 01:09:37 -07:00
mbecker20 8b8c89d976 1.7.3 procedure stage alias 2024-06-11 00:51:16 -07:00
mbecker20 25c8d25636 1.7.2 default resource config parsing 2024-06-11 00:44:41 -07:00
mbecker20 ea242de2e4 default the config if not exists 2024-06-11 00:34:11 -07:00
mbecker20 be03547407 reorder struct fields for improved toml 2024-06-11 00:04:20 -07:00
mbecker20 9c0d28b311 allow inline arrow up to max length 2024-06-10 23:53:23 -07:00
mbecker20 f269deb99c update toml_pretty 2024-06-10 23:30:17 -07:00
mbecker20 3df8163131 improve procedure toml 2024-06-10 23:14:04 -07:00
mbecker20 33a16a9bd2 need 2 \n 2024-06-10 22:36:17 -07:00
mbecker20 215e7d1bdc update toml_pretty 2024-06-10 22:11:40 -07:00
mbecker20 25e0905c0c fix deserializers 2024-06-10 21:31:17 -07:00
mbecker20 1c07ccea85 bump toml for multiline string 2024-06-10 19:26:01 -07:00
mbecker20 405ec1b8cc bump toml_pretty for fix 2024-06-10 18:58:33 -07:00
mbecker20 4f212bd06f update toml_pretty with skip empty strings 2024-06-10 18:43:53 -07:00
mbecker20 074f4ea2db fix toml 2024-06-10 18:07:05 -07:00
mbecker20 c9abccaf02 build use string serialized version 2024-06-10 17:59:03 -07:00
mbecker20 6428fa6de2 1.7.1 2024-06-10 17:37:22 -07:00
mbecker20 883f54431d custom to toml serializer for api 2024-06-10 17:34:56 -07:00
mbecker20 28dc030e2b custom Vec<EnvVar>, Vec<Conversion> deserializers to support config them as string 2024-06-10 14:39:51 -07:00
mbecker20 145d933e63 pt-2 2024-06-10 01:47:46 -07:00
mbecker20 9772ca1a1c add Resource Sync system user 2024-06-10 01:46:26 -07:00
mbecker20 4059b69201 core auto refreshes all syncs every 5 min 2024-06-09 23:49:02 -07:00
mbecker20 8e175ea5a1 add pending sync alert variant 2024-06-09 23:23:40 -07:00
mbecker20 d931b8b4e7 fix deployment when image_type None 2024-06-09 23:15:52 -07:00
mbecker20 0982800ad2 update client to 1.7.0 2024-06-09 22:47:49 -07:00
mbecker20 4382ad0b3b migrate 1.6 to 1.7 2024-06-09 22:46:21 -07:00
mbecker20 e7891f7870 update docs for ghcr 2024-06-09 21:56:01 -07:00
mbecker20 6bada46841 add export variables / user groups 2024-06-09 21:32:53 -07:00
mbecker20 eae6cbd228 label the image 2024-06-09 20:55:09 -07:00
mbecker20 a0ee6180b2 finish 1.7.0 2024-06-09 19:45:46 -07:00
mbecker20 3ce3de8768 configure registry 2024-06-09 19:34:49 -07:00
mbecker20 6c46993b61 New Monitor logo cr. George Weston 2024-06-09 18:38:58 -07:00
mbecker20 fbd9d14aaa change handler loggin 2024-06-09 15:11:18 -07:00
mbecker20 1011ec60ab rename to ghcr 2024-06-09 14:55:26 -07:00
mbecker20 48e17a7c87 update config example 2024-06-09 03:43:26 -07:00
mbecker20 a94baded55 1.7.0 2024-06-09 03:06:17 -07:00
mbecker20 e97c0873cf get types 2024-06-09 03:05:07 -07:00
mbecker20 43a0b76811 small 2024-06-09 03:04:05 -07:00
mbecker20 2d2577e5ee ghcr 2024-06-09 02:46:57 -07:00
mbecker20 202ac77de3 from on the new types 2024-06-09 02:18:40 -07:00
mbecker20 568c963419 core / periphery support ghcr 2024-06-09 02:01:51 -07:00
mbecker20 5c3294241d add 1.6 build schema for 1.7 migration 2024-06-08 15:35:31 -07:00
mbecker20 648a04be88 add sleep execution for procedure 2024-06-08 14:51:19 -07:00
mbecker20 1b5822f649 custom version deserializer. support string versions 2024-06-08 14:23:26 -07:00
mbecker20 c41a008603 fix variable update 2024-06-08 05:22:32 -07:00
mbecker20 603243b0eb need partial default on alerter enabled 2024-06-08 04:56:52 -07:00
mbecker20 d09ab36696 any sync error shows up in log 2024-06-08 04:34:09 -07:00
mbecker20 ad168c87f7 use approp dialog menus 2024-06-08 04:12:55 -07:00
mbecker20 914f4c6197 seems to work 2024-06-08 03:35:22 -07:00
mbecker20 c73d918e18 no unnecessary user group sync 2024-06-08 02:56:53 -07:00
mbecker20 9d116f56cb sort lists by name 2024-06-08 02:21:32 -07:00
mbecker20 8a8dede5db resource sync state 2024-06-08 02:12:04 -07:00
mbecker20 d2cecf316c add pending update alert 2024-06-08 01:39:18 -07:00
mbecker20 cad1ee123e improv the sync 2024-06-08 00:50:30 -07:00
mbecker20 6aa801b705 lock sync dir access 2024-06-07 22:02:58 -07:00
mbecker20 078ba59002 ensure sync directory exist 2024-06-07 21:02:28 -07:00
mbecker20 5eacb7191b fix the fe errors with most boilerplate 2024-06-07 20:00:01 -07:00
mbecker20 45eafd10b9 finish sync backend? 2024-06-07 19:00:03 -07:00
mbecker20 42c486807c implement resource sync cli 2024-06-07 17:11:58 -07:00
mbecker20 8c31fcff02 backend for resource sync 2024-06-07 03:52:07 -07:00
mbecker20 49f1d40ce8 implement RunSync 2024-06-07 02:43:45 -07:00
mbecker20 bf85e886bd abit more 2024-06-06 03:02:25 -07:00
mbecker20 eda0b233ca implement sync 2024-06-06 02:38:47 -07:00
mbecker20 5efb227851 update ts client response 2024-06-05 23:46:05 -07:00
mbecker20 1a45fffe75 move some libraries out 2024-06-05 23:44:06 -07:00
mbecker20 fa72f2e5ef update execute task handling 2024-06-05 22:50:03 -07:00
mbecker20 c9152db300 unneeded import 2024-06-05 22:42:28 -07:00
mbecker20 25fcca7246 should fix procedure 2024-06-05 22:42:04 -07:00
mbecker20 ac449e38d5 init boilerplate 2024-06-05 17:29:59 -07:00
mbecker20 d6c66948ba skip update in execute task instrument 2024-06-05 16:16:21 -07:00
mbecker20 b6af790aef sort resources in selector 2024-06-05 15:39:47 -07:00
mbecker20 36a49210a0 fix filter by split 2024-06-05 15:16:30 -07:00
mbecker20 d2b2aa0550 its not really distributed 2024-06-05 02:41:08 -07:00
mbecker20 7f4c883416 hide / show to toggle alert area 2024-06-05 01:59:16 -07:00
mbecker20 676fb3c732 common filtering method 2024-06-04 05:50:36 -07:00
mbecker20 17da4bd2fa procedure setconfig on update 2024-06-04 04:46:40 -07:00
mbecker20 b44e57bbf6 improve update date format 2024-06-04 04:36:55 -07:00
mbecker20 6aa5b5faae use same search alg for all command inputs 2024-06-04 04:21:24 -07:00
mbecker20 9565855477 fix fe error 2024-06-04 02:23:53 -07:00
mbecker20 3504c083b4 export all resources toml filter resources by tag 2024-06-04 02:11:07 -07:00
mbecker20 5fdaa9a808 make overflowing tags wrap 2024-06-04 01:45:20 -07:00
mbecker20 ec35b14077 further improve BuildState if cancel. 2024-06-04 01:22:41 -07:00
mbecker20 158f3ad89b fmt update operation with regex everywhere 2024-06-03 16:36:53 -07:00
mbecker20 7257ecbaed version link to docs 2024-06-03 16:12:09 -07:00
mbecker20 a2a94f23ee publish client + cli 1.6.2 2024-06-03 15:03:43 -07:00
mbecker20 03cad5b23b partial config from files first merged onto full config default before diff with remote 2024-06-03 15:01:14 -07:00
mbecker20 2460b5edf7 update log internal scroll 2024-06-03 03:10:35 -07:00
mbecker20 83fdb180aa avoid deployment state change alert involving status Unknown 2024-06-03 03:01:23 -07:00
mbecker20 9b1d32ebdf base64 encode aws user data before send 2024-06-03 00:44:45 -07:00
mbecker20 ea4ae7651c readme 2024-06-02 21:19:46 -07:00
mbecker20 5f6fabd925 1.6.1 pass creds as args cli 2024-06-02 21:17:23 -07:00
mbecker20 38d9495ab1 fix cli readme 2024-06-02 21:03:37 -07:00
mbecker20 46ad5b3953 1.6.0 Improve procedure with multiple stages 2024-06-02 21:00:06 -07:00
mbecker20 e60b817208 improve saving 2024-06-02 20:57:44 -07:00
mbecker20 0ce5248292 improve changes made visibility 2024-06-02 20:54:41 -07:00
mbecker20 050c29f4a3 show when changes made 2024-06-02 20:30:06 -07:00
mbecker20 8580728933 alert config working 2024-06-02 20:15:49 -07:00
mbecker20 3c5868d111 alert refactor 2024-06-02 19:15:13 -07:00
mbecker20 40e1b1ff88 improve build cancel disabled logic to prevent redundant cancels 2024-06-02 17:50:33 -07:00
mbecker20 99641b2e39 improve update toast title 2024-06-02 17:42:15 -07:00
mbecker20 f0e7757eb4 improve validate CancelBuild 2024-06-02 17:39:13 -07:00
mbecker20 f7283b1fc1 update alerter to support type filtering. 2024-06-02 17:16:35 -07:00
mbecker20 771af21eae migrator support migrate permissions 2024-06-02 15:35:36 -07:00
mbecker20 0dda791ec7 fix build not try add_update 2024-06-02 04:56:37 -07:00
mbecker20 bc76b1c07e only push recently viewed if exists 2024-06-02 04:43:01 -07:00
mbecker20 8b537924fb correct execution target passed by name 2024-06-02 04:38:32 -07:00
mbecker20 f5ce3570e4 execute api returns update immediately 2024-06-02 04:14:51 -07:00
mbecker20 f1e51d275c move stages up / down 2024-06-02 02:39:48 -07:00
mbecker20 eaa10d96b5 finish new procedure config 2024-06-02 02:06:01 -07:00
mbecker20 037364068d refresh caches on create / update 2024-06-02 01:07:53 -07:00
mbecker20 2441bc8cbf fix lint 2024-06-02 00:44:47 -07:00
mbecker20 92ac003910 backend for updated procedure schema 2024-06-02 00:36:39 -07:00
mbecker20 693f24763f new deployment / repo from server page 2024-06-01 20:33:38 -07:00
mbecker20 d9d44ceee1 update readme with manual 2024-06-01 19:58:20 -07:00
mbecker20 30ab8ed17b update cli with execute features. 2024-06-01 19:47:46 -07:00
mbecker20 2bf2be54cc bookworm base 2024-05-29 13:09:52 -07:00
mbecker20 b7ea680958 alert table rename Target to Resource 2024-05-29 01:48:32 -07:00
mbecker20 2a56d09f89 improve periphery start command docs 2024-05-29 01:40:45 -07:00
mbecker20 2612f742b2 remove trailing whitespace in error log 2024-05-29 00:22:04 -07:00
mbecker20 29bdf5c71d pretty clone fail message 2024-05-29 00:20:58 -07:00
mbecker20 873d9ea433 builder instance failed reachability adds log that instance will be terminated 2024-05-29 00:16:55 -07:00
mbecker20 717f3afa89 fix build config when not builder 2024-05-28 14:33:52 -07:00
mbecker20 ec31d1af01 fix 2024-05-28 05:35:35 -07:00
mbecker20 9e5c52b9a4 update client version 2024-05-28 05:32:19 -07:00
mbecker20 762873d5be implement ui_write_disabled 2024-05-28 05:30:37 -07:00
mbecker20 67fa512975 core version in topbar 2024-05-28 05:06:39 -07:00
mbecker20 502dd3a4a8 update client version 2024-05-28 04:58:31 -07:00
mbecker20 8c22bdd473 1.5.4 add variable support to monitor cli 2024-05-28 04:57:41 -07:00
mbecker20 ba6801da11 cli much faster 2024-05-28 04:02:34 -07:00
mbecker20 309802093c 1.5.3 add ListFull methods 2024-05-28 03:42:35 -07:00
mbecker20 3d1e3009b3 add ListFull methods 2024-05-28 03:25:50 -07:00
mbecker20 fdc23c2650 improve docs 2024-05-28 03:06:42 -07:00
mbecker20 072ee6834e update dashboard screenshots 2024-05-28 01:44:11 -07:00
mbecker20 bedbf76349 red 2024-05-28 01:40:44 -07:00
mbecker20 e26d1211cc Cloud 2024-05-28 01:38:35 -07:00
mbecker20 0342ee4dd9 Hetzner 2024-05-28 01:38:08 -07:00
mbecker20 669d5c81b4 read. me. 2024-05-28 01:35:49 -07:00
mbecker20 defbab5955 monitor cli 2024-05-28 01:30:57 -07:00
mbecker20 9405295e4a update changelog 2024-05-28 01:23:44 -07:00
mbecker20 28c077ed4c remove hetzner automount 2024-05-26 02:30:56 -07:00
mbecker20 61406c1b00 add back wait for volume 2024-05-26 02:10:34 -07:00
mbecker20 64638730b9 waiting for volumes makes no difference. dont seem to automount 2024-05-26 01:34:14 -07:00
mbecker20 c0942c6d1d remove execute fail message 2024-05-26 00:42:33 -07:00
mbecker20 ff964cd0fe fix updates 2024-05-26 00:38:59 -07:00
mbecker20 d56f632a11 improve unknown server styling 2024-05-26 00:21:43 -07:00
mbecker20 a7f22b6cfb instrument ServerTemplate write api 2024-05-26 00:09:38 -07:00
mbecker20 6053fc1d99 hetzner poll volumes for ready before launch server 2024-05-26 00:04:31 -07:00
mbecker20 573ff1863c 1.5.2 2024-05-25 23:34:14 -07:00
mbecker20 dd4a9b0cb5 add defaults to Hetzner volume 2024-05-25 23:32:16 -07:00
mbecker20 d243cf2da7 all resources search case insensitive 2024-05-25 23:07:54 -07:00
mbecker20 4e06e788ae PushRecentlyViewed and SetLastSeenUpdate should be debug instrument 2024-05-25 21:13:25 -07:00
mbecker20 a0f71f8af5 table search not case sensitive 2024-05-25 21:07:53 -07:00
mbecker20 fcbb75d0c0 update some tracing stuff 2024-05-25 20:46:56 -07:00
mbecker20 0a8419bb13 update client to 1.5.1 2024-05-25 20:38:35 -07:00
mbecker20 40fe76cf27 1.5.1 move routes to /user 2024-05-25 20:36:52 -07:00
mbecker20 5594d3c1d9 add server to repo table / info 2024-05-25 19:45:23 -07:00
mbecker20 b12aeb259f clean up 2024-05-25 18:38:17 -07:00
mbecker20 b121b0ac07 fix remove from recently viewed 2024-05-25 18:34:05 -07:00
mbecker20 a9f1d91b1b update deps 2024-05-25 18:06:49 -07:00
mbecker20 abf48d0243 1.5.0 doc update and add other_data 2024-05-25 17:57:31 -07:00
mbecker20 447690d8bf remove TextUpdateMenu update on enter 2024-05-25 16:58:34 -07:00
mbecker20 a70c0a2697 increase hetzner polling time 2024-05-25 15:49:10 -07:00
mbecker20 0758e6ff81 get ip after instance is running 2024-05-25 15:29:11 -07:00
mbecker20 ea0e059ee1 hetzner response optional parsing 2024-05-25 14:26:26 -07:00
mbecker20 c9e0524794 add repos tab to server page 2024-05-25 14:05:02 -07:00
mbecker20 81ceaf1eae move automount 2024-05-25 13:50:59 -07:00
mbecker20 37c07ff748 only actually add automount if volumes nonempty 2024-05-25 13:43:06 -07:00
mbecker20 62e8943ebe improve client error message 2024-05-25 13:40:05 -07:00
mbecker20 99ccffbc38 configure hetzner template working 2024-05-25 03:09:31 -07:00
mbecker20 84dc29b77f update ts types 2024-05-25 01:37:05 -07:00
mbecker20 81bab4aa50 clean up some unused stuff 2024-05-25 01:35:28 -07:00
mbecker20 9fa2fd0f58 implement hetzner server launch 2024-05-25 01:16:54 -07:00
mbecker20 3745967690 ensure env overrides fully applied 2024-05-24 16:40:15 -07:00
mbecker20 e8cfc13342 implement transparent mode 2024-05-23 02:19:42 -07:00
mbecker20 ec47bb11ee start on hetzner 2024-05-23 01:47:02 -07:00
mbecker20 d008c95853 no destructive update toasts 2024-05-22 04:27:33 -07:00
mbecker20 4986d70506 remove unneded build toasts 2024-05-22 04:23:53 -07:00
mbecker20 1372a5fb39 3s 2024-05-22 04:22:06 -07:00
mbecker20 f54224650f fix update 2024-05-22 04:20:48 -07:00
mbecker20 2eee1459e7 fix ws 2024-05-22 04:02:37 -07:00
mbecker20 5a3fd891c4 huh 2024-05-22 04:00:43 -07:00
mbecker20 ba3f288c2d improve toasts 2024-05-22 03:55:26 -07:00
mbecker20 6d5fd7dc5d improve update table 2024-05-22 03:25:49 -07:00
mbecker20 df3fd7c4e9 update builder ami 2024-05-22 03:15:30 -07:00
mbecker20 395f032ee2 fix update details name 2024-05-22 03:15:23 -07:00
mbecker20 de2bd800c4 update client to 1.4.1 2024-05-22 02:01:40 -07:00
mbecker20 75352a91ff 1.4.1 fix cli - shouldn't send update if no change 2024-05-22 01:59:25 -07:00
mbecker20 9b12270d04 update client published version 2024-05-22 00:29:34 -07:00
mbecker20 7fc378798f fix cli toml patch 2024-05-22 00:28:21 -07:00
mbecker20 3db2c93303 expande resource table status column 2024-05-22 00:25:54 -07:00
mbecker20 150d6562bf improve table with better row sizing 2024-05-21 01:31:46 -07:00
mbecker20 c3b549b051 resource to_list_item should be infallible 2024-05-21 00:46:13 -07:00
mbecker20 931f2bd92d log auto bottom and increase height 2024-05-20 22:56:32 -07:00
mbecker20 6b6324d79c theme toggle indicator 2024-05-20 22:52:25 -07:00
mbecker20 2c65d924f9 dashboard recents 2 cols unless 2xl 2024-05-20 22:46:21 -07:00
mbecker20 dd1fecf190 a little smaller 2024-05-20 22:36:01 -07:00
mbecker20 aa96a37db4 decrease sidebar vertical size 2024-05-20 21:40:14 -07:00
mbecker20 ec9e9638f5 more gap between resources on dashboard 2024-05-20 03:55:58 -07:00
mbecker20 e33019cab8 add prune images to prune loop 2024-05-20 03:55:01 -07:00
mbecker20 951cb82e0c supports 2024-05-20 03:34:59 -07:00
mbecker20 0643f96053 log 2024-05-20 03:34:07 -07:00
mbecker20 56d835f2d2 not 2024-05-20 03:33:02 -07:00
mbecker20 d8fb8f8649 fix 2024-05-20 03:31:52 -07:00
mbecker20 7197d628e5 fit 2024-05-20 03:30:34 -07:00
mbecker20 96083178dd add changelog to readme 2024-05-20 03:29:45 -07:00
mbecker20 9d1b705ab1 improve login page 2024-05-20 01:36:14 -07:00
mbecker20 2582bc9ba3 simplify resource components, improve update details loading behavior 2024-05-19 15:27:02 -07:00
mbecker20 44f34b9b40 show core secrets on variables page 2024-05-19 02:56:32 -07:00
mbecker20 bbb18d8280 standard search bar location 2024-05-19 02:38:24 -07:00
mbecker20 da95b7d074 add lizard logo / favicon 2024-05-19 02:20:43 -07:00
mbecker20 6b25309aed variables working 2024-05-19 01:51:09 -07:00
mbecker20 f8e371af31 periphery needs axum feature 2024-05-18 16:00:20 -07:00
mbecker20 a0f5ae8c7f add core side interpolation to update 2024-05-18 15:58:23 -07:00
mbecker20 2f371af288 1.4.0 2024-05-18 15:37:30 -07:00
mbecker20 76840efddc implement core variables / secrets on the backend 2024-05-18 15:34:58 -07:00
mbecker20 8f01e441a4 increase per page limit of alerts and updates 2024-05-18 02:42:03 -07:00
mbecker20 41a6e0a65a improve dashboard 2024-05-18 02:34:47 -07:00
mbecker20 40027f7430 improve alert table 2024-05-18 01:07:23 -07:00
mbecker20 a2c69aba87 improve deployment build version selector 2024-05-18 00:49:57 -07:00
mbecker20 a5d3fbedc6 remove build versions pagination 2024-05-18 00:12:08 -07:00
mbecker20 b311b11785 label recents 2024-05-18 00:05:44 -07:00
mbecker20 7a0b29b387 move mongo / dockers deps to client features 2024-05-17 23:48:05 -07:00
mbecker20 d3a87fdb5f update snippet 2024-05-17 22:40:23 -07:00
mbecker20 9b7ab6d98a update resolver and remove async-trait 2024-05-17 22:38:06 -07:00
mbecker20 c302e28d86 improve omnibar search suggestion 2024-05-17 02:13:53 -07:00
mbecker20 33be989e3a dashboard2 2024-05-17 02:07:48 -07:00
mbecker20 c9d65300c9 work on dashboard 2 2024-05-17 01:35:34 -07:00
mbecker20 e96b676366 add build / repo / procedure state stuff to summary 2024-05-17 01:10:48 -07:00
mbecker20 0bff4a5e51 10 recents per resource type 2024-05-16 00:02:09 -07:00
mbecker20 9b12334922 update ts types and fix stuff 2024-05-15 19:17:54 -07:00
mbecker20 68659630fc split recently viewed by resource type 2024-05-15 19:15:03 -07:00
mbecker20 8b33647620 add prop 2024-05-15 02:32:21 -07:00
mbecker20 871aba62d5 move description to right 2024-05-15 02:21:06 -07:00
mbecker20 c649094a8a intro 2024-05-14 23:15:57 -07:00
mbecker20 e3c11db89e update deps 2024-05-14 02:10:10 -07:00
mbecker20 c43293109d add repo and homepage 2024-05-14 01:55:13 -07:00
mbecker20 d3e4f9f638 1.3.0 clean publish 2024-05-14 01:43:01 -07:00
mbecker20 eecf583b0e add dotenv for docsite convenience 2024-05-14 01:37:50 -07:00
mbecker20 a518806d8b docs 2024-05-14 01:33:38 -07:00
mbecker20 8a4611c380 fix link 2024-05-14 01:20:30 -07:00
mbecker20 d679fbe72f client readme 2024-05-14 01:17:55 -07:00
mbecker20 1cf02bc4b4 docs 2024-05-14 01:00:04 -07:00
mbecker20 bf703eef35 frontend support procedure state 2024-05-13 22:28:45 -07:00
mbecker20 985058afb0 ts types 2024-05-13 15:30:37 -07:00
mbecker20 eac1145958 add ProcedureState 2024-05-13 15:30:23 -07:00
mbecker20 1b408d92d9 theme 2024-05-13 01:32:24 -07:00
mbecker20 ee95c2c76b update screenshots 2024-05-13 01:31:49 -07:00
mbecker20 e83124ebff build ConfigOrDeployments 2024-05-13 01:11:12 -07:00
mbecker20 e912ae050a deploy working 2024-05-13 01:01:06 -07:00
mbecker20 99253d6182 build extra arg suggestions 2024-05-13 00:14:12 -07:00
mbecker20 ef91577ac5 check disabled 2024-05-12 23:48:53 -07:00
mbecker20 b97f9b30b3 aws config looking good 2024-05-12 23:44:06 -07:00
mbecker20 7cb11dbc5d improve builder config 2024-05-12 23:33:33 -07:00
mbecker20 6d815629fc everything looking good 2024-05-12 23:12:54 -07:00
mbecker20 f8021d8541 repo config good 2024-05-12 23:01:25 -07:00
mbecker20 1f444fdbc2 builds looking good 2024-05-12 22:58:03 -07:00
mbecker20 af76dd1be4 more config 2024-05-12 22:38:57 -07:00
mbecker20 5cb91c6f8d improve config 2024-05-12 21:35:47 -07:00
mbecker20 de5502aec7 dark less blue 2024-05-12 19:39:23 -07:00
mbecker20 ef4ae4c5f2 state aware repo actions 2024-05-12 16:03:56 -07:00
mbecker20 866eb6d81b looking good 2024-05-12 15:55:49 -07:00
mbecker20 58d6c16eea update sheet from top 2024-05-12 15:37:56 -07:00
mbecker20 ccbf13ae84 get username return avatar 2024-05-12 13:44:32 -07:00
mbecker20 21f6acd3d7 add other resources to omnisearch 2024-05-12 13:11:52 -07:00
mbecker20 dce59d1383 center search 2024-05-12 13:07:10 -07:00
mbecker20 2fb544c3b0 sidebar gap 2024-05-12 13:02:47 -07:00
mbecker20 1ba288be79 split sidebar home 2024-05-12 12:59:45 -07:00
mbecker20 1ff21d2986 more cli loggin 2024-05-12 12:21:38 -07:00
mbecker20 79cc2c1bb7 improve cli logging 2024-05-12 12:15:25 -07:00
mbecker20 17b2e6660c rename alert_logger -> alerter. cli colors 2024-05-12 11:59:49 -07:00
mbecker20 4ef095fe55 improve server stats tables 2024-05-12 04:23:38 -07:00
mbecker20 fb0a7352e3 filter out docker overlay disks 2024-05-12 03:49:29 -07:00
mbecker20 9a087e5975 clean up resource / tree view 2024-05-12 03:26:45 -07:00
mbecker20 814e47031d dope search 2024-05-12 02:55:13 -07:00
mbecker20 1304565e40 fix log to bottom 2024-05-12 01:54:31 -07:00
mbecker20 85616d0669 command loops 2024-05-12 01:46:22 -07:00
mbecker20 feff4647e7 add display for repo latest hash / message 2024-05-12 01:41:41 -07:00
mbecker20 549e15bfe2 add more image tags 2024-05-12 01:24:43 -07:00
mbecker20 a08baf8432 actually mount GetLatestCommit 2024-05-12 01:18:33 -07:00
mbecker20 99c47ce133 just mess with stuff 2024-05-12 01:09:58 -07:00
mbecker20 26a4691c0b server page - swap version / stats link 2024-05-11 23:55:14 -07:00
mbecker20 addb35aa69 give placeholders 2024-05-11 23:38:48 -07:00
mbecker20 16bf78f9ad enable commit config 2024-05-11 23:11:41 -07:00
mbecker20 3ed4f91d82 periphery cli args name to periphery 2024-05-11 23:06:04 -07:00
mbecker20 653fb894a2 delete file 2024-05-11 22:44:13 -07:00
mbecker20 0f9798a5f2 1.2.0 2024-05-11 22:34:35 -07:00
mbecker20 6776a20ec5 build repo state cache 2024-05-11 22:30:33 -07:00
mbecker20 fb21e8586f clone commit hash 2024-05-11 21:38:29 -07:00
mbecker20 8b2c4d604a periphery get repo status 2024-05-11 18:42:21 -07:00
mbecker20 c0b010d5ce rename DockerContainerState -> DeploymentState 2024-05-11 18:29:40 -07:00
mbecker20 97de34a088 align state / status distinction 2024-05-11 18:22:24 -07:00
mbecker20 6c0b76a270 add big icons 2024-05-11 18:04:30 -07:00
mbecker20 eebd44ab9b common resource filter 2024-05-11 17:42:38 -07:00
mbecker20 783250c5ce sort build / repo status update query by most recent 2024-05-11 16:12:11 -07:00
mbecker20 70ff93050f choose between config / log for deployment 2024-05-11 16:06:36 -07:00
mbecker20 1cc1813185 fix dev fe build 2024-05-11 14:50:05 -07:00
mbecker20 b4f9b87d06 update resources, cli 2024-05-11 14:48:26 -07:00
mbecker20 26b09a767e unlink 2024-05-11 13:44:51 -07:00
mbecker20 bba6c4d8b6 add status to build / repo 2024-05-10 22:59:20 -07:00
mbecker20 ea440235c4 configure enable / disable action on webhook recieve 2024-05-10 21:54:41 -07:00
mbecker20 f9949bf988 readOnly webhhok copy 2024-05-10 20:40:54 -07:00
mbecker20 b978db012e slight style 2024-05-10 20:37:56 -07:00
mbecker20 bc2fbdd657 remove rws and code faster reconnect 2024-05-10 18:04:35 -07:00
mbecker20 a5571bcf4d 2xl show 8 recents 2024-05-10 17:30:20 -07:00
mbecker20 683a528dd9 mx-8 2024-05-10 17:23:28 -07:00
mbecker20 4a283b6052 remove container on topbar 2024-05-10 17:21:11 -07:00
mbecker20 37224ee1ad dont req for username of built in users 2024-05-10 17:14:55 -07:00
mbecker20 5e7445b10d add a god damn sidebar 2024-05-10 17:07:51 -07:00
mbecker20 1829a7da34 update diff looking good 2024-05-10 03:59:51 -07:00
mbecker20 4a1a653bd9 rust 1.78.0 2024-05-10 03:40:30 -07:00
mbecker20 840c1a87d0 try putting the html in th diff directly 2024-05-10 03:19:18 -07:00
mbecker20 c90368e2af add periphery build repo 2024-05-10 02:29:47 -07:00
mbecker20 1f9d74fadb fix default creds path 2024-05-10 02:28:06 -07:00
mbecker20 5b261058fe add updates to dashboard 2024-05-10 02:04:23 -07:00
mbecker20 cf6632ba02 partial_derive2 0.4.2 better diffs 2024-05-10 01:50:08 -07:00
mbecker20 c7124bd63c colored diff 2024-05-10 01:03:23 -07:00
mbecker20 ba19e45607 imporve config styling 2024-05-10 00:50:29 -07:00
mbecker20 20282ffcbb update frontend deps 2024-05-10 00:10:40 -07:00
mbecker20 cb8ad90838 cli readme and default creds path 2024-05-09 22:29:40 -07:00
mbecker20 caac3fdcc4 v{version} 2024-05-09 15:51:54 -07:00
mbecker20 44da282060 improve server version 2024-05-09 15:47:58 -07:00
mbecker20 a2e27b09fc show version 2024-05-09 15:39:30 -07:00
mbecker20 c1b1f397fd comments 2024-05-09 15:36:15 -07:00
mbecker20 1d0f239594 delete binary before recurl 2024-05-09 15:34:03 -07:00
mbecker20 549bc78799 log version 2024-05-09 15:28:32 -07:00
mbecker20 9eb9b57e36 install load latest version automatically if its not passed 2024-05-09 15:25:57 -07:00
mbecker20 c38849961e add Repos to search 2024-05-09 14:08:01 -07:00
mbecker20 3acfa0c4b1 more client docs 2024-05-09 03:25:37 -07:00
mbecker20 62b083c3be docsite 2024-05-09 03:12:53 -07:00
mbecker20 ee6fc4c590 include description / tags in diff 2024-05-09 01:53:29 -07:00
mbecker20 4fa550b3d3 monrun diffing 2024-05-09 01:33:03 -07:00
mbecker20 1c44ae98fb monrun / api better diffs 2024-05-09 00:34:21 -07:00
mbecker20 148223f995 config diff update log 2024-05-08 00:34:41 -07:00
mbecker20 4678f83542 fix clippy lints 2024-05-08 00:21:37 -07:00
mbecker20 340bac078f unneeded in cargo toml 2024-05-07 23:31:31 -07:00
mbecker20 3f8e75bbd8 update to partial derive 0.4.0 2024-05-07 23:29:57 -07:00
mbecker20 c4278d14a9 only show actions if more than 0 Actions on resource 2024-05-07 04:08:12 -07:00
mbecker20 4909106c3c turn off build icon spinning. still turns green 2024-05-07 03:45:00 -07:00
mbecker20 0f04a2848a clean up action states 2024-05-07 03:43:20 -07:00
mbecker20 ba073bf8b2 minimize update diffs 2024-05-07 03:40:30 -07:00
mbecker20 640809aa6b use trait for resource crud 2024-05-07 02:54:01 -07:00
mbecker20 943bb4c61a handle update config / description / tags seperately 2024-05-06 03:10:57 -07:00
mbecker20 ef43cb7920 clean up deployments toml 2024-05-06 02:39:04 -07:00
mbecker20 1abe634679 export individual resources 2024-05-06 02:24:35 -07:00
mbecker20 18ab18b6f6 fix for server template sync 2024-05-06 01:55:14 -07:00
mbecker20 aad971a599 sync working 2024-05-06 01:44:20 -07:00
mbecker20 3d9da97d7b monrun better check for empty 2024-05-06 01:36:22 -07:00
mbecker20 2d0a09f760 monrun 2024-05-06 01:12:14 -07:00
mbecker20 568c317d6d create repo does not clone 2024-05-06 00:47:27 -07:00
mbecker20 f0e1f253f4 add queue to listeners using locks 2024-05-06 00:33:19 -07:00
mbecker20 7634b9fa1d example 2024-05-06 00:21:03 -07:00
mbecker20 2e1795eba6 webhook 2024-05-06 00:16:22 -07:00
mbecker20 0f6b3d6e9b add github webhook copiers 2024-05-06 00:00:47 -07:00
mbecker20 a70afcc461 fix build client 2024-05-05 23:20:45 -07:00
mbecker20 d78fc2b282 improve onpull 2024-05-05 23:17:27 -07:00
mbecker20 249610afce frontend onpull 2024-05-05 22:55:37 -07:00
mbecker20 b93b639b40 add extra args style 2024-05-05 22:40:27 -07:00
mbecker20 13ceb55fe8 fix primary dropdown templates 2024-05-05 20:36:56 -07:00
mbecker20 7ebd38d350 slight style 2024-05-05 20:32:19 -07:00
mbecker20 c12c74005e finish configure server templates 2024-05-05 19:08:47 -07:00
mbecker20 c44dc1c6f6 clean server template schema 2024-05-05 18:39:10 -07:00
mbecker20 cd2b9ec4ed validate name not taken on LaunchServer 2024-05-05 16:35:55 -07:00
mbecker20 dc62442c00 LaunchServer revise 2024-05-05 14:16:19 -07:00
mbecker20 81a90f7ae4 launch server 2024-05-05 14:08:49 -07:00
mbecker20 af092e0d88 include server template in monrun export / import 2024-05-05 13:18:50 -07:00
mbecker20 0e70015fd1 add server template page 2024-05-05 13:02:25 -07:00
mbecker20 a214deef86 fix GetServerTemplatesSummary 2024-05-05 13:00:46 -07:00
mbecker20 0efb2966b0 set build version to 0.0.0 on Copy 2024-05-04 23:54:09 -07:00
mbecker20 1e8422a506 custom placeholder for AccountSelector 2024-05-04 23:10:46 -07:00
mbecker20 7bb386e6d0 Account Selector can set back to None 2024-05-04 18:57:42 -07:00
mbecker20 c464ca5612 resources clear their config after update completes 2024-05-04 18:11:23 -07:00
mbecker20 742630fdee create new resources navs to created resource 2024-05-04 17:54:21 -07:00
mbecker20 154fd899fe fix all the links 2024-05-04 17:49:06 -07:00
mbecker20 aa25f9d4c9 servers init disabled 2024-05-04 17:33:05 -07:00
mbecker20 eeed94c8fd === PERIPHERY INSTALLER === 2024-05-04 16:45:39 -07:00
mbecker20 b2f771199d default repos path 2024-05-04 16:39:20 -07:00
mbecker20 4bceb97a66 comment out fields that are just being set to the defautl 2024-05-04 16:25:55 -07:00
mbecker20 8734e6fc4c clean up periphery example config 2024-05-04 16:22:01 -07:00
mbecker20 effd9315cb curl silent 2024-05-04 16:12:54 -07:00
mbecker20 d45f60f604 fix system case 2024-05-04 16:10:30 -07:00
mbecker20 1c7582d0a2 note about root user 2024-05-04 16:05:43 -07:00
mbecker20 566e1090da clearer 2024-05-04 15:46:36 -07:00
mbecker20 a9b3054de3 readme 2024-05-04 15:45:34 -07:00
mbecker20 73d179b355 docs for scripts 2024-05-04 15:45:24 -07:00
mbecker20 97486242a0 setup scripts 2024-05-04 15:43:09 -07:00
mbecker20 a0ddff0618 dashboard 2024-05-04 15:37:19 -07:00
mbecker20 f91e95bf63 readme 2024-05-04 15:35:57 -07:00
mbecker20 431ef82f3e add screenshots 2024-05-04 15:27:02 -07:00
mbecker20 a07bc9fbca make alert details text larger 2024-05-04 15:18:47 -07:00
mbecker20 1e13cd9261 add setup-periphery.py and readme 2024-05-04 15:09:50 -07:00
mbecker20 ae0b59179d version 1.0.0 2024-05-04 15:05:41 -07:00
mbecker20 a9ef12d687 finish api docs 2024-05-04 01:22:52 -07:00
mbecker20 b73c13172a docs 2024-05-04 00:54:54 -07:00
mbecker20 c9b41e7449 server and builder GetAccount includes account in core config 2024-05-03 04:59:16 -07:00
mbecker20 fccc15df4a add docsite (docusaurus v3) 2024-05-03 04:48:18 -07:00
Maxwell BeckerandGitHub 4f7eeacebc Merge pull request #5 from mbecker20/next
merge v1 into main
2024-05-03 04:10:16 -07:00
mbecker20 9b172a833a 1.1.0 2024-05-03 04:08:54 -07:00
mbecker20 fd6fc925d6 core can pass creds to peripheries 2024-05-03 04:06:37 -07:00
mbecker20 6318670b6c implement core pass tokens 2024-05-03 03:50:50 -07:00
mbecker20 7fa5fd83d2 ServerTemplate table 2024-05-03 03:12:49 -07:00
mbecker20 e286acb123 kind of implement server template frontend 2024-05-03 02:51:17 -07:00
mbecker20 ace6dd5a9a implement ServerTemplate api 2024-05-03 02:23:38 -07:00
mbecker20 de70ab5eda rework builder / server 2024-05-03 00:25:46 -07:00
mbecker20 56ab104ac5 frontend implement suggest for extra args 2024-05-02 00:42:43 -07:00
mbecker20 56da5c04f2 add refetch to a lot of important status 2024-05-02 00:14:17 -07:00
mbecker20 3d73f325fe ListCommonExtraArgs 2024-05-02 00:02:59 -07:00
mbecker20 7b778631f3 remove container user param 2024-05-01 23:41:33 -07:00
mbecker20 de4c657868 remove updated_at from ResourceToml 2024-05-01 23:22:19 -07:00
mbecker20 b6df4a08b1 improve export toml endpoint 2024-05-01 23:04:40 -07:00
mbecker20 923c1d6cf6 improve export styling 2024-05-01 22:23:16 -07:00
mbecker20 ff892afa16 add example toml for the configs 2024-04-28 23:24:26 -07:00
mbecker20 3e9e0a9be4 finish docs for config 2024-04-28 18:26:51 -07:00
mbecker20 dc796900cd document core config 2024-04-28 15:41:01 -07:00
mbecker20 b8afd43d07 move config entities to client for docs 2024-04-28 15:16:57 -07:00
mbecker20 ba52ce79fc description trigger placeholder 2024-04-28 14:57:57 -07:00
mbecker20 9936f9f357 fix process args placeholder 2024-04-28 14:56:40 -07:00
mbecker20 677e1a3830 move to sync mutex for action state 2024-04-28 14:44:48 -07:00
mbecker20 d46ff30540 doc 2024-04-28 13:47:47 -07:00
mbecker20 98453580c0 fmt. add procedure webhjook 2024-04-28 04:57:33 -07:00
mbecker20 5683929bbe monrun diffing 2024-04-28 04:33:27 -07:00
mbecker20 ca368340d5 diff resources in sync 2024-04-28 03:56:08 -07:00
mbecker20 e66d2fac95 implement all the diffs 2024-04-28 02:32:18 -07:00
mbecker20 1c74d388dc update partial_derive2 0.3.0 2024-04-28 02:26:09 -07:00
mbecker20 6e698fec05 prep for diffs 2024-04-28 02:22:57 -07:00
mbecker20 d06b2abea4 doccos 2024-04-28 01:04:36 -07:00
mbecker20 e7a4a364c2 rustdoc resource write api 2024-04-27 21:26:19 -07:00
mbecker20 31bcbf36dd finish documenting read api 2024-04-27 21:01:53 -07:00
mbecker20 afbf28668b fix fe 2024-04-27 20:17:27 -07:00
mbecker20 7427a6d6d1 trim down deployment api 2024-04-27 20:16:43 -07:00
mbecker20 fab4e8e534 improve the resource busy locks 2024-04-27 15:09:30 -07:00
mbecker20 0fc6e89ffe docs 2024-04-27 12:20:58 -07:00
mbecker20 9509b23dc1 doc more api 2024-04-27 02:20:09 -07:00
mbecker20 b5ea6d43f3 export to toml work. log scroll button better 2024-04-26 04:52:52 -07:00
mbecker20 92ef0addd5 ResourceToml 2024-04-26 03:09:17 -07:00
mbecker20 a61d50b049 update first 2024-04-26 02:27:29 -07:00
mbecker20 9021f1beea just install ca-certs 2024-04-26 02:23:41 -07:00
mbecker20 f3ecf30b3d install ssl in dockerfile 2024-04-26 02:19:18 -07:00
mbecker20 ea40073fcc mount 2024-04-26 01:55:04 -07:00
mbecker20 ed7e0a38d8 implement toml export 2024-04-26 01:51:45 -07:00
mbecker20 0d8d41f85d use debian:bullseye-slim for core 2024-04-26 01:51:36 -07:00
mbecker20 45bf8ae6b0 pb-12 2024-04-25 23:05:11 -07:00
mbecker20 db9c2d924c standard update text menu 2024-04-23 22:24:43 -07:00
mbecker20 62b34ab9a5 user group toml 2024-04-23 21:26:18 -07:00
mbecker20 85157ddfb9 block non existant resource hook 2024-04-23 21:26:09 -07:00
mbecker20 de746096ab user group sync 2024-04-22 23:38:38 -07:00
mbecker20 f272612e74 clean up logger init 2024-04-22 16:57:36 -07:00
mbecker20 d85bd25ed4 fix fe type 2024-04-22 01:47:54 -07:00
mbecker20 f39c786a64 mount SetUsersInUserGroup 2024-04-22 01:42:39 -07:00
mbecker20 26cae20505 Set user group users whole vec 2024-04-22 01:40:21 -07:00
mbecker20 58a8ebee0c push user to user group with $addToSet for idempotence 2024-04-22 01:23:31 -07:00
mbecker20 5c8adf031c better user group user modify and update permission api 2024-04-22 01:13:59 -07:00
mbecker20 9b168d35d6 works on docs 2024-04-21 23:56:30 -07:00
mbecker20 c4ece715f9 update frontend dockerfile 2024-04-21 23:03:20 -07:00
mbecker20 8e02de909f no use clietn 2024-04-21 23:02:13 -07:00
mbecker20 176b12f18c server stats chart 2024-04-21 19:22:22 -07:00
mbecker20 43514acc92 manage user groups 2024-04-21 17:37:31 -07:00
mbecker20 4894568651 ListApiKeysForServiceUser 2024-04-21 16:22:15 -07:00
mbecker20 f16d079e66 fix fe build 2024-04-21 15:08:33 -07:00
mbecker20 4ed71812a4 rename GetUsers to ListUsers for consistency 2024-04-21 15:01:58 -07:00
mbecker20 577b93e2dc able to query for UserGroup permissions 2024-04-21 14:36:32 -07:00
mbecker20 c676b5168a fmt. fix ws tracing span 2024-04-21 04:28:32 -07:00
mbecker20 4674af2f1b update list item fix username 2024-04-21 04:00:01 -07:00
mbecker20 2429ab050d fix query resource ids for non admin 2024-04-21 03:49:52 -07:00
mbecker20 8698c0f5be non admins able to get procedure updates 2024-04-21 03:22:03 -07:00
mbecker20 c583a5dc62 deployment actions show build if attached 2024-04-21 02:45:04 -07:00
mbecker20 528b74d156 only show actions when user has write or execute 2024-04-21 02:27:15 -07:00
mbecker20 ebedebd761 return write permission for all admin 2024-04-21 02:23:59 -07:00
mbecker20 ee2953b2e9 fix frontend errors 2024-04-21 02:21:49 -07:00
mbecker20 7a2044b395 pass through disabled 2024-04-21 02:07:31 -07:00
mbecker20 3123d021d9 GetPermissionLevel 2024-04-21 01:59:07 -07:00
mbecker20 c46b2cf59d add ListPermissions route 2024-04-21 01:46:01 -07:00
mbecker20 79b4bae40a fix query for user permission on resource 2024-04-21 00:59:30 -07:00
mbecker20 cb9e0ae252 sortable resource table 2024-04-21 00:18:47 -07:00
mbecker20 a7a7d0552b fix permissioning endpoint 2024-04-20 17:37:22 -07:00
mbecker20 01ea85e627 login flow nice 2024-04-20 15:29:12 -07:00
mbecker20 e81be79cb4 update aws deps 2024-04-20 13:06:24 -07:00
mbecker20 fca324480f update serror 2024-04-20 13:04:53 -07:00
mbecker20 762317e08a update deps 2024-04-18 14:57:25 -07:00
mbecker20 6d2f43e40a node 20.12 2024-04-18 04:49:23 -07:00
mbecker20 3823df8362 work on sortable tables 2024-04-18 04:42:32 -07:00
mbecker20 17398fc932 fix tags filter 2024-04-18 03:17:57 -07:00
mbecker20 9b0e96a59a design some improvements 2024-04-18 02:15:33 -07:00
mbecker20 c5fdb914ff refactor RequiredResourceComponents 2024-04-18 01:31:41 -07:00
mbecker20 65ae5d9465 fix ListUserPermissions 2024-04-17 22:58:10 -07:00
mbecker20 c4548f9e7e fix existing frontend for user permissions 2024-04-17 22:56:18 -07:00
mbecker20 86cfb2ebc7 update ts client 2024-04-17 18:25:46 -07:00
mbecker20 3bad049682 implement user group api 2024-04-17 18:19:52 -07:00
mbecker20 5352afee06 api check UserGroup for access 2024-04-17 02:23:41 -07:00
mbecker20 a83dedbcd0 work on docs 2024-04-17 01:30:35 -07:00
mbecker20 beee584cc2 rework / simplify procedure model 2024-04-15 23:17:55 -07:00
mbecker20 ccc7852576 prog on doc entity 2024-04-15 04:31:32 -07:00
mbecker20 0bdb3ddfea alert details use GetAlert 2024-04-15 03:56:54 -07:00
mbecker20 0f2b23bb6c prog on alert pages 2024-04-15 03:29:24 -07:00
mbecker20 a5537a0758 refactor server info 2024-04-14 02:27:47 -07:00
mbecker20 a64723269f update frontend with new stats api 2024-04-14 01:35:38 -07:00
mbecker20 2b93aa3dca protect system info 2024-04-14 01:27:48 -07:00
mbecker20 992054f943 rework / simplify server system stats 2024-04-14 01:22:06 -07:00
mbecker20 784aa754f7 disable stats monitoring on enabled server 2024-04-13 23:53:42 -07:00
mbecker20 5124d3aae8 log search work 2024-04-12 05:29:31 -07:00
mbecker20 0fb746bc03 fix log finally? maybe? 2024-04-12 05:00:17 -07:00
mbecker20 a2d301bfbc SearchLog allow for search Or / And the multiple terms 2024-04-12 04:36:10 -07:00
mbecker20 4523f3e112 writes shouldn't be debug 2024-04-12 03:54:31 -07:00
mbecker20 012cea8fce debug ExchangeForJwt 2024-04-12 03:48:48 -07:00
mbecker20 48a62232a7 skip user on the task instrumentation 2024-04-12 03:29:03 -07:00
mbecker20 46ee857c22 improve insturmentation / serror 2024-04-12 03:07:09 -07:00
mbecker20 7bbd22b4b2 mount the route periphery 2024-04-12 02:05:38 -07:00
mbecker20 862c5b7a7c further improve SearchLog 2024-04-12 01:05:23 -07:00
mbecker20 5eb7e27732 implement aws terminate instance with retry 2024-04-11 23:32:32 -07:00
mbecker20 1231193c89 search log 2024-04-11 22:45:36 -07:00
mbecker20 719938f442 tail docker logs search for some length safety 2024-04-11 22:33:01 -07:00
mbecker20 7b5f2ea69b update styling for light mode 2024-04-11 22:21:47 -07:00
mbecker20 01709deced stop all deployments adds update before finalize / update update 2024-04-11 22:18:27 -07:00
mbecker20 23c87bfaa4 frontend add labels config 2024-04-11 18:36:31 -07:00
mbecker20 0ef8e1861b ContainerSummary contains labels 2024-04-11 17:52:22 -07:00
mbecker20 10b278a141 implement docker tag support 2024-04-11 17:47:58 -07:00
mbecker20 72c884f9ec log elapsed at the end 2024-04-11 17:16:37 -07:00
mbecker20 ecdae0e8df add startup config log 2024-04-11 17:10:51 -07:00
mbecker20 2e67b16ba2 move logs outside of tokio spawn 2024-04-11 04:41:31 -07:00
mbecker20 18d31235d4 PeripheryRequest is debug 2024-04-11 03:55:06 -07:00
mbecker20 4f8c150c0b add deployment table to build page 2024-04-11 03:41:54 -07:00
mbecker20 f26c937747 finish instrumenting 2024-04-11 03:37:57 -07:00
mbecker20 34176c336e config examples 2024-04-11 02:49:10 -07:00
mbecker20 635f3678ef enable otlp exporting to url 2024-04-11 02:45:56 -07:00
mbecker20 12bd8cc265 skip user capture in tracing span 2024-04-11 01:22:02 -07:00
mbecker20 64316a8a61 update merge config files 2024-04-11 00:51:49 -07:00
mbecker20 b85c3b25f7 fix auth log spam 2024-04-11 00:43:01 -07:00
mbecker20 144344bcfc full env core configuration 2024-04-11 00:15:05 -07:00
mbecker20 7736ba8999 core and periphery both env overrides 2024-04-10 23:08:28 -07:00
mbecker20 90c7bd56bf version before repo 2024-04-10 22:53:10 -07:00
mbecker20 714edd70fb lame 2024-04-10 22:52:12 -07:00
mbecker20 9c31813a16 fix logger with global subscriber 2024-04-10 22:48:52 -07:00
mbecker20 d6b76134a3 update config examples 2024-04-10 22:24:40 -07:00
mbecker20 b7692e39c8 add log config to core and periphery config 2024-04-10 22:14:54 -07:00
mbecker20 60b5179b3e add json and loki logging options 2024-04-10 22:10:24 -07:00
mbecker20 46a1c86cb6 implement logger with loki support 2024-04-10 21:34:13 -07:00
mbecker20 393363b33e instrument most of core 2024-04-10 20:15:11 -07:00
mbecker20 e762363d96 add tracing to core client websocket subscription 2024-04-10 19:00:07 -07:00
mbecker20 5d2082b478 add tracing spans to periphery 2024-04-10 18:23:35 -07:00
mbecker20 ad09fd81b4 improve logging 2024-04-10 05:57:16 -07:00
mbecker20 54fdbf9fd0 one line logging 2024-04-10 04:13:41 -07:00
mbecker20 8c3de939da core serves the frontend 2024-04-10 01:39:26 -07:00
mbecker20 2f3a3f8f23 log on one line 2024-04-10 00:56:15 -07:00
mbecker20 daf13d693d imrpove tags config 2024-04-10 00:43:52 -07:00
mbecker20 bbd5384589 improve dashboard 2024-04-10 00:09:52 -07:00
mbecker20 fb72bbf81e fix short date formatting 2024-04-09 23:56:47 -07:00
mbecker20 29edd6b9b5 add set title hook to pages 2024-04-09 23:53:30 -07:00
mbecker20 215b35575d improve dark mode styling 2024-04-09 23:36:38 -07:00
mbecker20 6394275570 improve serverunreachable alerting behavior 2024-04-08 15:53:44 -07:00
mbecker20 eaca72991b alert logger 2024-04-08 05:47:39 -07:00
mbecker20 634e895469 fix default PORT 2024-04-08 02:19:25 -07:00
mbecker20 77c8033d22 only show redeploy on build if image type if Build 2024-04-08 01:43:26 -07:00
mbecker20 7508ae21b8 websocket show real connected 2024-04-08 01:13:56 -07:00
mbecker20 fd90a62af1 fix auto redeploy service user 2024-04-07 22:50:36 -07:00
mbecker20 707dd682ed override api host with build arg 2024-04-07 22:29:45 -07:00
mbecker20 7396988032 add frontend PORT override 2024-04-07 22:09:06 -07:00
mbecker20 58960cdc6e style 2024-04-07 21:59:42 -07:00
mbecker20 31a42dce7e new resource name reset on close 2024-04-07 21:58:13 -07:00
mbecker20 9b248a8fa4 clean up NewResource 2024-04-07 21:54:48 -07:00
mbecker20 f099e5c6b7 config alerter enabled 2024-04-07 21:26:25 -07:00
mbecker20 5c093c81ab enable / disable alerters backend 2024-04-07 21:23:51 -07:00
mbecker20 e73146533a GetCoreInfo give github webhook base url 2024-04-07 21:02:10 -07:00
mbecker20 db1237184f repo pull add latest commit hash log 2024-04-07 20:56:33 -07:00
mbecker20 1e5ed1d29e fix repo list item extra info 2024-04-07 17:37:19 -07:00
mbecker20 d68ea2c28f add repo info 2024-04-07 17:23:34 -07:00
mbecker20 9700ab2cb6 show / set description on resources 2024-04-07 16:53:24 -07:00
mbecker20 d8321c873e rename confirm button 2024-04-07 16:23:55 -07:00
mbecker20 44784487a0 consolidate Resource components 2024-04-07 16:08:23 -07:00
mbecker20 76471fa694 go back to resource type list on resource delete 2024-04-07 15:32:22 -07:00
mbecker20 6ec7b4305c add alerts to other pages 2024-04-07 06:10:33 -07:00
mbecker20 896a344ac7 add more resource 2024-04-07 05:35:57 -07:00
mbecker20 34cefcdaf6 frontend build work 2024-04-07 04:55:03 -07:00
mbecker20 4c8eb68611 add reason (err) server unreachable in alert 2024-04-07 04:52:58 -07:00
mbecker20 50866659ea overide signals 2024-04-07 04:09:42 -07:00
mbecker20 8262cb858e look into deployment term signal 2024-04-07 03:40:02 -07:00
mbecker20 e710317cac deployment log show ansi colors 2024-04-07 02:54:49 -07:00
mbecker20 138cf781f3 improve procedure update logs 2024-04-07 02:31:14 -07:00
mbecker20 70d315b2d7 remove nav actually 2024-04-07 02:13:55 -07:00
mbecker20 bca8ca52da navigate on table row click 2024-04-07 02:11:32 -07:00
mbecker20 42c769ed56 fix only show version if non none 2024-04-07 02:00:17 -07:00
mbecker20 592af39550 service user detection 2024-04-07 01:51:22 -07:00
mbecker20 79620030bc alert / update logger resources 2024-04-07 00:54:04 -07:00
kv 294bc8712b fix recents on safari 2024-04-06 19:41:39 -07:00
mbecker20 f75d30d8ce resource updates page open details 2024-04-06 05:41:26 -07:00
mbecker20 de81e1e790 backend resolve server alerts before deleting server 2024-04-06 04:32:10 -07:00
mbecker20 450f5c45a1 server stats 2024-04-06 04:23:28 -07:00
mbecker20 b864b32cb2 basic server stats 2024-04-06 03:58:40 -07:00
mbecker20 0d13ac8f38 start server stats 2024-04-06 03:31:17 -07:00
mbecker20 edd517e21c add git clone and pull 2024-04-06 03:17:19 -07:00
mbecker20 32d356600a fix server actions 2024-04-06 03:07:09 -07:00
mbecker20 9e290278d0 add Danger zone (delete + copy) 2024-04-06 02:56:41 -07:00
mbecker20 4140dcb9dc manage build version 2024-04-06 02:07:17 -07:00
mbecker20 1ed3d31011 alert details 2024-04-06 01:54:20 -07:00
mbecker20 3915f921f1 builder config 2024-04-04 22:17:00 -07:00
mbecker20 050571196c alerter 2024-04-04 22:03:24 -07:00
mbecker20 3c1a129ac9 repo config 2024-04-04 02:27:00 -07:00
mbecker20 57a3561aa8 finish build config 2024-04-04 01:45:10 -07:00
mbecker20 ec37bfc0c6 all server config 2024-04-03 01:30:05 -07:00
mbecker20 82387e95da periphery secrets in dep env working 2024-04-03 01:24:13 -07:00
mbecker20 36b018151d periphery secrets visible 2024-04-03 00:59:47 -07:00
mbecker20 b4754b64d5 improve PruneAll using docker system prune -a -f 2024-04-03 00:28:58 -07:00
mbecker20 3a5530a186 fix frontend 2024-04-01 04:31:17 -07:00
mbecker20 ee22044b34 add service user replacement in procedure 2024-04-01 04:18:19 -07:00
mbecker20 8f5570128d improve topbar navigation 2024-04-01 02:28:51 -07:00
mbecker20 24d2e744a4 actions 2024-03-31 23:20:13 -07:00
mbecker20 bc9877133c smth 2024-03-31 22:41:40 -07:00
mbecker20 05886bed71 fix update details too thin 2024-03-31 13:33:31 -07:00
mbecker20 c882c12890 fix combo box 2024-03-31 13:23:05 -07:00
mbecker20 12647896c4 update shadcn 2024-03-31 12:56:22 -07:00
mbecker20 ebbec7d68c clean up resource links 2024-03-31 12:24:47 -07:00
mbecker20 cf054395bb improve info 2024-03-31 12:04:36 -07:00
mbecker20 8c04cf3db2 finish standard fmt 2024-03-31 10:32:45 -07:00
mbecker20 516690b260 standardize coloring 2024-03-31 10:29:21 -07:00
mbecker20 fc602054ba add state 2024-03-31 09:39:16 -07:00
mbecker20 e2244429ce add repo to build list item 2024-03-30 18:56:24 -07:00
mbecker20 b60ac6e1be favicon and port 2024-03-30 18:37:14 -07:00
mbecker20 6999fb8c2a fix google scopes 2024-03-30 18:22:31 -07:00
mbecker20 fd84b5920b cleanup 2024-03-30 18:05:04 -07:00
mbecker20 d02df02bbe manage users 2024-03-30 18:04:45 -07:00
mbecker20 1f143b814e ListUserPermissions 2024-03-30 15:45:07 -07:00
mbecker20 65700ad70e login looking good 2024-03-30 14:42:20 -07:00
mbecker20 6a16f1344a oauth login should work 2024-03-30 14:37:55 -07:00
mbecker20 3ecacc69a5 update useUser 2024-03-30 04:45:10 -07:00
mbecker20 9db2e858d2 frotnend 2024-03-30 04:34:09 -07:00
mbecker20 f8cdf6bf45 update ts 2024-03-30 04:17:24 -07:00
mbecker20 1b97d85023 move GetUser to auth api. Doesn't require user enabled to call 2024-03-30 04:16:04 -07:00
mbecker20 74a5f429e9 resource query tags match on name or id, All or Any mode 2024-03-30 01:55:30 -07:00
mbecker20 7554055767 api sanitize update tags api with name->id replacement, tag auto create 2024-03-29 21:55:10 -07:00
mbecker20 f617f91d96 fix tags update validate 2024-03-29 21:05:53 -07:00
mbecker20 7b95e6b276 more logs use serror 2024-03-29 06:24:56 -07:00
mbecker20 1510ed2147 do stuff 2024-03-29 03:35:22 -07:00
mbecker20 f8237217b0 fix incorrect resource type 2024-03-29 03:19:05 -07:00
mbecker20 5f95fe16fd procedure working 2024-03-29 02:34:15 -07:00
mbecker20 cf8fd893fa getting there 2024-03-28 06:31:39 -07:00
mbecker20 1032629800 fix validate and prevent recurse a bit 2024-03-28 04:27:49 -07:00
mbecker20 12576711fa vailidate UpdateProcedure config 2024-03-28 04:18:37 -07:00
mbecker20 233c8085b5 work on procedure config 2024-03-28 04:13:53 -07:00
mbecker20 1b1650eba4 sync working clean 2024-03-27 06:35:57 -07:00
mbecker20 73c17c68c4 fix 2024-03-27 05:54:40 -07:00
mbecker20 3492eb3126 validate 2024-03-27 05:54:15 -07:00
mbecker20 6dcc4b30de validate attached resources by name / id 2024-03-27 05:33:34 -07:00
mbecker20 a5e15f9c5e fix the type change 2024-03-27 04:18:57 -07:00
mbecker20 851c1f450c update procedure 2024-03-27 03:59:51 -07:00
mbecker20 fd12921f6d refactor StateResource 2024-03-27 03:20:08 -07:00
mbecker20 1100e160de impl procedure monrun 2024-03-27 00:48:43 -07:00
mbecker20 bfcdf011c4 change default periphery port to 8120 2024-03-26 23:37:55 -07:00
mbecker20 48ff1ebefa monrun sync work 2024-03-26 04:42:24 -07:00
mbecker20 f1c9d05abc resources sync ready for test 2024-03-26 04:25:00 -07:00
mbecker20 73217c9178 change to tags. begin test monrun 2024-03-26 02:42:45 -07:00
mbecker20 7c5acb8c21 api for service users 2024-03-26 01:09:54 -07:00
mbecker20 10662ec679 use enum for UserConfig 2024-03-25 23:49:35 -07:00
mbecker20 65aba12dd0 permissions table 2024-03-25 23:19:08 -07:00
mbecker20 8d1410b181 more resource permissions to own table 2024-03-25 20:03:10 -07:00
mbecker20 d9fbccb644 guard local user empty username 2024-03-25 18:09:34 -07:00
mbecker20 c9334074f6 fix show updates 2024-03-25 16:34:15 -07:00
mbecker20 f4b77598c3 add updates page basic 2024-03-25 08:04:30 -07:00
mbecker20 478a046074 fix build cancel 2024-03-25 07:24:51 -07:00
mbecker20 c4cb1f9c66 update ts types 2024-03-25 06:54:32 -07:00
mbecker20 f84a3a9034 improve build cancel 2024-03-25 06:53:27 -07:00
mbecker20 fa389515cd dont show save if no change 2024-03-25 06:29:08 -07:00
mbecker20 06c480ef00 wrap table in section 2024-03-25 06:24:19 -07:00
mbecker20 71dd07ac28 add deployment table to server 2024-03-25 06:19:34 -07:00
mbecker20 30f15cf7e8 fix frontend build issue 2024-03-25 06:07:09 -07:00
mbecker20 cc99e91935 resource tags look better 2024-03-25 05:59:02 -07:00
mbecker20 6544c1887a implement redirect oauth 2024-03-25 05:11:01 -07:00
mbecker20 9636d4d347 build table tags 2024-03-25 04:49:21 -07:00
mbecker20 c7ce80c5e3 cancel build 2024-03-25 04:41:47 -07:00
mbecker20 5d0050535d all the dashboards 2024-03-25 04:00:37 -07:00
mbecker20 340d013bb3 deleting tags deletes across 2024-03-25 01:14:45 -07:00
mbecker20 f6c99c4c20 resource type dropdown handle keys and tree 2024-03-24 19:08:55 -07:00
mbecker20 7c6eecb0e9 manage api keys 2024-03-24 18:39:08 -07:00
mbecker20 59b09e580e api key page 2024-03-24 17:33:09 -07:00
mbecker20 4174eebffe TagsWithBadge 2024-03-24 16:07:52 -07:00
mbecker20 200aefac54 remove RequestUser 2024-03-24 15:44:48 -07:00
mbecker20 b200088093 monrun sync 2024-03-24 06:35:56 -07:00
mbecker20 8669ed8c73 need to add back async db 2024-03-24 05:14:52 -07:00
mbecker20 8a96724247 fix unused import 2024-03-24 04:40:30 -07:00
mbecker20 3a913fba44 fix frontend build 2024-03-24 04:35:18 -07:00
mbecker20 05a08d8640 run resource up 2024-03-24 04:26:45 -07:00
mbecker20 2bea16d003 parse resource files 2024-03-24 03:51:07 -07:00
mbecker20 475f0438f9 remove the await on tram_db call 2024-03-24 02:28:02 -07:00
mbecker20 c9720c15b9 extract periphery client lib from bin 2024-03-20 18:14:54 -07:00
mbecker20 0232fc1c2c update deps, remove db_client crate 2024-03-20 17:27:54 -07:00
mbecker20 9d5550bf5f default log level to logger::Level 2024-03-20 16:04:55 -07:00
mbecker20 873d9c2df6 remove more impl on State 2024-03-19 05:32:38 -07:00
mbecker20 5266b01e4c refactor core to use global &'statics 2024-03-19 05:18:42 -07:00
mbecker20 12315e90de clean up periphery 2024-03-19 02:12:04 -07:00
mbecker20 c32905cca4 support enable / disable components of sequence / parallel procedure 2024-01-21 15:43:46 -08:00
kv 71571e2625 update tags in deployment table 2024-01-17 00:45:19 -08:00
kv 25eb29946b update resource updates UI 2024-01-17 00:45:14 -08:00
kv bb98d3209d clear input when opening tags dropdown 2024-01-17 00:12:58 -08:00
kv 73f624d6a1 invalidate list tags when creating a tag 2024-01-17 00:09:52 -08:00
kv d1f8c130a1 fix tags behaviour 2024-01-17 00:01:24 -08:00
kv 93e16c4b7c tags ui 2024-01-16 11:40:52 -08:00
mbecker20 ee91d1d83e add use public id to build log 2024-01-16 03:33:35 -08:00
mbecker20 46d408056c add use_public_ip under builder config 2024-01-16 03:30:39 -08:00
mbecker20 d8ff64c1d9 difference between assign and use public ip 2024-01-16 03:25:57 -08:00
kv 453600a70c init tags 2024-01-16 02:43:59 -08:00
kv 597e466d84 update ts types 2024-01-16 02:43:56 -08:00
kv 65164bd8ef update ui components 2024-01-16 02:43:49 -08:00
kv dbf589da91 update tags api 2024-01-16 02:43:41 -08:00
mbecker20 0857cbfa92 update serror and rustc build version 2024-01-16 02:20:40 -08:00
mbecker20 225edce50a update serror to 0.1.6 2024-01-15 12:09:15 -08:00
mbecker20 5893fdefaf remove line from log 2024-01-15 01:57:11 -08:00
mbecker20 2b4ebd6e10 implement tracing 2024-01-15 01:29:26 -08:00
mbecker20 f97e48e886 comment out recently viewed 2024-01-15 01:03:13 -08:00
mbecker20 b788bc6b6a comment out recently viewed 2024-01-15 01:00:15 -08:00
mbecker20 8c91b01dd9 fix up the tables 2024-01-15 00:56:40 -08:00
kv 30b6dac7dd improve Icon 2024-01-14 14:09:00 -08:00
kv b6ec89e4aa merge kv changes 2024-01-14 13:57:37 -08:00
mbecker20 39a0fcb358 update deps and fix react query mutate isLoading -> isPending 2024-01-14 13:36:22 -08:00
mbecker20 9d353806a3 make some prog on the row data 2024-01-14 06:28:18 -08:00
mbecker20 d24f8d5c7c add deployment filtering by build id 2024-01-14 05:43:01 -08:00
mbecker20 cd7d51a16b update derive_default_builder 2024-01-14 04:30:10 -08:00
mbecker20 d438fbba49 builders for the resource queries 2024-01-14 03:19:12 -08:00
mbecker20 d73c71e18b fix typeshare error 2024-01-13 18:10:14 -08:00
kv 3f4d1983e1 improve some udpates stuff 2024-01-13 17:23:18 -08:00
kv 1c87ac0546 various 2024-01-13 17:16:51 -08:00
mbecker20 a11aa5c751 add created_at to ListItem 2024-01-13 17:16:29 -08:00
kv 5cae0b99c7 i fixed everything 2024-01-13 14:25:16 -08:00
mbecker20 58d0b6b458 implement new list api 2024-01-13 14:05:27 -08:00
mbecker20 4815d225d7 AddFilters for all resource queries 2024-01-13 13:51:04 -08:00
kv fd487350f5 update ws login message 2024-01-13 13:42:26 -08:00
kv dafcf22d49 fix ts types 2024-01-13 13:40:17 -08:00
kv 78cefe19bd update ts client 2024-01-13 13:40:14 -08:00
mbecker20 bbca105077 implement ResourceQuery 2024-01-13 13:10:33 -08:00
mbecker20 e3e4278206 sanitize the api keys 2024-01-13 01:12:58 -08:00
mbecker20 e479e62cce implement ListApiKeys 2024-01-13 00:44:05 -08:00
mbecker20 8f73b08fbf update logger 2024-01-12 23:50:38 -08:00
mbecker20 5d02a8874f client ws again 2024-01-12 23:49:50 -08:00
mbecker20 2ec5789d71 ws login support api keys 2024-01-12 23:40:47 -08:00
mbecker20 1c7a159e40 periphery use AppError 2024-01-08 03:13:10 -08:00
mbecker20 81a4caf23c update auth 2024-01-08 03:07:27 -08:00
kv 9065b6034b child procedures 2024-01-07 18:55:10 -08:00
kv 49104922c1 fix procedure selectors 2024-01-07 18:49:46 -08:00
kv 409e064452 start permissions UI etc 2024-01-07 18:28:56 -08:00
mbecker20 61f7efaa85 inserted_id as_object_id first before to_string 2024-01-07 15:57:34 -08:00
mbecker20 f0baa7496f stop log if fail to record server stats 2024-01-07 15:23:53 -08:00
mbecker20 7ed115cddb CRUD for procedures 2024-01-07 14:54:57 -08:00
mbecker20 cb6dc29469 get procedure action state 2024-01-07 14:48:20 -08:00
mbecker20 44032e2a9e Execution type None 2024-01-07 14:44:09 -08:00
mbecker20 7390a9421d gen types 2024-01-07 14:19:14 -08:00
mbecker20 f1cbe6fba9 replace ProcedureConfigVariant 2024-01-07 14:18:54 -08:00
mbecker20 5925cf5cc9 fix runfile 2024-01-07 14:17:49 -08:00
mbecker20 219a914cb1 update ts client 2024-01-07 14:15:48 -08:00
kv 6b6d6337c8 fix action type 2024-01-07 14:09:34 -08:00
kv c511ae09a1 add stashed frontend updates 2024-01-07 13:50:05 -08:00
mbecker20 e658cb3aa0 procedure execution api 2024-01-07 01:12:50 -08:00
mbecker20 9f4cf475b6 move types into client 2024-01-06 19:44:29 -08:00
mbecker20 af53cbebed update axum to 0.7 2024-01-06 18:56:52 -08:00
mbecker20 1895ebcf25 update to sysinfo 0.30 2024-01-06 17:26:44 -08:00
mbecker20 02a313d70b move back to /frontend 2024-01-06 15:20:29 -08:00
mbecker20 57ed905140 rename frontend-v2 to frontend 2024-01-06 15:16:24 -08:00
mbecker20 d46741c5ae stricly handle unknown instance type 2024-01-06 15:15:08 -08:00
mbecker20 ba3692e085 update to mungos 0.5 2024-01-06 14:24:03 -08:00
mbecker20 593cad65dd update runfile 2023-11-26 12:44:58 -05:00
mbecker20 8bd05144da update api name 2023-11-03 01:16:56 -04:00
mbecker20 899ebcd681 update default monitor api url 2023-11-03 00:52:28 -04:00
mbecker20 f4917762c0 migrator finished 2023-10-23 02:20:53 -04:00
mbecker20 88c35281bf reformat to 2 spaces for tab 2023-10-23 01:46:29 -04:00
mbecker20 7e1810a0a7 implement conversion from legacy -> next for build, deployment, server 2023-10-22 14:44:38 -04:00
mbecker20 7eb62de74b control type of alerts that are enabled 2023-10-15 02:50:23 -04:00
mbecker20 69bb7ef1fd stop all containers on server action 2023-10-09 21:57:11 -04:00
mbecker20 57f0404a02 option to disable send_alerts on various servers + deployments 2023-10-09 21:14:50 -04:00
mbecker20 d8dccda1c0 update GetAvailableAccounts method name 2023-10-09 08:40:01 -04:00
mbecker20 16f3c4190d frontend use correct call for GetDockerNetworks 2023-10-09 00:41:02 -04:00
mbecker20 823878e963 add Read, Write, Execute traits on requests 2023-10-08 16:00:58 -04:00
mbecker20 f4246e3e0a terminate aws instance if fail to connect 2023-09-30 20:28:33 +03:00
mbecker20 9a8730a832 improve start aws builder log 2023-09-30 16:33:22 +03:00
kv 31d01aef11 header updates and resource updates show status better 2023-09-16 15:00:50 -07:00
mbecker20 a61da39038 fix build auto increment 2023-09-16 16:12:48 -04:00
kv 7b9def2aeb more permissions stuff 2023-09-15 13:46:44 -07:00
mbecker20 92b8cc9f6b tests cargo toml 2023-09-15 03:09:11 -04:00
kv d3d5f7a745 invalidate update details on new update msg wiht same id 2023-09-14 12:27:31 -07:00
mbecker20 be78e4c48a add get users route (admin only) 2023-09-14 04:19:44 -04:00
karamvir be9937355b hide permissions when only admins 2023-09-12 22:28:10 -07:00
karamvir f1d36f0f4a fix version selector etc 2023-09-12 16:22:29 -07:00
kv 2a3f223a2b init repo config 2023-09-12 14:24:38 -07:00
karamvir 21401c544d add resource permissions 2023-09-12 13:14:09 -07:00
karamvir 6ceb7404bb slight signup 2023-09-11 23:01:34 -07:00
karamvir a60d2e7f0f fix for build 2023-09-11 16:37:08 -07:00
karamvir 4480eff46d fix resources page responsive 2023-09-11 09:45:35 -07:00
karamvir b265bd68b3 responsive header hotfix 2023-09-11 09:44:52 -07:00
karamvir 9e47eb3470 update header dropdowns 2023-09-11 09:34:51 -07:00
karamvir a050f419e4 cleanup 2023-09-11 00:36:50 -07:00
kv d1373aa5b3 add resource icon to dropdown 2023-09-11 00:28:43 -07:00
kv c53704849a fix resource page header 2023-09-11 00:04:39 -07:00
kv e27a25b147 cleanup type imports 2023-09-10 23:41:47 -07:00
karamvir b1a88a9c2d add row mode to resources page 2023-09-10 22:23:53 -07:00
karamvir 780fd0490e improve styles, add notif dot for unseen updates 2023-09-09 14:11:11 -07:00
karamvir b3d0ee7080 add home option to omnibar 2023-09-09 13:27:57 -07:00
karamvir 6d2287c4e5 force scrollbar, add header updates etc 2023-09-09 13:22:13 -07:00
karamvir 8ae8c43c3c cleanup 2023-09-09 04:39:27 -07:00
karamvir 2a18321225 redesign header slight 2023-09-09 04:14:28 -07:00
karamvir 2f67e4de94 improve invalidations etc 2023-09-09 03:50:51 -07:00
karamvir 84c6af1ee9 more cleanup and refactor 2023-09-09 03:46:32 -07:00
karamvir caae39b2de cleanup 2023-09-09 03:25:51 -07:00
karamvir 2af30856ba refactor resources etc 2023-09-09 03:24:55 -07:00
karamvir a85e842911 plenty more configs etc 2023-09-09 03:16:37 -07:00
karamvir 2f9805e97b refactor alerter 2023-09-09 03:09:06 -07:00
karamvir 2c66b9ecc5 new resource, slight cleanup etc 2023-09-09 02:30:09 -07:00
karamvir 35e5f8928a make charts clickable 2023-09-09 01:48:02 -07:00
mbecker20 4c647db584 refetch list deployments 2023-09-09 04:29:53 -04:00
karamvir e523ea0cc3 lockfile 2023-09-09 01:18:55 -07:00
karamvir 404343e5e7 fix copy 2023-09-09 01:00:46 -07:00
karamvir 27f5353ee6 init frontend v2 2023-09-09 00:47:46 -07:00
karamvir 848fd4d4c8 fix node version 2023-09-07 04:44:16 -07:00
karamvir 2f58bf2dd1 fix button intent 2023-09-07 04:33:13 -07:00
karamvir dbe32056d0 update some components, maybe fix invalidations? 2023-09-07 04:27:56 -07:00
karamvir c88e210136 init new shadcn cli 2023-09-06 20:36:18 -07:00
mbecker20 c002ba2f00 update deps 2023-09-05 03:44:01 -04:00
mbecker20 a8c076f2c9 fmt 2023-09-04 02:57:46 -04:00
mbecker20 99da264f31 update api host 2023-09-02 16:37:18 -04:00
mbecker20 6c862be9b9 fix update server 2023-08-29 23:56:10 -04:00
mbecker20 6c75d64b5d fix copy build / deployment to enforce to_monitor_name 2023-08-29 23:51:02 -04:00
mbecker20 bf59390f68 slow reconnect 2023-08-29 23:46:59 -04:00
karamvir 406cfaefef cleanup 2023-08-29 20:07:27 -07:00
karamvir 1a1919f38c fuck me 2023-08-29 20:04:28 -07:00
mbecker20 3d99c300a4 rename server 2023-08-29 22:50:55 -04:00
karamvir 1a63be7746 rename server, deployment 2023-08-29 19:31:58 -07:00
mbecker20 ae7e50396e fix config parse error 2023-08-29 22:30:33 -04:00
mbecker20 f7ffb41dea server name on DeploymentStateChange 2023-08-29 02:27:53 -04:00
mbecker20 1e6987a229 send message on connect 2023-08-29 02:09:00 -04:00
mbecker20 d0bc5caebf wrap env values in quotes 2023-08-29 02:06:48 -04:00
mbecker20 59c7481fd7 add update logger dockerfile 2023-08-29 01:59:05 -04:00
mbecker20 70a11de316 monitor client update ws subscription with reconnect 2023-08-29 01:57:42 -04:00
mbecker20 3da88135c2 prune state and alerts based on config 2023-08-28 02:33:27 -04:00
mbecker20 d2a706fd9d properly forward periphery trace into core error trace 2023-08-27 13:59:59 -04:00
mbecker20 8f57af0667 uniform alert ts 2023-08-27 02:40:27 -04:00
mbecker20 cb46029f35 dont call create many on empty alerts 2023-08-27 02:24:24 -04:00
mbecker20 e35c63ff5d alert logger needs 1.71.1 2023-08-27 02:21:16 -04:00
mbecker20 440d54f500 use rust 1.71.1 to build 2023-08-27 06:13:35 +00:00
mbecker20 f4a78ef397 workspace resolver 2 2023-08-27 06:02:37 +00:00
mbecker20 96831e5973 update list alert api to take mongo document query 2023-08-27 01:54:55 -04:00
mbecker20 c5f9cc104c create deployment status alerts on db 2023-08-27 01:45:53 -04:00
mbecker20 dc6c391798 implement serror for core and periphery errors 2023-08-27 01:36:19 -04:00
mbecker20 80e5879c43 stop requests to disabled periphery 2023-08-27 01:07:59 -04:00
mbecker20 d9864f31c1 GetCoreInfo api 2023-08-22 00:08:08 -04:00
mbecker20 75b0d25d3e fix include resolved 2023-08-21 03:41:29 -04:00
mbecker20 625de2e38e resolved shoudl be tru 2023-08-21 03:40:40 -04:00
mbecker20 0f7d64350f alert logger 7000. fix alert open when empty 2023-08-21 03:21:42 -04:00
mbecker20 63620f246b update startup log 2023-08-21 03:17:40 -04:00
mbecker20 f416d628ae alert logger 2023-08-21 02:55:13 -04:00
mbecker20 24bf642dfa fix ts 2023-08-21 02:42:33 -04:00
mbecker20 8a0321bf67 alerting should work 2023-08-21 02:40:52 -04:00
mbecker20 5e033e1f76 handle cpu and mem alerts 2023-08-20 22:41:49 -04:00
mbecker20 ec47fa42b8 ListAlerts api 2023-08-20 17:16:20 -04:00
mbecker20 080ecf9fd9 prog on monitoring 2023-08-20 04:29:55 -04:00
mbecker20 f0deda32dc prog on alerting 2023-08-20 03:55:30 -04:00
mbecker20 7b3298e9e3 update aws sdk 2023-08-18 18:06:58 +00:00
mbecker20 f26661459d format periphery request error 2023-08-18 18:04:53 +00:00
mbecker20 b4c0047d45 prog on alerting 2023-08-18 03:12:18 -04:00
karamvir 4e03cf9675 add refresh to logs 2023-08-17 21:03:33 -07:00
karamvir 33eb5fc020 reduce config button gap 2023-08-17 21:03:29 -07:00
karamvir 01d62a6a15 shrink omniar on mobile 2023-08-17 21:03:06 -07:00
karamvir bae22e0104 update favicon 2023-08-17 21:02:40 -07:00
karamvir 33b49d46ce add standalone display mode 2023-08-17 20:38:48 -07:00
karamvir c6beda1b8f add pwa icons 2023-08-17 20:38:08 -07:00
karamvir 8faba6ec5b init manifest 2023-08-17 19:47:41 -07:00
karamvir e2824da846 update builder config 2023-08-17 19:00:12 -07:00
karamvir b4dcadbf17 add docker accounts and github accounts to builder config 2023-08-17 18:18:30 -07:00
karamvir e96e1ebb9c update configs 2023-08-17 17:38:49 -07:00
karamvir b59158d314 split server warnigs by type 2023-08-17 17:24:19 -07:00
karamvir 82588a0f83 split configs into cards within each tab 2023-08-17 17:17:04 -07:00
karamvir c6c91b6b11 add keys to auto config components 2023-08-17 16:59:29 -07:00
karamvir a8918156f6 cleanup some styles etc 2023-08-17 16:52:06 -07:00
karamvir df56123ceb upgrade server stats to shadcn progress 2023-08-17 16:34:11 -07:00
karamvir 76d331ef03 re-cast to any to ensure build 2023-08-17 15:38:02 -07:00
karamvir 71524b2073 cleanup 2023-08-17 15:34:51 -07:00
karamvir f286efb174 ensure proper number handling by configinput 2023-08-17 15:34:44 -07:00
karamvir feea9384c1 add security group ids 2023-08-17 15:31:02 -07:00
karamvir 7dfcc9a3b6 update to consts 2023-08-17 15:05:47 -07:00
karamvir 43efd8e60b remove any cast 2023-08-17 15:05:36 -07:00
karamvir 28fce80476 upgrade env component 2023-08-17 15:05:26 -07:00
karamvir cf4c4664e1 rm console log 2023-08-17 15:05:19 -07:00
karamvir 9a8b2a308d integrate action state to deployment 2023-08-17 15:04:59 -07:00
karamvir 188a70de21 rm conditonal hook call, make consistent 2023-08-17 15:04:43 -07:00
karamvir 5fccd1064f improve types 2023-08-17 15:03:58 -07:00
karamvir 9f58943dfc improve types, remove any cast 2023-08-17 15:03:43 -07:00
karamvir 9111f8b8c7 provide loader for deployment actions 2023-08-17 15:03:28 -07:00
karamvir 89dba2dbc0 improve recently viewed to avoid calling conditionally 2023-08-17 15:03:12 -07:00
karamvir 0b79f125ed fix confirm button onclick behaviour 2023-08-17 10:41:07 -07:00
karamvir 59bc429ce5 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-17 10:36:05 -07:00
karamvir 46db31c695 upgrade environment to textarea 2023-08-17 10:36:02 -07:00
karamvir c45a617a90 pass classname to config items 2023-08-17 10:34:43 -07:00
karamvir 2982ac051f cleanup main 2023-08-17 10:34:25 -07:00
karamvir a6b1b385e5 always toast write and exec fails 2023-08-17 10:33:53 -07:00
karamvir 3f8c62c1a4 defer invalidation by 100ms 2023-08-17 10:33:08 -07:00
mbecker20 e31443b285 1.0.1 2023-08-17 01:36:11 -04:00
mbecker20 10beeab4b5 progress on alerting 2023-08-16 23:31:23 -04:00
karamvir a5fdfc3b67 add log tails 2023-08-16 09:30:11 -07:00
karamvir 73d0e2589b add process args and extra args 2023-08-16 08:45:15 -07:00
karamvir 2c6d2a30d7 add volumes, fix double inputs, etc 2023-08-16 08:34:03 -07:00
karamvir d0566bf336 add placeholders to doubl input, fix ports congi 2023-08-16 08:14:42 -07:00
karamvir e93e00f4f7 fix restart and network 2023-08-16 07:55:44 -07:00
karamvir 720252f6ae add keys to tabs 2023-08-16 07:55:37 -07:00
karamvir 43ee9b05a3 fix restart mode selector values, and format names 2023-08-16 07:49:36 -07:00
karamvir bcfb847412 rm old config stuff 2023-08-15 15:17:00 -07:00
karamvir 402a7a730d Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-15 14:26:20 -07:00
karamvir 5e975a6acd major cleanup and refactor 2023-08-15 14:26:15 -07:00
mbecker20 f1639126df work on alerting logic 2023-08-13 01:54:14 -04:00
karamvir 7aa048a512 refactor accounts selector 2023-08-12 16:39:36 -07:00
karamvir 76b958ed89 add accounts selectors for github and docker 2023-08-12 16:37:39 -07:00
karamvir d0c8e0bb49 modify to generic account selector, and enable docker accounts 2023-08-12 16:16:25 -07:00
karamvir 7f1df2d24c add github account selector 2023-08-12 16:12:12 -07:00
mbecker20 7331fdc083 move some files around 2023-08-12 01:59:57 -04:00
karamvir 980d675572 cast to any to prevent awful recursive type 2023-08-11 18:08:53 -07:00
karamvir 749df9e554 add image type selector for depl config 2023-08-11 18:06:13 -07:00
karamvir 7bb97dadf4 move config etc 2023-08-11 01:38:56 -07:00
karamvir a223357ef1 more cleanup 2023-08-11 01:36:34 -07:00
karamvir 9ef1e3cd9b cleanup build config 2023-08-11 01:28:54 -07:00
karamvir 1459b05189 init cleanup build config 2023-08-11 01:21:15 -07:00
karamvir 2c992a0d23 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-11 01:11:25 -07:00
karamvir f6a712c17b generic resource selector, what a revelation 2023-08-11 01:11:23 -07:00
mbecker20 97c03d97ad implement get builder avaialabel accounts 2023-08-11 03:54:35 -04:00
mbecker20 b80589ea63 get available accounts 2023-08-11 03:49:35 -04:00
mbecker20 4fd3e606fd LaunchServer 2023-08-11 03:17:58 -04:00
karamvir d1ae925454 give action buttons 150px 2023-08-10 23:36:01 -07:00
karamvir 3e260e4464 delete server 2023-08-10 23:35:21 -07:00
karamvir b7d841be14 decent build config 2023-08-10 23:31:27 -07:00
karamvir 854ce07e8f add username to toast 2023-08-10 23:10:59 -07:00
mbecker20 5e572ce5ef clean up ws correctly 2023-08-11 02:09:47 -04:00
karamvir bec8b3ce0f cleanup for build 2023-08-10 22:46:00 -07:00
karamvir df302015a3 migrate deployment configs 2023-08-10 22:17:28 -07:00
karamvir fd9c7848c2 fix defaults on variant configs 2023-08-10 21:23:49 -07:00
karamvir 3c6a2e2546 config.... again? 2023-08-10 21:13:55 -07:00
karamvir af7229acb0 updates 2023-08-10 20:30:54 -07:00
karamvir e1fbbe7c4e updatse 2023-08-10 18:14:41 -07:00
karamvir cdc7827b7e config..... again? 2023-08-10 16:43:39 -07:00
mbecker20 f24005e02e generic list item 2023-08-10 02:27:36 -04:00
mbecker20 0ff9a7e50d load migrator config 2023-08-10 01:46:07 -04:00
mbecker20 3ec810ed82 start migrator 2023-08-10 01:37:14 -04:00
karamvir 93fc8f7452 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-09 21:36:42 -07:00
karamvir 172b3daeab init some manual configs 2023-08-09 21:36:39 -07:00
karamvir 1b3e3b19cf rm unnecessary console log 2023-08-09 17:09:54 -07:00
mbecker20 29b31ffa1e create / delete docker network on server api 2023-08-09 01:48:29 -04:00
karamvir f1aca3df4d add icons to create resource dropdown 2023-08-08 03:01:29 -07:00
karamvir 180ac60d21 add keydown listner to omnibar 2023-08-08 02:56:15 -07:00
karamvir b22621e3c4 cleanup 2023-08-08 02:39:53 -07:00
karamvir db1d61f1e3 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-08 02:33:05 -07:00
karamvir 8e0c5b3792 update serach button 2023-08-08 02:32:58 -07:00
karamvir 1b30a8edd4 add theme toggle to header 2023-08-08 02:32:41 -07:00
mbecker20 fe1376a70c SetLastSeenUpdate 2023-08-08 03:52:50 -04:00
karamvir 2dbce8f695 update login page 2023-08-07 00:10:14 -07:00
karamvir b1700f3d14 update login flow 2023-08-07 00:10:02 -07:00
karamvir 63e3e0b9d3 rm old useuser hooks 2023-08-07 00:09:54 -07:00
karamvir cdd55ee61d disble builder config for the moment 2023-08-07 00:08:06 -07:00
karamvir c72050d329 add new builder selection config 2023-08-06 22:23:12 -07:00
mbecker20 9a89b82f83 builder tyeps 2023-08-07 01:10:29 -04:00
mbecker20 37becd9327 make server a builder type 2023-08-07 01:06:20 -04:00
karamvir d5f2ae7871 cleanup 2023-08-06 21:22:30 -07:00
karamvir 08b74b5304 fix security group ids override 2023-08-06 21:21:28 -07:00
karamvir 6183dbf653 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-06 21:19:46 -07:00
karamvir 939f3db719 always pass type for builder config 2023-08-06 21:19:44 -07:00
karamvir d3e2ee2974 ensure confirm update closes on click 2023-08-06 21:19:27 -07:00
mbecker20 0b6d955672 client 2023-08-06 23:17:15 -04:00
mbecker20 6884453b0f GetAvailableAccounts / GetAvailableNetworks 2023-08-06 23:13:00 -04:00
mbecker20 0d37600cda send UpdateListItem over ws 2023-08-06 22:57:05 -04:00
mbecker20 c32211aa7a Merge branch 'feat/Resource' into next 2023-08-06 21:40:42 -04:00
mbecker20 e0776952ee use generic Resource struct 2023-08-06 21:40:29 -04:00
karamvir d92e4a014a improve new resource dialog 2023-08-06 15:10:34 -07:00
karamvir 48fc51a89a add label to builder type selector 2023-08-06 14:47:37 -07:00
karamvir 0af29e146d fix styles 2023-08-06 14:46:03 -07:00
karamvir 4467c64edd builder config is mostly go 2023-08-06 14:44:18 -07:00
karamvir 1314d7744f init builder config 2023-08-06 14:29:26 -07:00
karamvir 9a984e7e41 fix builder configs 2023-08-06 13:52:56 -07:00
karamvir 89e4010566 builder config 2023-08-06 13:44:03 -07:00
karamvir 1ecfc56f19 init builder config 2023-08-06 13:32:38 -07:00
karamvir 579c6eaf77 add volumes config 2023-08-06 13:32:28 -07:00
karamvir 4cecfd1611 improve button text for clarity 2023-08-06 11:24:05 -07:00
karamvir 5a9899c63d add ports config override 2023-08-06 11:23:00 -07:00
karamvir a1d3c46127 improve state mapping for deployment status icon 2023-08-06 11:19:09 -07:00
karamvir 902f3f7d00 hide logs when deployment not deployed 2023-08-06 10:56:10 -07:00
karamvir 8ef32c50c2 cleanup for build 2023-08-06 10:49:31 -07:00
karamvir e0d0512777 add image config, confirm update, etc 2023-08-06 10:48:52 -07:00
karamvir 9c73ffd7da fix dom nesting err 2023-08-06 10:48:39 -07:00
karamvir 0e2ca7c0d9 cleanup, add keyed fragment 2023-08-06 10:48:29 -07:00
karamvir 2d8f5ef337 add confirm update dialog 2023-08-06 10:48:09 -07:00
karamvir a02e9b8318 deployment image config override component 2023-08-06 01:37:26 -07:00
karamvir 4f844f673f init alerter config 2023-08-06 01:20:07 -07:00
karamvir 1a068ed82b add image selector stuff 2023-08-06 01:20:02 -07:00
karamvir b8df4caf75 fix resource updates 2023-08-06 01:19:32 -07:00
karamvir 11da11c326 fix deployment action button show states 2023-08-05 23:22:56 -07:00
karamvir 3e41a5568f fix omnibar types 2023-08-05 23:14:39 -07:00
karamvir 3f73ee6cff cleanup 2023-08-05 23:14:15 -07:00
karamvir 866137e128 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-05 23:13:35 -07:00
karamvir 66b0ac7d51 add build args and extra args 2023-08-05 23:13:34 -07:00
mbecker20 7ac7edb437 to monitor name create 2023-08-06 01:54:52 -04:00
mbecker20 82ed74429c fix delete resource also remove from user recently viewed 2023-08-06 01:49:12 -04:00
karamvir 417dc7dac1 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-08-05 22:45:19 -07:00
karamvir 999ce044cb add ui for extra args override 2023-08-05 22:45:17 -07:00
mbecker20 8759508c86 update resource flatten set 2023-08-06 01:25:45 -04:00
karamvir 56a40edc66 add alerter and repo cards to recently viewed 2023-08-05 22:17:06 -07:00
karamvir 5021c77e53 ensure all resource cards are equal height 2023-08-05 22:16:34 -07:00
karamvir 3fbe859c28 fix types for resources 2023-08-05 22:14:41 -07:00
karamvir 560e94a232 update builder card 2023-08-05 22:14:29 -07:00
karamvir d982ccd4d1 update alerter card 2023-08-05 22:14:21 -07:00
karamvir 45b38c23a5 add resource updates to build page 2023-08-05 22:06:40 -07:00
karamvir 442180014d fix missing fields in delpoyment card 2023-08-05 22:04:37 -07:00
karamvir 867a968b5b add proper server region to card 2023-08-05 22:00:51 -07:00
karamvir 334529ab67 improve build config 2023-08-05 21:58:24 -07:00
mbecker20 c66fc2d6de add fields to list resources 2023-08-04 20:43:57 -04:00
mbecker20 5eea95d118 update description api 2023-08-04 04:24:31 -04:00
karamvir 804a215190 server config 2023-08-04 00:29:22 -07:00
karamvir da6dfc9bcc refactor new config component, apply to server 2023-08-04 00:20:11 -07:00
karamvir 0d4ec308d6 flex responsive for last 3 dashboard resource cards 2023-08-04 00:05:43 -07:00
karamvir 24437e7b10 new config who dis 2023-08-04 00:00:15 -07:00
karamvir 1abaff6625 cleanup for build 2023-08-03 21:57:21 -07:00
karamvir 0c1692866c add operator username to update card 2023-08-03 21:53:25 -07:00
karamvir d28e9bb51c init new config layout 2023-08-03 21:51:40 -07:00
karamvir 760c5d521b refactor config 2023-08-03 21:48:37 -07:00
karamvir 6ce14ffc80 place servers first on dashboard 2023-08-03 17:22:51 -07:00
karamvir cc54725011 add new repo, complete resource adding options 2023-08-03 17:04:54 -07:00
karamvir c1a6f2b957 fix alerter and repo routing, alerter card link 2023-08-03 17:01:17 -07:00
karamvir 5d5578f89f add all resources to omnibar 2023-08-03 16:56:32 -07:00
karamvir e7bb58397e cleanup for build 2023-08-03 16:19:02 -07:00
karamvir 04690f1949 add clickable escape context to confirm button 2023-08-03 16:18:28 -07:00
karamvir 4df449c724 add confirm button, update actions 2023-08-03 16:10:27 -07:00
karamvir 315be1b61d update config pages 2023-08-03 15:58:42 -07:00
karamvir 4c30fb09ab fix new resource buttons 2023-08-03 15:26:00 -07:00
karamvir 2a24d1d9b7 only display update operation 2023-08-03 15:18:19 -07:00
karamvir fe771928ee cleanup message registrars 2023-08-03 15:16:04 -07:00
karamvir 768baccea7 refactor create resource button 2023-08-03 15:04:21 -07:00
karamvir c5a6f06c35 cleanup for build 2023-08-03 14:57:32 -07:00
karamvir e57a90ca01 improve dashboard, overview cards etc 2023-08-03 14:56:11 -07:00
karamvir 9d95750092 improve layout standardisation, dashboard 2023-08-03 10:50:52 -07:00
karamvir 3c5a218152 refactor add to recently viewed to own hook 2023-08-03 09:20:59 -07:00
karamvir cf6148eca5 upgrade to new resource component 2023-08-03 00:25:03 -07:00
karamvir df1945fb38 cleanup files and imports 2023-08-03 00:10:51 -07:00
karamvir ed93efd1bd add script to regen client 2023-08-02 23:55:44 -07:00
karamvir 2d48073262 init alerters 2023-08-02 23:52:25 -07:00
karamvir 8bba27361f fix responsive 2023-08-02 23:41:31 -07:00
karamvir de54794506 make server card info text sm 2023-08-02 23:38:29 -07:00
karamvir 0aee4d8466 refactor updates to use new apis 2023-08-02 23:35:42 -07:00
karamvir 1d8e0c7b1d add update username 2023-08-02 23:32:13 -07:00
karamvir 6fbec9fbd7 add event listeners in a useeffect 2023-08-02 23:28:04 -07:00
mbecker20 6d20457f0e GetUpdate 2023-08-03 02:09:34 -04:00
karamvir 73dc18fb78 fix recents hooks 2023-08-02 22:37:34 -07:00
mbecker20 440284663a fix update list nonsense 2023-08-03 01:16:46 -04:00
mbecker20 796793eefd get username and list updates list item 2023-08-03 01:13:59 -04:00
karamvir 260fd572bf improve recents, builder card 2023-08-02 22:09:24 -07:00
mbecker20 d6c03a51b1 add recently viewed max count 2023-08-03 00:54:21 -04:00
mbecker20 fe89c5835c push recently viewed 2023-08-03 00:48:54 -04:00
karamvir ddcc35ec58 add resource updates to builder 2023-08-02 21:34:47 -07:00
karamvir 0f39497bee add res target type to resource udpates 2023-08-02 21:34:40 -07:00
karamvir 58f1346124 add new builder, builder page, etc 2023-08-02 21:31:38 -07:00
karamvir 997366655c add recently viewed 2023-08-02 21:05:47 -07:00
karamvir de057fd08f update server card 2023-08-02 21:05:35 -07:00
karamvir d15607048e reduce gap 2023-08-02 20:40:07 -07:00
karamvir 5460d19f80 hide tags 2023-08-02 20:39:59 -07:00
karamvir ed4ae5efb8 hide tags 2023-08-02 20:30:15 -07:00
karamvir 530da2589d add 1 to month ffs 2023-08-02 20:29:00 -07:00
karamvir 05b2a16746 copy name when click it 2023-08-02 20:28:49 -07:00
mbecker20 35cbad2177 rename post_image to process_args 2023-08-02 17:05:16 +00:00
karamvir f13ac340b1 cleanup files 2023-08-02 09:59:12 -07:00
karamvir 14dc5e40fe init refactor 2023-08-01 21:14:39 -07:00
karamvir 4820313158 fix tabs value for logs 2023-08-01 11:35:42 -07:00
karamvir 9d573932f5 update resource cards, deployment logs 2023-08-01 10:42:44 -07:00
karamvir ab88fbc1bd init confirmation dialog 2023-08-01 08:43:34 -07:00
karamvir 1a806c209b cleanup for build 2023-07-31 21:59:55 -07:00
karamvir f15540e860 add basic socket toast and invalidate 2023-07-31 21:50:04 -07:00
mbecker20 d1be56923f testss 2023-08-01 00:20:03 -04:00
karamvir 493044f4e4 deployments chart uses summary 2023-07-31 21:02:20 -07:00
mbecker20 d15ca72e07 update should sort by start_ts 2023-07-31 23:53:55 -04:00
karamvir 9a16634c2e add max height to dropdown 2023-07-31 20:50:13 -07:00
karamvir 997940b4ab add omnibar etc 2023-07-31 20:44:00 -07:00
karamvir fee4585ca4 cleanup for build 2023-07-31 19:40:26 -07:00
karamvir 94c1df79ef improve depl config 2023-07-31 19:37:50 -07:00
karamvir b4e6626feb init server stats page 2023-07-31 12:29:55 -07:00
mbecker20 b8ad77085c add the configs 2023-07-30 15:51:59 -04:00
mbecker20 750f1fc379 collection name Tag 2023-07-30 14:04:32 -04:00
mbecker20 8b93b5d273 rename system stats collection 2023-07-30 14:02:01 -04:00
mbecker20 2ffbb9d9fa work on monitoring 2023-07-30 13:52:31 -04:00
mbecker20 97180c2f04 server stats history api 2023-07-30 01:08:34 -04:00
mbecker20 a9b9ed2d99 resource to list item 2023-07-29 22:17:13 -04:00
mbecker20 097f6c3b64 new search method 2023-07-29 14:50:35 -04:00
mbecker20 a279982187 implement list resources with ListItem on Resource trait 2023-07-29 14:35:36 -04:00
mbecker20 26c692978a paginated list updates 2023-07-29 04:08:36 -04:00
mbecker20 482f8bb862 build monthly stats and build versions 2023-07-28 01:54:29 -04:00
kv 3ac7cc1a59 cleanup for build 2023-07-27 14:02:59 -07:00
kv 7c5aff44a9 resource update cards now clickable to reveal details 2023-07-27 14:02:32 -07:00
kv a55d4e3c6f fix deployment update cards 2023-07-27 13:55:48 -07:00
kv 485b337c72 fix update sheet trigger, scrolling 2023-07-27 13:55:43 -07:00
kv 02f16168d3 cleanup for build 2023-07-27 13:42:32 -07:00
kv bd42af9c3e update deployment page, more to come 2023-07-27 13:41:44 -07:00
karamvir ce5a5a6e50 Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-07-27 12:55:40 -07:00
karamvir bf893ca8b4 first draft of updates and config pages 2023-07-27 12:55:38 -07:00
mbecker20 20a450d1d4 alerter api ts. fmt 2023-07-27 03:03:03 -04:00
mbecker20 70d11c3f5d replace those helpers with Resource trait 2023-07-27 03:00:36 -04:00
mbecker20 dc7690f4cf Config 2023-07-26 03:36:27 -04:00
karamvir 603260df5d Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-07-25 22:29:05 -07:00
karamvir 51bdc6b221 cleanup unnecessary type cast 2023-07-25 22:29:04 -07:00
mbecker20 4f6a9545e9 builder and repo summary 2023-07-26 00:59:20 -04:00
mbecker20 14edb4f913 finalize create build update before add 2023-07-25 21:06:11 -04:00
mbecker20 ffe0459994 remove solid snippets 2023-07-25 19:56:19 -04:00
karamvir 5138ce2040 cleanup 2023-07-25 16:45:34 -07:00
karamvir db85de0722 update cards, fix responsive layouts 2023-07-25 16:44:43 -07:00
karamvir 082a76bfb7 improve updates 2023-07-25 16:06:37 -07:00
karamvir 9a3b160e62 improve cards, add tags and accordion 2023-07-25 14:27:48 -07:00
karamvir c8b312bcb1 update cards etc 2023-07-25 13:23:55 -07:00
karamvir 9cc3fa0897 improve updates 2023-07-25 12:24:36 -07:00
karamvir 49063cac26 cleanup 2023-07-25 11:58:18 -07:00
karamvir 59a06c84a1 consolidate page layouts, cleanup 2023-07-25 11:56:57 -07:00
karamvir 6f5a7b97b5 update use read to now take config as well 2023-07-25 10:23:58 -07:00
karamvir 0db97fd5d5 refactor useRead, improve dx alot 2023-07-25 02:39:15 -07:00
karamvir b31f74f0c5 cleanup for build 2023-07-25 01:27:43 -07:00
karamvir ffb3957d4f playing around with new resource page, etc 2023-07-25 01:20:28 -07:00
karamvir 3b46e9a6bb Merge branch 'next' of https://github.com/mbecker20/monitor into next 2023-07-25 00:42:38 -07:00
karamvir b67cb3bd9e fix dockerfile 2023-07-25 00:42:36 -07:00
mbecker20 91a6485c20 add other status cases for summaries 2023-07-25 02:53:34 -04:00
mbecker20 da24634774 implement the summaries 2023-07-25 02:51:46 -04:00
karamvir 88198f7696 add summary stuff, cleanup vite build errors 2023-07-24 23:41:34 -07:00
mbecker20 2bdc7cd39a list updates for admin 2023-07-25 02:33:48 -04:00
karamvir ec4fcbc553 add summaries 2023-07-24 23:20:14 -07:00
mbecker20 30820a8d81 add summary response types 2023-07-24 23:10:29 -04:00
mbecker20 1c439a0392 add summary and update api 2023-07-24 22:31:38 -04:00
mbecker20 d6fe3461f7 get resource summaries 2023-07-24 22:28:47 -04:00
mbecker20 18a211592d fix default url 2023-07-24 21:39:11 -04:00
karamvir 8396c04b4a ooooo thats a sexy dashboard 2023-07-24 16:51:20 -07:00
karamvir 872254e865 cleanup header layout 2023-07-24 15:43:45 -07:00
karamvir 2a8fb84dc3 cleanup 2023-07-24 13:56:58 -07:00
karamvir 008dab08f8 normalise resources, better card hoverability 2023-07-24 13:55:07 -07:00
karamvir 781a3958d8 add template for deployment info 2023-07-24 11:37:19 -07:00
karamvir ec344cafb5 improve styles, actions 2023-07-24 10:30:50 -07:00
karamvir ec20b288be improve execute hooks, cleanup a bit etc 2023-07-24 09:24:02 -07:00
karamvir 9206e00a99 refactor component layout 2023-07-24 09:07:32 -07:00
karamvir 438a172e77 improve hoverable prop behavior, add link to /builds on dashboard 2023-07-24 01:24:50 -07:00
karamvir 8aaa559fbb slightly improve login / logout 2023-07-24 01:18:35 -07:00
karamvir 79b3d30c1e fixes 2023-07-24 01:09:49 -07:00
karamvir 3c5293b60b add actions, refactor deployments 2023-07-24 00:59:17 -07:00
karamvir d16da528af fixes 2023-07-24 00:34:00 -07:00
karamvir 6407a3a06d add builds page, build page, etc 2023-07-24 00:21:20 -07:00
karamvir 145804c05d add deployments back to header, fix pie variants 2023-07-23 23:53:54 -07:00
karamvir 7b5da511fe ok better dashboard 2023-07-23 23:49:19 -07:00
karamvir cc65d5ac23 add servers list page, upgrade server card with stats 2023-07-23 23:38:29 -07:00
karamvir 6c41362bfa add single deployment page etc 2023-07-23 23:31:19 -07:00
karamvir b57688033e add some basic dashboard components, add server page 2023-07-23 16:08:02 -07:00
mbecker20 9c120118e9 fix types imports 2023-07-22 12:30:52 -04:00
mbecker20 70ce321c75 remove server to_notify 2023-07-22 11:42:46 -04:00
mbecker20 ed3df2beba fix 2 responses 2023-07-22 11:25:38 -04:00
mbecker20 613d445c99 vite config resolve monitor client 2023-07-22 02:53:21 -04:00
mbecker20 5cc3885e32 fix the ts client 2023-07-22 02:25:02 -04:00
mbecker20 7e66eca6ae Merge branch 'next-fe' into next 2023-07-21 23:16:57 -04:00
Karamvir eef1c3f41d init frontend 2023-07-20 14:36:01 -07:00
Karamvir 09defb60a5 update client 2023-07-20 14:35:51 -07:00
mbecker20 4fd931c508 add cors layer 2023-07-20 21:02:14 +00:00
mbecker20 9b656a13e6 add container log search via | grep 2023-07-18 18:41:53 +00:00
mbecker20 e95fe8f410 fix doc indexes 2023-07-17 06:14:16 +00:00
mbecker20 91628c61ee update tests 2023-07-17 05:18:51 +00:00
mbecker20 0747bc9bc1 env override config port 2023-07-17 04:40:57 +00:00
mbecker20 2df881d43f include commit msg in commit hash log 2023-07-16 22:51:23 -04:00
mbecker20 cd10eaf9db smth 2023-07-16 22:39:55 -04:00
mbecker20 e0f69a6420 work on monitoring 2023-07-16 22:24:44 -04:00
mbecker20 e8828ed74b update slack version 2023-07-16 12:21:03 -04:00
mbecker20 ce29e0685c move helpers around 2023-07-16 15:38:01 +00:00
mbecker20 6c7dda2b67 alert enum and send alert logic 2023-07-16 05:51:45 +00:00
mbecker20 7694e3f565 update ts lib 2023-07-15 04:21:10 -04:00
mbecker20 b8c9731081 work on ts client 2023-07-15 04:14:46 -04:00
mbecker20 14ec8b2bcb Merge branch 'temp' into next 2023-07-15 03:54:58 -04:00
mbecker20 e9d7543c43 tag crud 2023-07-15 03:54:49 -04:00
mbecker20 311e090826 update update after build clone 2023-07-14 06:37:18 +00:00
mbecker20 1c1172e0e7 fix core dockerfile 2023-07-14 06:24:22 +00:00
mbecker20 2a1b719f67 move core, periphery, tests under /bin 2023-07-14 06:06:16 +00:00
mbecker20 9b9cf8ad9f add commit hash to clone log 2023-07-14 04:32:38 +00:00
mbecker20 946f427c4b standard logger 2023-07-13 23:42:28 -04:00
beckerinj 100f34a32b pretty debug print error 2023-07-13 17:12:24 -04:00
beckerinj 7a9b506958 update mungos version, remove created_at (its in _id) 2023-07-13 16:34:03 -04:00
mbecker20 4cd074ee2a implement cancel build 2023-07-13 08:24:47 +00:00
mbecker20 e2a76274ef update partial derive version 2023-07-12 08:24:00 +00:00
beckerinj aa197bd89e cleanup 2023-07-11 14:55:19 -04:00
beckerinj fde1165408 update startup log 2023-07-11 14:54:51 -04:00
beckerinj 731b91e0a9 startup log after logger init 2023-07-11 14:46:22 -04:00
mbecker20 60205ece9d use partial derive to impl Froms 2023-07-11 03:12:52 -04:00
mbecker20 6b8fc5d624 alerter api 2023-07-11 03:03:19 -04:00
mbecker20 54df0af48e implement some alerter crud 2023-07-10 05:10:13 +00:00
mbecker20 f1101cae5a update mungos & fmt 2023-07-09 18:26:03 +00:00
mbecker20 f7a1edb7e5 update mungos to 0.4.10 2023-07-09 18:11:19 +00:00
mbecker20 4e7311bdfe define more api for alerter / tag 2023-07-09 06:03:21 +00:00
mbecker20 818210805f create alerter resource 2023-07-09 05:46:20 +00:00
mbecker20 0e1026a100 more dynamic mongo info loading from config 2023-07-08 05:54:12 +00:00
mbecker20 553a2e3dad create the example configs 2023-07-08 03:17:50 +00:00
mbecker20 c12eb40459 add api to search resources by tag 2023-07-07 06:55:47 +00:00
mbecker20 9b68283eb3 improve typeshare types 2023-07-06 20:54:27 +00:00
mbecker20 e9ee467940 derive EnumVariant enum 2023-07-06 20:30:27 +00:00
mbecker20 91f1de5d95 change tags api, add tags requests, get list items 2023-07-06 06:06:14 +00:00
mbecker20 6ce6b55b68 get server / deployment status from cache 2023-07-05 23:48:07 +00:00
mbecker20 d0b5e3e241 split api into read, write, execute 2023-07-04 05:34:13 +00:00
mbecker20 f16176f298 get build action states 2023-07-04 03:56:48 +00:00
mbecker20 2dc4d41524 move github listener to potentially add more listeners 2023-07-04 03:44:45 +00:00
mbecker20 2bf2901dcd move clients 2023-07-04 03:20:11 +00:00
mbecker20 99c2d6195d crud for builder 2023-07-04 00:02:27 +00:00
mbecker20 c5b1079be9 repo api 2023-07-03 22:12:54 +00:00
mbecker20 c25e05fde7 work on repo api 2023-07-03 20:09:44 +00:00
mbecker20 c6290cb7f6 start repo crud / actions 2023-07-03 07:05:01 +00:00
mbecker20 b08d5090a2 github build listener 2023-07-03 06:48:58 +00:00
mbecker20 3da824b4c3 finish permissions routes 2023-07-03 06:27:44 +00:00
mbecker20 9a1619c554 test periphery api 2023-07-03 06:09:30 +00:00
mbecker20 312f7a9003 work on permissions api 2023-07-03 01:11:36 -04:00
mbecker20 a197948837 change bin name for monitor core 2023-07-02 07:58:54 +00:00
mbecker20 765d510127 some more core routes 2023-07-02 07:49:13 +00:00
mbecker20 b5179e443b setup some more deployment reqs 2023-06-30 05:41:32 +00:00
mbecker20 fe6c371a9f fmt. copy deployment / build 2023-06-30 05:29:15 +00:00
mbecker20 d66c5ed618 ts client 2023-06-30 04:52:08 +00:00
mbecker20 70ba120cfb ts client 2023-06-29 07:40:35 +00:00
mbecker20 33f1d85194 work on typescript client 2023-06-29 06:55:32 +00:00
mbecker20 e3f1b449e2 add the deployment actions 2023-06-28 12:16:25 -04:00
beckerinj 7e580efc77 add temp monitoring stuff and start of builder crud 2023-06-25 02:38:16 -04:00
mbecker20 b33501cce6 implement basic deployment api 2023-06-24 07:21:15 +00:00
mbecker20 402259ef11 implement build request 2023-06-24 00:42:14 +00:00
mbecker20 2d4c682fac work on Aws builds / RunBuild 2023-06-23 08:07:35 +00:00
mbecker20 2affc5f555 work on the state change monitoring 2023-06-22 07:57:25 +00:00
mbecker20 9ce1dded38 crud for deployment and build 2023-06-22 06:39:57 +00:00
mbecker20 383b0d125c implement some deployment crud 2023-06-20 08:02:48 +00:00
mbecker20 8023710702 make deployment request resolver file 2023-06-20 07:23:20 +00:00
mbecker20 82c0cbc8bd refactor api router lcoation 2023-06-20 07:20:44 +00:00
mbecker20 893a7f22a4 refactor api router out of main 2023-06-20 07:15:53 +00:00
mbecker20 b903e511b5 finish server api 2023-06-20 07:13:14 +00:00
mbecker20 86379d1c79 rename server 2023-06-19 08:03:58 +00:00
mbecker20 6320d6fbeb implement the ws 2023-06-19 07:34:36 +00:00
mbecker20 636023167d start implementing core server api 2023-06-18 21:12:26 +00:00
mbecker20 8a86825357 fix useless conversion 2023-06-18 17:55:58 +00:00
mbecker20 f5d68b4d78 add api secret create / delete methods 2023-06-18 17:55:37 +00:00
mbecker20 7295e656eb fix dockerfile 2023-06-18 06:18:52 +00:00
mbecker20 5e51050fa3 initialize monitor client 2023-06-18 06:15:31 +00:00
mbecker20 c510a64207 add dockerfile 2023-06-18 05:54:21 +00:00
mbecker20 7269050cf4 auth should work 2023-06-18 05:49:32 +00:00
mbecker20 5730e5d2ae implement db and some auth functions 2023-06-18 05:07:05 +00:00
mbecker20 76ddbe1869 start working on core 2023-06-16 06:20:26 +00:00
mbecker20 b5ef174312 finish basic periphery resolvers 2023-06-15 08:02:51 +00:00
mbecker20 31007f3d78 template the entities 2023-06-11 17:51:46 +00:00
mbecker20 c5a66709ad partial derive working 2023-06-11 17:49:14 +00:00
mbecker20 aa4815b01d restructure and add more requests 2023-06-11 05:48:46 +00:00
mbecker20 7d6b975592 implement resolver api 2023-06-10 22:34:42 +00:00
mbecker20 3a57712dd2 add periphery client to core deps 2023-06-10 08:42:25 +00:00
mbecker20 62eb35340e move periphery api requests to seperate crate 2023-06-10 08:41:34 +00:00
mbecker20 e9880d00ab resolve request 2023-06-10 08:15:07 +00:00
mbecker20 73c1792fea implement the periphery stats api 2023-06-10 07:14:08 +00:00
mbecker20 d36f399d08 rename core AppState to State 2023-06-09 15:50:37 +00:00
mbecker20 c8a24dbcbc work to make backend Req -> Res typesafe using Resolve trait 2023-06-09 15:50:13 +00:00
mbecker20 fecc5134ad resolver trait 2023-06-09 09:01:55 +00:00
mbecker20 8ad84a8cec rework the stats routes 2023-06-09 08:31:46 +00:00
mbecker20 51ebee2910 add types for system stats 2023-06-08 16:17:48 +00:00
mbecker20 20d496e617 implement the periphery request guard 2023-06-08 06:38:36 +00:00
mbecker20 44a16dd214 init monitor next (v5) 2023-06-07 07:30:50 +00:00
mbecker20 4695b79b73 delete everything for next rewrite 2023-06-07 04:59:00 +00:00
mbecker20 9c0be07ae1 refactor caching to use custom Cache struct 2023-05-28 08:31:18 +00:00
mbecker20 ab945aadde fix set termination timeout 0 2023-05-25 20:18:39 +00:00
mbecker20 c1c461c273 add command crud and run api 2023-05-24 07:45:52 +00:00
mbecker20 336742ee69 update author and clap args 2023-05-24 05:28:46 +00:00
beckerinj 405dacce1c sanitize container logs for any script tags 2023-05-13 02:23:05 -04:00
beckerinj 9acd45aa93 fix log whitespace non preservation issue 2023-05-13 02:14:14 -04:00
beckerinj c889c2cc03 clean up log component imports 2023-05-13 01:49:06 -04:00
mbecker20 7ac91ef416 view images on server 2023-05-12 07:22:56 +00:00
mbecker20 8e28669aa1 potentially fix deployment update getting crossed with another deployment 2023-05-09 21:17:10 +00:00
mbecker20 6cdb91f8b8 more readable container state in header 2023-05-04 01:12:55 +00:00
mbecker20 e892474713 modify create deployment initializer 2023-05-03 21:03:57 +00:00
mbecker20 abdae98816 core handle term signal 2023-05-03 20:02:37 +00:00
mbecker20 ab4fe49f33 deployment / build config reset 2023-05-03 19:31:59 +00:00
mbecker20 1ace35103b fix ansi-to-html install 2023-05-03 07:21:38 +00:00
beckerinj dbee729eee show ansi colors in the logs correctly 2023-05-03 03:13:42 -04:00
mbecker20 792576ce59 add auto redeploy user 2023-05-02 17:21:25 +00:00
mbecker20 a07624e9b9 0.3.4 fix docker stop --signal on older docker versions 2023-05-01 21:03:25 +00:00
mbecker20 bb8054af8a log version first 2023-05-01 08:43:00 +00:00
mbecker20 7738f3e066 core logs version on startup 2023-05-01 08:34:20 +00:00
955 changed files with 160917 additions and 41742 deletions
+2
View File
@@ -0,0 +1,2 @@
[build]
rustflags = ["-Wunused-crate-dependencies"]
+33
View File
@@ -0,0 +1,33 @@
services:
dev:
image: mcr.microsoft.com/devcontainers/rust:1-1-bullseye
volumes:
# Mount the root folder that contains .git
- ../:/workspace:cached
- /var/run/docker.sock:/var/run/docker.sock
- /proc:/proc
- repos:/etc/komodo/repos
- stacks:/etc/komodo/stacks
command: sleep infinity
ports:
- "9121:9121"
environment:
KOMODO_FIRST_SERVER: http://localhost:8120
KOMODO_DATABASE_ADDRESS: db
KOMODO_ENABLE_NEW_USERS: true
KOMODO_LOCAL_AUTH: true
KOMODO_JWT_SECRET: a_random_secret
links:
- db
# ...
db:
extends:
file: ../dev.compose.yaml
service: ferretdb
volumes:
data:
repo-cache:
repos:
stacks:
+46
View File
@@ -0,0 +1,46 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "Komodo",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
//"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
"dockerComposeFile": ["dev.compose.yaml"],
"workspaceFolder": "/workspace",
"service": "dev",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20.12.2"
},
"ghcr.io/devcontainers-community/features/deno:1": {
}
},
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
"mounts": [
{
"source": "devcontainer-cargo-cache-${devcontainerId}",
"target": "/usr/local/cargo",
"type": "volume"
}
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
9121
],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "./.devcontainer/postCreate.sh",
"runServices": [
"db"
]
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cargo install typeshare-cli
-4
View File
@@ -1,4 +0,0 @@
/target
/config_example
config.*
.env
+2
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
+13 -12
View File
@@ -1,12 +1,13 @@
target
/frontend/build
node_modules
dist
.env
.env.development
repos
config.json
config.toml
secrets.json
secrets.toml
target
node_modules
dist
.env
.env.development
.DS_Store
.idea
/frontend/build
/lib/ts_client/build
creds.toml
.dev
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"vadimcn.vscode-lldb",
"denoland.vscode-deno"
]
}
+25
View File
@@ -0,0 +1,25 @@
{
"resolve": {
"scope": "rust",
"prefix": "resolve",
"body": [
"impl Resolve<${1}, User> for State {",
"\tasync fn resolve(&self, ${1} { ${0} }: ${1}, _: User) -> anyhow::Result<${2}> {",
"\t\ttodo!()",
"\t}",
"}"
]
},
"static": {
"scope": "rust",
"prefix": "static",
"body": [
"fn ${1}() -> &'static ${2} {",
"\tstatic ${3}: OnceLock<${2}> = OnceLock::new();",
"\t${3}.get_or_init(|| {",
"\t\t${0}",
"\t})",
"}"
]
}
}
-64
View File
@@ -1,64 +0,0 @@
{
"component": {
"scope": "typescriptreact,javascriptreact",
"prefix": "comp",
"body": [
"import { Component } from \"solid-js\";",
"",
"const ${1:$TM_FILENAME_BASE}: Component<{}> = (p) => {",
"\treturn (",
"\t\t<div>",
"\t\t\t${0}",
"\t\t</div>",
"\t);",
"}",
"",
"export default ${1:$TM_FILENAME_BASE};"
]
},
"component-with-css": {
"scope": "typescriptreact,javascriptreact",
"prefix": "css-comp",
"body": [
"import { Component } from \"solid-js\";",
"import s from \"./${1:$TM_FILENAME_BASE}.module.scss\";",
"",
"const ${2:$TM_FILENAME_BASE}: Component<{}> = (p) => {",
"\treturn (",
"\t\t<div class={s.${2:$TM_FILENAME_BASE}} >",
"\t\t\t${0}",
"\t\t</div>",
"\t);",
"}",
"",
"export default ${2:$TM_FILENAME_BASE};"
]
},
"context": {
"scope": "typescriptreact,javascriptreact",
"prefix": "provider",
"body": [
"import { ParentComponent, createContext, useContext } from \"solid-js\";",
"",
"const value = () => {",
"\treturn {};",
"}",
"",
"export type Value = ReturnType<typeof value>;",
"",
"const context = createContext<Value>();",
"",
"export const Provider: ParentComponent<{}> = (p) => {",
"\treturn (",
"\t\t<context.Provider value={value()}>",
"\t\t\t{p.children}",
"\t\t</context.Provider>",
"\t);",
"}",
"",
"export function useValue() {",
"\treturn useContext(context) as Value;",
"}"
]
}
}
+179 -164
View File
@@ -1,164 +1,179 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cargo",
"command": "build",
"group": {
"kind": "build",
"isDefault": true
},
"label": "rust: cargo build"
},
{
"type": "cargo",
"command": "fmt",
"label": "rust: cargo fmt"
},
{
"type": "cargo",
"command": "check",
"label": "rust: cargo check"
},
{
"label": "start dev",
"dependsOn": [
"run core",
"yarn: start frontend"
],
"problemMatcher": []
},
{
"type": "shell",
"command": "yarn start",
"label": "yarn: start frontend",
"options": {
"cwd": "${workspaceFolder}/frontend"
},
"presentation": {
"group": "start"
}
},
{
"type": "cargo",
"command": "run",
"label": "run core",
"options": {
"cwd": "${workspaceFolder}/core"
},
"presentation": {
"group": "start"
}
},
{
"type": "cargo",
"command": "run",
"label": "run periphery",
"options": {
"cwd": "${workspaceFolder}/periphery"
}
},
{
"type": "shell",
"command": "cargo install --path . && if pgrep periphery; then pkill periphery; fi && periphery --daemon --config-path ~/.monitor/local.periphery.config.toml",
"label": "run periphery daemon",
"options": {
"cwd": "${workspaceFolder}/periphery"
},
"problemMatcher": []
},
{
"type": "cargo",
"command": "run",
"label": "run cli",
"options": {
"cwd": "${workspaceFolder}/cli"
}
},
{
"type": "cargo",
"command": "run",
"label": "run tests",
"options": {
"cwd": "${workspaceFolder}/tests"
}
},
{
"type": "cargo",
"command": "publish",
"args": ["--allow-dirty"],
"label": "publish monitor types",
"options": {
"cwd": "${workspaceFolder}/lib/types"
}
},
{
"type": "cargo",
"command": "publish",
"label": "publish monitor client",
"options": {
"cwd": "${workspaceFolder}/lib/monitor_client"
}
},
{
"type": "shell",
"command": "docker compose up -d",
"label": "docker compose up",
"options": {
"cwd": "${workspaceFolder}/tests"
},
"problemMatcher": []
},
{
"type": "shell",
"command": "docker compose down",
"label": "docker compose down",
"options": {
"cwd": "${workspaceFolder}/tests"
},
"problemMatcher": []
},
{
"type": "shell",
"command": "docker compose build",
"label": "docker compose build",
"options": {
"cwd": "${workspaceFolder}/tests"
},
"problemMatcher": []
},
{
"type": "shell",
"command": "docker compose down && docker compose up -d",
"label": "docker compose restart",
"options": {
"cwd": "${workspaceFolder}/tests"
},
"problemMatcher": []
},
{
"type": "shell",
"command": "docker compose build && docker compose down && docker compose up -d",
"label": "docker compose build and restart",
"options": {
"cwd": "${workspaceFolder}/tests"
},
"problemMatcher": []
},
{
"type": "shell",
"command": "docker compose build periphery",
"label": "docker compose build periphery",
"options": {
"cwd": "${workspaceFolder}/tests"
},
"problemMatcher": []
},
{
"type": "shell",
"command": "typeshare ./lib/types --lang=typescript --output-file=./frontend/src/types.ts && typeshare ./core --lang=typescript --output-file=./frontend/src/util/client_types.ts",
"label": "generate typescript types",
"problemMatcher": []
}
]
}
{
"version": "2.0.0",
"tasks": [
{
"label": "Run Core",
"command": "cargo",
"args": [
"run",
"-p",
"komodo_core",
"--release"
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"KOMODO_CONFIG_PATH": "test.core.config.toml"
}
},
"problemMatcher": [
"$rustc"
]
},
{
"label": "Build Core",
"command": "cargo",
"args": [
"build",
"-p",
"komodo_core",
"--release"
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"KOMODO_CONFIG_PATH": "test.core.config.toml"
}
},
"problemMatcher": [
"$rustc"
]
},
{
"label": "Run Periphery",
"command": "cargo",
"args": [
"run",
"-p",
"komodo_periphery",
"--release"
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"KOMODO_CONFIG_PATH": "test.periphery.config.toml"
}
},
"problemMatcher": [
"$rustc"
]
},
{
"label": "Build Periphery",
"command": "cargo",
"args": [
"build",
"-p",
"komodo_periphery",
"--release"
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"KOMODO_CONFIG_PATH": "test.periphery.config.toml"
}
},
"problemMatcher": [
"$rustc"
]
},
{
"label": "Run Backend",
"dependsOn": [
"Run Core",
"Run Periphery"
],
"problemMatcher": [
"$rustc"
]
},
{
"label": "Build TS Client Types",
"type": "process",
"command": "node",
"args": [
"./client/core/ts/generate_types.mjs"
],
"problemMatcher": []
},
{
"label": "Init TS Client",
"type": "shell",
"command": "yarn && yarn build && yarn link",
"options": {
"cwd": "${workspaceFolder}/client/core/ts",
},
"problemMatcher": []
},
{
"label": "Init Frontend Client",
"type": "shell",
"command": "yarn link komodo_client && yarn install",
"options": {
"cwd": "${workspaceFolder}/frontend",
},
"problemMatcher": []
},
{
"label": "Init Frontend",
"dependsOn": [
"Build TS Client Types",
"Init TS Client",
"Init Frontend Client"
],
"dependsOrder": "sequence",
"problemMatcher": []
},
{
"label": "Build Frontend",
"type": "shell",
"command": "yarn build",
"options": {
"cwd": "${workspaceFolder}/frontend",
},
"problemMatcher": []
},
{
"label": "Prepare Frontend For Run",
"type": "shell",
"command": "cp -r ./client/core/ts/dist/. frontend/public/client/.",
"options": {
"cwd": "${workspaceFolder}",
},
"dependsOn": [
"Build TS Client Types",
"Build Frontend"
],
"dependsOrder": "sequence",
"problemMatcher": []
},
{
"label": "Run Frontend",
"type": "shell",
"command": "yarn dev",
"options": {
"cwd": "${workspaceFolder}/frontend",
},
"dependsOn": ["Prepare Frontend For Run"],
"problemMatcher": []
},
{
"label": "Init",
"dependsOn": [
"Build Backend",
"Init Frontend"
],
"dependsOrder": "sequence",
"problemMatcher": []
},
{
"label": "Run Komodo",
"dependsOn": [
"Run Core",
"Run Periphery",
"Run Frontend"
],
"problemMatcher": []
},
]
}
Generated
+3845 -1601
View File
File diff suppressed because it is too large Load Diff
+124 -14
View File
@@ -1,14 +1,124 @@
[workspace]
members = [
"cli",
"core",
"periphery",
"tests",
"lib/axum_oauth2",
"lib/db_client",
"lib/helpers",
"lib/periphery_client",
"lib/types",
"lib/monitor_client"
]
[workspace]
resolver = "2"
members = [
"bin/*",
"lib/*",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "1.17.2"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
repository = "https://github.com/moghtech/komodo"
homepage = "https://komo.do"
[workspace.dependencies]
# LOCAL
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.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"
async_timing_util = "1.0.0"
partial_derive2 = "0.4.3"
derive_variants = "1.0.0"
mongo_indexed = "2.0.1"
resolver_api = "3.0.0"
toml_pretty = "1.1.2"
mungos = "3.2.0"
svi = "1.0.1"
# ASYNC
reqwest = { version = "0.12.15", default-features = false, features = ["json", "rustls-tls-native-roots"] }
tokio = { version = "1.44.1", features = ["full"] }
tokio-util = "0.7.14"
futures = "0.3.31"
futures-util = "0.3.31"
arc-swap = "1.7.1"
# SERVER
axum-extra = { version = "0.10.0", features = ["typed-header"] }
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
tokio-tungstenite = "0.26.2"
# SER/DE
ordered_hash_map = { version = "0.4.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.20"
# ERROR
anyhow = "1.0.98"
thiserror = "2.0.12"
# LOGGING
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.0"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.36", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.16.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.17.0"
base64 = "0.22.1"
rustls = "0.23.26"
hmac = "0.12.1"
sha2 = "0.10.8"
rand = "0.9.0"
hex = "0.4.3"
# SYSTEM
bollard = "0.18.1"
sysinfo = "0.34.2"
# CLOUD
aws-config = "1.6.1"
aws-sdk-ec2 = "1.121.1"
aws-credential-types = "1.2.2"
## CRON
english-to-cron = "0.1.4"
chrono-tz = "0.10.3"
chrono = "0.4.40"
croner = "2.1.0"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.4"
octorust = "0.10.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.1"
bson = "2.14.0"
+27
View File
@@ -0,0 +1,27 @@
## Builds the Komodo Core and Periphery binaries
## for a specific architecture.
FROM rust:1.86.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
# Compile bin
RUN \
cargo build -p komodo_core --release && \
cargo build -p komodo_periphery --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
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
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "komodo_cli"
description = "Command line tool to execute Komodo actions"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[[bin]]
name = "komodo"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# local
# komodo_client = "1.16.12"
komodo_client.workspace = true
# external
tracing-subscriber.workspace = true
merge_config_files.workspace = true
futures.workspace = true
tracing.workspace = true
colored.workspace = true
anyhow.workspace = true
tokio.workspace = true
serde.workspace = true
clap.workspace = true
+118
View File
@@ -0,0 +1,118 @@
# Komodo CLI
Komodo CLI is a tool to execute actions on your Komodo instance from shell scripts.
## Install
```sh
cargo install komodo_cli
```
Note: On Ubuntu, also requires `apt install build-essential pkg-config libssl-dev`.
## Usage
### Credentials
Configure a file `~/.config/komodo/creds.toml` file with contents:
```toml
url = "https://your.komodo.address"
key = "YOUR-API-KEY"
secret = "YOUR-API-SECRET"
```
Note. You can specify a different creds file by using `--creds ./other/path.toml`.
You can also bypass using any file and pass the information using `--url`, `--key`, `--secret`:
```sh
komodo --url "https://your.komodo.address" --key "YOUR-API-KEY" --secret "YOUR-API-SECRET" ...
```
### Run Executions
```sh
# Triggers an example build
komodo execute run-build test_build
```
#### Manual
`komodo --help`
```md
Command line tool to execute Komodo actions
Usage: komodo [OPTIONS] <COMMAND>
Commands:
execute Runs an execution
help Print this message or the help of the given subcommand(s)
Options:
--creds <CREDS> The path to a creds file [default: /Users/max/.config/komodo/creds.toml]
--url <URL> Pass url in args instead of creds file
--key <KEY> Pass api key in args instead of creds file
--secret <SECRET> Pass api secret in args instead of creds file
-y, --yes Always continue on user confirmation prompts
-h, --help Print help (see more with '--help')
-V, --version Print version
```
`komodo execute --help`
```md
Runs an execution
Usage: komodo execute <COMMAND>
Commands:
none The "null" execution. Does nothing
run-procedure Runs the target procedure. Response: [Update]
run-build Runs the target build. Response: [Update]
cancel-build Cancels the target build. Only does anything if the build is `building` when called. Response: [Update]
deploy Deploys the container for the target deployment. Response: [Update]
start-deployment Starts the container for the target deployment. Response: [Update]
restart-deployment Restarts the container for the target deployment. Response: [Update]
pause-deployment Pauses the container for the target deployment. Response: [Update]
unpause-deployment Unpauses the container for the target deployment. Response: [Update]
stop-deployment Stops the container for the target deployment. Response: [Update]
destroy-deployment Stops and destroys the container for the target deployment. Reponse: [Update]
clone-repo Clones the target repo. Response: [Update]
pull-repo Pulls the target repo. Response: [Update]
build-repo Builds the target repo, using the attached builder. Response: [Update]
cancel-repo-build Cancels the target repo build. Only does anything if the repo build is `building` when called. Response: [Update]
start-container Starts the container on the target server. Response: [Update]
restart-container Restarts the container on the target server. Response: [Update]
pause-container Pauses the container on the target server. Response: [Update]
unpause-container Unpauses the container on the target server. Response: [Update]
stop-container Stops the container on the target server. Response: [Update]
destroy-container Stops and destroys the container on the target server. Reponse: [Update]
start-all-containers Starts all containers on the target server. Response: [Update]
restart-all-containers Restarts all containers on the target server. Response: [Update]
pause-all-containers Pauses all containers on the target server. Response: [Update]
unpause-all-containers Unpauses all containers on the target server. Response: [Update]
stop-all-containers Stops all containers on the target server. Response: [Update]
prune-containers Prunes the docker containers on the target server. Response: [Update]
delete-network Delete a docker network. Response: [Update]
prune-networks Prunes the docker networks on the target server. Response: [Update]
delete-image Delete a docker image. Response: [Update]
prune-images Prunes the docker images on the target server. Response: [Update]
delete-volume Delete a docker volume. Response: [Update]
prune-volumes Prunes the docker volumes on the target server. Response: [Update]
prune-system Prunes the docker system on the target server, including volumes. Response: [Update]
run-sync Runs the target resource sync. Response: [Update]
deploy-stack Deploys the target stack. `docker compose up`. Response: [Update]
start-stack Starts the target stack. `docker compose start`. Response: [Update]
restart-stack Restarts the target stack. `docker compose restart`. Response: [Update]
pause-stack Pauses the target stack. `docker compose pause`. Response: [Update]
unpause-stack Unpauses the target stack. `docker compose unpause`. Response: [Update]
stop-stack Starts the target stack. `docker compose stop`. Response: [Update]
destroy-stack Destoys the target stack. `docker compose down`. Response: [Update]
sleep
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
```
### --yes
You can use `--yes` to avoid any human prompt to continue, for use in automated environments.
+55
View File
@@ -0,0 +1,55 @@
use clap::{Parser, Subcommand};
use komodo_client::api::execute::Execution;
use serde::Deserialize;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct CliArgs {
/// Sync or Exec
#[command(subcommand)]
pub command: Command,
/// The path to a creds file.
///
/// Note: If each of `url`, `key` and `secret` are passed,
/// no file is required at this path.
#[arg(long, default_value_t = default_creds())]
pub creds: String,
/// Pass url in args instead of creds file
#[arg(long)]
pub url: Option<String>,
/// Pass api key in args instead of creds file
#[arg(long)]
pub key: Option<String>,
/// Pass api secret in args instead of creds file
#[arg(long)]
pub secret: Option<String>,
/// Always continue on user confirmation prompts.
#[arg(long, short, default_value_t = false)]
pub yes: bool,
}
fn default_creds() -> String {
let home =
std::env::var("HOME").unwrap_or_else(|_| String::from("/root"));
format!("{home}/.config/komodo/creds.toml")
}
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
/// Runs an execution
Execute {
#[command(subcommand)]
execution: Execution,
},
// Room for more
}
#[derive(Debug, Deserialize)]
pub struct CredsFile {
pub url: String,
pub key: String,
pub secret: String,
}
+485
View File
@@ -0,0 +1,485 @@
use std::time::Duration;
use colored::Colorize;
use komodo_client::{
api::execute::{BatchExecutionResponse, Execution},
entities::update::Update,
};
use crate::{
helpers::wait_for_enter,
state::{cli_args, komodo_client},
};
pub enum ExecutionResult {
Single(Update),
Batch(BatchExecutionResponse),
}
pub async fn run(execution: Execution) -> anyhow::Result<()> {
if matches!(execution, Execution::None(_)) {
println!("Got 'none' execution. Doing nothing...");
tokio::time::sleep(Duration::from_secs(3)).await;
println!("Finished doing nothing. Exiting...");
std::process::exit(0);
}
println!("\n{}: Execution", "Mode".dimmed());
match &execution {
Execution::None(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Deploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchCloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchBuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelRepoBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyContainer(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopAllContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneContainers(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteNetwork(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneNetworks(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteImage(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneImages(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeleteVolume(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneVolumes(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneDockerBuilders(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneBuildx(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PruneSystem(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CommitSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::TestAlerter(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
}
if !cli_args().yes {
wait_for_enter("run execution")?;
}
info!("Running Execution...");
let res = match execution {
Execution::RunAction(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchRunAction(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::RunProcedure(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchRunProcedure(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::RunBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchRunBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::CancelBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::Deploy(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDeploy(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::PullDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::CloneRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchCloneRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::PullRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchPullRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::BuildRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchBuildRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::CancelRepoBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DestroyContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DeleteNetwork(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneNetworks(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DeleteImage(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneImages(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DeleteVolume(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneVolumes(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneDockerBuilders(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneBuildx(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneSystem(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RunSync(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::CommitSync(request) => komodo_client()
.write(request)
.await
.map(ExecutionResult::Single),
Execution::DeployStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDeployStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::DeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::PullStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DestroyStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDestroyStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::TestAlerter(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);
tokio::time::sleep(duration).await;
println!("Finished sleeping!");
std::process::exit(0)
}
Execution::None(_) => unreachable!(),
};
match res {
Ok(ExecutionResult::Single(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Ok(ExecutionResult::Batch(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Err(e) => println!("{}\n\n{e:#?}", "ERROR".red()),
}
Ok(())
}
+17
View File
@@ -0,0 +1,17 @@
use std::io::Read;
use anyhow::Context;
use colored::Colorize;
pub fn wait_for_enter(press_enter_to: &str) -> anyhow::Result<()> {
println!(
"\nPress {} to {}\n",
"ENTER".green(),
press_enter_to.bold()
);
let buffer = &mut [0u8];
std::io::stdin()
.read_exact(buffer)
.context("failed to read ENTER")?;
Ok(())
}
+32
View File
@@ -0,0 +1,32 @@
#[macro_use]
extern crate tracing;
use colored::Colorize;
use komodo_client::api::read::GetVersion;
mod args;
mod exec;
mod helpers;
mod state;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt().with_target(false).init();
info!(
"Komodo CLI version: {}",
env!("CARGO_PKG_VERSION").blue().bold()
);
let version =
state::komodo_client().read(GetVersion {}).await?.version;
info!("Komodo Core version: {}", version.blue().bold());
match &state::cli_args().command {
args::Command::Execute { execution } => {
exec::run(execution.to_owned()).await?
}
}
Ok(())
}
+48
View File
@@ -0,0 +1,48 @@
use std::sync::OnceLock;
use clap::Parser;
use komodo_client::KomodoClient;
use merge_config_files::parse_config_file;
pub fn cli_args() -> &'static crate::args::CliArgs {
static CLI_ARGS: OnceLock<crate::args::CliArgs> = OnceLock::new();
CLI_ARGS.get_or_init(crate::args::CliArgs::parse)
}
pub fn komodo_client() -> &'static KomodoClient {
static KOMODO_CLIENT: OnceLock<KomodoClient> = OnceLock::new();
KOMODO_CLIENT.get_or_init(|| {
let args = cli_args();
let crate::args::CredsFile { url, key, secret } =
match (&args.url, &args.key, &args.secret) {
(Some(url), Some(key), Some(secret)) => {
crate::args::CredsFile {
url: url.clone(),
key: key.clone(),
secret: secret.clone(),
}
}
(url, key, secret) => {
let mut creds: crate::args::CredsFile =
parse_config_file(cli_args().creds.as_str())
.expect("failed to parse Komodo credentials");
if let Some(url) = url {
creds.url.clone_from(url);
}
if let Some(key) = key {
creds.key.clone_from(key);
}
if let Some(secret) = secret {
creds.secret.clone_from(secret);
}
creds
}
};
futures::executor::block_on(
KomodoClient::new(url, key, secret).with_healthcheck(),
)
.expect("failed to initialize Komodo client")
})
}
+81
View File
@@ -0,0 +1,81 @@
[package]
name = "komodo_core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[[bin]]
name = "core"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# local
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"] }
merge_config_files.workspace = true
async_timing_util.workspace = true
partial_derive2.workspace = true
derive_variants.workspace = true
mongo_indexed.workspace = true
resolver_api.workspace = true
toml_pretty.workspace = true
mungos.workspace = true
slack.workspace = true
svi.workspace = true
# external
aws-credential-types.workspace = true
ordered_hash_map.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
tokio-util.workspace = true
axum-extra.workspace = true
tower-http.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
typeshare.workspace = true
chrono-tz.workspace = true
octorust.workspace = true
wildcard.workspace = true
arc-swap.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
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
axum.workspace = true
toml.workspace = true
uuid.workspace = true
envy.workspace = true
rand.workspace = true
hmac.workspace = true
sha2.workspace = true
hex.workspace = true
+55
View File
@@ -0,0 +1,55 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.86.0-bullseye AS core-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
# Compile app
RUN cargo build -p komodo_core --release
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link 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
COPY ./config/core.config.toml /config/config.toml
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
# 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
ENTRYPOINT [ "core" ]
+50
View File
@@ -0,0 +1,50 @@
## 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
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
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" ]
+44
View File
@@ -0,0 +1,44 @@
## 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
WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link komodo_client && yarn && yarn build
FROM debian:bullseye-slim
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Copy
COPY ./config/core.config.toml /config/config.toml
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
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" ]
+277
View File
@@ -0,0 +1,277 @@
use std::sync::OnceLock;
use serde::Serialize;
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 **{name}** is **working**\n{link}"
)
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | **{name}**{region} is now **reachable**\n{link}"
)
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\n**error**: {e:#?}"))
.unwrap_or_default();
format!(
"{level} | **{name}**{region} is **unreachable** ❌\n{link}{err}"
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | **{name}**{region} cpu usage at **{percentage:.1}%**\n{link}"
)
}
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} | **{name}**{region} memory usage at **{percentage:.1}%** 💾\n\nUsing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
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} | **{name}**{region} disk usage at **{percentage:.1}%** 💿\nmount point: `{path:?}`\nusing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
AlertData::ContainerStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
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}"
)
}
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,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
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}"
)
}
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}"
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{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}"
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
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() {
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(())
}
async fn send_message(
url: &str,
content: &str,
) -> anyhow::Result<()> {
let body = DiscordMessageBody { content };
let response = http_client()
.post(url)
.json(&body)
.send()
.await
.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!("Failed to send message to Discord | {status} | failed to get response text")
})?;
Err(anyhow::anyhow!(
"Failed to send message to Discord | {status} | {text}"
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}
#[derive(Serialize)]
struct DiscordMessageBody<'a> {
content: &'a str,
}
+272
View File
@@ -0,0 +1,272 @@
use ::slack::types::Block;
use anyhow::{Context, anyhow};
use derive_variants::ExtractVariant;
use futures::future::join_all;
use komodo_client::entities::{
ResourceTargetVariant,
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
alerter::*,
deployment::DeploymentState,
stack::StackState,
};
use mungos::{find::find_collect, mongodb::bson::doc};
use std::collections::HashSet;
use tracing::Instrument;
use crate::helpers::interpolate::interpolate_variables_secrets_into_string;
use crate::helpers::query::get_variables_and_secrets;
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;
}
let span =
info_span!("send_alerts", alerts = format!("{alerts:?}"));
async {
let Ok(alerters) = find_collect(
&db_client().alerters,
doc! { "config.enabled": true },
None,
)
.await
.inspect_err(|e| {
error!(
"ERROR sending alerts | failed to get alerters from db | {e:#}"
)
}) else {
return;
};
let handles =
alerts.iter().map(|alert| send_alert(&alerters, alert));
join_all(handles).await;
}
.instrument(span)
.await
}
#[instrument(level = "debug")]
async fn send_alert(alerters: &[Alerter], alert: &Alert) {
if alerters.is_empty() {
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(());
}
let alert_type = alert.data.extract_variant();
// 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)
{
return Ok(());
}
// Don't send if resource is in the blacklist
if alerter.config.except_resources.contains(&alert.target) {
return Ok(());
}
// Don't send if whitelist configured and target is not included
if !alerter.config.resources.is_empty()
&& !alerter.config.resources.contains(&alert.target)
{
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
)
})
}
AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url }) => {
ntfy::send_alert(url, 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")]
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_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() {
let text = res
.text()
.await
.context("failed to get response text on alerter response")?;
return Err(anyhow!(
"post to alerter failed | {status} | {text}"
));
}
Ok(())
}
fn fmt_region(region: &Option<String>) -> String {
match region {
Some(region) => format!(" ({region})"),
None => String::new(),
}
}
fn fmt_docker_container_state(state: &DeploymentState) -> String {
match state {
DeploymentState::Running => String::from("Running ▶️"),
DeploymentState::Exited => String::from("Exited 🛑"),
DeploymentState::Restarting => String::from("Restarting 🔄"),
DeploymentState::NotDeployed => String::from("Not Deployed"),
_ => state.to_string(),
}
}
fn fmt_stack_state(state: &StackState) -> String {
match state {
StackState::Running => String::from("Running ▶️"),
StackState::Stopped => String::from("Stopped 🛑"),
StackState::Restarting => String::from("Restarting 🔄"),
StackState::Down => String::from("Down ⬇️"),
_ => state.to_string(),
}
}
fn fmt_level(level: SeverityLevel) -> &'static str {
match level {
SeverityLevel::Critical => "CRITICAL 🚨",
SeverityLevel::Warning => "WARNING ‼️",
SeverityLevel::Ok => "OK ✅",
}
}
fn resource_link(
resource_type: ResourceTargetVariant,
id: &str,
) -> String {
let path = match resource_type {
ResourceTargetVariant::System => unreachable!(),
ResourceTargetVariant::Build => format!("/builds/{id}"),
ResourceTargetVariant::Builder => {
format!("/builders/{id}")
}
ResourceTargetVariant::Deployment => {
format!("/deployments/{id}")
}
ResourceTargetVariant::Stack => {
format!("/stacks/{id}")
}
ResourceTargetVariant::Server => {
format!("/servers/{id}")
}
ResourceTargetVariant::Repo => format!("/repos/{id}"),
ResourceTargetVariant::Alerter => {
format!("/alerters/{id}")
}
ResourceTargetVariant::Procedure => {
format!("/procedures/{id}")
}
ResourceTargetVariant::Action => {
format!("/actions/{id}")
}
ResourceTargetVariant::ServerTemplate => {
format!("/server-templates/{id}")
}
ResourceTargetVariant::ResourceSync => {
format!("/resource-syncs/{id}")
}
};
format!("{}{path}", core_config().host)
}
+266
View File
@@ -0,0 +1,266 @@
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 {} 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, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
content: String,
) -> anyhow::Result<()> {
let response = http_client()
.post(url)
.header("Title", "ntfy Alert")
.body(content)
.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)
}
+270
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)
}
+461
View File
@@ -0,0 +1,461 @@
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &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,
region,
err,
} => {
let region = fmt_region(region);
match alert.level {
SeverityLevel::Ok => {
let text =
format!("{level} | *{name}*{region} is now *reachable*");
let blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} is now *reachable*"
)),
];
(text, blocks.into())
}
SeverityLevel::Critical => {
let text =
format!("{level} | *{name}*{region} is *unreachable* ❌");
let err = err
.as_ref()
.map(|e| format!("\nerror: {e:#?}"))
.unwrap_or_default();
let blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} is *unreachable* ❌{err}"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
match alert.level {
SeverityLevel::Ok => {
let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} cpu usage at *{percentage:.1}%*"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
_ => {
let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} cpu usage at *{percentage:.1}%* 📈"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
}
}
AlertData::ServerMem {
id,
name,
region,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
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 blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
)),
Block::section(format!(
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
_ => {
let text = format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
)),
Block::section(format!(
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
}
}
AlertData::ServerDisk {
id,
name,
region,
path,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
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 blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
)),
Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
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 blocks = vec![
Block::header(level),
Block::section(format!(
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
)),
Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
}
}
AlertData::ContainerStateChange {
name,
server_name,
from,
to,
id,
..
} => {
let to = fmt_docker_container_state(to);
let text = format!("📦 Container *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: {server_name}\nprevious: {from}",
)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
)),
];
(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,
from,
to,
id,
..
} => {
let to = fmt_stack_state(to);
let text = format!("🥞 Stack *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"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,
id,
)),
];
(text, blocks.into())
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
let text = format!(
"{level} | Failed to terminated AWS builder instance "
);
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"instance id: *{instance_id}*\n{message}"
)),
];
(text, blocks.into())
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let text = format!(
"{level} | Pending resource sync updates on *{name}*"
);
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"sync id: *{id}*\nsync name: *{name}*",
)),
Block::section(resource_link(
ResourceTargetVariant::ResourceSync,
id,
)),
];
(text, blocks.into())
}
AlertData::BuildFailed { id, name, version } => {
let text = format!("{level} | Build {name} has failed");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!("version: *v{version}*",)),
Block::section(resource_link(
ResourceTargetVariant::Build,
id,
)),
];
(text, blocks.into())
}
AlertData::RepoBuildFailed { id, name } => {
let text =
format!("{level} | Repo build for *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Repo,
id,
)),
];
(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 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(())
}
+144
View File
@@ -0,0 +1,144 @@
use std::{sync::OnceLock, time::Instant};
use axum::{Router, http::HeaderMap, routing::post};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::auth::*, entities::user::User};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::{
get_user_id_from_headers,
github::{self, client::github_oauth_client},
google::{self, client::google_oauth_client},
oidc,
},
config::core_config,
helpers::query::get_user,
state::jwt_client,
};
pub struct AuthArgs {
pub headers: HeaderMap,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[args(AuthArgs)]
#[response(Response)]
#[error(serror::Error)]
#[variant_derive(Debug)]
#[serde(tag = "type", content = "params")]
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
pub enum AuthRequest {
GetLoginOptions(GetLoginOptions),
CreateLocalUser(CreateLocalUser),
LoginLocalUser(LoginLocalUser),
ExchangeForJwt(ExchangeForJwt),
GetUser(GetUser),
}
pub fn router() -> Router {
let mut router = Router::new().route("/", post(handler));
if core_config().local_auth {
info!("🔑 Local Login Enabled");
}
if github_oauth_client().is_some() {
info!("🔑 Github Login Enabled");
router = router.nest("/github", github::router())
}
if google_oauth_client().is_some() {
info!("🔑 Github Login Enabled");
router = router.nest("/google", google::router())
}
if core_config().oidc_enabled {
info!("🔑 OIDC Login Enabled");
router = router.nest("/oidc", oidc::router())
}
router
}
#[instrument(name = "AuthHandler", level = "debug", skip(headers))]
async fn handler(
headers: HeaderMap,
Json(request): Json<AuthRequest>,
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!(
"/auth request {req_id} | METHOD: {:?}",
request.extract_variant()
);
let res = request.resolve(&AuthArgs { headers }).await;
if let Err(e) = &res {
debug!("/auth request {req_id} | error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/auth request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}
fn login_options_reponse() -> &'static GetLoginOptionsResponse {
static GET_LOGIN_OPTIONS_RESPONSE: OnceLock<
GetLoginOptionsResponse,
> = OnceLock::new();
GET_LOGIN_OPTIONS_RESPONSE.get_or_init(|| {
let config = core_config();
GetLoginOptionsResponse {
local: config.local_auth,
github: 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(),
registration_disabled: config.disable_user_registration,
}
})
}
impl Resolve<AuthArgs> for GetLoginOptions {
#[instrument(name = "GetLoginOptions", level = "debug", skip(self))]
async fn resolve(
self,
_: &AuthArgs,
) -> serror::Result<GetLoginOptionsResponse> {
Ok(*login_options_reponse())
}
}
impl Resolve<AuthArgs> for ExchangeForJwt {
#[instrument(name = "ExchangeForJwt", level = "debug", skip(self))]
async fn resolve(
self,
_: &AuthArgs,
) -> serror::Result<ExchangeForJwtResponse> {
let jwt = jwt_client().redeem_exchange_token(&self.token).await?;
Ok(ExchangeForJwtResponse { jwt })
}
}
impl Resolve<AuthArgs> for GetUser {
#[instrument(name = "GetUser", level = "debug", skip(self))]
async fn resolve(
self,
AuthArgs { headers }: &AuthArgs,
) -> serror::Result<User> {
let user_id = get_user_id_from_headers(headers).await?;
Ok(get_user(&user_id).await?)
}
}
+370
View File
@@ -0,0 +1,370 @@
use std::{
collections::HashSet,
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use anyhow::Context;
use command::run_komodo_command;
use komodo_client::{
api::{
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
update::Update,
user::action_user,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
alert::send_alerts,
api::{execute::ExecuteRequest, user::UserArgs},
config::core_config,
helpers::{
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_string,
},
query::get_variables_and_secrets,
random_string,
update::update_update,
},
resource::{self, 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 {
ExecuteRequest::RunAction(RunAction { action })
}
}
impl Resolve<ExecuteArgs> for BatchRunAction {
#[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for RunAction {
#[instrument(name = "RunAction", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut action = resource::get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Execute,
)
.await?;
// get the action state for the action (or insert default).
let action_state = action_states()
.action
.get_or_insert_default(&action.id)
.await;
// This will set action state back to default when dropped.
// Will also check to ensure action not already busy before updating.
let _action_guard =
action_state.update(|state| state.running = true)?;
let mut update = update.clone();
update_update(update.clone()).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;
// Wrap the file contents in the execution context.
*contents = full_contents(contents, &key, &secret);
let replacers =
interpolate(contents, &mut update, key.clone(), secret.clone())
.await?
.into_iter()
.collect::<Vec<_>>();
let file = format!("{}.ts", random_string(10));
let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() {
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 mut res = run_komodo_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!(
"deno run --allow-all{https_cert_flag} {}",
path.display()
),
)
.await;
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
.replace(&key, "<ACTION_API_KEY>");
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
.replace(&secret, "<ACTION_API_SECRET>");
cleanup_run(file + ".js", &path).await;
if let Err(e) = (DeleteApiKey { key })
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await
{
warn!(
"Failed to delete API key after action execution | {:#}",
e.error
);
};
update.logs.push(res);
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with update_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_client().updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_action_state_cache().await;
}
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)
}
}
async fn interpolate(
contents: &mut String,
update: &mut Update,
key: String,
secret: String,
) -> serror::Result<HashSet<(String, String)>> {
let mut vars_and_secrets = get_variables_and_secrets().await?;
vars_and_secrets
.secrets
.insert(String::from("ACTION_API_KEY"), key);
vars_and_secrets
.secrets
.insert(String::from("ACTION_API_SECRET"), secret);
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
contents,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(update, &global_replacers, &secret_replacers);
Ok(secret_replacers)
}
fn full_contents(contents: &str, key: &str, secret: &str) -> String {
let CoreConfig {
port, ssl_enabled, ..
} = core_config();
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 * as __YAML__ from 'jsr:@std/yaml';
import * as __TOML__ from 'jsr:@std/toml';
const YAML = {{
stringify: __YAML__.stringify,
parse: __YAML__.parse,
parseAll: __YAML__.parseAll,
parseDockerCompose: __YAML__.parse,
}}
const TOML = {{
stringify: __TOML__.stringify,
parse: __TOML__.parse,
parseResourceToml: __TOML__.parse,
parseCargoToml: __TOML__.parse,
}}
const komodo = KomodoClient('{base_url}', {{
type: 'api-key',
params: {{ key: '{key}', secret: '{secret}' }}
}});
async function main() {{
{contents}
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));
}}
Deno.exit(1)
}});"
)
}
/// Cleans up file at given path.
/// ALSO if $DENO_DIR is set,
/// will clean up the generated file matching "file"
async fn cleanup_run(file: String, path: &Path) {
if let Err(e) = fs::remove_file(path).await {
warn!(
"Failed to delete action file after action execution | {e:#}"
);
}
// If $DENO_DIR is set (will be in container),
// will clean up the generated file matching "file" (NOT under path)
let Some(deno_dir) = deno_dir() else {
return;
};
delete_file(deno_dir.join("gen/file"), file).await;
}
fn deno_dir() -> Option<&'static Path> {
static DENO_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
DENO_DIR
.get_or_init(|| {
let deno_dir = std::env::var("DENO_DIR").ok()?;
PathBuf::from_str(&deno_dir).ok()
})
.as_deref()
}
/// file is just the terminating file path,
/// it may be nested multiple folder under path,
/// this will find the nested file and delete it.
/// Assumes the file is only there once.
fn delete_file(
dir: PathBuf,
file: String,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send>>
{
Box::pin(async move {
let Ok(mut dir) = fs::read_dir(dir).await else {
return false;
};
// Collect the nested folders for recursing
// only after checking all the files in directory.
let mut folders = Vec::<PathBuf>::new();
while let Ok(Some(entry)) = dir.next_entry().await {
let Ok(meta) = entry.metadata().await else {
continue;
};
if meta.is_file() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
if name == file {
if let Err(e) = fs::remove_file(entry.path()).await {
warn!(
"Failed to clean up generated file after action execution | {e:#}"
);
};
return true;
}
} else {
folders.push(entry.path());
}
}
if folders.len() == 1 {
// unwrap ok, folders definitely is not empty
let folder = folders.pop().unwrap();
delete_file(folder, file).await
} else {
// Check folders with file.clone
for folder in folders {
if delete_file(folder, file.clone()).await {
return true;
}
}
false
}
})
}
+73
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,
resource::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,
)
.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)
}
}
+662
View File
@@ -0,0 +1,662 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
RunBuild,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
build::{Build, BuildConfig, ImageRegistryConfig},
builder::{Builder, BuilderConfig},
deployment::DeploymentState,
komodo_timestamp,
permission::PermissionLevel,
update::{Log, Update},
user::auto_redeploy_user,
},
};
use mungos::{
by_id::update_one_by_id,
find::find_collect,
mongodb::{
bson::{doc, to_bson, to_document},
options::FindOneOptions,
},
};
use periphery_client::api;
use resolver_api::Resolve;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
helpers::{
builder::{cleanup_builder_instance, get_builder_periphery},
channel::build_cancel_channel,
git_token,
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_extra_args,
interpolate_variables_secrets_into_string,
interpolate_variables_secrets_into_system_command,
},
query::{get_deployment_state, get_variables_and_secrets},
registry_token,
update::{init_execution_update, update_update},
},
resource::{self, refresh_build_state_cache},
state::{action_states, db_client},
};
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchRunBuild {
type Resource = Build;
fn single_request(build: String) -> ExecuteRequest {
ExecuteRequest::RunBuild(RunBuild { build })
}
}
impl Resolve<ExecuteArgs> for BatchRunBuild {
#[instrument(name = "BatchRunBuild", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for RunBuild {
#[instrument(name = "RunBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute,
)
.await?;
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").into());
}
// get the action state for the build (or insert default).
let action_state =
action_states().build.get_or_insert_default(&build.id).await;
// This will set action state back to default when dropped.
// Will also check to ensure build not already busy before updating.
let _action_guard =
action_state.update(|state| state.building = true)?;
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?;
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 registry_token =
validate_account_extract_registry_token(&build).await?;
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
let mut cancel_recv =
build_cancel_channel().receiver.resubscribe();
let build_id = build.id.clone();
let builder =
resource::get::<Builder>(&build.config.builder_id).await?;
let is_server_builder =
matches!(&builder.config, BuilderConfig::Server(_));
tokio::spawn(async move {
let poll = async {
loop {
let (incoming_build_id, mut update) = tokio::select! {
_ = cancel_clone.cancelled() => return Ok(()),
id = cancel_recv.recv() => id?
};
if incoming_build_id == build_id {
if is_server_builder {
update.push_error_log("Cancel acknowledged", "Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.");
} else {
update.push_simple_log("Cancel acknowledged", "The build cancellation has been queued, it may still take some time.");
}
update.finalize();
let id = update.id.clone();
if let Err(e) = update_update(update).await {
warn!("failed to modify Update {id} on db | {e:#}");
}
if !is_server_builder {
cancel_clone.cancel();
}
return Ok(());
}
}
#[allow(unreachable_code)]
anyhow::Ok(())
};
tokio::select! {
_ = cancel_clone.cancelled() => {}
_ = poll => {}
}
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
build.name.clone(),
Some(build.config.version),
builder,
&mut update,
)
.await
{
Ok(builder) => builder,
Err(e) => {
warn!(
"failed to get builder for build {} | {e:#}",
build.name
);
update.logs.push(Log::error(
"get builder",
format_serror(&e.context("failed to get builder").into()),
));
return handle_early_return(
update, build.id, build.name, false,
)
.await;
}
};
// INTERPOLATE VARIABLES
let secret_replacers = if !build.config.skip_secret_interp {
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut build.config.pre_build,
&mut global_replacers,
&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,
&secret_replacers,
);
secret_replacers
} else {
Default::default()
};
let commit_message = if !build.config.files_on_host
&& !build.config.repo.is_empty()
{
// CLONE REPO
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: 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(),
registry_token,
replacers: secret_replacers.into_iter().collect(),
// Push a commit hash tagged image
additional_tags: if update.commit_hash.is_empty() {
Default::default()
} else {
vec![update.commit_hash.clone()]
},
}) => res.context("failed at call to periphery to build"),
_ = cancel.cancelled() => {
info!("build cancelled during build, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
cleanup_builder_instance(cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
},
};
match res {
Ok(logs) => {
debug!("finished build");
update.logs.extend(logs);
}
Err(e) => {
warn!("error in build | {e:#}");
update.push_error_log(
"build",
format_serror(&e.context("failed to build").into()),
)
}
};
}
update.finalize();
let db = db_client();
if update.success {
let _ = db
.builds
.update_one(
doc! { "name": &build.name },
doc! { "$set": {
"config.version": to_bson(&build.config.version)
.context("failed at converting version to bson")?,
"info.last_built_at": komodo_timestamp(),
"info.built_hash": &update.commit_hash,
"info.built_message": commit_message
}},
)
.await;
}
// stop the cancel listening task from going forever
cancel.cancel();
// 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.
// 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_build_state_cache().await;
}
update_update(update.clone()).await?;
if update.success {
// don't hold response up for user
tokio::spawn(async move {
handle_post_build_redeploy(&build.id).await;
});
} else {
warn!("build unsuccessful, alerting...");
let target = update.target.clone();
let version = update.version;
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::BuildFailed {
id: build.id,
name: build.name,
version,
},
};
send_alerts(&[alert]).await
});
}
Ok(update.clone())
}
}
#[instrument(skip(update))]
async fn handle_early_return(
mut update: Update,
build_id: String,
build_name: String,
is_cancel: bool,
) -> serror::Result<Update> {
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_client().updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_build_state_cache().await;
}
update_update(update.clone()).await?;
if !update.success && !is_cancel {
warn!("build unsuccessful, alerting...");
let target = update.target.clone();
let version = update.version;
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::BuildFailed {
id: build_id,
name: build_name,
version,
},
};
send_alerts(&[alert]).await
});
}
Ok(update.clone())
}
pub async fn validate_cancel_build(
request: &ExecuteRequest,
) -> anyhow::Result<()> {
if let ExecuteRequest::CancelBuild(req) = request {
let build = resource::get::<Build>(&req.build).await?;
let db = db_client();
let (latest_build, latest_cancel) = tokio::try_join!(
db.updates
.find_one(doc! {
"operation": "RunBuild",
"target.id": &build.id,
},)
.with_options(
FindOneOptions::builder()
.sort(doc! { "start_ts": -1 })
.build()
)
.into_future(),
db.updates
.find_one(doc! {
"operation": "CancelBuild",
"target.id": &build.id,
},)
.with_options(
FindOneOptions::builder()
.sort(doc! { "start_ts": -1 })
.build()
)
.into_future()
)?;
match (latest_build, latest_cancel) {
(Some(build), Some(cancel)) => {
if cancel.start_ts > build.start_ts {
return Err(anyhow!("Build has already been cancelled"));
}
}
(None, _) => return Err(anyhow!("No build in progress")),
_ => {}
};
}
Ok(())
}
impl Resolve<ExecuteArgs> for CancelBuild {
#[instrument(name = "CancelBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute,
)
.await?;
// make sure the build is building
if !action_states()
.build
.get(&build.id)
.await
.and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default()
{
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",
);
update_update(update.clone()).await?;
build_cancel_channel()
.sender
.lock()
.await
.send((build.id, update.clone()))?;
// Make sure cancel is set to complete after some time in case
// no reciever is there to do it. Prevents update stuck in InProgress.
let update_id = update.id.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(e) = update_one_by_id(
&db_client().updates,
&update_id,
doc! { "$set": { "status": "Complete" } },
None,
)
.await
{
warn!(
"failed to set CancelBuild Update status Complete after timeout | {e:#}"
)
}
});
Ok(update)
}
}
#[instrument]
async fn handle_post_build_redeploy(build_id: &str) {
let Ok(redeploy_deployments) = find_collect(
&db_client().deployments,
doc! {
"config.image.params.build_id": build_id,
"config.redeploy_on_build": true
},
None,
)
.await
else {
return;
};
let futures =
redeploy_deployments
.into_iter()
.map(|deployment| async move {
let state =
get_deployment_state(&deployment).await.unwrap_or_default();
if state == DeploymentState::Running {
let req = super::ExecuteRequest::Deploy(Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
stop_time: None,
});
let user = auto_redeploy_user().to_owned();
let res = async {
let update = init_execution_update(&req, &user).await?;
Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
stop_time: None,
}
.resolve(&ExecuteArgs { user, update })
.await
}
.await;
Some((deployment.id.clone(), res))
} else {
None
}
});
for res in join_all(futures).await {
let Some((id, res)) = res else {
continue;
};
if let Err(e) = res {
warn!(
"failed post build redeploy for deployment {id}: {:#}",
e.error
);
}
}
}
/// This will make sure that a build with non-none image registry has an account attached,
/// and will check the core config for a token matching requirements.
/// Otherwise it is left to periphery.
async fn validate_account_extract_registry_token(
Build {
config:
BuildConfig {
image_registry:
ImageRegistryConfig {
domain, account, ..
},
..
},
..
}: &Build,
) -> 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}"
)
.into(),
);
}
let registry_token = registry_token(domain, account).await.with_context(
|| format!("Failed to get registry token in call to db. Stopping run. | {domain} | {account}"),
)?;
Ok(registry_token)
}
+741
View File
@@ -0,0 +1,741 @@
use std::{collections::HashSet, sync::OnceLock};
use anyhow::{Context, anyhow};
use cache::TimeoutCache;
use formatting::format_serror;
use komodo_client::{
api::execute::*,
entities::{
Version,
build::{Build, ImageRegistryConfig},
deployment::{
Deployment, DeploymentImage, extract_registry_domain,
},
get_image_name, komodo_timestamp, optional_string,
permission::PermissionLevel,
server::Server,
update::{Log, Update},
user::User,
},
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_extra_args,
interpolate_variables_secrets_into_string,
},
periphery_client,
query::get_variables_and_secrets,
registry_token,
update::update_update,
},
monitor::update_cache_for_server,
resource,
state::action_states,
};
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchDeploy {
type Resource = Deployment;
fn single_request(deployment: String) -> ExecuteRequest {
ExecuteRequest::Deploy(Deploy {
deployment,
stop_signal: None,
stop_time: None,
})
}
}
impl Resolve<ExecuteArgs> for BatchDeploy {
#[instrument(name = "BatchDeploy", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeploy>(&self.pattern, user)
.await?,
)
}
}
async fn setup_deployment_execution(
deployment: &str,
user: &User,
) -> anyhow::Result<(Deployment, Server)> {
let deployment = resource::get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Execute,
)
.await?;
if deployment.config.server_id.is_empty() {
return Err(anyhow!("Deployment has no Server configured"));
}
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
if !server.config.enabled {
return Err(anyhow!("Attached Server is not enabled"));
}
Ok((deployment, server))
}
impl Resolve<ExecuteArgs> for Deploy {
#[instrument(name = "Deploy", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut deployment, server) =
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.deploying = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
// This block resolves the attached Build to an actual versioned image
let (version, registry_token) = match &deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let build = resource::get::<Build>(build_id).await?;
let image_name = get_image_name(&build)
.context("failed to create image name")?;
let version = if version.is_none() {
build.config.version
} else {
*version
};
let version_str = version.to_string();
// Potentially add the build image_tag postfix
let version_str = if build.config.image_tag.is_empty() {
version_str
} else {
format!("{version_str}-{}", build.config.image_tag)
};
// replace image with corresponding build image.
deployment.config.image = DeploymentImage::Image {
image: format!("{image_name}:{version_str}"),
};
if build.config.image_registry.domain.is_empty() {
(version, None)
} else {
let ImageRegistryConfig {
domain, account, ..
} = build.config.image_registry;
if deployment.config.image_registry_account.is_empty() {
deployment.config.image_registry_account = account
}
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
};
(version, 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
};
(Version::default(), token)
}
};
// interpolate variables / secrets, returning the sanitizing replacers to send to
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
let secret_replacers = if !deployment.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 deployment.config.environment,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut deployment.config.ports,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut deployment.config.volumes,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut deployment.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut deployment.config.command,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
&secret_replacers,
);
secret_replacers
} else {
Default::default()
};
update.version = version;
update_update(update.clone()).await?;
match periphery_client(&server)?
.request(api::container::Deploy {
deployment,
stop_signal: self.stop_signal,
stop_time: self.stop_time,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
.await
{
Ok(log) => update.logs.push(log),
Err(e) => {
update.push_error_log(
"Deploy Container",
format_serror(&e.into()),
);
}
};
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
/// 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,
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()
.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()
.deployment
.get_or_insert_default(&deployment.id)
.await;
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.starting = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
.request(api::container::StartContainer {
name: deployment.name,
})
.await
{
Ok(log) => log,
Err(e) => Log::error(
"start container",
format_serror(&e.context("failed to start container").into()),
),
};
update.logs.push(log);
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for RestartDeployment {
#[instrument(name = "RestartDeployment", 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()
.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.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 log = match periphery_client(&server)?
.request(api::container::RestartContainer {
name: deployment.name,
})
.await
{
Ok(log) => log,
Err(e) => Log::error(
"restart container",
format_serror(
&e.context("failed to restart container").into(),
),
),
};
update.logs.push(log);
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for PauseDeployment {
#[instrument(name = "PauseDeployment", 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()
.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.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 log = match periphery_client(&server)?
.request(api::container::PauseContainer {
name: deployment.name,
})
.await
{
Ok(log) => log,
Err(e) => Log::error(
"pause container",
format_serror(&e.context("failed to pause container").into()),
),
};
update.logs.push(log);
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for UnpauseDeployment {
#[instrument(name = "UnpauseDeployment", 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()
.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.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 log = match periphery_client(&server)?
.request(api::container::UnpauseContainer {
name: deployment.name,
})
.await
{
Ok(log) => log,
Err(e) => Log::error(
"unpause container",
format_serror(
&e.context("failed to unpause container").into(),
),
),
};
update.logs.push(log);
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for StopDeployment {
#[instrument(name = "StopDeployment", 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()
.deployment
.get_or_insert_default(&deployment.id)
.await;
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.stopping = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?;
let log = match periphery_client(&server)?
.request(api::container::StopContainer {
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
.into(),
time: self
.time
.unwrap_or(deployment.config.termination_timeout)
.into(),
})
.await
{
Ok(log) => log,
Err(e) => Log::error(
"stop container",
format_serror(&e.context("failed to stop container").into()),
),
};
update.logs.push(log);
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl super::BatchExecute for BatchDestroyDeployment {
type Resource = Deployment;
fn single_request(deployment: String) -> ExecuteRequest {
ExecuteRequest::DestroyDeployment(DestroyDeployment {
deployment,
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(&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.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 log = match periphery_client(&server)?
.request(api::container::RemoveContainer {
name: deployment.name,
signal: self
.signal
.unwrap_or(deployment.config.termination_signal)
.into(),
time: self
.time
.unwrap_or(deployment.config.termination_timeout)
.into(),
})
.await
{
Ok(log) => log,
Err(e) => Log::error(
"stop container",
format_serror(&e.context("failed to stop container").into()),
),
};
update.logs.push(log);
update.finalize();
update_cache_for_server(&server).await;
update_update(update.clone()).await?;
Ok(update)
}
}
+308
View File
@@ -0,0 +1,308 @@
use std::{pin::Pin, time::Instant};
use anyhow::Context;
use axum::{Extension, Router, 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,
update::{Log, Update},
user::User,
},
};
use mungos::by_id::find_one_by_id;
use resolver_api::Resolve;
use response::JsonString;
use serde::{Deserialize, Serialize};
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::update::{init_execution_update, update_update},
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;
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, Resolve, EnumVariants,
)]
#[variant_derive(Debug)]
#[args(ExecuteArgs)]
#[response(JsonString)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
pub enum ExecuteRequest {
// ==== SERVER ====
StartContainer(StartContainer),
RestartContainer(RestartContainer),
PauseContainer(PauseContainer),
UnpauseContainer(UnpauseContainer),
StopContainer(StopContainer),
DestroyContainer(DestroyContainer),
StartAllContainers(StartAllContainers),
RestartAllContainers(RestartAllContainers),
PauseAllContainers(PauseAllContainers),
UnpauseAllContainers(UnpauseAllContainers),
StopAllContainers(StopAllContainers),
PruneContainers(PruneContainers),
DeleteNetwork(DeleteNetwork),
PruneNetworks(PruneNetworks),
DeleteImage(DeleteImage),
PruneImages(PruneImages),
DeleteVolume(DeleteVolume),
PruneVolumes(PruneVolumes),
PruneDockerBuilders(PruneDockerBuilders),
PruneBuildx(PruneBuildx),
PruneSystem(PruneSystem),
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
PullDeployment(PullDeployment),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// ==== BUILD ====
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
CancelBuild(CancelBuild),
// ==== REPO ====
CloneRepo(CloneRepo),
BatchCloneRepo(BatchCloneRepo),
PullRepo(PullRepo),
BatchPullRepo(BatchPullRepo),
BuildRepo(BuildRepo),
BatchBuildRepo(BatchBuildRepo),
CancelRepoBuild(CancelRepoBuild),
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
// ==== ACTION ====
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer),
// ==== ALERTER ====
TestAlerter(TestAlerter),
// ==== SYNC ====
RunSync(RunSync),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.layer(middleware::from_fn(auth_request))
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let res = match inner_handler(request, user).await? {
ExecutionResult::Single(update) => serde_json::to_string(&update)
.context("Failed to serialize Update")?,
ExecutionResult::Batch(res) => res,
};
Ok((TypedHeader(ContentType::json()), res))
}
pub enum ExecutionResult {
Single(Update),
/// The batch contents will be pre serialized here
Batch(String),
}
pub fn inner_handler(
request: ExecuteRequest,
user: User,
) -> 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?;
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:#}"
);
}
}
});
Ok(ExecutionResult::Single(update))
})
}
#[instrument(
name = "ExecuteRequest",
skip(user, update),
fields(
user_id = user.id,
update_id = update.id,
request = format!("{:?}", request.extract_variant()))
)
]
async fn task(
req_id: Uuid,
request: ExecuteRequest,
user: User,
update: Update,
) -> anyhow::Result<String> {
info!("/execute request {req_id} | user: {}", user.username);
let timer = Instant::now();
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:#}");
}
let elapsed = timer.elapsed();
debug!("/execute request {req_id} | resolve time: {elapsed:?}");
res
}
trait BatchExecute {
type Resource: KomodoResource;
fn single_request(name: String) -> ExecuteRequest;
}
async fn batch_execute<E: BatchExecute>(
pattern: &str,
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let resources = list_full_for_user_using_pattern::<E::Resource>(
pattern,
Default::default(),
user,
&[],
)
.await?;
let futures = resources.into_iter().map(|resource| {
let user = user.clone();
async move {
inner_handler(E::single_request(resource.name.clone()), user)
.await
.map(|r| {
let ExecutionResult::Single(update) = r else {
unreachable!()
};
update
})
.map_err(|e| BatchExecutionResponseItemErr {
name: resource.name,
error: e.into(),
})
.into()
}
});
Ok(join_all(futures).await)
}
+167
View File
@@ -0,0 +1,167 @@
use std::pin::Pin;
use formatting::{Color, bold, colored, format_serror, muted};
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
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};
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},
};
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchRunProcedure {
type Resource = Procedure;
fn single_request(procedure: String) -> ExecuteRequest {
ExecuteRequest::RunProcedure(RunProcedure { procedure })
}
}
impl Resolve<ExecuteArgs> for BatchRunProcedure {
#[instrument(name = "BatchRunProcedure", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for RunProcedure {
#[instrument(name = "RunProcedure", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
Ok(
resolve_inner(self.procedure, user.clone(), update.clone())
.await?,
)
}
}
fn resolve_inner(
procedure: String,
user: User,
mut update: Update,
) -> Pin<
Box<
dyn std::future::Future<Output = anyhow::Result<Update>> + Send,
>,
> {
Box::pin(async move {
let procedure = resource::get_check_permissions::<Procedure>(
&procedure,
&user,
PermissionLevel::Execute,
)
.await?;
// Need to push the initial log, as execute_procedure
// assumes first log is already created
// and will panic otherwise.
update.push_simple_log(
"Execute procedure",
format!(
"{}: executing procedure '{}'",
muted("INFO"),
bold(&procedure.name)
),
);
// get the action state for the procedure (or insert default).
let action_state = action_states()
.procedure
.get_or_insert_default(&procedure.id)
.await;
// This will set action state back to default when dropped.
// Will also check to ensure procedure not already busy before updating.
let _action_guard =
action_state.update(|state| state.running = true)?;
update_update(update.clone()).await?;
let update = Mutex::new(update);
let res = execute_procedure(&procedure, &update).await;
let mut update = update.into_inner();
match res {
Ok(_) => {
update.push_simple_log(
"Execution ok",
format!(
"{}: The procedure has {} with no errors",
muted("INFO"),
colored("completed", Color::Green)
),
);
}
Err(e) => update
.push_error_log("execution error", format_serror(&e.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_client().updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_procedure_state_cache().await;
}
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)
})
}
+750
View File
@@ -0,0 +1,750 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use komodo_client::{
api::{execute::*, write::RefreshRepoCache},
entities::{
alert::{Alert, AlertData, SeverityLevel},
builder::{Builder, BuilderConfig},
komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
server::Server,
update::{Log, Update},
},
};
use mungos::{
by_id::update_one_by_id,
mongodb::{
bson::{doc, to_document},
options::FindOneOptions,
},
};
use periphery_client::api;
use resolver_api::Resolve;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
api::write::WriteArgs,
helpers::{
builder::{cleanup_builder_instance, get_builder_periphery},
channel::repo_cancel_channel,
git_token,
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_string,
interpolate_variables_secrets_into_system_command,
},
periphery_client,
query::get_variables_and_secrets,
update::update_update,
},
resource::{self, refresh_repo_state_cache},
state::{action_states, db_client},
};
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchCloneRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<ExecuteArgs> for BatchCloneRepo {
#[instrument(name = "BatchCloneRepo", skip( user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for CloneRepo {
#[instrument(name = "CloneRepo", skip( user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
// get the action state for the repo (or insert default).
let action_state =
action_states().repo.get_or_insert_default(&repo.id).await;
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let _action_guard =
action_state.update(|state| state.cloning = true)?;
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").into());
}
let git_token = git_token(
&repo.config.git_provider,
&repo.config.git_account,
|https| repo.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. | {} | {}", repo.config.git_provider, repo.config.git_account),
)?;
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let periphery = periphery_client(&server)?;
// interpolate variables / secrets, returning the sanitizing replacers to send to
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
let secret_replacers =
interpolate(&mut repo, &mut update).await?;
let logs = match periphery
.request(api::git::CloneRepo {
args: (&repo).into(),
git_token,
environment: repo.config.env_vars()?,
env_file_path: repo.config.env_file_path,
skip_secret_interp: repo.config.skip_secret_interp,
replacers: secret_replacers.into_iter().collect(),
})
.await
{
Ok(res) => res.logs,
Err(e) => {
vec![Log::error(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
)]
}
};
update.logs.extend(logs);
update.finalize();
if update.success {
update_last_pulled_time(&repo.name).await;
}
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(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}
impl super::BatchExecute for BatchPullRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<ExecuteArgs> for BatchPullRepo {
#[instrument(name = "BatchPullRepo", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for PullRepo {
#[instrument(name = "PullRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
// get the action state for the repo (or insert default).
let action_state =
action_states().repo.get_or_insert_default(&repo.id).await;
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let _action_guard =
action_state.update(|state| state.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").into());
}
let git_token = git_token(
&repo.config.git_provider,
&repo.config.git_account,
|https| repo.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. | {} | {}", repo.config.git_provider, repo.config.git_account),
)?;
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let periphery = periphery_client(&server)?;
// interpolate variables / secrets, returning the sanitizing replacers to send to
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
let secret_replacers =
interpolate(&mut repo, &mut update).await?;
let logs = match periphery
.request(api::git::PullRepo {
args: (&repo).into(),
git_token,
environment: repo.config.env_vars()?,
env_file_path: repo.config.env_file_path,
skip_secret_interp: repo.config.skip_secret_interp,
replacers: secret_replacers.into_iter().collect(),
})
.await
{
Ok(res) => {
update.commit_hash = res.commit_hash.unwrap_or_default();
res.logs
}
Err(e) => {
vec![Log::error(
"pull repo",
format_serror(&e.context("failed to pull repo").into()),
)]
}
};
update.logs.extend(logs);
update.finalize();
if update.success {
update_last_pulled_time(&repo.name).await;
}
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(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}
#[instrument(skip_all, fields(update_id = update.id))]
async fn handle_server_update_return(
update: 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,
// but will fail to update cache in that case.
if let Ok(update_doc) = to_document(&update) {
let _ = update_one_by_id(
&db_client().updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_repo_state_cache().await;
}
update_update(update.clone()).await?;
Ok(update)
}
#[instrument]
async fn update_last_pulled_time(repo_name: &str) {
let res = db_client()
.repos
.update_one(
doc! { "name": repo_name },
doc! { "$set": { "info.last_pulled_at": komodo_timestamp() } },
)
.await;
if let Err(e) = res {
warn!(
"failed to update repo last_pulled_at | repo: {repo_name} | {e:#}",
);
}
}
impl super::BatchExecute for BatchBuildRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<ExecuteArgs> for BatchBuildRepo {
#[instrument(name = "BatchBuildRepo", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for BuildRepo {
#[instrument(name = "BuildRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
if repo.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to BuildRepo").into());
}
// get the action state for the repo (or insert default).
let action_state =
action_states().repo.get_or_insert_default(&repo.id).await;
// This will set action state back to default when dropped.
// Will also check to ensure repo not already busy before updating.
let _action_guard =
action_state.update(|state| state.building = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let git_token = git_token(
&repo.config.git_provider,
&repo.config.git_account,
|https| repo.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. | {} | {}", repo.config.git_provider, repo.config.git_account),
)?;
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
let mut cancel_recv =
repo_cancel_channel().receiver.resubscribe();
let repo_id = repo.id.clone();
let builder =
resource::get::<Builder>(&repo.config.builder_id).await?;
let is_server_builder =
matches!(&builder.config, BuilderConfig::Server(_));
tokio::spawn(async move {
let poll = async {
loop {
let (incoming_repo_id, mut update) = tokio::select! {
_ = cancel_clone.cancelled() => return Ok(()),
id = cancel_recv.recv() => id?
};
if incoming_repo_id == repo_id {
if is_server_builder {
update.push_error_log("Cancel acknowledged", "Repo Build cancellation is not possible on server builders at this time. Use an AWS builder to enable this feature.");
} else {
update.push_simple_log("Cancel acknowledged", "The repo build cancellation has been queued, it may still take some time.");
}
update.finalize();
let id = update.id.clone();
if let Err(e) = update_update(update).await {
warn!("failed to modify Update {id} on db | {e:#}");
}
if !is_server_builder {
cancel_clone.cancel();
}
return Ok(());
}
}
#[allow(unreachable_code)]
anyhow::Ok(())
};
tokio::select! {
_ = cancel_clone.cancelled() => {}
_ = poll => {}
}
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
repo.name.clone(),
None,
builder,
&mut update,
)
.await
{
Ok(builder) => builder,
Err(e) => {
warn!("failed to get builder for repo {} | {e:#}", repo.name);
update.logs.push(Log::error(
"get builder",
format_serror(&e.context("failed to get builder").into()),
));
return handle_builder_early_return(
update, repo.id, repo.name, false,
)
.await;
}
};
// CLONE REPO
// interpolate variables / secrets, returning the sanitizing replacers to send to
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
let secret_replacers =
interpolate(&mut repo, &mut update).await?;
let res = tokio::select! {
res = periphery
.request(api::git::CloneRepo {
args: (&repo).into(),
git_token,
environment: repo.config.env_vars()?,
env_file_path: repo.config.env_file_path,
skip_secret_interp: repo.config.skip_secret_interp,
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(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_builder_early_return(update, repo.id, repo.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();
res.commit_message.unwrap_or_default()
}
Err(e) => {
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
};
update.finalize();
let db = db_client();
if update.success {
let _ = db
.repos
.update_one(
doc! { "name": &repo.name },
doc! { "$set": {
"info.last_built_at": komodo_timestamp(),
"info.built_hash": &update.commit_hash,
"info.built_message": commit_message
}},
)
.await;
}
// stop the cancel listening task from going forever
cancel.cancel();
// 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.
// 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_repo_state_cache().await;
}
update_update(update.clone()).await?;
if !update.success {
warn!("repo build unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::RepoBuildFailed {
id: repo.id,
name: repo.name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
}
}
#[instrument(skip(update))]
async fn handle_builder_early_return(
mut update: Update,
repo_id: String,
repo_name: String,
is_cancel: bool,
) -> serror::Result<Update> {
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_client().updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_repo_state_cache().await;
}
update_update(update.clone()).await?;
if !update.success && !is_cancel {
warn!("repo build unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::RepoBuildFailed {
id: repo_id,
name: repo_name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
}
#[instrument(skip_all)]
pub async fn validate_cancel_repo_build(
request: &ExecuteRequest,
) -> anyhow::Result<()> {
if let ExecuteRequest::CancelRepoBuild(req) = request {
let repo = resource::get::<Repo>(&req.repo).await?;
let db = db_client();
let (latest_build, latest_cancel) = tokio::try_join!(
db.updates
.find_one(doc! {
"operation": "BuildRepo",
"target.id": &repo.id,
},)
.with_options(
FindOneOptions::builder()
.sort(doc! { "start_ts": -1 })
.build()
)
.into_future(),
db.updates
.find_one(doc! {
"operation": "CancelRepoBuild",
"target.id": &repo.id,
},)
.with_options(
FindOneOptions::builder()
.sort(doc! { "start_ts": -1 })
.build()
)
.into_future()
)?;
match (latest_build, latest_cancel) {
(Some(build), Some(cancel)) => {
if cancel.start_ts > build.start_ts {
return Err(anyhow!(
"Repo build has already been cancelled"
));
}
}
(None, _) => return Err(anyhow!("No repo build in progress")),
_ => {}
};
}
Ok(())
}
impl Resolve<ExecuteArgs> for CancelRepoBuild {
#[instrument(name = "CancelRepoBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
// make sure the build is building
if !action_states()
.repo
.get(&repo.id)
.await
.and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default()
{
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",
);
update_update(update.clone()).await?;
repo_cancel_channel()
.sender
.lock()
.await
.send((repo.id, update.clone()))?;
// Make sure cancel is set to complete after some time in case
// no reciever is there to do it. Prevents update stuck in InProgress.
let update_id = update.id.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(e) = update_one_by_id(
&db_client().updates,
&update_id,
doc! { "$set": { "status": "Complete" } },
None,
)
.await
{
warn!(
"failed to set CancelRepoBuild Update status Complete after timeout | {e:#}"
)
}
});
Ok(update)
}
}
async fn interpolate(
repo: &mut Repo,
update: &mut Update,
) -> anyhow::Result<HashSet<(String, String)>> {
if !repo.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 repo.config.environment,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut repo.config.on_clone,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut repo.config.on_pull,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
update,
&global_replacers,
&secret_replacers,
);
Ok(secret_replacers)
} else {
Ok(Default::default())
}
}
File diff suppressed because it is too large Load Diff
+156
View File
@@ -0,0 +1,156 @@
use anyhow::{Context, anyhow};
use formatting::format_serror;
use komodo_client::{
api::{execute::LaunchServer, write::CreateServer},
entities::{
permission::PermissionLevel,
server::PartialServerConfig,
server_template::{ServerTemplate, ServerTemplateConfig},
update::Update,
},
};
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
cloud::{
aws::ec2::launch_ec2_instance, hetzner::launch_hetzner_server,
},
helpers::update::update_update,
resource,
state::db_client,
};
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for LaunchServer {
#[instrument(name = "LaunchServer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
// validate name isn't already taken by another server
if db_client()
.servers
.find_one(doc! {
"name": &self.name
})
.await
.context("failed to query db for servers")?
.is_some()
{
return Err(anyhow!("name is already taken").into());
}
let template = resource::get_check_permissions::<ServerTemplate>(
&self.server_template,
user,
PermissionLevel::Execute,
)
.await?;
let mut update = update.clone();
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(&self.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 {} on ip {}",
self.name, 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(&self.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 {} on ip {}",
self.name, 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 (CreateServer {
name: self.name,
config,
})
.resolve(&WriteArgs { user: user.clone() })
.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.error.context("failed to create server").into(),
),
);
}
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
+646
View File
@@ -0,0 +1,646 @@
use std::collections::HashSet;
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::{execute::*, write::RefreshStackCache},
entities::{
permission::PermissionLevel,
server::Server,
stack::{Stack, StackInfo},
update::{Log, Update},
},
};
use mungos::mongodb::bson::{doc, to_document};
use periphery_client::api::compose::*;
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_extra_args,
interpolate_variables_secrets_into_string,
interpolate_variables_secrets_into_system_command,
},
periphery_client,
query::get_variables_and_secrets,
update::{add_update_without_send, update_update},
},
monitor::update_cache_for_server,
resource,
stack::{execute::execute_compose, get_stack_and_server},
state::{action_states, db_client},
};
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<ExecuteArgs> for BatchDeployStack {
#[instrument(name = "BatchDeployStack", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for DeployStack {
#[instrument(name = "DeployStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut stack, server) = get_stack_and_server(
&self.stack,
user,
PermissionLevel::Execute,
true,
)
.await?;
// 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.deploying = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
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 = 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),
)?;
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, returning the sanitizing replacers to send to
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
let secret_replacers = 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,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut stack.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut stack.config.build_extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut stack.config.pre_deploy,
&mut global_replacers,
&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,
&secret_replacers,
);
secret_replacers
} else {
Default::default()
};
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(),
services: self.services,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
.await?;
update.logs.extend(logs);
let update_info = async {
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,
// as it may have changed since the last deploy.
let project_name = stack.project_name(true);
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(),
)
} else {
(
stack.info.deployed_services,
stack.info.deployed_contents,
stack.info.deployed_config,
stack.info.deployed_hash,
stack.info.deployed_message,
)
};
let info = StackInfo {
missing_files,
deployed_project_name: project_name.into(),
deployed_services,
deployed_contents,
deployed_config,
deployed_hash,
deployed_message,
latest_services,
remote_contents: stack
.config
.file_contents
.is_empty()
.then_some(file_contents),
remote_errors: stack
.config
.file_contents
.is_empty()
.then_some(remote_errors),
latest_hash: commit_hash,
latest_message: commit_message,
};
let info = to_document(&info)
.context("failed to serialize stack info to bson")?;
db_client()
.stacks
.update_one(
doc! { "name": &stack.name },
doc! { "$set": { "info": info } },
)
.await
.context("failed to update stack info on db")?;
anyhow::Ok(())
};
// This will be weird with single service deploys. Come back to it.
if let Err(e) = update_info.await {
update.push_error_log(
"refresh stack info",
format_serror(
&e.context("failed to refresh stack info on db").into(),
),
)
}
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(&server).await;
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl super::BatchExecute for BatchDeployStackIfChanged {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack,
stop_time: None,
})
}
}
impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
#[instrument(name = "BatchDeployStackIfChanged", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStackIfChanged>(
&self.pattern,
user,
)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for DeployStackIfChanged {
#[instrument(name = "DeployStackIfChanged", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Execute,
)
.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,
&stack.info.remote_contents,
) {
(Some(deployed_contents), Some(latest_contents)) => {
let changed = || {
for latest in latest_contents {
let Some(deployed) = deployed_contents
.iter()
.find(|c| c.path == latest.path)
else {
return true;
};
if latest.contents != deployed.contents {
return true;
}
}
false
};
changed()
}
(None, _) => true,
_ => false,
};
let mut update = update.clone();
if !changed {
update.push_simple_log(
"Diff compose files",
String::from("Deploy cancelled after no changes detected."),
);
update.finalize();
return Ok(update);
}
// Don't actually send it here, let the handler send it after it can set action state.
// This is usually done in crate::helpers::update::init_execution_update.
update.id = add_update_without_send(&update).await?;
DeployStack {
stack: stack.name,
services: Vec::new(),
stop_time: self.stop_time,
}
.resolve(&ExecuteArgs {
user: user.clone(),
update,
})
.await
}
}
pub async fn pull_stack_inner(
mut stack: Stack,
services: Vec<String>,
server: &Server,
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 = 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),
)?;
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,
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,
true,
)
.await?;
// 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,
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>(
&self.stack,
self.services,
user,
|state| {
state.restarting = true;
},
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<ExecuteArgs> for PauseStack {
#[instrument(name = "PauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<PauseStack>(
&self.stack,
self.services,
user,
|state| state.pausing = true,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<ExecuteArgs> for UnpauseStack {
#[instrument(name = "UnpauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<UnpauseStack>(
&self.stack,
self.services,
user,
|state| state.unpausing = true,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<ExecuteArgs> for StopStack {
#[instrument(name = "StopStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<StopStack>(
&self.stack,
self.services,
user,
|state| state.stopping = true,
update.clone(),
self.stop_time,
)
.await
.map_err(Into::into)
}
}
impl super::BatchExecute for BatchDestroyStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
services: Vec::new(),
remove_orphans: false,
stop_time: None,
})
}
}
impl Resolve<ExecuteArgs> for BatchDestroyStack {
#[instrument(name = "BatchDestroyStack", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
.await
.map_err(Into::into)
}
}
impl Resolve<ExecuteArgs> for DestroyStack {
#[instrument(name = "DestroyStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<DestroyStack>(
&self.stack,
self.services,
user,
|state| state.destroying = true,
update.clone(),
(self.stop_time, self.remove_orphans),
)
.await
.map_err(Into::into)
}
}
+561
View File
@@ -0,0 +1,561 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, anyhow};
use formatting::{Color, colored, format_serror};
use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{
self, ResourceTargetVariant,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
komodo_timestamp,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
update::{Log, Update},
user::sync_user,
},
};
use mongo_indexed::doc;
use mungos::{by_id::update_one_by_id, mongodb::bson::oid::ObjectId};
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{query::get_id_to_tags, update::update_update},
resource,
state::{action_states, db_client},
sync::{
AllResourcesById, ResourceSyncTrait,
deploy::{
SyncDeployParams, build_deploy_cache, deploy_from_cache,
},
execute::{ExecuteResourceSync, get_updates_for_execution},
remote::RemoteResources,
},
};
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,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let RunSync {
sync,
resource_type: match_resource_type,
resources: match_resources,
} = self;
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, user, PermissionLevel::Execute)
.await?;
// get the action state for the sync (or insert default).
let action_state = action_states()
.resource_sync
.get_or_insert_default(&sync.id)
.await;
// This will set action state back to default when dropped.
// Will also check to ensure sync not already busy before updating.
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?;
let RemoteResources {
resources,
logs,
hash,
message,
file_errors,
..
} = crate::sync::remote::get_remote_resources(&sync)
.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.").into(),
);
}
let resources = resources?;
let id_to_tags = get_id_to_tags(None).await?;
let all_resources = AllResourcesById::load().await?;
// Convert all match_resources to names
let match_resources = match_resources.map(|resources| {
resources
.into_iter()
.filter_map(|name_or_id| {
let Some(resource_type) = match_resource_type else {
return Some(name_or_id);
};
match ObjectId::from_str(&name_or_id) {
Ok(_) => match resource_type {
ResourceTargetVariant::Alerter => all_resources
.alerters
.get(&name_or_id)
.map(|a| a.name.clone()),
ResourceTargetVariant::Build => all_resources
.builds
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Builder => all_resources
.builders
.get(&name_or_id)
.map(|b| b.name.clone()),
ResourceTargetVariant::Deployment => all_resources
.deployments
.get(&name_or_id)
.map(|d| d.name.clone()),
ResourceTargetVariant::Procedure => all_resources
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
.map(|r| r.name.clone()),
ResourceTargetVariant::Server => all_resources
.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)
.map(|s| s.name.clone()),
ResourceTargetVariant::ResourceSync => all_resources
.syncs
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::System => None,
},
Err(_) => Some(name_or_id),
}
})
.collect::<Vec<_>>()
});
let deployments_by_name = all_resources
.deployments
.values()
.filter(|deployment| {
Deployment::include_resource(
&deployment.name,
&deployment.config,
match_resource_type,
match_resources.as_deref(),
&deployment.tags,
&id_to_tags,
&sync.config.match_tags,
)
})
.map(|deployment| (deployment.name.clone(), deployment.clone()))
.collect::<HashMap<_, _>>();
let stacks_by_name = all_resources
.stacks
.values()
.filter(|stack| {
Stack::include_resource(
&stack.name,
&stack.config,
match_resource_type,
match_resources.as_deref(),
&stack.tags,
&id_to_tags,
&sync.config.match_tags,
)
})
.map(|stack| (stack.name.clone(), stack.clone()))
.collect::<HashMap<_, _>>();
let deploy_cache = build_deploy_cache(SyncDeployParams {
deployments: &resources.deployments,
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 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?
} 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?
} else {
Default::default()
};
let deployment_deltas = if sync.config.include_resources {
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?
} 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?
} 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?
} else {
Default::default()
};
let procedure_deltas = if sync.config.include_resources {
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?
} 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?
} 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?
} 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?
} else {
Default::default()
};
let server_template_deltas = if sync.config.include_resources {
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?
} else {
Default::default()
};
let resource_sync_deltas = if sync.config.include_resources {
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?
} else {
Default::default()
};
let (
variables_to_create,
variables_to_update,
variables_to_delete,
) = if match_resource_type.is_none()
&& match_resources.is_none()
&& sync.config.include_variables
{
crate::sync::variables::get_updates_for_execution(
resources.variables,
delete,
)
.await?
} else {
Default::default()
};
let (
user_groups_to_create,
user_groups_to_update,
user_groups_to_delete,
) = if match_resource_type.is_none()
&& match_resources.is_none()
&& sync.config.include_user_groups
{
crate::sync::user_groups::get_updates_for_execution(
resources.user_groups,
delete,
&all_resources,
)
.await?
} else {
Default::default()
};
if deploy_cache.is_empty()
&& resource_sync_deltas.no_changes()
&& server_template_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()
&& variables_to_create.is_empty()
&& variables_to_update.is_empty()
&& variables_to_delete.is_empty()
{
update.push_simple_log(
"No Changes",
format!(
"{}. exiting.",
colored("nothing to do", Color::Green)
),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
// =================
// No deps
maybe_extend(
&mut update.logs,
crate::sync::variables::run_updates(
variables_to_create,
variables_to_update,
variables_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
crate::sync::user_groups::run_updates(
user_groups_to_create,
user_groups_to_update,
user_groups_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
);
maybe_extend(
&mut update.logs,
ServerTemplate::execute_sync_updates(server_template_deltas)
.await,
);
maybe_extend(
&mut update.logs,
Server::execute_sync_updates(server_deltas).await,
);
maybe_extend(
&mut update.logs,
Alerter::execute_sync_updates(alerter_deltas).await,
);
maybe_extend(
&mut update.logs,
Action::execute_sync_updates(action_deltas).await,
);
// Dependent on server
maybe_extend(
&mut update.logs,
Builder::execute_sync_updates(builder_deltas).await,
);
maybe_extend(
&mut update.logs,
Repo::execute_sync_updates(repo_deltas).await,
);
// Dependant on builder
maybe_extend(
&mut update.logs,
Build::execute_sync_updates(build_deltas).await,
);
// Dependant on server / build
maybe_extend(
&mut update.logs,
Deployment::execute_sync_updates(deployment_deltas).await,
);
// stack only depends on server, but maybe will depend on build later.
maybe_extend(
&mut update.logs,
Stack::execute_sync_updates(stack_deltas).await,
);
// Dependant on everything
maybe_extend(
&mut update.logs,
Procedure::execute_sync_updates(procedure_deltas).await,
);
// Execute the deploy cache
deploy_from_cache(deploy_cache, &mut update.logs).await;
let db = db_client();
if let Err(e) = update_one_by_id(
&db.resource_syncs,
&sync.id,
doc! {
"$set": {
"info.last_sync_ts": komodo_timestamp(),
"info.last_sync_hash": hash,
"info.last_sync_message": message,
}
},
None,
)
.await
{
warn!(
"failed to update resource sync {} info after sync | {e:#}",
sync.name
)
}
if let Err(e) = (RefreshResourceSyncPending { sync: sync.id })
.resolve(&WriteArgs {
user: sync_user().to_owned(),
})
.await
{
warn!(
"failed to refresh sync {} after run | {:#}",
sync.name, e.error
);
update.push_error_log(
"refresh sync",
format_serror(
&e.error
.context("failed to refresh sync pending after run")
.into(),
),
);
}
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
fn maybe_extend(logs: &mut Vec<Log>, log: Option<Log>) {
if let Some(log) = log {
logs.push(log);
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod auth;
pub mod execute;
pub mod read;
pub mod user;
pub mod write;
+137
View File
@@ -0,0 +1,137 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
action::{
Action, ActionActionState, ActionListItem, ActionState,
},
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{action_state_cache, action_states},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAction {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Action> {
Ok(
resource::get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListActions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ActionListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Action>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullActions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullActionsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Action>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetActionActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ActionActionState> {
let action = resource::get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.action
.get(&action.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetActionsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
user,
&[],
)
.await
.context("failed to get actions from db")?;
let mut res = GetActionsSummaryResponse::default();
let cache = action_state_cache();
let action_states = action_states();
for action in actions {
res.total += 1;
match (
cache.get(&action.id).await.unwrap_or_default(),
action_states
.action
.get(&action.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.running => {
res.running += 1;
}
(ActionState::Ok, _) => res.ok += 1,
(ActionState::Failed, _) => res.failed += 1,
(ActionState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the running state, since that comes from action states
(ActionState::Running, _) => unreachable!(),
}
}
Ok(res)
}
}
+88
View File
@@ -0,0 +1,88 @@
use anyhow::Context;
use komodo_client::{
api::read::{
GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,
},
entities::{
deployment::Deployment, server::Server, stack::Stack,
sync::ResourceSync,
},
};
use mungos::{
by_id::find_one_by_id,
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use resolver_api::Resolve;
use crate::{
config::core_config, resource::get_resource_ids_for_user,
state::db_client,
};
use super::ReadArgs;
const NUM_ALERTS_PER_PAGE: u64 = 100;
impl Resolve<ReadArgs> for ListAlerts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListAlertsResponse> {
let mut query = self.query.unwrap_or_default();
if !user.admin && !core_config().transparent_mode {
let server_ids =
get_resource_ids_for_user::<Server>(user).await?;
let stack_ids =
get_resource_ids_for_user::<Stack>(user).await?;
let deployment_ids =
get_resource_ids_for_user::<Deployment>(user).await?;
let sync_ids =
get_resource_ids_for_user::<ResourceSync>(user).await?;
query.extend(doc! {
"$or": [
{ "target.type": "Server", "target.id": { "$in": &server_ids } },
{ "target.type": "Stack", "target.id": { "$in": &stack_ids } },
{ "target.type": "Deployment", "target.id": { "$in": &deployment_ids } },
{ "target.type": "ResourceSync", "target.id": { "$in": &sync_ids } },
]
});
}
let alerts = find_collect(
&db_client().alerts,
query,
FindOptions::builder()
.sort(doc! { "ts": -1 })
.limit(NUM_ALERTS_PER_PAGE as i64)
.skip(self.page * NUM_ALERTS_PER_PAGE)
.build(),
)
.await
.context("failed to get alerts from db")?;
let next_page = if alerts.len() < NUM_ALERTS_PER_PAGE as usize {
None
} else {
Some((self.page + 1) as i64)
};
let res = ListAlertsResponse { next_page, alerts };
Ok(res)
}
}
impl Resolve<ReadArgs> for GetAlert {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetAlertResponse> {
Ok(
find_one_by_id(&db_client().alerts, &self.id)
.await
.context("failed to query db for alert")?
.context("no alert found with given id")?,
)
}
}
+96
View File
@@ -0,0 +1,96 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
alerter::{Alerter, AlerterListItem},
permission::PermissionLevel,
},
};
use mongo_indexed::Document;
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, resource, state::db_client,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAlerter {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Alerter> {
Ok(
resource::get_check_permissions::<Alerter>(
&self.alerter,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListAlerters {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<AlerterListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Alerter>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullAlerters {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullAlertersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Alerter>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetAlertersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetAlertersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Alerter,
>(user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let total = db_client()
.alerters
.count_documents(query)
.await
.context("failed to count all alerter documents")?;
let res = GetAlertersSummaryResponse {
total: total as u32,
};
Ok(res)
}
}
+373
View File
@@ -0,0 +1,373 @@
use std::collections::{HashMap, HashSet};
use anyhow::Context;
use async_timing_util::unix_timestamp_ms;
use futures::TryStreamExt;
use komodo_client::{
api::read::*,
entities::{
Operation,
build::{Build, BuildActionState, BuildListItem, BuildState},
config::core::CoreConfig,
permission::PermissionLevel,
update::UpdateStatus,
},
};
use mungos::{
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{
action_states, build_state_cache, db_client, github_client,
},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuild {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Build> {
Ok(
resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListBuilds {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Build>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullBuilds {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullBuildsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Build>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetBuildActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<BuildActionState> {
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.build
.get(&build.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetBuildsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildsSummaryResponse> {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
user,
&[],
)
.await
.context("failed to get all builds")?;
let mut res = GetBuildsSummaryResponse::default();
let cache = build_state_cache();
let action_states = action_states();
for build in builds {
res.total += 1;
match (
cache.get(&build.id).await.unwrap_or_default(),
action_states
.build
.get(&build.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.building => {
res.building += 1;
}
(BuildState::Ok, _) => res.ok += 1,
(BuildState::Failed, _) => res.failed += 1,
(BuildState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the building state, since that comes from action states
(BuildState::Building, _) => unreachable!(),
}
}
Ok(res)
}
}
const ONE_DAY_MS: i64 = 86400000;
impl Resolve<ReadArgs> for GetBuildMonthlyStats {
async fn resolve(
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 - self.page as i64 * 30 * ONE_DAY_MS;
let open_ts = close_ts - 30 * ONE_DAY_MS;
let mut build_updates = db_client()
.updates
.find(doc! {
"start_ts": {
"$gte": open_ts,
"$lt": close_ts
},
"operation": Operation::RunBuild.to_string(),
})
.await
.context("failed to get updates cursor")?;
let mut days = HashMap::<i64, BuildStatsDay>::with_capacity(32);
let mut curr = open_ts;
while curr < close_ts {
let stats = BuildStatsDay {
ts: curr as f64,
..Default::default()
};
days.insert(curr, stats);
curr += ONE_DAY_MS;
}
while let Some(update) = build_updates.try_next().await? {
if let Some(end_ts) = update.end_ts {
let day = update.start_ts - update.start_ts % ONE_DAY_MS;
let entry = days.entry(day).or_default();
entry.count += 1.0;
entry.time += ms_to_hour(end_ts - update.start_ts);
}
}
Ok(GetBuildMonthlyStatsResponse::new(
days.into_values().collect(),
))
}
}
const MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0;
fn ms_to_hour(duration: i64) -> f64 {
duration as f64 / MS_TO_HOUR_DIVISOR
}
impl Resolve<ReadArgs> for ListBuildVersions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildVersionResponseItem>> {
let ListBuildVersions {
build,
major,
minor,
patch,
limit,
} = self;
let build = resource::get_check_permissions::<Build>(
&build,
user,
PermissionLevel::Read,
)
.await?;
let mut filter = doc! {
"target": {
"type": "Build",
"id": build.id
},
"operation": Operation::RunBuild.to_string(),
"status": UpdateStatus::Complete.to_string(),
"success": true
};
if let Some(major) = major {
filter.insert("version.major", major);
}
if let Some(minor) = minor {
filter.insert("version.minor", minor);
}
if let Some(patch) = patch {
filter.insert("version.patch", patch);
}
let versions = find_collect(
&db_client().updates,
filter,
FindOptions::builder()
.sort(doc! { "_id": -1 })
.limit(limit)
.build(),
)
.await
.context("failed to pull versions from mongo")?
.into_iter()
.map(|u| (u.version, u.start_ts))
.filter(|(v, _)| !v.is_none())
.map(|(version, ts)| BuildVersionResponseItem { version, ts })
.collect();
Ok(versions)
}
}
impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
async fn resolve(
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>(
self.query, user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
for build in builds {
for extra_arg in build.config.extra_args {
res.insert(extra_arg);
}
}
let mut res = res.into_iter().collect::<Vec<_>>();
res.sort();
Ok(res)
}
}
impl Resolve<ReadArgs> for GetBuildWebhookEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildWebhookEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
};
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read,
)
.await?;
if build.config.git_provider != "github.com"
|| build.config.repo.is_empty()
{
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(GetBuildWebhookEnabledResponse {
managed: true,
enabled: true,
});
}
}
Ok(GetBuildWebhookEnabledResponse {
managed: true,
enabled: false,
})
}
}
+96
View File
@@ -0,0 +1,96 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
builder::{Builder, BuilderListItem},
permission::PermissionLevel,
},
};
use mongo_indexed::Document;
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, resource, state::db_client,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuilder {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Builder> {
Ok(
resource::get_check_permissions::<Builder>(
&self.builder,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListBuilders {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuilderListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Builder>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullBuilders {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullBuildersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Builder>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetBuildersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Builder,
>(user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let total = db_client()
.builders
.count_documents(query)
.await
.context("failed to count all builder documents")?;
let res = GetBuildersSummaryResponse {
total: total as u32,
};
Ok(res)
}
}
+310
View File
@@ -0,0 +1,310 @@
use std::{cmp, collections::HashSet};
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
deployment::{
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
},
docker::container::ContainerStats,
permission::PermissionLevel,
server::Server,
update::Log,
},
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{periphery_client, query::get_all_tags},
resource,
state::{action_states, deployment_status_cache},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Deployment> {
Ok(
resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListDeployments {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<DeploymentListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let deployments = resource::list_for_user::<Deployment>(
self.query, user, &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<ReadArgs> for ListFullDeployments {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullDeploymentsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Deployment>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetDeploymentContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentContainerResponse> {
let deployment = resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read,
)
.await?;
let status = deployment_status_cache()
.get(&deployment.id)
.await
.unwrap_or_default();
let response = GetDeploymentContainerResponse {
state: status.curr.state,
container: status.curr.container.clone(),
};
Ok(response)
}
}
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<ReadArgs> for GetDeploymentLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let GetDeploymentLog {
deployment,
tail,
timestamps,
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read,
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerLog {
name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchDeploymentLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let SearchDeploymentLog {
deployment,
terms,
combinator,
invert,
timestamps,
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read,
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerLogSearch {
name,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetDeploymentStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ContainerStats> {
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read,
)
.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 = resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.deployment
.get(&deployment.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetDeploymentsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentsSummaryResponse> {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
user,
&[],
)
.await
.context("failed to get deployments from db")?;
let mut res = GetDeploymentsSummaryResponse::default();
let status_cache = deployment_status_cache();
for deployment in deployments {
res.total += 1;
let status =
status_cache.get(&deployment.id).await.unwrap_or_default();
match status.curr.state {
DeploymentState::Running => {
res.running += 1;
}
DeploymentState::Exited | DeploymentState::Paused => {
res.stopped += 1;
}
DeploymentState::NotDeployed => {
res.not_deployed += 1;
}
DeploymentState::Unknown => {
res.unknown += 1;
}
_ => {
res.unhealthy += 1;
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
async fn resolve(
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>(
self.query, user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
for deployment in deployments {
for extra_arg in deployment.config.extra_args {
res.insert(extra_arg);
}
}
let mut res = res.into_iter().collect::<Vec<_>>();
res.sort();
Ok(res)
}
}
+562
View File
@@ -0,0 +1,562 @@
use std::{collections::HashSet, sync::OnceLock, time::Instant};
use anyhow::{Context, anyhow};
use axum::{Extension, Router, middleware, routing::post};
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
build::Build,
builder::{Builder, BuilderConfig},
config::{DockerRegistry, GitProvider},
repo::Repo,
server::Server,
sync::ResourceSync,
user::User,
},
};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request, config::core_config, helpers::periphery_client,
resource,
};
mod action;
mod alert;
mod alerter;
mod build;
mod builder;
mod deployment;
mod permission;
mod procedure;
mod provider;
mod repo;
mod server;
mod server_template;
mod stack;
mod sync;
mod tag;
mod toml;
mod update;
mod user;
mod user_group;
mod variable;
pub struct ReadArgs {
pub user: User,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
#[args(ReadArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
enum ReadRequest {
GetVersion(GetVersion),
GetCoreInfo(GetCoreInfo),
ListSecrets(ListSecrets),
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
// ==== USER ====
GetUsername(GetUsername),
GetPermissionLevel(GetPermissionLevel),
FindUser(FindUser),
ListUsers(ListUsers),
ListApiKeys(ListApiKeys),
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
ListPermissions(ListPermissions),
ListUserTargetPermissions(ListUserTargetPermissions),
// ==== USER GROUP ====
GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups),
// ==== PROCEDURE ====
GetProceduresSummary(GetProceduresSummary),
GetProcedure(GetProcedure),
GetProcedureActionState(GetProcedureActionState),
ListProcedures(ListProcedures),
ListFullProcedures(ListFullProcedures),
// ==== ACTION ====
GetActionsSummary(GetActionsSummary),
GetAction(GetAction),
GetActionActionState(GetActionActionState),
ListActions(ListActions),
ListFullActions(ListFullActions),
// ==== SERVER TEMPLATE ====
GetServerTemplate(GetServerTemplate),
GetServerTemplatesSummary(GetServerTemplatesSummary),
ListServerTemplates(ListServerTemplates),
ListFullServerTemplates(ListFullServerTemplates),
// ==== SERVER ====
GetServersSummary(GetServersSummary),
GetServer(GetServer),
GetServerState(GetServerState),
GetPeripheryVersion(GetPeripheryVersion),
GetServerActionState(GetServerActionState),
GetHistoricalServerStats(GetHistoricalServerStats),
ListServers(ListServers),
ListFullServers(ListFullServers),
InspectDockerContainer(InspectDockerContainer),
GetResourceMatchingContainer(GetResourceMatchingContainer),
GetContainerLog(GetContainerLog),
SearchContainerLog(SearchContainerLog),
InspectDockerNetwork(InspectDockerNetwork),
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
ListDockerNetworks(ListDockerNetworks),
ListDockerImages(ListDockerImages),
ListDockerVolumes(ListDockerVolumes),
ListComposeProjects(ListComposeProjects),
// ==== DEPLOYMENT ====
GetDeploymentsSummary(GetDeploymentsSummary),
GetDeployment(GetDeployment),
GetDeploymentContainer(GetDeploymentContainer),
GetDeploymentActionState(GetDeploymentActionState),
GetDeploymentStats(GetDeploymentStats),
GetDeploymentLog(GetDeploymentLog),
SearchDeploymentLog(SearchDeploymentLog),
ListDeployments(ListDeployments),
ListFullDeployments(ListFullDeployments),
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
// ==== BUILD ====
GetBuildsSummary(GetBuildsSummary),
GetBuild(GetBuild),
GetBuildActionState(GetBuildActionState),
GetBuildMonthlyStats(GetBuildMonthlyStats),
ListBuildVersions(ListBuildVersions),
GetBuildWebhookEnabled(GetBuildWebhookEnabled),
ListBuilds(ListBuilds),
ListFullBuilds(ListFullBuilds),
ListCommonBuildExtraArgs(ListCommonBuildExtraArgs),
// ==== REPO ====
GetReposSummary(GetReposSummary),
GetRepo(GetRepo),
GetRepoActionState(GetRepoActionState),
GetRepoWebhooksEnabled(GetRepoWebhooksEnabled),
ListRepos(ListRepos),
ListFullRepos(ListFullRepos),
// ==== SYNC ====
GetResourceSyncsSummary(GetResourceSyncsSummary),
GetResourceSync(GetResourceSync),
GetResourceSyncActionState(GetResourceSyncActionState),
GetSyncWebhooksEnabled(GetSyncWebhooksEnabled),
ListResourceSyncs(ListResourceSyncs),
ListFullResourceSyncs(ListFullResourceSyncs),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackLog(GetStackLog),
SearchStackLog(SearchStackLog),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== BUILDER ====
GetBuildersSummary(GetBuildersSummary),
GetBuilder(GetBuilder),
ListBuilders(ListBuilders),
ListFullBuilders(ListFullBuilders),
// ==== ALERTER ====
GetAlertersSummary(GetAlertersSummary),
GetAlerter(GetAlerter),
ListAlerters(ListAlerters),
ListFullAlerters(ListFullAlerters),
// ==== TOML ====
ExportAllResourcesToToml(ExportAllResourcesToToml),
ExportResourcesToToml(ExportResourcesToToml),
// ==== TAG ====
GetTag(GetTag),
ListTags(ListTags),
// ==== UPDATE ====
GetUpdate(GetUpdate),
ListUpdates(ListUpdates),
// ==== ALERT ====
ListAlerts(ListAlerts),
GetAlert(GetAlert),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
ListSystemProcesses(ListSystemProcesses),
// ==== VARIABLE ====
GetVariable(GetVariable),
ListVariables(ListVariables),
// ==== PROVIDER ====
GetGitProviderAccount(GetGitProviderAccount),
ListGitProviderAccounts(ListGitProviderAccounts),
GetDockerRegistryAccount(GetDockerRegistryAccount),
ListDockerRegistryAccounts(ListDockerRegistryAccounts),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.layer(middleware::from_fn(auth_request))
}
#[instrument(name = "ReadHandler", level = "debug", skip(user), fields(user_id = user.id))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ReadRequest>,
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!("/read request | user: {}", user.username);
let res = request.resolve(&ReadArgs { user }).await;
if let Err(e) = &res {
debug!("/read request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/read request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}
impl Resolve<ReadArgs> for GetVersion {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetVersionResponse> {
Ok(GetVersionResponse {
version: env!("CARGO_PKG_VERSION").to_string(),
})
}
}
fn core_info() -> &'static GetCoreInfoResponse {
static CORE_INFO: OnceLock<GetCoreInfoResponse> = OnceLock::new();
CORE_INFO.get_or_init(|| {
let config = core_config();
GetCoreInfoResponse {
title: config.title.clone(),
monitoring_interval: config.monitoring_interval,
webhook_base_url: if config.webhook_base_url.is_empty() {
config.host.clone()
} else {
config.webhook_base_url.clone()
},
transparent_mode: config.transparent_mode,
ui_write_disabled: config.ui_write_disabled,
disable_confirm_dialog: config.disable_confirm_dialog,
disable_non_admin_create: config.disable_non_admin_create,
github_webhook_owners: config
.github_webhook_app
.installations
.iter()
.map(|i| i.namespace.to_string())
.collect(),
}
})
}
impl Resolve<ReadArgs> for GetCoreInfo {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetCoreInfoResponse> {
Ok(core_info().clone())
}
}
impl Resolve<ReadArgs> for ListSecrets {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<ListSecretsResponse> {
let mut secrets = core_config()
.secrets
.keys()
.cloned()
.collect::<HashSet<_>>();
if let Some(target) = self.target {
let server_id = match target {
ResourceTarget::Server(id) => Some(id),
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => None,
BuilderConfig::Server(config) => Some(config.server_id),
BuilderConfig::Aws(config) => {
secrets.extend(config.secrets);
None
}
}
}
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
}
};
if let Some(id) = server_id {
let server = resource::get::<Server>(&id).await?;
let more = periphery_client(&server)?
.request(periphery_client::api::ListSecrets {})
.await
.with_context(|| {
format!(
"failed to get secrets from server {}",
server.name
)
})?;
secrets.extend(more);
}
}
let mut secrets = secrets.into_iter().collect::<Vec<_>>();
secrets.sort();
Ok(secrets)
}
}
impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListGitProvidersFromConfigResponse> {
let mut providers = core_config().git_providers.clone();
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_git_providers_for_server(&mut providers, &id).await?;
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_git_providers_for_server(
&mut providers,
&config.server_id,
)
.await?;
}
BuilderConfig::Aws(config) => {
merge_git_providers(
&mut providers,
config.git_providers,
);
}
}
}
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
}
}
}
let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>(
Default::default(),
user,
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
user,
&[]
),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
user,
&[]
),
)?;
for build in builds {
if !providers
.iter()
.any(|provider| provider.domain == build.config.git_provider)
{
providers.push(GitProvider {
domain: build.config.git_provider,
https: build.config.git_https,
accounts: Default::default(),
});
}
}
for repo in repos {
if !providers
.iter()
.any(|provider| provider.domain == repo.config.git_provider)
{
providers.push(GitProvider {
domain: repo.config.git_provider,
https: repo.config.git_https,
accounts: Default::default(),
});
}
}
for sync in syncs {
if !providers
.iter()
.any(|provider| provider.domain == sync.config.git_provider)
{
providers.push(GitProvider {
domain: sync.config.git_provider,
https: sync.config.git_https,
accounts: Default::default(),
});
}
}
providers.sort();
Ok(providers)
}
}
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone();
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_docker_registries_for_server(&mut registries, &id)
.await?;
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_docker_registries_for_server(
&mut registries,
&config.server_id,
)
.await?;
}
BuilderConfig::Aws(config) => {
merge_docker_registries(
&mut registries,
config.docker_registries,
);
}
}
}
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
}
}
}
registries.sort();
Ok(registries)
}
}
async fn merge_git_providers_for_server(
providers: &mut Vec<GitProvider>,
server_id: &str,
) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
.request(periphery_client::api::ListGitProviders {})
.await
.with_context(|| {
format!(
"failed to get git providers from server {}",
server.name
)
})?;
merge_git_providers(providers, more);
Ok(())
}
fn merge_git_providers(
providers: &mut Vec<GitProvider>,
more: Vec<GitProvider>,
) {
for incoming_provider in more {
if let Some(provider) = providers
.iter_mut()
.find(|provider| provider.domain == incoming_provider.domain)
{
for account in incoming_provider.accounts {
if !provider.accounts.contains(&account) {
provider.accounts.push(account);
}
}
} else {
providers.push(incoming_provider);
}
}
}
async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>,
server_id: &str,
) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
.request(periphery_client::api::ListDockerRegistries {})
.await
.with_context(|| {
format!(
"failed to get docker registries from server {}",
server.name
)
})?;
merge_docker_registries(registries, more);
Ok(())
}
fn merge_docker_registries(
registries: &mut Vec<DockerRegistry>,
more: Vec<DockerRegistry>,
) {
for incoming_registry in more {
if let Some(registry) = registries
.iter_mut()
.find(|registry| registry.domain == incoming_registry.domain)
{
for account in incoming_registry.accounts {
if !registry.accounts.contains(&account) {
registry.accounts.push(account);
}
}
} else {
registries.push(incoming_registry);
}
}
}
+71
View File
@@ -0,0 +1,71 @@
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::{
GetPermissionLevel, GetPermissionLevelResponse, ListPermissions,
ListPermissionsResponse, ListUserTargetPermissions,
ListUserTargetPermissionsResponse,
},
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,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for ListPermissions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListPermissionsResponse> {
let res = find_collect(
&db_client().permissions,
doc! {
"user_target.type": "User",
"user_target.id": &user.id
},
None,
)
.await
.context("failed to query db for permissions")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetPermissionLevel {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPermissionLevelResponse> {
if user.admin {
return Ok(PermissionLevel::Write);
}
Ok(get_user_permission_on_target(user, &self.target).await?)
}
}
impl Resolve<ReadArgs> for ListUserTargetPermissions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUserTargetPermissionsResponse> {
if !user.admin {
return Err(anyhow!("this method is admin only").into());
}
let (variant, id) = self.user_target.extract_variant_id();
let res = find_collect(
&db_client().permissions,
doc! {
"user_target.type": variant.as_ref(),
"user_target.id": id
},
None,
)
.await
.context("failed to query db for permissions")?;
Ok(res)
}
}
+137
View File
@@ -0,0 +1,137 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel,
procedure::{Procedure, ProcedureState},
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{action_states, procedure_state_cache},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetProcedure {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureResponse> {
Ok(
resource::get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListProcedures {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListProceduresResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Procedure>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullProcedures {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullProceduresResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Procedure>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetProceduresSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProceduresSummaryResponse> {
let procedures = resource::list_full_for_user::<Procedure>(
Default::default(),
user,
&[],
)
.await
.context("failed to get procedures from db")?;
let mut res = GetProceduresSummaryResponse::default();
let cache = procedure_state_cache();
let action_states = action_states();
for procedure in procedures {
res.total += 1;
match (
cache.get(&procedure.id).await.unwrap_or_default(),
action_states
.procedure
.get(&procedure.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.running => {
res.running += 1;
}
(ProcedureState::Ok, _) => res.ok += 1,
(ProcedureState::Failed, _) => res.failed += 1,
(ProcedureState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the running state, since that comes from action states
(ProcedureState::Running, _) => unreachable!(),
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for GetProcedureActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureActionStateResponse> {
let procedure = resource::get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.procedure
.get(&procedure.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
+115
View File
@@ -0,0 +1,115 @@
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;
use super::ReadArgs;
impl Resolve<ReadArgs> for GetGitProviderAccount {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read git provider accounts").into(),
);
}
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",
)?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListGitProviderAccounts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListGitProviderAccountsResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read git provider accounts").into(),
);
}
let mut filter = Document::new();
if let Some(domain) = self.domain {
filter.insert("domain", domain);
}
if let Some(username) = self.username {
filter.insert("username", username);
}
let res = find_collect(
&db_client().git_accounts,
filter,
FindOptions::builder()
.sort(doc! { "domain": 1, "username": 1 })
.build(),
)
.await
.context("failed to query db for git provider accounts")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetDockerRegistryAccount {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
.into(),
);
}
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<ReadArgs> for ListDockerRegistryAccounts {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerRegistryAccountsResponse> {
if !user.admin {
return Err(
anyhow!("Only admins can read docker registry accounts")
.into(),
);
}
let mut filter = Document::new();
if let Some(domain) = self.domain {
filter.insert("domain", domain);
}
if let Some(username) = self.username {
filter.insert("username", username);
}
let res = find_collect(
&db_client().registry_accounts,
filter,
FindOptions::builder()
.sort(doc! { "domain": 1, "username": 1 })
.build(),
)
.await
.context("failed to query db for docker registry accounts")?;
Ok(res)
}
}
+248
View File
@@ -0,0 +1,248 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
permission::PermissionLevel,
repo::{Repo, RepoActionState, RepoListItem, RepoState},
},
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{action_states, github_client, repo_state_cache},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetRepo {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Repo> {
Ok(
resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListRepos {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<RepoListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Repo>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullRepos {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullReposResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Repo>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetRepoActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<RepoActionState> {
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.repo
.get(&repo.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetReposSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetReposSummaryResponse> {
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
user,
&[],
)
.await
.context("failed to get repos from db")?;
let mut res = GetReposSummaryResponse::default();
let cache = repo_state_cache();
let action_states = action_states();
for repo in repos {
res.total += 1;
match (
cache.get(&repo.id).await.unwrap_or_default(),
action_states
.repo
.get(&repo.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.cloning => {
res.cloning += 1;
}
(_, action_states) if action_states.pulling => {
res.pulling += 1;
}
(_, action_states) if action_states.building => {
res.building += 1;
}
(RepoState::Ok, _) => res.ok += 1,
(RepoState::Failed, _) => res.failed += 1,
(RepoState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the building state, since that comes from action states
(RepoState::Cloning, _)
| (RepoState::Pulling, _)
| (RepoState::Building, _) => {
unreachable!()
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for GetRepoWebhooksEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetRepoWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
};
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read,
)
.await?;
if repo.config.git_provider != "github.com"
|| repo.config.repo.is_empty()
{
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let clone_url =
format!("{host}/listener/github/repo/{}/clone", repo.id);
let pull_url =
format!("{host}/listener/github/repo/{}/pull", repo.id);
let build_url =
format!("{host}/listener/github/repo/{}/build", repo.id);
let mut clone_enabled = false;
let mut pull_enabled = false;
let mut build_enabled = false;
for webhook in webhooks {
if !webhook.active {
continue;
}
if webhook.config.url == clone_url {
clone_enabled = true
}
if webhook.config.url == pull_url {
pull_enabled = true
}
if webhook.config.url == build_url {
build_enabled = true
}
}
Ok(GetRepoWebhooksEnabledResponse {
managed: true,
clone_enabled,
pull_enabled,
build_enabled,
})
}
}
+814
View File
@@ -0,0 +1,814 @@
use std::{
cmp,
collections::HashMap,
sync::{Arc, OnceLock},
};
use anyhow::{Context, anyhow};
use async_timing_util::{
FIFTEEN_SECONDS_MS, get_timelength_in_ms, unix_timestamp_ms,
};
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
permission::PermissionLevel,
server::{
Server, ServerActionState, ServerListItem, ServerState,
},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
},
};
use mungos::{
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use periphery_client::api::{
self as periphery,
container::InspectContainer,
image::{ImageHistory, InspectImage},
network::InspectNetwork,
volume::InspectVolume,
};
use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
helpers::{periphery_client, query::get_all_tags},
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetServersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServersSummaryResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
user,
&[],
)
.await?;
let mut res = GetServersSummaryResponse::default();
for server in servers {
res.total += 1;
match server.info.state {
ServerState::Ok => {
res.healthy += 1;
}
ServerState::NotOk => {
res.unhealthy += 1;
}
ServerState::Disabled => {
res.disabled += 1;
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for GetPeripheryVersion {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPeripheryVersionResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let version = server_status_cache()
.get(&server.id)
.await
.map(|s| s.version.clone())
.unwrap_or(String::from("unknown"));
Ok(GetPeripheryVersionResponse { version })
}
}
impl Resolve<ReadArgs> for GetServer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Server> {
Ok(
resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListServers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ServerListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Server>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullServers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullServersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Server>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetServerState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerStateResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let status = server_status_cache()
.get(&server.id)
.await
.ok_or(anyhow!("did not find cached status for server"))?;
let response = GetServerStateResponse {
status: status.state,
};
Ok(response)
}
}
impl Resolve<ReadArgs> for GetServerActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ServerActionState> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.server
.get(&server.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
// This protects the peripheries from spam requests
const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
type SystemInfoCache =
Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;
fn system_info_cache() -> &'static SystemInfoCache {
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
OnceLock::new();
SYSTEM_INFO_CACHE.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for GetSystemInformation {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SystemInformation> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let mut lock = system_info_cache().lock().await;
let res = match lock.get(&server.id) {
Some(cached) if cached.1 > unix_timestamp_ms() => {
cached.0.clone()
}
_ => {
let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemInformation {})
.await?;
lock.insert(
server.id,
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
.into(),
);
stats
}
};
Ok(res)
}
}
impl Resolve<ReadArgs> for GetSystemStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSystemStatsResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let status =
server_status_cache().get(&server.id).await.with_context(
|| format!("did not find status for server at {}", server.id),
)?;
let stats = status
.stats
.as_ref()
.context("server stats not available")?;
Ok(stats.clone())
}
}
// This protects the peripheries from spam requests
const PROCESSES_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
type ProcessesCache =
Mutex<HashMap<String, Arc<(Vec<SystemProcess>, u128)>>>;
fn processes_cache() -> &'static ProcessesCache {
static PROCESSES_CACHE: OnceLock<ProcessesCache> = OnceLock::new();
PROCESSES_CACHE.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for ListSystemProcesses {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSystemProcessesResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let mut lock = processes_cache().lock().await;
let res = match lock.get(&server.id) {
Some(cached) if cached.1 > unix_timestamp_ms() => {
cached.0.clone()
}
_ => {
let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemProcesses {})
.await?;
lock.insert(
server.id,
(stats.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY)
.into(),
);
stats
}
};
Ok(res)
}
}
const STATS_PER_PAGE: i64 = 200;
impl Resolve<ReadArgs> for GetHistoricalServerStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetHistoricalServerStatsResponse> {
let GetHistoricalServerStats {
server,
granularity,
page,
} = self;
let server = resource::get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read,
)
.await?;
let granularity =
get_timelength_in_ms(granularity.to_string().parse().unwrap())
as i64;
let mut ts_vec = Vec::<i64>::new();
let curr_ts = unix_timestamp_ms() as i64;
let mut curr_ts = curr_ts
- curr_ts % granularity
- granularity * STATS_PER_PAGE * page as i64;
for _ in 0..STATS_PER_PAGE {
ts_vec.push(curr_ts);
curr_ts -= granularity;
}
let stats = find_collect(
&db_client().stats,
doc! {
"sid": server.id,
"ts": { "$in": ts_vec },
},
FindOptions::builder()
.sort(doc! { "ts": -1 })
.skip(page as u64 * STATS_PER_PAGE as u64)
.limit(STATS_PER_PAGE)
.build(),
)
.await
.context("failed to pull stats from db")?;
let next_page = if stats.len() == STATS_PER_PAGE as usize {
Some(page + 1)
} else {
None
};
let res = GetHistoricalServerStatsResponse { stats, next_page };
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerContainersResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
Ok(containers.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for ListAllDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListAllDockerContainersResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
user,
&[],
)
.await?
.into_iter()
.filter(|server| {
self.servers.is_empty()
|| self.servers.contains(&server.id)
|| self.servers.contains(&server.name)
});
let mut containers = Vec::<ContainerListItem>::new();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(more_containers) = &cache.containers {
containers.extend(more_containers.clone());
}
}
Ok(containers)
}
}
impl Resolve<ReadArgs> for GetDockerContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
user,
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetDockerContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
for container in containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.request(InspectContainer {
name: self.container,
})
.await?;
Ok(res)
}
}
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let GetContainerLog {
server,
container,
tail,
timestamps,
} = self;
let server = resource::get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read,
)
.await?;
let res = periphery_client(&server)?
.request(periphery::container::GetContainerLog {
name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchContainerLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let SearchContainerLog {
server,
container,
terms,
combinator,
invert,
timestamps,
} = self;
let server = resource::get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read,
)
.await?;
let res = periphery_client(&server)?
.request(periphery::container::GetContainerLogSearch {
name: container,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetResourceMatchingContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceMatchingContainerResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
// first check deployments
if let Ok(deployment) =
resource::get::<Deployment>(&self.container).await
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// then check stacks
let stacks =
resource::list_full_for_user_using_document::<Stack>(
doc! { "config.server_id": &server.id },
user,
)
.await?;
// check matching stack
for stack in stacks {
for StackServiceNames {
service_name,
container_name,
..
} in stack
.info
.deployed_services
.unwrap_or(stack.info.latest_services)
{
let is_match = match compose_container_match_regex(&container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
Ok(regex) => regex,
Err(e) => {
warn!("{e:#}");
continue;
}
}.is_match(&self.container);
if is_match {
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Stack(stack.id).into(),
});
}
}
}
Ok(GetResourceMatchingContainerResponse { resource: None })
}
}
impl Resolve<ReadArgs> for ListDockerNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerNetworksResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(networks) = &cache.networks {
Ok(networks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerNetwork {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Network> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.request(InspectNetwork { name: self.network })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerImages {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerImagesResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(images) = &cache.images {
Ok(images.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerImage {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Image> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect image: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)?
.request(InspectImage { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerImageHistory {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ImageHistoryResponseItem>> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot get image history: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.request(ImageHistory { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListDockerVolumes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerVolumesResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(volumes) = &cache.volumes {
Ok(volumes.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectDockerVolume {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Volume> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)?
.request(InspectVolume { name: self.volume })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListComposeProjectsResponse> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(projects) = &cache.projects {
Ok(projects.clone())
} else {
Ok(Vec::new())
}
}
}
+97
View File
@@ -0,0 +1,97 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
},
};
use mongo_indexed::Document;
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, resource, state::db_client,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetServerTemplate {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerTemplateResponse> {
Ok(
resource::get_check_permissions::<ServerTemplate>(
&self.server_template,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListServerTemplates {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListServerTemplatesResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<ServerTemplate>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullServerTemplates {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullServerTemplatesResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<ServerTemplate>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetServerTemplatesSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::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)
}
}
+378
View File
@@ -0,0 +1,378 @@
use std::collections::HashSet;
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
permission::PermissionLevel,
stack::{Stack, StackActionState, StackListItem, StackState},
},
};
use periphery_client::api::compose::{
GetComposeLog, GetComposeLogSearch,
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::{periphery_client, query::get_all_tags},
resource,
stack::get_stack_and_server,
state::{action_states, github_client, stack_status_cache},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetStack {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Stack> {
Ok(
resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListStackServices {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListStackServicesResponse> {
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read,
)
.await?;
let services = stack_status_cache()
.get(&stack.id)
.await
.unwrap_or_default()
.curr
.services
.clone();
Ok(services)
}
}
impl Resolve<ReadArgs> for GetStackLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackLogResponse> {
let GetStackLog {
stack,
services,
tail,
timestamps,
} = self;
let (stack, server) =
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLog {
project: stack.project_name(false),
services,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchStackLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchStackLogResponse> {
let SearchStackLog {
stack,
services,
terms,
combinator,
invert,
timestamps,
} = self;
let (stack, server) =
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLogSearch {
project: stack.project_name(false),
services,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?;
Ok(res)
}
}
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>(
self.query, user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
for stack in stacks {
for extra_arg in stack.config.extra_args {
res.insert(extra_arg);
}
}
let mut res = res.into_iter().collect::<Vec<_>>();
res.sort();
Ok(res)
}
}
impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
async fn resolve(
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>(
self.query, user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
for stack in stacks {
for extra_arg in stack.config.build_extra_args {
res.insert(extra_arg);
}
}
let mut res = res.into_iter().collect::<Vec<_>>();
res.sort();
Ok(res)
}
}
impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let stacks =
resource::list_for_user::<Stack>(self.query, user, &all_tags)
.await?;
let stacks = if only_update_available {
stacks
.into_iter()
.filter(|stack| {
stack
.info
.services
.iter()
.any(|service| service.update_available)
})
.collect()
} else {
stacks
};
Ok(stacks)
}
}
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Stack>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetStackActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<StackActionState> {
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.stack
.get(&stack.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetStacksSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStacksSummaryResponse> {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
user,
&[],
)
.await
.context("failed to get stacks from db")?;
let mut res = GetStacksSummaryResponse::default();
let cache = stack_status_cache();
for stack in stacks {
res.total += 1;
match cache.get(&stack.id).await.unwrap_or_default().curr.state
{
StackState::Running => res.running += 1,
StackState::Stopped | StackState::Paused => res.stopped += 1,
StackState::Down => res.down += 1,
StackState::Unknown => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for GetStackWebhooksEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
};
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read,
)
.await?;
if stack.config.git_provider != "github.com"
|| stack.config.repo.is_empty()
{
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
}
let mut split = stack.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let refresh_url =
format!("{host}/listener/github/stack/{}/refresh", stack.id);
let deploy_url =
format!("{host}/listener/github/stack/{}/deploy", stack.id);
let mut refresh_enabled = false;
let mut deploy_enabled = false;
for webhook in webhooks {
if webhook.active && webhook.config.url == refresh_url {
refresh_enabled = true
}
if webhook.active && webhook.config.url == deploy_url {
deploy_enabled = true
}
}
Ok(GetStackWebhooksEnabledResponse {
managed: true,
refresh_enabled,
deploy_enabled,
})
}
}
+236
View File
@@ -0,0 +1,236 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
},
},
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{action_states, github_client},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetResourceSync {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSync> {
Ok(
resource::get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListResourceSyncs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ResourceSyncListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<ResourceSync>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullResourceSyncs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullResourceSyncsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<ResourceSync>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetResourceSyncActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSyncActionState> {
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.resource_sync
.get(&sync.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetResourceSyncsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceSyncsSummaryResponse> {
let resource_syncs =
resource::list_full_for_user::<ResourceSync>(
Default::default(),
user,
&[],
)
.await
.context("failed to get resource_syncs from db")?;
let mut res = GetResourceSyncsSummaryResponse::default();
let action_states = action_states();
for resource_sync in resource_syncs {
res.total += 1;
if !(resource_sync.info.pending_deploy.to_deploy == 0
&& resource_sync.info.resource_updates.is_empty()
&& resource_sync.info.variable_updates.is_empty()
&& resource_sync.info.user_group_updates.is_empty())
{
res.pending += 1;
continue;
} else if resource_sync.info.pending_error.is_some()
|| !resource_sync.info.remote_errors.is_empty()
{
res.failed += 1;
continue;
}
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<ReadArgs> for GetSyncWebhooksEnabled {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSyncWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
};
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read,
)
.await?;
if sync.config.git_provider != "github.com"
|| sync.config.repo.is_empty()
{
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
}
let mut split = sync.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let refresh_url =
format!("{host}/listener/github/sync/{}/refresh", sync.id);
let sync_url =
format!("{host}/listener/github/sync/{}/sync", sync.id);
let mut refresh_enabled = false;
let mut sync_enabled = false;
for webhook in webhooks {
if webhook.active && webhook.config.url == refresh_url {
refresh_enabled = true
}
if webhook.active && webhook.config.url == sync_url {
sync_enabled = true
}
}
Ok(GetSyncWebhooksEnabledResponse {
managed: true,
refresh_enabled,
sync_enabled,
})
}
}
+31
View File
@@ -0,0 +1,31 @@
use anyhow::Context;
use komodo_client::{
api::read::{GetTag, ListTags},
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};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetTag {
async fn resolve(self, _: &ReadArgs) -> serror::Result<Tag> {
Ok(get_tag(&self.tag).await?)
}
}
impl Resolve<ReadArgs> for ListTags {
async fn resolve(self, _: &ReadArgs) -> serror::Result<Vec<Tag>> {
let res = find_collect(
&db_client().tags,
self.query,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.context("failed to get tags from db")?;
Ok(res)
}
}
+545
View File
@@ -0,0 +1,545 @@
use anyhow::Context;
use komodo_client::{
api::read::{
ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
ExportResourcesToToml, ExportResourcesToTomlResponse,
ListUserGroups,
},
entities::{
ResourceTarget, 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,
sync::ResourceSync, toml::ResourcesToml, user::User,
},
};
use mungos::find::find_collect;
use resolver_api::Resolve;
use crate::{
helpers::query::{
get_all_tags, get_id_to_tags, get_user_user_group_ids,
},
resource,
state::db_client,
sync::{
AllResourcesById,
toml::{TOML_PRETTY_OPTIONS, ToToml, convert_resource},
user_groups::convert_user_groups,
},
};
use super::ReadArgs;
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_for_user::<Alerter>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Alerter(resource.id)),
);
targets.extend(
resource::list_for_user::<Builder>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Builder(resource.id)),
);
targets.extend(
resource::list_for_user::<Server>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_for_user::<Stack>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_for_user::<Deployment>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_for_user::<Build>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Build(resource.id)),
);
targets.extend(
resource::list_for_user::<Repo>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Repo(resource.id)),
);
targets.extend(
resource::list_for_user::<Procedure>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_for_user::<Action>(
ResourceQuery::builder().tags(tags).build(),
user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(tags).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).build(),
user,
&all_tags,
)
.await?
.into_iter()
// These will already be filtered by [ExportResourcesToToml]
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
Ok(targets)
}
impl Resolve<ReadArgs> for ExportAllResourcesToToml {
async fn resolve(
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,
} = 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>(
&id,
user,
PermissionLevel::Read,
)
.await?;
res.alerters.push(convert_resource::<Alerter>(
alerter,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::ResourceSync(id) => {
let sync = resource::get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Read,
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty())
{
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
vec![],
&id_to_tags,
))
}
}
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>(
&id,
user,
PermissionLevel::Read,
)
.await?;
res.servers.push(convert_resource::<Server>(
server,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Builder(id) => {
let mut builder =
resource::get_check_permissions::<Builder>(
&id,
user,
PermissionLevel::Read,
)
.await?;
Builder::replace_ids(&mut builder, &all);
res.builders.push(convert_resource::<Builder>(
builder,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Build(id) => {
let mut build = resource::get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read,
)
.await?;
Build::replace_ids(&mut build, &all);
res.builds.push(convert_resource::<Build>(
build,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Deployment(id) => {
let mut deployment = resource::get_check_permissions::<
Deployment,
>(
&id, user, PermissionLevel::Read
)
.await?;
Deployment::replace_ids(&mut deployment, &all);
res.deployments.push(convert_resource::<Deployment>(
deployment,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Repo(id) => {
let mut repo = resource::get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Read,
)
.await?;
Repo::replace_ids(&mut repo, &all);
res.repos.push(convert_resource::<Repo>(
repo,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Stack(id) => {
let mut stack = resource::get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read,
)
.await?;
Stack::replace_ids(&mut stack, &all);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Procedure(id) => {
let mut procedure = resource::get_check_permissions::<
Procedure,
>(
&id, user, PermissionLevel::Read
)
.await?;
Procedure::replace_ids(&mut procedure, &all);
res.procedures.push(convert_resource::<Procedure>(
procedure,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::Action(id) => {
let mut action = resource::get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Read,
)
.await?;
Action::replace_ids(&mut action, &all);
res.actions.push(convert_resource::<Action>(
action,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::System(_) => continue,
};
}
add_user_groups(user_groups, &mut res, &all, args)
.await
.context("failed to add user groups")?;
if include_variables {
res.variables =
find_collect(&db_client().variables, None, None)
.await
.context("failed to get variables from db")?
.into_iter()
.map(|mut variable| {
if !user.admin && variable.is_secret {
variable.value = "#".repeat(variable.value.len())
}
variable
})
.collect();
}
let toml = serialize_resources_toml(res)
.context("failed to serialize resources to toml")?;
Ok(ExportResourcesToTomlResponse { toml })
}
}
async fn add_user_groups(
user_groups: Vec<String>,
res: &mut ResourcesToml,
all: &AllResourcesById,
args: &ReadArgs,
) -> anyhow::Result<()> {
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?;
res.user_groups = ug.into_iter().map(|ug| ug.1).collect();
Ok(())
}
fn serialize_resources_toml(
resources: ResourcesToml,
) -> anyhow::Result<String> {
let mut toml = String::new();
for server in resources.servers {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[server]]\n");
Server::push_to_toml_string(server, &mut toml)?;
}
for stack in resources.stacks {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[stack]]\n");
Stack::push_to_toml_string(stack, &mut toml)?;
}
for deployment in resources.deployments {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[deployment]]\n");
Deployment::push_to_toml_string(deployment, &mut toml)?;
}
for build in resources.builds {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[build]]\n");
Build::push_to_toml_string(build, &mut toml)?;
}
for repo in resources.repos {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[repo]]\n");
Repo::push_to_toml_string(repo, &mut toml)?;
}
for procedure in resources.procedures {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[procedure]]\n");
Procedure::push_to_toml_string(procedure, &mut toml)?;
}
for action in resources.actions {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[action]]\n");
Action::push_to_toml_string(action, &mut toml)?;
}
for alerter in resources.alerters {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[alerter]]\n");
Alerter::push_to_toml_string(alerter, &mut toml)?;
}
for builder in resources.builders {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[builder]]\n");
Builder::push_to_toml_string(builder, &mut toml)?;
}
for 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");
}
toml.push_str("[[resource_sync]]\n");
ResourceSync::push_to_toml_string(resource_sync, &mut toml)?;
}
for variable in &resources.variables {
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")?,
);
}
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")?,
);
}
Ok(toml)
}
+338
View File
@@ -0,0 +1,338 @@
use std::collections::HashMap;
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{
ResourceTarget,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
update::{Update, UpdateListItem},
user::User,
},
};
use mungos::{
by_id::find_one_by_id,
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use resolver_api::Resolve;
use crate::{config::core_config, resource, state::db_client};
use super::ReadArgs;
const UPDATES_PER_PAGE: i64 = 100;
impl Resolve<ReadArgs> for ListUpdates {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUpdatesResponse> {
let query = if user.admin || core_config().transparent_mode {
self.query
} else {
let server_query =
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 deployment_query =
resource::get_resource_ids_for_user::<Deployment>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Deployment", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
let stack_query =
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 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 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 procedure_query =
resource::get_resource_ids_for_user::<Procedure>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Procedure", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query =
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,
)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let mut query = self.query.unwrap_or_default();
query.extend(doc! {
"$or": [
server_query,
deployment_query,
stack_query,
build_query,
repo_query,
procedure_query,
action_query,
alerter_query,
builder_query,
server_template_query,
resource_sync_query,
]
});
query.into()
};
let usernames = find_collect(&db_client().users, None, None)
.await
.context("failed to pull users from db")?
.into_iter()
.map(|u| (u.id, u.username))
.collect::<HashMap<_, _>>();
let updates = find_collect(
&db_client().updates,
query,
FindOptions::builder()
.sort(doc! { "start_ts": -1 })
.skip(self.page as u64 * UPDATES_PER_PAGE as u64)
.limit(UPDATES_PER_PAGE)
.build(),
)
.await
.context("failed to pull updates from db")?
.into_iter()
.map(|u| {
let username = if User::is_service_user(&u.operator) {
u.operator.clone()
} else {
usernames
.get(&u.operator)
.cloned()
.unwrap_or("unknown".to_string())
};
UpdateListItem {
username,
id: u.id,
operation: u.operation,
start_ts: u.start_ts,
success: u.success,
operator: u.operator,
target: u.target,
status: u.status,
version: u.version,
other_data: u.other_data,
}
})
.collect::<Vec<_>>();
let next_page = if updates.len() == UPDATES_PER_PAGE as usize {
Some(self.page + 1)
} else {
None
};
Ok(ListUpdatesResponse { updates, next_page })
}
}
impl Resolve<ReadArgs> for GetUpdate {
async fn resolve(
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")?;
if user.admin || core_config().transparent_mode {
return Ok(update);
}
match &update.target {
ResourceTarget::System(_) => {
return Err(
anyhow!("user must be admin to view system updates").into(),
);
}
ResourceTarget::Server(id) => {
resource::get_check_permissions::<Server>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Deployment(id) => {
resource::get_check_permissions::<Deployment>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Build(id) => {
resource::get_check_permissions::<Build>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Repo(id) => {
resource::get_check_permissions::<Repo>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Builder(id) => {
resource::get_check_permissions::<Builder>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Alerter(id) => {
resource::get_check_permissions::<Alerter>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Procedure(id) => {
resource::get_check_permissions::<Procedure>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::ResourceSync(id) => {
resource::get_check_permissions::<ResourceSync>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Stack(id) => {
resource::get_check_permissions::<Stack>(
id,
user,
PermissionLevel::Read,
)
.await?;
}
}
Ok(update)
}
}
+137
View File
@@ -0,0 +1,137 @@
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::{
FindUser, FindUserResponse, GetUsername, GetUsernameResponse,
ListApiKeys, ListApiKeysForServiceUser,
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
ListUsers, ListUsersResponse,
},
entities::user::{UserConfig, admin_service_user},
};
use mungos::{
by_id::find_one_by_id,
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use resolver_api::Resolve;
use crate::{helpers::query::get_user, state::db_client};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetUsername {
async fn resolve(
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, &self.user_id)
.await
.context("failed at mongo query for user")?
.context("no user found with id")?;
let avatar = match user.config {
UserConfig::Github { avatar, .. } => Some(avatar),
UserConfig::Google { avatar, .. } => Some(avatar),
_ => None,
};
Ok(GetUsernameResponse {
username: user.username,
avatar,
})
}
}
impl Resolve<ReadArgs> for FindUser {
async fn resolve(
self,
ReadArgs { user: admin }: &ReadArgs,
) -> serror::Result<FindUserResponse> {
if !admin.admin {
return Err(anyhow!("This method is admin only.").into());
}
Ok(get_user(&self.user).await?)
}
}
impl Resolve<ReadArgs> for ListUsers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUsersResponse> {
if !user.admin {
return Err(
anyhow!("this route is only accessable by admins").into(),
);
}
let mut users = find_collect(
&db_client().users,
None,
FindOptions::builder().sort(doc! { "username": 1 }).build(),
)
.await
.context("failed to pull users from db")?;
users.iter_mut().for_each(|user| user.sanitize());
Ok(users)
}
}
impl Resolve<ReadArgs> for ListApiKeys {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListApiKeysResponse> {
let api_keys = find_collect(
&db_client().api_keys,
doc! { "user_id": &user.id },
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.context("failed to query db for api keys")?
.into_iter()
.map(|mut api_keys| {
api_keys.sanitize();
api_keys
})
.collect();
Ok(api_keys)
}
}
impl Resolve<ReadArgs> for ListApiKeysForServiceUser {
async fn resolve(
self,
ReadArgs { user: admin }: &ReadArgs,
) -> serror::Result<ListApiKeysForServiceUserResponse> {
if !admin.admin {
return Err(anyhow!("This method is admin only.").into());
}
let user = get_user(&self.user).await?;
let UserConfig::Service { .. } = user.config else {
return Err(anyhow!("Given user is not service user").into());
};
let api_keys = find_collect(
&db_client().api_keys,
doc! { "user_id": &user.id },
None,
)
.await
.context("failed to query db for api keys")?
.into_iter()
.map(|mut api_keys| {
api_keys.sanitize();
api_keys
})
.collect();
Ok(api_keys)
}
}
+60
View File
@@ -0,0 +1,60 @@
use std::str::FromStr;
use anyhow::Context;
use komodo_client::api::read::*;
use mungos::{
find::find_collect,
mongodb::{
bson::{Document, doc, oid::ObjectId},
options::FindOptions,
},
};
use resolver_api::Resolve;
use crate::state::db_client;
use super::ReadArgs;
impl Resolve<ReadArgs> for GetUserGroup {
async fn resolve(
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": &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);
}
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")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListUserGroups {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUserGroupsResponse> {
let mut filter = Document::new();
if !user.admin {
filter.insert("users", &user.id);
}
let res = find_collect(
&db_client().user_groups,
filter,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.context("failed to query db for UserGroups")?;
Ok(res)
}
}
+51
View File
@@ -0,0 +1,51 @@
use anyhow::Context;
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};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetVariable {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetVariableResponse> {
let mut variable = get_variable(&self.name).await?;
if !variable.is_secret || user.admin {
return Ok(variable);
}
variable.value = "#".repeat(variable.value.len());
Ok(variable)
}
}
impl Resolve<ReadArgs> for ListVariables {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListVariablesResponse> {
let variables = find_collect(
&db_client().variables,
None,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
.await
.context("failed to query db for variables")?;
if user.admin {
return Ok(variables);
}
let variables = variables
.into_iter()
.map(|mut variable| {
if variable.is_secret {
variable.value = "#".repeat(variable.value.len());
}
variable
})
.collect();
Ok(variables)
}
}
+193
View File
@@ -0,0 +1,193 @@
use std::{collections::VecDeque, time::Instant};
use anyhow::{Context, anyhow};
use axum::{Extension, Json, Router, middleware, routing::post};
use derive_variants::EnumVariants;
use komodo_client::{
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::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::{query::get_user, random_string},
state::db_client,
};
pub struct UserArgs {
pub user: User,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[args(UserArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
enum UserRequest {
PushRecentlyViewed(PushRecentlyViewed),
SetLastSeenUpdate(SetLastSeenUpdate),
CreateApiKey(CreateApiKey),
DeleteApiKey(DeleteApiKey),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.layer(middleware::from_fn(auth_request))
}
#[instrument(name = "UserHandler", level = "debug", skip(user))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<UserRequest>,
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!(
"/user request {req_id} | user: {} ({})",
user.username, user.id
);
let res = request.resolve(&UserArgs { user }).await;
if let Err(e) = &res {
warn!("/user request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/user request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}
const RECENTLY_VIEWED_MAX: usize = 10;
impl Resolve<UserArgs> for PushRecentlyViewed {
#[instrument(
name = "PushRecentlyViewed",
level = "debug",
skip(user)
)]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<PushRecentlyViewedResponse> {
let user = get_user(&user.id).await?;
let (resource_type, id) = self.resource.extract_variant_id();
let update = match user.recents.get(&resource_type) {
Some(recents) => {
let mut recents = recents
.iter()
.filter(|_id| !id.eq(*_id))
.take(RECENTLY_VIEWED_MAX - 1)
.collect::<VecDeque<_>>();
recents.push_front(id);
doc! { format!("recents.{resource_type}"): to_bson(&recents)? }
}
None => {
doc! { format!("recents.{resource_type}"): [id] }
}
};
update_one_by_id(
&db_client().users,
&user.id,
mungos::update::Update::Set(update),
None,
)
.await
.with_context(|| {
format!("failed to update recents.{resource_type}")
})?;
Ok(PushRecentlyViewedResponse {})
}
}
impl Resolve<UserArgs> for SetLastSeenUpdate {
#[instrument(
name = "SetLastSeenUpdate",
level = "debug",
skip(user)
)]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<SetLastSeenUpdateResponse> {
update_one_by_id(
&db_client().users,
&user.id,
mungos::update::Update::Set(doc! {
"last_update_view": komodo_timestamp()
}),
None,
)
.await
.context("failed to update user last_update_view")?;
Ok(SetLastSeenUpdateResponse {})
}
}
const SECRET_LENGTH: usize = 40;
const BCRYPT_COST: u32 = 10;
impl Resolve<UserArgs> for CreateApiKey {
#[instrument(name = "CreateApiKey", level = "debug", skip(user))]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<CreateApiKeyResponse> {
let user = get_user(&user.id).await?;
let key = format!("K-{}", random_string(SECRET_LENGTH));
let secret = format!("S-{}", random_string(SECRET_LENGTH));
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
.context("failed at hashing secret string")?;
let api_key = ApiKey {
name: self.name,
key: key.clone(),
secret: secret_hash,
user_id: user.id.clone(),
created_at: komodo_timestamp(),
expires: self.expires,
};
db_client()
.api_keys
.insert_one(api_key)
.await
.context("failed to create api key on db")?;
Ok(CreateApiKeyResponse { key, secret })
}
}
impl Resolve<UserArgs> for DeleteApiKey {
#[instrument(name = "DeleteApiKey", level = "debug", skip(user))]
async fn resolve(
self,
UserArgs { user }: &UserArgs,
) -> serror::Result<DeleteApiKeyResponse> {
let client = db_client();
let key = client
.api_keys
.find_one(doc! { "key": &self.key })
.await
.context("failed at db query")?
.context("no api key with key found")?;
if user.id != key.user_id {
return Err(anyhow!("api key does not belong to user").into());
}
client
.api_keys
.delete_one(doc! { "key": key.key })
.await
.context("failed to delete api key from db")?;
Ok(DeleteApiKeyResponse {})
}
}
+71
View File
@@ -0,0 +1,71 @@
use komodo_client::{
api::write::*,
entities::{
action::Action, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use crate::resource;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateAction {
#[instrument(name = "CreateAction", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
Ok(
resource::create::<Action>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyAction {
#[instrument(name = "CopyAction", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
let Action { config, .. } =
resource::get_check_permissions::<Action>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Action>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<WriteArgs> for UpdateAction {
#[instrument(name = "UpdateAction", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
Ok(resource::update::<Action>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameAction {
#[instrument(name = "RenameAction", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Action>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for DeleteAction {
#[instrument(name = "DeleteAction", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Action> {
Ok(resource::delete::<Action>(&self.id, args).await?)
}
}
+77
View File
@@ -0,0 +1,77 @@
use komodo_client::{
api::write::*,
entities::{
alerter::Alerter, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use crate::resource;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateAlerter {
#[instrument(name = "CreateAlerter", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
Ok(
resource::create::<Alerter>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyAlerter {
#[instrument(name = "CopyAlerter", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
let Alerter { config, .. } =
resource::get_check_permissions::<Alerter>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Alerter>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<WriteArgs> for DeleteAlerter {
#[instrument(name = "DeleteAlerter", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<Alerter> {
Ok(resource::delete::<Alerter>(&self.id, args).await?)
}
}
impl Resolve<WriteArgs> for UpdateAlerter {
#[instrument(name = "UpdateAlerter", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
Ok(
resource::update::<Alerter>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for RenameAlerter {
#[instrument(name = "RenameAlerter", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Alerter>(&self.id, &self.name, user).await?)
}
}
+675
View File
@@ -0,0 +1,675 @@
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,
server::ServerState,
update::Update,
},
};
use mongo_indexed::doc;
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, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
resource,
state::{db_client, github_client},
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateBuild {
#[instrument(name = "CreateBuild", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
Ok(
resource::create::<Build>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyBuild {
#[instrument(name = "CopyBuild", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
let Build { mut config, .. } =
resource::get_check_permissions::<Build>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
// reset version to 0.0.0
config.version = Default::default();
Ok(
resource::create::<Build>(&self.name, config.into(), 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<WriteArgs> for UpdateBuild {
#[instrument(name = "UpdateBuild", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
Ok(resource::update::<Build>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameBuild {
#[instrument(name = "RenameBuild", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Build>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for WriteBuildFileContents {
#[instrument(name = "WriteBuildFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let build = resource::get_check_permissions::<Build>(
&self.build,
&args.user,
PermissionLevel::Write,
)
.await?;
if !build.config.files_on_host && build.config.repo.is_empty() {
return Err(anyhow!(
"Build is not configured to use Files on Host or Git Repo, can't write dockerfile contents"
).into());
}
let mut update =
make_update(&build, Operation::WriteDockerfile, &args.user);
update.push_simple_log("Dockerfile to write", &self.contents);
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
.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 = (&build).into();
let root = clone_args.unique_path(&core_config().repo_directory)?;
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:?}"
)
})?;
}
// 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() {
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
};
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);
}
}
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 = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute,
)
.await?;
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 !build.config.repo.is_empty() {
// ================
// REPO BASED BUILD
// ================
if build.config.git_provider.is_empty() {
// Nothing to do here
return Ok(NoData {});
}
let config = core_config();
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;
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()))),
};
(
Some(relative_path.display().to_string()),
contents,
error,
hash,
message,
)
} 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,
};
let info = to_document(&info)
.context("failed to serialize build info to bson")?;
db_client()
.builds
.update_one(
doc! { "name": &build.name },
doc! { "$set": { "info": info } },
)
.await
.context("failed to update build info on db")?;
Ok(NoData {})
}
}
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
}
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<CreateBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let WriteArgs { user } = args;
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write,
)
.await?;
if build.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if build.config.webhook_secret.is_empty() {
webhook_secret
} else {
&build.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo, &request)
.await
.context("failed to create webhook")?;
if !build.config.webhook_enabled {
UpdateBuild {
id: build.id,
config: PartialBuildConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update build to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteBuildWebhook {
#[instrument(name = "DeleteBuildWebhook", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write,
)
.await?;
if build.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if build.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't delete webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
}
+77
View File
@@ -0,0 +1,77 @@
use komodo_client::{
api::write::*,
entities::{
builder::Builder, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use crate::resource;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateBuilder {
#[instrument(name = "CreateBuilder", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
Ok(
resource::create::<Builder>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyBuilder {
#[instrument(name = "CopyBuilder", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
let Builder { config, .. } =
resource::get_check_permissions::<Builder>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Builder>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<WriteArgs> for DeleteBuilder {
#[instrument(name = "DeleteBuilder", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<Builder> {
Ok(resource::delete::<Builder>(&self.id, args).await?)
}
}
impl Resolve<WriteArgs> for UpdateBuilder {
#[instrument(name = "UpdateBuilder", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
Ok(
resource::update::<Builder>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for RenameBuilder {
#[instrument(name = "RenameBuilder", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Builder>(&self.id, &self.name, user).await?)
}
}
+261
View File
@@ -0,0 +1,261 @@
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::*,
entities::{
Operation,
deployment::{
Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode,
},
docker::container::RestartPolicyNameEnum,
komodo_timestamp,
permission::PermissionLevel,
server::{Server, ServerState},
to_komodo_name,
update::Update,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use crate::{
helpers::{
periphery_client,
query::get_deployment_state,
update::{add_update, make_update},
},
resource,
state::{action_states, db_client, server_status_cache},
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateDeployment {
#[instrument(name = "CreateDeployment", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
resource::create::<Deployment>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyDeployment {
#[instrument(name = "CopyDeployment", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let Deployment { config, .. } =
resource::get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Deployment>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
#[instrument(name = "CreateDeploymentFromContainer", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write,
)
.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<WriteArgs> for DeleteDeployment {
#[instrument(name = "DeleteDeployment", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(resource::delete::<Deployment>(&self.id, args).await?)
}
}
impl Resolve<WriteArgs> for UpdateDeployment {
#[instrument(name = "UpdateDeployment", skip(user))]
async fn resolve(
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 = resource::get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
.deployment
.get_or_insert_default(&deployment.id)
.await;
// Will check to ensure deployment not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&self.name);
let container_state = get_deployment_state(&deployment).await?;
if container_state == DeploymentState::Unknown {
return Err(
anyhow!(
"Cannot rename Deployment when container status is unknown"
)
.into(),
);
}
let mut update =
make_update(&deployment, Operation::RenameDeployment, user);
update_one_by_id(
&db_client().deployments,
&deployment.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("Failed to update Deployment name on db")?;
if container_state != DeploymentState::NotDeployed {
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
let log = periphery_client(&server)?
.request(api::container::RenameContainer {
curr_name: deployment.name.clone(),
new_name: name.clone(),
})
.await
.context("Failed to rename container on server")?;
update.logs.push(log);
}
update.push_simple_log(
"Rename Deployment",
format!(
"Renamed Deployment from {} to {}",
deployment.name, name
),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
+123
View File
@@ -0,0 +1,123 @@
use anyhow::anyhow;
use komodo_client::{
api::write::{UpdateDescription, UpdateDescriptionResponse},
entities::{
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment, procedure::Procedure,
repo::Repo, server::Server, server_template::ServerTemplate,
stack::Stack, sync::ResourceSync,
},
};
use resolver_api::Resolve;
use crate::resource;
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateDescription {
#[instrument(name = "UpdateDescription", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateDescriptionResponse> {
match self.target {
ResourceTarget::System(_) => {
return Err(
anyhow!(
"cannot update description of System resource target"
)
.into(),
);
}
ResourceTarget::Server(id) => {
resource::update_description::<Server>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Deployment(id) => {
resource::update_description::<Deployment>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Build(id) => {
resource::update_description::<Build>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Repo(id) => {
resource::update_description::<Repo>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Builder(id) => {
resource::update_description::<Builder>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Alerter(id) => {
resource::update_description::<Alerter>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Procedure(id) => {
resource::update_description::<Procedure>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Action(id) => {
resource::update_description::<Action>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::update_description::<ServerTemplate>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::ResourceSync(id) => {
resource::update_description::<ResourceSync>(
&id,
&self.description,
user,
)
.await?;
}
ResourceTarget::Stack(id) => {
resource::update_description::<Stack>(
&id,
&self.description,
user,
)
.await?;
}
}
Ok(UpdateDescriptionResponse {})
}
}
+245
View File
@@ -0,0 +1,245 @@
use std::time::Instant;
use anyhow::Context;
use axum::{Extension, Router, middleware, routing::post};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::write::*, entities::user::User};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::auth::auth_request;
mod action;
mod alerter;
mod build;
mod builder;
mod deployment;
mod description;
mod permissions;
mod procedure;
mod provider;
mod repo;
mod server;
mod server_template;
mod service_user;
mod stack;
mod sync;
mod tag;
mod user;
mod user_group;
mod variable;
pub struct WriteArgs {
pub user: User,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[variant_derive(Debug)]
#[args(WriteArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
pub enum WriteRequest {
// ==== USER ====
UpdateUserUsername(UpdateUserUsername),
UpdateUserPassword(UpdateUserPassword),
DeleteUser(DeleteUser),
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER GROUP ====
CreateUserGroup(CreateUserGroup),
RenameUserGroup(RenameUserGroup),
DeleteUserGroup(DeleteUserGroup),
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
UpdateUserBasePermissions(UpdateUserBasePermissions),
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
// ==== DESCRIPTION ====
UpdateDescription(UpdateDescription),
// ==== SERVER ====
CreateServer(CreateServer),
DeleteServer(DeleteServer),
UpdateServer(UpdateServer),
RenameServer(RenameServer),
CreateNetwork(CreateNetwork),
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
CopyDeployment(CopyDeployment),
CreateDeploymentFromContainer(CreateDeploymentFromContainer),
DeleteDeployment(DeleteDeployment),
UpdateDeployment(UpdateDeployment),
RenameDeployment(RenameDeployment),
// ==== BUILD ====
CreateBuild(CreateBuild),
CopyBuild(CopyBuild),
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
// ==== BUILDER ====
CreateBuilder(CreateBuilder),
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== SERVER TEMPLATE ====
CreateServerTemplate(CreateServerTemplate),
CopyServerTemplate(CopyServerTemplate),
DeleteServerTemplate(DeleteServerTemplate),
UpdateServerTemplate(UpdateServerTemplate),
RenameServerTemplate(RenameServerTemplate),
// ==== REPO ====
CreateRepo(CreateRepo),
CopyRepo(CopyRepo),
DeleteRepo(DeleteRepo),
UpdateRepo(UpdateRepo),
RenameRepo(RenameRepo),
RefreshRepoCache(RefreshRepoCache),
CreateRepoWebhook(CreateRepoWebhook),
DeleteRepoWebhook(DeleteRepoWebhook),
// ==== ALERTER ====
CreateAlerter(CreateAlerter),
CopyAlerter(CopyAlerter),
DeleteAlerter(DeleteAlerter),
UpdateAlerter(UpdateAlerter),
RenameAlerter(RenameAlerter),
// ==== PROCEDURE ====
CreateProcedure(CreateProcedure),
CopyProcedure(CopyProcedure),
DeleteProcedure(DeleteProcedure),
UpdateProcedure(UpdateProcedure),
RenameProcedure(RenameProcedure),
// ==== ACTION ====
CreateAction(CreateAction),
CopyAction(CopyAction),
DeleteAction(DeleteAction),
UpdateAction(UpdateAction),
RenameAction(RenameAction),
// ==== SYNC ====
CreateResourceSync(CreateResourceSync),
CopyResourceSync(CopyResourceSync),
DeleteResourceSync(DeleteResourceSync),
UpdateResourceSync(UpdateResourceSync),
RenameResourceSync(RenameResourceSync),
WriteSyncFileContents(WriteSyncFileContents),
CommitSync(CommitSync),
RefreshResourceSyncPending(RefreshResourceSyncPending),
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 ====
CreateVariable(CreateVariable),
UpdateVariableValue(UpdateVariableValue),
UpdateVariableDescription(UpdateVariableDescription),
UpdateVariableIsSecret(UpdateVariableIsSecret),
DeleteVariable(DeleteVariable),
// ==== PROVIDERS ====
CreateGitProviderAccount(CreateGitProviderAccount),
UpdateGitProviderAccount(UpdateGitProviderAccount),
DeleteGitProviderAccount(DeleteGitProviderAccount),
CreateDockerRegistryAccount(CreateDockerRegistryAccount),
UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),
DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.layer(middleware::from_fn(auth_request))
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<WriteRequest>,
) -> serror::Result<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:#}");
}
res?
}
#[instrument(
name = "WriteRequest",
skip(user, request),
fields(
user_id = user.id,
request = format!("{:?}", request.extract_variant())
)
)]
async fn task(
req_id: Uuid,
request: WriteRequest,
user: User,
) -> serror::Result<axum::response::Response> {
info!("/write request | user: {}", user.username);
let timer = Instant::now();
let res = request.resolve(&WriteArgs { user }).await;
if let Err(e) = &res {
warn!("/write request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/write request {req_id} | resolve time: {elapsed:?}");
res.map(|res| res.0)
}
+452
View File
@@ -0,0 +1,452 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::*,
entities::{
ResourceTarget, ResourceTargetVariant,
permission::{UserTarget, UserTargetVariant},
},
};
use mungos::{
by_id::{find_one_by_id, update_one_by_id},
mongodb::{
bson::{Document, doc, oid::ObjectId},
options::UpdateOptions,
},
};
use resolver_api::Resolve;
use crate::{helpers::query::get_user, state::db_client};
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateUserAdmin {
#[instrument(name = "UpdateUserAdmin", skip(super_admin))]
async fn resolve(
self,
WriteArgs { user: super_admin }: &WriteArgs,
) -> serror::Result<UpdateUserAdminResponse> {
if !super_admin.super_admin {
return Err(
anyhow!("Only super admins can call this method.").into(),
);
}
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.").into(),
);
}
if user.super_admin {
return Err(anyhow!("Cannot update other super admins").into());
}
update_one_by_id(
&db_client().users,
&self.user_id,
doc! { "$set": { "admin": self.admin } },
None,
)
.await?;
Ok(UpdateUserAdminResponse {})
}
}
impl Resolve<WriteArgs> for UpdateUserBasePermissions {
#[instrument(name = "UpdateUserBasePermissions", skip(admin))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdateUserBasePermissionsResponse> {
let UpdateUserBasePermissions {
user_id,
enabled,
create_servers,
create_builds,
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
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"
)
.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 {
update_doc.insert("enabled", enabled);
}
if let Some(create_servers) = create_servers {
update_doc.insert("create_server_permissions", create_servers);
}
if let Some(create_builds) = create_builds {
update_doc.insert("create_build_permissions", create_builds);
}
update_one_by_id(
&db_client().users,
&user_id,
mungos::update::Update::Set(update_doc),
None,
)
.await?;
Ok(UpdateUserBasePermissionsResponse {})
}
}
impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
#[instrument(name = "UpdatePermissionOnResourceType", skip(admin))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {
let UpdatePermissionOnResourceType {
user_target,
resource_type,
permission,
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if user.admin {
return Err(
anyhow!(
"cannot use this method to update other admins permissions"
)
.into(),
);
}
if !user.enabled {
return Err(anyhow!("user not enabled").into());
}
}
let (user_target_variant, user_target_id) =
extract_user_target_with_validation(&user_target).await?;
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() } };
match user_target_variant {
UserTargetVariant::User => {
db_client()
.users
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {permission} on db")
})?;
}
UserTargetVariant::UserGroup => {
db_client()
.user_groups
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {permission} on db")
})?;
}
}
Ok(UpdatePermissionOnResourceTypeResponse {})
}
}
impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
#[instrument(name = "UpdatePermissionOnTarget", skip(admin))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnTargetResponse> {
let UpdatePermissionOnTarget {
user_target,
resource_target,
permission,
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if user.admin {
return Err(
anyhow!(
"cannot use this method to update other admins permissions"
)
.into(),
);
}
if !user.enabled {
return Err(anyhow!("user not enabled").into());
}
}
let (user_target_variant, user_target_id) =
extract_user_target_with_validation(&user_target).await?;
let (resource_variant, resource_id) =
extract_resource_target_with_validation(&resource_target)
.await?;
let (user_target_variant, resource_variant) =
(user_target_variant.as_ref(), resource_variant.as_ref());
db_client()
.permissions
.update_one(
doc! {
"user_target.type": user_target_variant,
"user_target.id": &user_target_id,
"resource_target.type": resource_variant,
"resource_target.id": &resource_id
},
doc! {
"$set": {
"user_target.type": user_target_variant,
"user_target.id": user_target_id,
"resource_target.type": resource_variant,
"resource_target.id": resource_id,
"level": permission.as_ref(),
}
},
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await?;
Ok(UpdatePermissionOnTargetResponse {})
}
}
/// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_user_target_with_validation(
user_target: &UserTarget,
) -> serror::Result<(UserTargetVariant, String)> {
match user_target {
UserTarget::User(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": ident },
};
let id = db_client()
.users
.find_one(filter)
.await
.context("failed to query db for users")?
.context("no matching user found")?
.id;
Ok((UserTargetVariant::User, id))
}
UserTarget::UserGroup(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.user_groups
.find_one(filter)
.await
.context("failed to query db for user_groups")?
.context("no matching user_group found")?
.id;
Ok((UserTargetVariant::UserGroup, id))
}
}
}
/// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_resource_target_with_validation(
resource_target: &ResourceTarget,
) -> serror::Result<(ResourceTargetVariant, String)> {
match resource_target {
ResourceTarget::System(_) => {
let res = resource_target.extract_variant_id();
Ok((res.0, res.1.clone()))
}
ResourceTarget::Build(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.builds
.find_one(filter)
.await
.context("failed to query db for builds")?
.context("no matching build found")?
.id;
Ok((ResourceTargetVariant::Build, id))
}
ResourceTarget::Builder(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.builders
.find_one(filter)
.await
.context("failed to query db for builders")?
.context("no matching builder found")?
.id;
Ok((ResourceTargetVariant::Builder, id))
}
ResourceTarget::Deployment(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.deployments
.find_one(filter)
.await
.context("failed to query db for deployments")?
.context("no matching deployment found")?
.id;
Ok((ResourceTargetVariant::Deployment, id))
}
ResourceTarget::Server(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.servers
.find_one(filter)
.await
.context("failed to query db for servers")?
.context("no matching server found")?
.id;
Ok((ResourceTargetVariant::Server, id))
}
ResourceTarget::Repo(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.repos
.find_one(filter)
.await
.context("failed to query db for repos")?
.context("no matching repo found")?
.id;
Ok((ResourceTargetVariant::Repo, id))
}
ResourceTarget::Alerter(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.alerters
.find_one(filter)
.await
.context("failed to query db for alerters")?
.context("no matching alerter found")?
.id;
Ok((ResourceTargetVariant::Alerter, id))
}
ResourceTarget::Procedure(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.procedures
.find_one(filter)
.await
.context("failed to query db for procedures")?
.context("no matching procedure found")?
.id;
Ok((ResourceTargetVariant::Procedure, id))
}
ResourceTarget::Action(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.actions
.find_one(filter)
.await
.context("failed to query db for actions")?
.context("no matching action found")?
.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 },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.resource_syncs
.find_one(filter)
.await
.context("failed to query db for resource syncs")?
.context("no matching resource sync found")?
.id;
Ok((ResourceTargetVariant::ResourceSync, id))
}
ResourceTarget::Stack(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.stacks
.find_one(filter)
.await
.context("failed to query db for stacks")?
.context("no matching stack found")?
.id;
Ok((ResourceTargetVariant::Stack, id))
}
}
}
+80
View File
@@ -0,0 +1,80 @@
use komodo_client::{
api::write::*,
entities::{
permission::PermissionLevel, procedure::Procedure, update::Update,
},
};
use resolver_api::Resolve;
use crate::resource;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateProcedure {
#[instrument(name = "CreateProcedure", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateProcedureResponse> {
Ok(
resource::create::<Procedure>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyProcedure {
#[instrument(name = "CopyProcedure", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CopyProcedureResponse> {
let Procedure { config, .. } =
resource::get_check_permissions::<Procedure>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Procedure>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<WriteArgs> for UpdateProcedure {
#[instrument(name = "UpdateProcedure", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateProcedureResponse> {
Ok(
resource::update::<Procedure>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for RenameProcedure {
#[instrument(name = "RenameProcedure", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(
resource::rename::<Procedure>(&self.id, &self.name, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for DeleteProcedure {
#[instrument(name = "DeleteProcedure", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<DeleteProcedureResponse> {
Ok(resource::delete::<Procedure>(&self.id, args).await?)
}
}
+410
View File
@@ -0,0 +1,410 @@
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::*,
entities::{
Operation, ResourceTarget,
provider::{DockerRegistryAccount, GitProviderAccount},
},
};
use mungos::{
by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},
mongodb::bson::{doc, to_document},
};
use resolver_api::Resolve;
use crate::{
helpers::update::{add_update, make_update},
state::db_client,
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateGitProviderAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can create git provider accounts")
.into(),
);
}
let mut account: GitProviderAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string.").into());
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string.").into());
}
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateGitProviderAccount,
user,
);
account.id = db_client()
.git_accounts
.insert_one(&account)
.await
.context("failed to create git provider account on db")?
.inserted_id
.as_object_id()
.context("inserted id is not ObjectId")?
.to_string();
update.push_simple_log(
"create git provider account",
format!(
"Created git provider account for {} with username {}",
account.domain, account.username
),
);
update.finalize();
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for create git provider account | {e:#}")
})
.ok();
Ok(account)
}
}
impl Resolve<WriteArgs> for UpdateGitProviderAccount {
async fn resolve(
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can update git provider accounts")
.into(),
);
}
if let Some(domain) = &self.account.domain {
if domain.is_empty() {
return Err(
anyhow!("cannot update git provider with empty domain")
.into(),
);
}
}
if let Some(username) = &self.account.username {
if username.is_empty() {
return Err(
anyhow!("cannot update git provider with empty username")
.into(),
);
}
}
// Ensure update does not change id
self.account.id = None;
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
user,
);
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,
&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, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id").into());
};
update.push_simple_log(
"update git provider account",
format!(
"Updated git provider account for {} with username {}",
account.domain, account.username
),
);
update.finalize();
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for update git provider account | {e:#}")
})
.ok();
Ok(account)
}
}
impl Resolve<WriteArgs> for DeleteGitProviderAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteGitProviderAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can delete git provider accounts")
.into(),
);
}
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
user,
);
let db = db_client();
let Some(account) = find_one_by_id(&db.git_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id").into());
};
delete_one_by_id(&db.git_accounts, &self.id, None)
.await
.context("failed to delete git account on db")?;
update.push_simple_log(
"delete git provider account",
format!(
"Deleted git provider account for {} with username {}",
account.domain, account.username
),
);
update.finalize();
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for delete git provider account | {e:#}")
})
.ok();
Ok(account)
}
}
impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!(
"only admins can create docker registry account accounts"
)
.into(),
);
}
let mut account: DockerRegistryAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string.").into());
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string.").into());
}
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateDockerRegistryAccount,
user,
);
account.id = db_client()
.registry_accounts
.insert_one(&account)
.await
.context(
"failed to create docker registry account account on db",
)?
.inserted_id
.as_object_id()
.context("inserted id is not ObjectId")?
.to_string();
update.push_simple_log(
"create docker registry account",
format!(
"Created docker registry account account for {} with username {}",
account.domain, account.username
),
);
update.finalize();
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for create docker registry account | {e:#}")
})
.ok();
Ok(account)
}
}
impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
async fn resolve(
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can update docker registry accounts")
.into(),
);
}
if let Some(domain) = &self.account.domain {
if domain.is_empty() {
return Err(
anyhow!(
"cannot update docker registry account with empty domain"
)
.into(),
);
}
}
if let Some(username) = &self.account.username {
if username.is_empty() {
return Err(
anyhow!(
"cannot update docker registry account with empty username"
)
.into(),
);
}
}
self.account.id = None;
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
user,
);
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,
&self.id,
doc! { "$set": account },
None,
)
.await
.context(
"failed to update docker registry account account on db",
)?;
let Some(account) =
find_one_by_id(&db.registry_accounts, &self.id)
.await
.context("failed to query db for registry accounts")?
else {
return Err(anyhow!("no account found with given id").into());
};
update.push_simple_log(
"update docker registry account",
format!(
"Updated docker registry account account for {} with username {}",
account.domain, account.username
),
);
update.finalize();
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for update docker registry account | {e:#}")
})
.ok();
Ok(account)
}
}
impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteDockerRegistryAccountResponse> {
if !user.admin {
return Err(
anyhow!("only admins can delete docker registry accounts")
.into(),
);
}
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
user,
);
let db = db_client();
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").into());
};
delete_one_by_id(&db.registry_accounts, &self.id, None)
.await
.context("failed to delete registry account on db")?;
update.push_simple_log(
"delete registry account",
format!(
"Deleted registry account for {} with username {}",
account.domain, account.username
),
);
update.finalize();
add_update(update)
.await
.inspect_err(|e| {
error!("failed to add update for delete docker registry account | {e:#}")
})
.ok();
Ok(account)
}
}
+460
View File
@@ -0,0 +1,460 @@
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,
update::{Log, Update},
},
};
use mongo_indexed::doc;
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::{
git_token, periphery_client,
update::{add_update, make_update},
},
resource,
state::{action_states, db_client, github_client},
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateRepo {
#[instrument(name = "CreateRepo", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
Ok(resource::create::<Repo>(&self.name, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for CopyRepo {
#[instrument(name = "CopyRepo", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
let Repo { config, .. } =
resource::get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Repo>(&self.name, config.into(), 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<WriteArgs> for UpdateRepo {
#[instrument(name = "UpdateRepo", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
Ok(resource::update::<Repo>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameRepo {
#[instrument(name = "RenameRepo", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
if repo.config.server_id.is_empty()
|| !repo.config.path.is_empty()
{
return Ok(
resource::rename::<Repo>(&repo.id, &self.name, user).await?,
);
}
// get the action state for the repo (or insert default).
let action_state =
action_states().repo.get_or_insert_default(&repo.id).await;
// Will check to ensure repo not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&self.name);
let mut update = make_update(&repo, Operation::RenameRepo, user);
update_one_by_id(
&db_client().repos,
&repo.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("Failed to update Repo name on db")?;
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let log = match periphery_client(&server)?
.request(api::git::RenameRepo {
curr_name: to_komodo_name(&repo.name),
new_name: name.clone(),
})
.await
.context("Failed to rename Repo directory on Server")
{
Ok(log) => log,
Err(e) => Log::error(
"Rename Repo directory failure",
format_serror(&e.into()),
),
};
update.logs.push(log);
update.push_simple_log(
"Rename Repo",
format!("Renamed Repo from {} to {}", repo.name, name),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<WriteArgs> for RefreshRepoCache {
#[instrument(
name = "RefreshRepoCache",
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
// repo should be able to do this.
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
if repo.config.git_provider.is_empty()
|| repo.config.repo.is_empty()
{
// Nothing to do
return Ok(NoData {});
}
let mut clone_args: CloneArgs = (&repo).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;
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,
&core_config().repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.with_context(|| {
format!("Failed to update repo at {repo_path:?}")
})?;
let info = RepoInfo {
last_pulled_at: repo.info.last_pulled_at,
last_built_at: repo.info.last_built_at,
built_hash: repo.info.built_hash,
built_message: repo.info.built_message,
latest_hash: hash,
latest_message: message,
};
let info = to_document(&info)
.context("failed to serialize repo info to bson")?;
db_client()
.repos
.update_one(
doc! { "name": &repo.name },
doc! { "$set": { "info": info } },
)
.await
.context("failed to update repo info on db")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for CreateRepoWebhook {
#[instrument(name = "CreateRepoWebhook", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<CreateRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
&args.user,
PermissionLevel::Write,
)
.await?;
if repo.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if repo.config.webhook_secret.is_empty() {
webhook_secret
} else {
&repo.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
RepoWebhookAction::Pull => {
format!("{host}/listener/github/repo/{}/pull", repo.id)
}
RepoWebhookAction::Build => {
format!("{host}/listener/github/repo/{}/build", repo.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo_name, &request)
.await
.context("failed to create webhook")?;
if !repo.config.webhook_enabled {
UpdateRepo {
id: repo.id,
config: PartialRepoConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update repo to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteRepoWebhook {
#[instrument(name = "DeleteRepoWebhook", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Write,
)
.await?;
if repo.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if repo.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
RepoWebhookAction::Pull => {
format!("{host}/listener/github/repo/{}/pull", repo.id)
}
RepoWebhookAction::Build => {
format!("{host}/listener/github/repo/{}/build", repo.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo_name, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
}
+103
View File
@@ -0,0 +1,103 @@
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
Operation,
permission::PermissionLevel,
server::Server,
update::{Update, UpdateStatus},
},
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{
periphery_client,
update::{add_update, make_update, update_update},
},
resource,
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServer {
#[instrument(name = "CreateServer", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
Ok(
resource::create::<Server>(&self.name, self.config, 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<WriteArgs> for UpdateServer {
#[instrument(name = "UpdateServer", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
Ok(resource::update::<Server>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameServer {
#[instrument(name = "RenameServer", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Server>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for CreateNetwork {
#[instrument(name = "CreateNetwork", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write,
)
.await?;
let periphery = periphery_client(&server)?;
let mut update =
make_update(&server, Operation::CreateNetwork, user);
update.status = UpdateStatus::InProgress;
update.id = add_update(update.clone()).await?;
match periphery
.request(api::network::CreateNetwork {
name: self.name,
driver: None,
})
.await
{
Ok(log) => update.logs.push(log),
Err(e) => update.push_error_log(
"create network",
format_serror(&e.context("failed to create network").into()),
),
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
+92
View File
@@ -0,0 +1,92 @@
use komodo_client::{
api::write::{
CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate,
RenameServerTemplate, UpdateServerTemplate,
},
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
update::Update,
},
};
use resolver_api::Resolve;
use crate::resource;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServerTemplate {
#[instrument(name = "CreateServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ServerTemplate> {
Ok(
resource::create::<ServerTemplate>(
&self.name,
self.config,
user,
)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyServerTemplate {
#[instrument(name = "CopyServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ServerTemplate> {
let ServerTemplate { config, .. } =
resource::get_check_permissions::<ServerTemplate>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<ServerTemplate>(
&self.name,
config.into(),
user,
)
.await?,
)
}
}
impl Resolve<WriteArgs> for DeleteServerTemplate {
#[instrument(name = "DeleteServerTemplate", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<ServerTemplate> {
Ok(resource::delete::<ServerTemplate>(&self.id, args).await?)
}
}
impl Resolve<WriteArgs> for UpdateServerTemplate {
#[instrument(name = "UpdateServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ServerTemplate> {
Ok(
resource::update::<ServerTemplate>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for RenameServerTemplate {
#[instrument(name = "RenameServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(
resource::rename::<ServerTemplate>(&self.id, &self.name, user)
.await?,
)
}
}
+157
View File
@@ -0,0 +1,157 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use komodo_client::{
api::{user::CreateApiKey, write::*},
entities::{
komodo_timestamp,
user::{User, UserConfig},
},
};
use mungos::{
by_id::find_one_by_id,
mongodb::bson::{doc, oid::ObjectId},
};
use resolver_api::Resolve;
use crate::{api::user::UserArgs, state::db_client};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServiceUser {
#[instrument(name = "CreateServiceUser", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("username cannot be valid ObjectId").into(),
);
}
let config = UserConfig::Service {
description: self.description,
};
let mut user = User {
id: Default::default(),
username: self.username,
config,
enabled: true,
admin: false,
super_admin: false,
create_server_permissions: false,
create_build_permissions: false,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
updated_at: komodo_timestamp(),
};
user.id = db_client()
.users
.insert_one(&user)
.await
.context("failed to create service user on db")?
.inserted_id
.as_object_id()
.context("inserted id is not object id")?
.to_string();
Ok(user)
}
}
impl Resolve<WriteArgs> for UpdateServiceUserDescription {
#[instrument(name = "UpdateServiceUserDescription", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateServiceUserDescriptionResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
}
let db = db_client();
let service_user = db
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("no user with given username")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user").into());
};
db.users
.update_one(
doc! { "username": &self.username },
doc! { "$set": { "config.data.description": self.description } },
)
.await
.context("failed to update user on db")?;
let res = db
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed to query db for user")?
.context("user with username not found")?;
Ok(res)
}
}
impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
#[instrument(name = "CreateApiKeyForServiceUser", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
}
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").into());
};
CreateApiKey {
name: self.name,
expires: self.expires,
}
.resolve(&UserArgs { user: service_user })
.await
}
}
impl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {
#[instrument(name = "DeleteApiKeyForServiceUser", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin").into());
}
let db = db_client();
let api_key = db
.api_keys
.find_one(doc! { "key": &self.key })
.await
.context("failed to query db for api key")?
.context("did not find matching api key")?;
let service_user =
find_one_by_id(&db_client().users, &api_key.user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user").into());
};
db.api_keys
.delete_one(doc! { "key": self.key })
.await
.context("failed to delete api key on db")?;
Ok(DeleteApiKeyForServiceUserResponse {})
}
}
+629
View File
@@ -0,0 +1,629 @@
use anyhow::{Context, anyhow};
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
FileContents, NoData, Operation,
config::core::CoreConfig,
permission::PermissionLevel,
server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo},
update::Update,
user::stack_user,
},
};
use mungos::mongodb::bson::{doc, to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::api::compose::{
GetComposeContentsOnHost, GetComposeContentsOnHostResponse,
WriteCommitComposeContents, WriteComposeContentsToHost,
};
use resolver_api::Resolve;
use crate::{
api::execute::pull_stack_inner,
config::core_config,
helpers::{
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
resource,
stack::{
get_stack_and_server,
remote::{RemoteComposeContents, get_repo_compose_contents},
services::extract_services_into_res,
},
state::{db_client, github_client},
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateStack {
#[instrument(name = "CreateStack", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
Ok(
resource::create::<Stack>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyStack {
#[instrument(name = "CopyStack", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
let Stack { config, .. } =
resource::get_check_permissions::<Stack>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Stack>(&self.name, config.into(), 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<WriteArgs> for UpdateStack {
#[instrument(name = "UpdateStack", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
Ok(resource::update::<Stack>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameStack {
#[instrument(name = "RenameStack", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Stack>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for WriteStackFileContents {
#[instrument(name = "WriteStackFileContents", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let WriteStackFileContents {
stack,
file_path,
contents,
} = self;
let (mut stack, server) = get_stack_and_server(
&stack,
user,
PermissionLevel::Write,
true,
)
.await?;
if !stack.config.files_on_host && stack.config.repo.is_empty() {
return Err(anyhow!(
"Stack is not configured to use Files on Host or Git Repo, can't write file contents"
).into());
}
let mut update =
make_update(&stack, Operation::WriteStackContents, user);
update.push_simple_log("File contents to write", &contents);
let stack_id = stack.id.clone();
if stack.config.files_on_host {
match periphery_client(&server)?
.request(WriteComposeContentsToHost {
name: stack.name,
run_directory: stack.config.run_directory,
file_path,
contents,
})
.await
.context("Failed to write contents to host")
{
Ok(log) => {
update.logs.push(log);
}
Err(e) => {
update.push_error_log(
"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
};
match periphery_client(&server)?
.request(WriteCommitComposeContents {
stack,
username: Some(user.username.clone()),
file_path,
contents,
git_token,
})
.await
.context("Failed to write contents to host")
{
Ok(res) => {
update.logs.extend(res.logs);
}
Err(e) => {
update.push_error_log(
"Write File Contents",
format_serror(&e.into()),
);
}
};
}
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",
)
{
update.push_error_log(
"Refresh stack cache",
format_serror(&e.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<WriteArgs> for RefreshStackCache {
#[instrument(
name = "RefreshStackCache",
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
// stack should be able to do this.
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Execute,
)
.await?;
let file_contents_empty = stack.config.file_contents.is_empty();
let repo_empty = stack.config.repo.is_empty();
if !stack.config.files_on_host
&& file_contents_empty
&& repo_empty
{
// Nothing to do without one of these
return Ok(NoData {});
}
let mut missing_files = Vec::new();
let (
latest_services,
remote_contents,
remote_errors,
latest_hash,
latest_message,
) = if stack.config.files_on_host {
// =============
// FILES ON HOST
// =============
let (server, state) = if stack.config.server_id.is_empty() {
(None, ServerState::Disabled)
} else {
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
(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 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
);
}
}
(services, Some(contents), Some(errors), None, None)
} else {
(vec![], None, None, None, None)
}
} else if !repo_empty {
// ================
// REPO BASED STACK
// ================
let RemoteComposeContents {
successful: remote_contents,
errored: remote_errors,
hash: latest_hash,
message: latest_message,
..
} = get_repo_compose_contents(&stack, Some(&mut missing_files))
.await?;
let project_name = stack.project_name(true);
let mut services = Vec::new();
for contents in &remote_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(remote_contents),
Some(remote_errors),
latest_hash,
latest_message,
)
} else {
// =============
// UI BASED FILE
// =============
let mut services = Vec::new();
if let Err(e) = extract_services_into_res(
// this should latest (not deployed), so make the project name fresh.
&stack.project_name(true),
&stack.config.file_contents,
&mut services,
) {
warn!(
"Failed to extract Stack services for {}, things may not work correctly. | {e:#}",
stack.name
);
services.extend(stack.info.latest_services.clone());
};
(services, None, None, None, None)
};
let info = StackInfo {
missing_files,
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,
latest_hash,
latest_message,
};
let info = to_document(&info)
.context("failed to serialize stack info to bson")?;
db_client()
.stacks
.update_one(
doc! { "name": &stack.name },
doc! { "$set": { "info": info } },
)
.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, None).await
{
warn!(
"Failed to pull latest images for Stack {name} | {e:#}",
);
}
}
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for CreateStackWebhook {
#[instrument(name = "CreateStackWebhook", skip(args))]
async fn resolve(
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"
)
.into(),
);
};
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write,
)
.await?;
if stack.config.repo.is_empty() {
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}")
.into(),
);
};
let repo =
split.next().context("Stack repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if stack.config.webhook_secret.is_empty() {
webhook_secret
} else {
&stack.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id)
}
StackWebhookAction::Deploy => {
format!("{host}/listener/github/stack/{}/deploy", stack.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo, &request)
.await
.context("failed to create webhook")?;
if !stack.config.webhook_enabled {
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<WriteArgs> for DeleteStackWebhook {
#[instrument(name = "DeleteStackWebhook", skip(user))]
async fn resolve(
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"
)
.into(),
);
};
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write,
)
.await?;
if stack.config.git_provider != "github.com" {
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").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}")
.into(),
);
};
let repo =
split.next().context("Sync repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id)
}
StackWebhookAction::Deploy => {
format!("{host}/listener/github/stack/{}/deploy", stack.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
}
File diff suppressed because it is too large Load Diff
+260
View File
@@ -0,0 +1,260 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
CreateTag, DeleteTag, RenameTag, UpdateTagColor,
UpdateTagsOnResource, UpdateTagsOnResourceResponse,
},
entities::{
ResourceTarget,
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, TagColor},
},
};
use mungos::{
by_id::{delete_one_by_id, update_one_by_id},
mongodb::bson::{doc, oid::ObjectId},
};
use resolver_api::Resolve;
use crate::{
helpers::query::{get_tag, get_tag_check_owner},
resource,
state::db_client,
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateTag {
#[instrument(name = "CreateTag", skip(user))]
async fn resolve(
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: self.name,
color: TagColor::Slate,
owner: user.id.clone(),
};
tag.id = db_client()
.tags
.insert_one(&tag)
.await
.context("failed to create tag on db")?
.inserted_id
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
Ok(tag)
}
}
impl Resolve<WriteArgs> for RenameTag {
#[instrument(name = "RenameTag", skip(user))]
async fn resolve(
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(&self.id, user).await?;
update_one_by_id(
&db_client().tags,
&self.id,
doc! { "$set": { "name": self.name } },
None,
)
.await
.context("failed to rename tag on db")?;
Ok(get_tag(&self.id).await?)
}
}
impl Resolve<WriteArgs> for UpdateTagColor {
#[instrument(name = "UpdateTagColor", skip(user))]
async fn resolve(
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>(&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),
resource::remove_tag_from_all::<ServerTemplate>(&self.id),
)?;
delete_one_by_id(&db_client().tags, &self.id, None).await?;
Ok(tag)
}
}
impl Resolve<WriteArgs> for UpdateTagsOnResource {
#[instrument(name = "UpdateTagsOnResource", skip(args))]
async fn resolve(
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>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Build>(&id, self.tags, args).await?;
}
ResourceTarget::Builder(id) => {
resource::get_check_permissions::<Builder>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Builder>(&id, self.tags, args).await?
}
ResourceTarget::Deployment(id) => {
resource::get_check_permissions::<Deployment>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Deployment>(&id, self.tags, args)
.await?
}
ResourceTarget::Server(id) => {
resource::get_check_permissions::<Server>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Server>(&id, self.tags, args).await?
}
ResourceTarget::Repo(id) => {
resource::get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Repo>(&id, self.tags, args).await?
}
ResourceTarget::Alerter(id) => {
resource::get_check_permissions::<Alerter>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Alerter>(&id, self.tags, args).await?
}
ResourceTarget::Procedure(id) => {
resource::get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Procedure>(&id, self.tags, args)
.await?
}
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Action>(&id, self.tags, args).await?
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<ServerTemplate>(&id, self.tags, args)
.await?
}
ResourceTarget::ResourceSync(id) => {
resource::get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<ResourceSync>(&id, self.tags, args)
.await?
}
ResourceTarget::Stack(id) => {
resource::get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Stack>(&id, self.tags, args).await?
}
};
Ok(UpdateTagsOnResourceResponse {})
}
}
+150
View File
@@ -0,0 +1,150 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
DeleteUser, DeleteUserResponse, UpdateUserPassword,
UpdateUserPasswordResponse, UpdateUserUsername,
UpdateUserUsernameResponse,
},
entities::{NoData, user::UserConfig},
};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
config::core_config, helpers::hash_password, state::db_client,
};
use super::WriteArgs;
//
impl Resolve<WriteArgs> for UpdateUserUsername {
async fn resolve(
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": &self.username })
.await
.context("Failed to query for existing users")?
.is_some()
{
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": self.username } },
)
.await
.context("Failed to update user username on database.")?;
Ok(NoData {})
}
}
//
impl Resolve<WriteArgs> for UpdateUserPassword {
async fn resolve(
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").into());
};
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(self.password)?;
db_client()
.users
.update_one(
doc! { "_id": id },
doc! { "$set": {
"config.data.password": hashed_password
} },
)
.await
.context("Failed to update user password on database.")?;
Ok(NoData {})
}
}
//
impl Resolve<WriteArgs> for DeleteUser {
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<DeleteUserResponse> {
if !admin.admin {
return Err(anyhow!("Calling user is not admin.").into());
}
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(&self.user) {
doc! { "_id": id }
} else {
doc! { "username": self.user }
};
let db = db_client();
let Some(user) = db
.users
.find_one(query.clone())
.await
.context("Failed to query database for users.")?
else {
return Err(
anyhow!("No user found with given id / username").into(),
);
};
if user.super_admin {
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.")
.into(),
);
}
db.users
.delete_one(query)
.await
.context("Failed to delete user from database")?;
Ok(user)
}
}
+242
View File
@@ -0,0 +1,242 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::{
AddUserToUserGroup, CreateUserGroup, DeleteUserGroup,
RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup,
},
entities::{komodo_timestamp, user_group::UserGroup},
};
use mungos::{
by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},
find::find_collect,
mongodb::bson::{doc, oid::ObjectId},
};
use resolver_api::Resolve;
use crate::state::db_client;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateUserGroup {
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let user_group = UserGroup {
id: Default::default(),
users: Default::default(),
all: Default::default(),
updated_at: komodo_timestamp(),
name: self.name,
};
let db = db_client();
let id = db
.user_groups
.insert_one(user_group)
.await
.context("failed to create UserGroup on db")?
.inserted_id
.as_object_id()
.context("inserted id is not ObjectId")?
.to_string();
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")?;
Ok(res)
}
}
impl Resolve<WriteArgs> for RenameUserGroup {
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();
update_one_by_id(
&db.user_groups,
&self.id,
doc! { "$set": { "name": self.name } },
None,
)
.await
.context("failed to rename UserGroup on db")?;
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")?;
Ok(res)
}
}
impl Resolve<WriteArgs> for DeleteUserGroup {
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 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, &self.id, None)
.await
.context("failed to delete UserGroup from db")?;
db.permissions
.delete_many(doc! {
"user_target.type": "UserGroup",
"user_target.id": self.id,
})
.await
.context("failed to clean up UserGroups permissions. User Group has been deleted")?;
Ok(ug)
}
}
impl Resolve<WriteArgs> for AddUserToUserGroup {
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) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": &self.user },
};
let user = db
.users
.find_one(filter)
.await
.context("failed to query mongo for users")?
.context("no matching user found")?;
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! { "$addToSet": { "users": &user.id } },
)
.await
.context("failed to add user to group on db")?;
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)
}
}
impl Resolve<WriteArgs> for RemoveUserFromUserGroup {
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) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": &self.user },
};
let user = db
.users
.find_one(filter)
.await
.context("failed to query mongo for users")?
.context("no matching user found")?;
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! { "$pull": { "users": &user.id } },
)
.await
.context("failed to add user to group on db")?;
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)
}
}
impl Resolve<WriteArgs> for SetUsersInUserGroup {
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 all_users = find_collect(&db.users, None, None)
.await
.context("failed to query db for users")?
.into_iter()
.map(|u| (u.username, u.id))
.collect::<HashMap<_, _>>();
// Make sure all users are user ids
let users = self
.users
.into_iter()
.filter_map(|user| match ObjectId::from_str(&user) {
Ok(_) => Some(user),
Err(_) => all_users.get(&user).cloned(),
})
.collect::<Vec<_>>();
let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
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")?;
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)
}
}
+190
View File
@@ -0,0 +1,190 @@
use anyhow::{Context, anyhow};
use komodo_client::{
api::write::*,
entities::{Operation, ResourceTarget, variable::Variable},
};
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::{
query::get_variable,
update::{add_update, make_update},
},
state::db_client,
};
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateVariable {
#[instrument(name = "CreateVariable", skip(user, self), fields(name = &self.name))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateVariableResponse> {
let CreateVariable {
name,
value,
description,
is_secret,
} = self;
if !user.admin {
return Err(anyhow!("only admins can create variables").into());
}
let variable = Variable {
name,
value,
description,
is_secret,
};
db_client()
.variables
.insert_one(&variable)
.await
.context("failed to create variable on db")?;
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateVariable,
user,
);
update
.push_simple_log("create variable", format!("{variable:#?}"));
update.finalize();
add_update(update).await?;
Ok(get_variable(&variable.name).await?)
}
}
impl Resolve<WriteArgs> for UpdateVariableValue {
#[instrument(name = "UpdateVariableValue", skip(user, self), fields(name = &self.name))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableValueResponse> {
if !user.admin {
return Err(anyhow!("only admins can update variables").into());
}
let UpdateVariableValue { name, value } = self;
let variable = get_variable(&name).await?;
if value == variable.value {
return Ok(variable);
}
db_client()
.variables
.update_one(
doc! { "name": &name },
doc! { "$set": { "value": &value } },
)
.await
.context("failed to update variable value on db")?;
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateVariableValue,
user,
);
let log = if variable.is_secret {
format!(
"<span class=\"text-muted-foreground\">variable</span>: '{name}'\n<span class=\"text-muted-foreground\">from</span>: <span class=\"text-red-500\">{}</span>\n<span class=\"text-muted-foreground\">to</span>: <span class=\"text-green-500\">{value}</span>",
variable.value.replace(|_| true, "#")
)
} else {
format!(
"<span class=\"text-muted-foreground\">variable</span>: '{name}'\n<span class=\"text-muted-foreground\">from</span>: <span class=\"text-red-500\">{}</span>\n<span class=\"text-muted-foreground\">to</span>: <span class=\"text-green-500\">{value}</span>",
variable.value
)
};
update.push_simple_log("update variable value", log);
update.finalize();
add_update(update).await?;
Ok(get_variable(&name).await?)
}
}
impl Resolve<WriteArgs> for UpdateVariableDescription {
#[instrument(name = "UpdateVariableDescription", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableDescriptionResponse> {
if !user.admin {
return Err(anyhow!("only admins can update variables").into());
}
db_client()
.variables
.update_one(
doc! { "name": &self.name },
doc! { "$set": { "description": &self.description } },
)
.await
.context("failed to update variable description on db")?;
Ok(get_variable(&self.name).await?)
}
}
impl Resolve<WriteArgs> for UpdateVariableIsSecret {
#[instrument(name = "UpdateVariableIsSecret", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateVariableIsSecretResponse> {
if !user.admin {
return Err(anyhow!("only admins can update variables").into());
}
db_client()
.variables
.update_one(
doc! { "name": &self.name },
doc! { "$set": { "is_secret": self.is_secret } },
)
.await
.context("failed to update variable is secret on db")?;
Ok(get_variable(&self.name).await?)
}
}
impl Resolve<WriteArgs> for DeleteVariable {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteVariableResponse> {
if !user.admin {
return Err(anyhow!("only admins can delete variables").into());
}
let variable = get_variable(&self.name).await?;
db_client()
.variables
.delete_one(doc! { "name": &self.name })
.await
.context("failed to delete variable on db")?;
let mut update = make_update(
ResourceTarget::system(),
Operation::DeleteVariable,
user,
);
update
.push_simple_log("delete variable", format!("{variable:#?}"));
update.finalize();
add_update(update).await?;
Ok(variable)
}
}
+235
View File
@@ -0,0 +1,235 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use komodo_client::entities::config::core::{
CoreConfig, OauthCredentials,
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use tokio::sync::Mutex;
use crate::{
auth::STATE_PREFIX_LENGTH, config::core_config,
helpers::random_string,
};
pub fn github_oauth_client() -> &'static Option<GithubOauthClient> {
static GITHUB_OAUTH_CLIENT: OnceLock<Option<GithubOauthClient>> =
OnceLock::new();
GITHUB_OAUTH_CLIENT
.get_or_init(|| GithubOauthClient::new(core_config()))
}
pub struct GithubOauthClient {
http: reqwest::Client,
client_id: String,
client_secret: String,
redirect_uri: String,
scopes: String,
states: Mutex<Vec<String>>,
user_agent: String,
}
impl GithubOauthClient {
pub fn new(
CoreConfig {
github_oauth:
OauthCredentials {
enabled,
id,
secret,
},
host,
..
}: &CoreConfig,
) -> Option<GithubOauthClient> {
if !enabled {
return None;
}
if host.is_empty() {
warn!(
"github oauth is enabled, but 'config.host' is not configured"
);
return None;
}
if id.is_empty() {
warn!(
"github oauth is enabled, but 'config.github_oauth.id' is not configured"
);
return None;
}
if secret.is_empty() {
warn!(
"github oauth is enabled, but 'config.github_oauth.secret' is not configured"
);
return None;
}
GithubOauthClient {
http: reqwest::Client::new(),
client_id: id.clone(),
client_secret: secret.clone(),
redirect_uri: format!("{host}/auth/github/callback"),
user_agent: Default::default(),
scopes: Default::default(),
states: Default::default(),
}
.into()
}
#[instrument(level = "debug", skip(self))]
pub async fn get_login_redirect_url(
&self,
redirect: Option<String>,
) -> String {
let state_prefix = random_string(STATE_PREFIX_LENGTH);
let state = match redirect {
Some(redirect) => format!("{state_prefix}{redirect}"),
None => state_prefix,
};
let redirect_url = format!(
"https://github.com/login/oauth/authorize?state={state}&client_id={}&redirect_uri={}&scope={}",
self.client_id, self.redirect_uri, self.scopes
);
let mut states = self.states.lock().await;
states.push(state);
redirect_url
}
#[instrument(level = "debug", skip(self))]
pub async fn check_state(&self, state: &str) -> bool {
let mut contained = false;
self.states.lock().await.retain(|s| {
if s.as_str() == state {
contained = true;
false
} else {
true
}
});
contained
}
#[instrument(level = "debug", skip(self))]
pub async fn get_access_token(
&self,
code: &str,
) -> anyhow::Result<AccessTokenResponse> {
self
.post::<(), _>(
"https://github.com/login/oauth/access_token",
&[
("client_id", self.client_id.as_str()),
("client_secret", self.client_secret.as_str()),
("redirect_uri", self.redirect_uri.as_str()),
("code", code),
],
None,
None,
)
.await
.context("failed to get github access token using code")
}
#[instrument(level = "debug", skip(self))]
pub async fn get_github_user(
&self,
token: &str,
) -> anyhow::Result<GithubUserResponse> {
self
.get("https://api.github.com/user", &[], Some(token))
.await
.context("failed to get github user using access token")
}
#[instrument(level = "debug", skip(self))]
async fn get<R: DeserializeOwned>(
&self,
endpoint: &str,
query: &[(&str, &str)],
bearer_token: Option<&str>,
) -> anyhow::Result<R> {
let mut req = self
.http
.get(endpoint)
.query(query)
.header("User-Agent", &self.user_agent);
if let Some(bearer_token) = bearer_token {
req =
req.header("Authorization", format!("Bearer {bearer_token}"));
}
let res = req.send().await.context("failed to reach github")?;
let status = res.status();
if status == StatusCode::OK {
let body = res
.json()
.await
.context("failed to parse body into expected type")?;
Ok(body)
} else {
let text = res.text().await.context(format!(
"status: {status} | failed to get response text"
))?;
Err(anyhow!("status: {status} | text: {text}"))
}
}
async fn post<B: Serialize, R: DeserializeOwned>(
&self,
endpoint: &str,
query: &[(&str, &str)],
body: Option<&B>,
bearer_token: Option<&str>,
) -> anyhow::Result<R> {
let mut req = self
.http
.post(endpoint)
.query(query)
.header("Accept", "application/json")
.header("User-Agent", &self.user_agent);
if let Some(body) = body {
req = req.json(body);
}
if let Some(bearer_token) = bearer_token {
req =
req.header("Authorization", format!("Bearer {bearer_token}"));
}
let res = req.send().await.context("failed to reach github")?;
let status = res.status();
if status == StatusCode::OK {
let body = res
.json()
.await
.context("failed to parse POST body into expected type")?;
Ok(body)
} else {
let text = res.text().await.with_context(|| format!(
"method: POST | status: {status} | failed to get response text"
))?;
Err(anyhow!("method: POST | status: {status} | text: {text}"))
}
}
}
#[derive(Deserialize)]
pub struct AccessTokenResponse {
pub access_token: String,
// pub scope: String,
// pub token_type: String,
}
#[derive(Deserialize)]
pub struct GithubUserResponse {
pub login: String,
pub id: u128,
pub avatar_url: String,
// pub email: Option<String>,
}
+125
View File
@@ -0,0 +1,125 @@
use anyhow::{Context, anyhow};
use axum::{
Router, extract::Query, response::Redirect, routing::get,
};
use komodo_client::entities::{
komodo_timestamp,
user::{User, UserConfig},
};
use mongo_indexed::Document;
use mungos::mongodb::bson::doc;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use crate::{
config::core_config,
state::{db_client, jwt_client},
};
use self::client::github_oauth_client;
use super::{RedirectQuery, STATE_PREFIX_LENGTH};
pub mod client;
pub fn router() -> Router {
Router::new()
.route(
"/login",
get(|Query(query): Query<RedirectQuery>| async {
Redirect::to(
&github_oauth_client()
.as_ref()
// OK: the router is only mounted in case that the client is populated
.unwrap()
.get_login_redirect_url(query.redirect)
.await,
)
}),
)
.route(
"/callback",
get(|query| async {
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
}),
)
}
#[derive(Debug, Deserialize)]
struct CallbackQuery {
state: String,
code: String,
}
#[instrument(name = "GithubCallback", level = "debug")]
async fn callback(
Query(query): Query<CallbackQuery>,
) -> anyhow::Result<Redirect> {
let client = github_oauth_client().as_ref().unwrap();
if !client.check_state(&query.state).await {
return Err(anyhow!("state mismatch"));
}
let token = client.get_access_token(&query.code).await?;
let github_user =
client.get_github_user(&token.access_token).await?;
let github_id = github_user.id.to_string();
let db_client = db_client();
let user = db_client
.users
.find_one(doc! { "config.data.github_id": &github_id })
.await
.context("failed at find user query from database")?;
let jwt = match user {
Some(user) => jwt_client()
.encode(user.id)
.context("failed to generate jwt")?,
None => {
let ts = komodo_timestamp();
let no_users_exist =
db_client.users.find_one(Document::new()).await?.is_none();
let core_config = core_config();
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
let user = User {
id: Default::default(),
username: github_user.login,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
updated_at: ts,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config: UserConfig::Github {
github_id,
avatar: github_user.avatar_url,
},
};
let user_id = db_client
.users
.insert_one(user)
.await
.context("failed to create user on mongo")?
.inserted_id
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id)
.context("failed to generate jwt")?
}
};
let exchange_token = jwt_client().create_exchange_token(jwt).await;
let redirect = &query.state[STATE_PREFIX_LENGTH..];
let redirect_url = if redirect.is_empty() {
format!("{}?token={exchange_token}", core_config().host)
} else {
let splitter = if redirect.contains('?') { '&' } else { '?' };
format!("{}{splitter}token={exchange_token}", redirect)
};
Ok(Redirect::to(&redirect_url))
}
+211
View File
@@ -0,0 +1,211 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use jsonwebtoken::{DecodingKey, Validation, decode};
use komodo_client::entities::config::core::{
CoreConfig, OauthCredentials,
};
use reqwest::StatusCode;
use serde::{Deserialize, de::DeserializeOwned};
use tokio::sync::Mutex;
use crate::{
auth::STATE_PREFIX_LENGTH, config::core_config,
helpers::random_string,
};
pub fn google_oauth_client() -> &'static Option<GoogleOauthClient> {
static GOOGLE_OAUTH_CLIENT: OnceLock<Option<GoogleOauthClient>> =
OnceLock::new();
GOOGLE_OAUTH_CLIENT
.get_or_init(|| GoogleOauthClient::new(core_config()))
}
pub struct GoogleOauthClient {
http: reqwest::Client,
client_id: String,
client_secret: String,
redirect_uri: String,
scopes: String,
states: Mutex<Vec<String>>,
user_agent: String,
}
impl GoogleOauthClient {
pub fn new(
CoreConfig {
google_oauth:
OauthCredentials {
enabled,
id,
secret,
},
host,
..
}: &CoreConfig,
) -> Option<GoogleOauthClient> {
if !enabled {
return None;
}
if host.is_empty() {
warn!(
"google oauth is enabled, but 'config.host' is not configured"
);
return None;
}
if id.is_empty() {
warn!(
"google oauth is enabled, but 'config.google_oauth.id' is not configured"
);
return None;
}
if secret.is_empty() {
warn!(
"google oauth is enabled, but 'config.google_oauth.secret' is not configured"
);
return None;
}
let scopes = urlencoding::encode(
&[
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
]
.join(" "),
)
.to_string();
GoogleOauthClient {
http: Default::default(),
client_id: id.clone(),
client_secret: secret.clone(),
redirect_uri: format!("{host}/auth/google/callback"),
user_agent: String::from("komodo"),
states: Default::default(),
scopes,
}
.into()
}
#[instrument(level = "debug", skip(self))]
pub async fn get_login_redirect_url(
&self,
redirect: Option<String>,
) -> String {
let state_prefix = random_string(STATE_PREFIX_LENGTH);
let state = match redirect {
Some(redirect) => format!("{state_prefix}{redirect}"),
None => state_prefix,
};
let redirect_url = format!(
"https://accounts.google.com/o/oauth2/v2/auth?response_type=code&state={state}&client_id={}&redirect_uri={}&scope={}",
self.client_id, self.redirect_uri, self.scopes
);
let mut states = self.states.lock().await;
states.push(state);
redirect_url
}
#[instrument(level = "debug", skip(self))]
pub async fn check_state(&self, state: &str) -> bool {
let mut contained = false;
self.states.lock().await.retain(|s| {
if s.as_str() == state {
contained = true;
false
} else {
true
}
});
contained
}
#[instrument(level = "debug", skip(self))]
pub async fn get_access_token(
&self,
code: &str,
) -> anyhow::Result<AccessTokenResponse> {
self
.post::<_>(
"https://oauth2.googleapis.com/token",
&[
("client_id", self.client_id.as_str()),
("client_secret", self.client_secret.as_str()),
("redirect_uri", self.redirect_uri.as_str()),
("code", code),
("grant_type", "authorization_code"),
],
None,
)
.await
.context("failed to get google access token using code")
}
#[instrument(level = "debug", skip(self))]
pub fn get_google_user(
&self,
id_token: &str,
) -> anyhow::Result<GoogleUser> {
let 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))]
async fn post<R: DeserializeOwned>(
&self,
endpoint: &str,
body: &[(&str, &str)],
bearer_token: Option<&str>,
) -> anyhow::Result<R> {
let mut req = self
.http
.post(endpoint)
.form(body)
.header("Accept", "application/json")
.header("User-Agent", &self.user_agent);
if let Some(bearer_token) = bearer_token {
req =
req.header("Authorization", format!("Bearer {bearer_token}"));
}
let res = req.send().await.context("failed to reach google")?;
let status = res.status();
if status == StatusCode::OK {
let body = res
.json()
.await
.context("failed to parse POST body into expected type")?;
Ok(body)
} else {
let text = res.text().await.context(format!(
"method: POST | status: {status} | failed to get response text"
))?;
Err(anyhow!("method: POST | status: {status} | text: {text}"))
}
}
}
#[derive(Deserialize)]
pub struct AccessTokenResponse {
// pub access_token: String,
pub id_token: String,
// pub scope: String,
// pub token_type: String,
}
#[derive(Deserialize, Clone)]
pub struct GoogleUser {
#[serde(rename = "sub")]
pub id: String,
pub email: String,
pub picture: String,
}
+140
View File
@@ -0,0 +1,140 @@
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use axum::{
Router, extract::Query, response::Redirect, routing::get,
};
use komodo_client::entities::user::{User, UserConfig};
use mongo_indexed::Document;
use mungos::mongodb::bson::doc;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use crate::{
config::core_config,
state::{db_client, jwt_client},
};
use self::client::google_oauth_client;
use super::{RedirectQuery, STATE_PREFIX_LENGTH};
pub mod client;
pub fn router() -> Router {
Router::new()
.route(
"/login",
get(|Query(query): Query<RedirectQuery>| async move {
Redirect::to(
&google_oauth_client()
.as_ref()
// OK: its not mounted unless the client is populated
.unwrap()
.get_login_redirect_url(query.redirect)
.await,
)
}),
)
.route(
"/callback",
get(|query| async {
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
}),
)
}
#[derive(Debug, Deserialize)]
struct CallbackQuery {
state: Option<String>,
code: Option<String>,
error: Option<String>,
}
#[instrument(name = "GoogleCallback", level = "debug")]
async fn callback(
Query(query): Query<CallbackQuery>,
) -> anyhow::Result<Redirect> {
// Safe: the method is only called after the client is_some
let client = google_oauth_client().as_ref().unwrap();
if let Some(error) = query.error {
return Err(anyhow!("auth error from google: {error}"));
}
let state = query
.state
.context("callback query does not contain state")?;
if !client.check_state(&state).await {
return Err(anyhow!("state mismatch"));
}
let token = client
.get_access_token(
&query.code.context("callback query does not contain code")?,
)
.await?;
let google_user = client.get_google_user(&token.id_token)?;
let google_id = google_user.id.to_string();
let db_client = db_client();
let user = db_client
.users
.find_one(doc! { "config.data.google_id": &google_id })
.await
.context("failed at find user query from mongo")?;
let jwt = match user {
Some(user) => jwt_client()
.encode(user.id)
.context("failed to generate jwt")?,
None => {
let ts = unix_timestamp_ms() as i64;
let no_users_exist =
db_client.users.find_one(Document::new()).await?.is_none();
let core_config = core_config();
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
let user = User {
id: Default::default(),
username: google_user
.email
.split('@')
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string(),
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
updated_at: ts,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config: UserConfig::Google {
google_id,
avatar: google_user.picture,
},
};
let user_id = db_client
.users
.insert_one(user)
.await
.context("failed to create user on mongo")?
.inserted_id
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id)
.context("failed to generate jwt")?
}
};
let exchange_token = jwt_client().create_exchange_token(jwt).await;
let redirect = &state[STATE_PREFIX_LENGTH..];
let redirect_url = if redirect.is_empty() {
format!("{}?token={exchange_token}", core_config().host)
} else {
let splitter = if redirect.contains('?') { '&' } else { '?' };
format!("{}{splitter}token={exchange_token}", redirect)
};
Ok(Redirect::to(&redirect_url))
}
+102
View File
@@ -0,0 +1,102 @@
use std::collections::HashMap;
use anyhow::{Context, anyhow};
use async_timing_util::{
Timelength, get_timelength_in_ms, unix_timestamp_ms,
};
use jsonwebtoken::{
DecodingKey, EncodingKey, Header, Validation, decode, encode,
};
use komodo_client::entities::config::core::CoreConfig;
use mungos::mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use crate::helpers::random_string;
type ExchangeTokenMap = Mutex<HashMap<String, (String, u128)>>;
#[derive(Serialize, Deserialize)]
pub struct JwtClaims {
pub id: String,
pub iat: u128,
pub exp: u128,
}
pub struct JwtClient {
header: Header,
validation: Validation,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
ttl_ms: u128,
exchange_tokens: ExchangeTokenMap,
}
impl JwtClient {
pub fn new(config: &CoreConfig) -> anyhow::Result<JwtClient> {
let secret = if config.jwt_secret.is_empty() {
random_string(40)
} else {
config.jwt_secret.clone()
};
Ok(JwtClient {
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()?,
),
exchange_tokens: Default::default(),
})
}
pub fn encode(&self, user_id: String) -> anyhow::Result<String> {
let iat = unix_timestamp_ms();
let exp = iat + self.ttl_ms;
let claims = JwtClaims {
id: user_id,
iat,
exp,
};
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)]
pub async fn create_exchange_token(&self, jwt: String) -> String {
let exchange_token = random_string(40);
self.exchange_tokens.lock().await.insert(
exchange_token.clone(),
(
jwt,
unix_timestamp_ms()
+ get_timelength_in_ms(Timelength::OneMinute),
),
);
exchange_token
}
#[instrument(level = "debug", skip(self))]
pub async fn redeem_exchange_token(
&self,
exchange_token: &str,
) -> anyhow::Result<String> {
let (jwt, valid_until) = self
.exchange_tokens
.lock()
.await
.remove(exchange_token)
.context("invalid exchange token: unrecognized")?;
if unix_timestamp_ms() < valid_until {
Ok(jwt)
} else {
Err(anyhow!("invalid exchange token: expired"))
}
}
}
+139
View File
@@ -0,0 +1,139 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use komodo_client::{
api::auth::{
CreateLocalUser, CreateLocalUserResponse, LoginLocalUser,
LoginLocalUserResponse,
},
entities::user::{User, UserConfig},
};
use mongo_indexed::Document;
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},
};
impl Resolve<AuthArgs> for CreateLocalUser {
#[instrument(name = "CreateLocalUser", skip(self))]
async fn resolve(
self,
_: &AuthArgs,
) -> serror::Result<CreateLocalUserResponse> {
let core_config = core_config();
if !core_config.local_auth {
return Err(anyhow!("Local auth is not enabled").into());
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty string").into());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("Username cannot be valid ObjectId").into(),
);
}
if self.password.is_empty() {
return Err(anyhow!("Password cannot be empty string").into());
}
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").into());
}
let ts = unix_timestamp_ms() as i64;
let user = User {
id: Default::default(),
username: self.username,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
updated_at: ts,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config: UserConfig::Local {
password: hashed_password,
},
};
let user_id = db_client()
.users
.insert_one(user)
.await
.context("failed to create user")?
.inserted_id
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
let jwt = jwt_client()
.encode(user_id)
.context("failed to generate jwt for user")?;
Ok(CreateLocalUserResponse { jwt })
}
}
impl Resolve<AuthArgs> for LoginLocalUser {
#[instrument(name = "LoginLocalUser", level = "debug", skip(self))]
async fn resolve(
self,
_: &AuthArgs,
) -> serror::Result<LoginLocalUserResponse> {
if !core_config().local_auth {
return Err(anyhow!("local auth is not enabled").into());
}
let user = db_client()
.users
.find_one(doc! { "username": &self.username })
.await
.context("failed at db query for users")?
.with_context(|| {
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"
)
.into(),
);
};
let verified = bcrypt::verify(self.password, &user_pw_hash)
.context("failed at verify password")?;
if !verified {
return Err(anyhow!("invalid credentials").into());
}
let jwt = jwt_client()
.encode(user.id)
.context("failed at generating jwt for user")?;
Ok(LoginLocalUserResponse { jwt })
}
}
+155
View File
@@ -0,0 +1,155 @@
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use axum::{
extract::Request, http::HeaderMap, middleware::Next,
response::Response,
};
use komodo_client::entities::{komodo_timestamp, user::User};
use mungos::mongodb::bson::doc;
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use crate::{
helpers::query::get_user,
state::{db_client, jwt_client},
};
use self::jwt::JwtClaims;
pub mod github;
pub mod google;
pub mod jwt;
pub mod oidc;
mod local;
const STATE_PREFIX_LENGTH: usize = 20;
#[derive(Debug, Deserialize)]
struct RedirectQuery {
redirect: Option<String>,
}
#[instrument(level = "debug")]
pub async fn auth_request(
headers: HeaderMap,
mut req: Request,
next: Next,
) -> serror::Result<Response> {
let user = authenticate_check_enabled(&headers)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
req.extensions_mut().insert(user);
Ok(next.run(req).await)
}
#[instrument(level = "debug")]
pub async fn get_user_id_from_headers(
headers: &HeaderMap,
) -> anyhow::Result<String> {
match (
headers.get("authorization"),
headers.get("x-api-key"),
headers.get("x-api-secret"),
) {
(Some(jwt), _, _) => {
// USE JWT
let jwt = jwt.to_str().context("jwt is not str")?;
auth_jwt_get_user_id(jwt)
.await
.context("failed to authenticate jwt")
}
(None, Some(key), Some(secret)) => {
// USE API KEY / SECRET
let key = key.to_str().context("key is not str")?;
let secret = secret.to_str().context("secret is not str")?;
auth_api_key_get_user_id(key, secret)
.await
.context("failed to authenticate api key")
}
_ => {
// AUTH FAIL
Err(anyhow!(
"must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"
))
}
}
}
#[instrument(level = "debug")]
pub async fn authenticate_check_enabled(
headers: &HeaderMap,
) -> anyhow::Result<User> {
let user_id = get_user_id_from_headers(headers).await?;
let user = get_user(&user_id).await?;
if user.enabled {
Ok(user)
} else {
Err(anyhow!("user not enabled"))
}
}
#[instrument(level = "debug")]
pub async fn auth_jwt_get_user_id(
jwt: &str,
) -> anyhow::Result<String> {
let claims: JwtClaims = jwt_client().decode(jwt)?;
if claims.exp > unix_timestamp_ms() {
Ok(claims.id)
} else {
Err(anyhow!("token has expired"))
}
}
#[instrument(level = "debug")]
pub async fn auth_jwt_check_enabled(
jwt: &str,
) -> anyhow::Result<User> {
let user_id = auth_jwt_get_user_id(jwt).await?;
check_enabled(user_id).await
}
#[instrument(level = "debug")]
pub async fn auth_api_key_get_user_id(
key: &str,
secret: &str,
) -> anyhow::Result<String> {
let key = db_client()
.api_keys
.find_one(doc! { "key": key })
.await
.context("failed to query db")?
.context("no api key matching key")?;
if key.expires != 0 && key.expires < komodo_timestamp() {
return Err(anyhow!("api key expired"));
}
if bcrypt::verify(secret, &key.secret)
.context("failed to verify secret hash")?
{
// secret matches
Ok(key.user_id)
} else {
// secret mismatch
Err(anyhow!("invalid api secret"))
}
}
#[instrument(level = "debug")]
pub async fn auth_api_key_check_enabled(
key: &str,
secret: &str,
) -> anyhow::Result<User> {
let user_id = auth_api_key_get_user_id(key, secret).await?;
check_enabled(user_id).await
}
#[instrument(level = "debug")]
async fn check_enabled(user_id: String) -> anyhow::Result<User> {
let user = get_user(&user_id).await?;
if user.enabled {
Ok(user)
} else {
Err(anyhow!("user not enabled"))
}
}
+94
View File
@@ -0,0 +1,94 @@
use std::{sync::OnceLock, time::Duration};
use anyhow::Context;
use arc_swap::ArcSwapOption;
use openidconnect::{
Client, ClientId, ClientSecret, EmptyAdditionalClaims,
EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl,
RedirectUrl, StandardErrorResponse, core::*,
};
use crate::config::core_config;
type OidcClient = Client<
EmptyAdditionalClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
CoreTokenResponse,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointMaybeSet,
EndpointMaybeSet,
>;
pub fn oidc_client() -> &'static ArcSwapOption<OidcClient> {
static OIDC_CLIENT: OnceLock<ArcSwapOption<OidcClient>> =
OnceLock::new();
OIDC_CLIENT.get_or_init(Default::default)
}
/// 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()
{
return;
}
reset_oidc_client()
.await
.context("Failed to initialize 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(())
}
+287
View File
@@ -0,0 +1,287 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use axum::{
Router, extract::Query, response::Redirect, routing::get,
};
use client::oidc_client;
use dashmap::DashMap;
use komodo_client::entities::{
komodo_timestamp,
user::{User, UserConfig},
};
use mungos::mongodb::bson::{Document, doc};
use openidconnect::{
AccessTokenHash, AuthorizationCode, CsrfToken, Nonce,
OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, Scope,
TokenResponse, core::CoreAuthenticationFlow,
};
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use crate::{
config::core_config,
state::{db_client, jwt_client},
};
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>;
/// 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 verifier_tokens() -> &'static VerifierMap {
static VERIFIERS: OnceLock<VerifierMap> = OnceLock::new();
VERIFIERS.get_or_init(Default::default)
}
pub fn router() -> Router {
Router::new()
.route(
"/login",
get(|query| async {
login(query).await.status_code(StatusCode::UNAUTHORIZED)
}),
)
.route(
"/callback",
get(|query| async {
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
}),
)
}
#[instrument(name = "OidcRedirect", level = "debug")]
async fn login(
Query(RedirectQuery { redirect }): Query<RedirectQuery>,
) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client =
client.as_ref().context("OIDC Client not configured")?;
let (pkce_challenge, pkce_verifier) =
PkceCodeChallenge::new_random_sha256();
// Generate the authorization URL.
let (auth_url, csrf_token, nonce) = client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.set_pkce_challenge(pkce_challenge)
.add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("email".to_string()))
.url();
// Data inserted here will be matched on callback side for csrf protection.
verifier_tokens().insert(
csrf_token.secret().clone(),
(
pkce_verifier,
nonce,
redirect,
komodo_timestamp() + CSRF_VALID_FOR_MS,
),
);
let config = core_config();
let redirect = if !config.oidc_redirect_host.is_empty() {
let auth_url = auth_url.as_str();
let (protocol, rest) = auth_url
.split_once("://")
.context("Invalid URL: Missing protocol (eg 'https://')")?;
let host = rest
.split_once(['/', '?'])
.map(|(host, _)| host)
.unwrap_or(rest);
Redirect::to(&auth_url.replace(
&format!("{protocol}://{host}"),
&config.oidc_redirect_host,
))
} else {
Redirect::to(auth_url.as_str())
};
Ok(redirect)
}
#[derive(Debug, Deserialize)]
struct CallbackQuery {
state: Option<String>,
code: Option<String>,
error: Option<String>,
}
#[instrument(name = "OidcCallback", level = "debug")]
async fn callback(
Query(query): Query<CallbackQuery>,
) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client =
client.as_ref().context("OIDC Client not configured")?;
if let Some(e) = query.error {
return Err(anyhow!("Provider returned error: {e}"));
}
let code = query.code.context("Provider did not return code")?;
let state = CsrfToken::new(
query.state.context("Provider did not return state")?,
);
let (_, (pkce_verifier, nonce, redirect, valid_until)) =
verifier_tokens()
.remove(state.secret())
.context("CSRF token invalid")?;
if komodo_timestamp() > valid_until {
return Err(anyhow!(
"CSRF token invalid (Timed out). The token must be used within 2 minutes."
));
}
let token_response = client
.exchange_code(AuthorizationCode::new(code))
.context("Failed to get Oauth token at exchange code")?
.set_pkce_verifier(pkce_verifier)
.request_async(reqwest_client())
.await
.context("Failed to get Oauth token")?;
// Extract the ID token claims after verifying its authenticity and nonce.
let id_token = token_response
.id_token()
.context("OIDC Server did not return an ID token")?;
// Some providers attach additional audiences, they must be added here
// so token verification succeeds.
let verifier = client.id_token_verifier();
let additional_audiences = &core_config().oidc_additional_audiences;
let verifier = if additional_audiences.is_empty() {
verifier
} else {
verifier.set_other_audience_verifier_fn(|aud| {
additional_audiences.contains(aud)
})
};
let claims = id_token
.claims(&verifier, &nonce)
.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.
if let Some(expected_access_token_hash) = claims.access_token_hash()
{
let actual_access_token_hash = AccessTokenHash::from_token(
token_response.access_token(),
id_token.signing_alg()?,
id_token.signing_key(&verifier)?,
)?;
if actual_access_token_hash != *expected_access_token_hash {
return Err(anyhow!("Invalid access token"));
}
}
let user_id = claims.subject().as_str();
let db_client = db_client();
let user = db_client
.users
.find_one(doc! {
"config.data.provider": &core_config().oidc_provider,
"config.data.user_id": user_id
})
.await
.context("failed at find user query from database")?;
let jwt = match user {
Some(user) => jwt_client()
.encode(user.id)
.context("failed to generate jwt")?,
None => {
let ts = komodo_timestamp();
let no_users_exist =
db_client.users.find_one(Document::new()).await?.is_none();
let core_config = core_config();
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
// Will use preferred_username, then email, then user_id if it isn't available.
let username = claims
.preferred_username()
.map(|username| username.to_string())
.unwrap_or_else(|| {
let email = claims
.email()
.map(|email| email.as_str())
.unwrap_or(user_id);
if core_config.oidc_use_full_email {
email
} else {
email
.split_once('@')
.map(|(username, _)| username)
.unwrap_or(email)
}
.to_string()
});
let user = User {
id: Default::default(),
username,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
updated_at: ts,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config: UserConfig::Oidc {
provider: core_config.oidc_provider.clone(),
user_id: user_id.to_string(),
},
};
let user_id = db_client
.users
.insert_one(user)
.await
.context("failed to create user on database")?
.inserted_id
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id)
.context("failed to generate jwt")?
}
};
let exchange_token = jwt_client().create_exchange_token(jwt).await;
let redirect_url = if let Some(redirect) = redirect {
let splitter = if redirect.contains('?') { '&' } else { '?' };
format!("{}{splitter}token={exchange_token}", redirect)
} else {
format!("{}?token={exchange_token}", core_config().host)
};
Ok(Redirect::to(&redirect_url))
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
pub mod ec2;
+157
View File
@@ -0,0 +1,157 @@
use anyhow::{Context, anyhow};
use axum::http::{HeaderName, HeaderValue};
use reqwest::{RequestBuilder, StatusCode};
use serde::{Serialize, de::DeserializeOwned};
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}"))
}
}
+280
View File
@@ -0,0 +1,280 @@
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,
}
@@ -0,0 +1,75 @@
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,
}
@@ -0,0 +1,36 @@
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,
}

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