Compare commits

...

2278 Commits

Author SHA1 Message Date
Maxwell Becker
a65fd4dca7 1.19.3 (#792)
* start. 1.19.3

* deploy 1.19.3-dev-1

* repo state from db includes BuildRepo success

* clean up version mismatch text

* feat(containers): debounced search input and added filter by server name (#796)

* Fix cleaning Alerter resource whitelist / blacklist on resource delete re #581

* fmt

* Fix signup button not working correctly (#801)

* Improve route protection and authentication flow (#798)

* Improve route protection and authentication flow

* Cleanup

* fix: inconsistent behaviour of new resource create button (#800)

* fix monaco crashing with absolute path config files

* deploy 1.19.3-dev-2

* proofread config

* Fix #427

* deploy 1.19.3-dev-3

* poll logs use println

* Sync: Only show commit / execute when viewing pending tab

* Improve sync UX

* deploy 1.19.3-dev-4

* bold link

* remove claims about database resource usage.

* 1.19.3

---------

Co-authored-by: mbecker20 <max@mogh.tech>
Co-authored-by: Antonio Sarro <tech@antoniosarro.dev>
Co-authored-by: jack <45038833+jackra1n@users.noreply.github.com>
2025-09-05 13:41:58 -07:00
mbecker20
0873104b5a fmt 2025-08-31 19:13:14 -07:00
Maxwell Becker
9a7b6ebd51 1.19.2 (#764)
* 1.19.2-dev-0

* deploy 1.19.2-dev-1

* Add option to make run command detachable (#766)

* improve missing files log to include the missing paths

* bump mungos for urlencoding mongo creds

* Update permissioning.md - typo: "priviledges" -> "privileges" (#770)

* Add support for monaco-yaml and docker compose spec validatiaon (#772)

* deploy 1.19.2-dev-2

* on delete user, remove from all user groups

* fix Google login issues around `picture`

* unsafe_unsanitized_startup_config

* improve git provider support re #355

* should fix #468

* should fix exit code re #597

* deploy 1.19.2-dev-3

* fix container ports sorting (#776)

* missing serde default

* deploy 1.19.2-dev-4

* ensure git tokens trimmed in remote url

* Add link to Authentik support docs

* Fix incorrect commit branch when using linked repo re #634

* Better display container port ranges (#786)

* ensure build and sync also commit to correct branch. re #634

* deploy 1.19.2-dev-5

* Improve login form (#788)

* Use proper form for login, add autocomplete and names to input fields

* Do not return null if loading

* Remove unused function

* Cleanup and streamline

* improve login screen flash on reload

* first builder given same name as first server

* 1.19.2

---------

Co-authored-by: mbecker20 <max@mogh.tech>
Co-authored-by: Brian Bradley <brian.bradley.p@gmail.com>
Co-authored-by: Ravi Wolter-Krishan <rkn@gedikas.net>
Co-authored-by: Christopher Hoage <iam@chrishoage.com>
Co-authored-by: jack <45038833+jackra1n@users.noreply.github.com>
2025-08-31 19:08:45 -07:00
mbecker20
a4153fa28b fix UI showing Redeploy when its actually None 2025-08-24 14:30:43 -07:00
Maxwell Becker
e732da3b05 1.19.1 (#740)
* start 1.19.1

* deploy 1.19.1-dev-1

* Global Auto Update rustdoc

* support stack additional files

* deploy 1.19.1-dev-2

* Fe support additional file language detection

* fix tsc

* Fix: Example code blocks got interpreted as rust code, leading to compilation errors (#743)

* Enhanced Server Stats Dashboard with Performance Optimizations (#746)

* Improve the layout of server mini stats in the dashboard.

- Server stats and tags made siblings for clearer responsibilities
- Changed margin to padding
- Unreachable indicator made into an overlay of the stats

* feat: optimize dashboard server stats with lazy loading and smart server availability checks

- Add enabled prop to ServerStatsMini for conditional data fetching
- Implement server availability check (only fetch stats for Ok servers, not NotOk/Disabled)
- Prevent 500 errors by avoiding API calls to offline servers
- Increase polling interval from 10s to 15s and add 5s stale time
- Add useMemo for expensive calculations to reduce re-renders
- Add conditional overlay rendering for unreachable servers
- Only render stats when showServerStats preference is enabled

* fix: show disabled servers with overlay instead of hiding component

- Maintain consistent layout by showing disabled state overlay
- Prevent UX inconsistency where disabled servers disappeared entirely

* fix: show button height

* feat: add enhance card animations

* cleanup

* gen types

* deploy 1.19.1-dev-3

* add .ini

* deploy 1.19.1-dev-4

* simple configure action args as JSON

* server enabled actually defaults false

* SendAlert via Action / CLI

* fix clippy if let string

* deploy 1.19.1-dev-5

* improve cli ergonomics

* gen types and fix responses formatting

* Add RunStackService API implementing `docker compose run` (#732)

* Add RunStackService API implementing `docker compose run`

* Add working Procedure configuration

* Remove `km execute run` alias. Remove redundant ``#[serde(default)]` on `Option`.

* Refactor command from `String` to `Vec<String>`

* Implement proper shell escaping

* bump deps

* Update configuration.md - fix typo: "affect" -> "effect" (#747)

* clean up SendAlert doc

* deploy 1.19.1-dev-6

* env file args won't double pass env file

* deploy 1.19.1-dev-7

* Add Enter Key Support for Dialog Confirmations (#750)

* start 1.19.1

* deploy 1.19.1-dev-1

* Implement usePromptHotkeys for enhanced dialog interactions and UX

* Refactor usePromptHotkeys to enhance confirm button detection and improve UX

* Remove forceConfirmDialog prop from ActionWithDialog and related logic for cleaner implementation

* Add dialog descriptions to ConfirmUpdate and ActionWithDialog for better clarity and resolve warnings

* fix

* Restore forceConfirmDialog prop to ActionWithDialog for enhanced confirmation handling

* cleanup

* Remove conditional className logic from ConfirmButton

---------

Co-authored-by: mbecker20 <max@mogh.tech>

* Support complex file depency action resolution

* get FE compile

* deploy 1.19.1-dev-8

* implement additional file dependency configuration

* deploy 1.19.1-dev-9

* UI default file dependency None

* default additional file requires is None

* deploy 1.19.1-dev-10

* rename additional_files => config_files for clarity

* deploy 1.19.1-dev-11

* fix skip serializing if None

* deploy 1.19.1-dev-12

* stack file dependency toml parsing aliases

* fmt

* Add: Server Version Mismatch Warnings & Alert System (#748)

* start 1.19.1

* deploy 1.19.1-dev-1

* feat: implement version mismatch warnings in server UI
- Replace orange warning colors with yellow for better visibility
- Add version mismatch detection that shows warnings instead of OK status
Implement responsive "VERSION MISMATCH" badge layout
- Update server dashboard to include warning counts
- Add backend version comparison logic for GetServersSummary

* feat: add warning count to server summary and update backup documentation link

* feat: add server version mismatch alert handling and update server summary invalidation logic

* fix: correct version mismatch alert config and disabled server display

- Use send_version_mismatch_alerts instead of send_unreachable_alerts
- Show 'Unknown' instead of 'Disabled' for disabled server versions
- Remove commented VersionAlert and Alerts UI components
- Update version to 1.19.0

* cleanup

* Update TypeScript types after merge

* cleanup

* cleanup

* cleanup

* Add "ServerVersionMismatch" to alert types

* Adjust color classes for warning states and revert server update invalidation logic

---------

Co-authored-by: mbecker20 <max@mogh.tech>

* backend for build multi registry push support

* deploy 1.19.1-dev-13

* build multi registry configuration

* deploy 1.19.1-dev-14

* fix invalid tokens JSON

* DeployStackIfChanged restarts also update stack.info.deployed_contents

* update deployed services comments

* deploy 1.19.1-dev-15

* Enhance server monitoring with load average data and new server monitoring table (#761)

* add monitoring page

* initial table

* moving monitoring table to servers

* add cpu load average

* typeshare doesnt allow tuples

* fix GetHistoricalServerStats

* add loadAvg to the server monitoring table

* improve styling

* add load average chart

* multiple colors for average loads chart

* make load average chart line and non-stacked

* cleanup

* use server thresholds

* cleanup

* Change "Dependents:" to "Services:" in config file service dependency
selector

* deploy 1.19.1-dev-16

* 1.19.1

---------

Co-authored-by: mbecker20 <max@mogh.tech>
Co-authored-by: Marcel Pfennig <82059270+MP-Tool@users.noreply.github.com>
Co-authored-by: Brian Bradley <brian.bradley.p@gmail.com>
Co-authored-by: Ravi Wolter-Krishan <rkn@gedikas.net>
Co-authored-by: jack <45038833+jackra1n@users.noreply.github.com>
2025-08-24 12:51:04 -07:00
Marcel Pfennig
75ffbd559b Fix: Correct environment variable name for container stats polling rate (#752)
* docs(config): Update environment variable name and default value for container stats polling rate

* fix(config): Update default value for container stats polling rate to 30 seconds
2025-08-21 15:01:10 -07:00
mbecker20
cae80b43e5 fix ferret v2 migration link 2025-08-18 16:38:03 -07:00
mbecker20
d924a8ace4 fix ferret v2 upgrade link 2025-08-18 11:36:25 -07:00
Karl Woditsch
dcfad5dc4e docs(docker-compose): Fix obsolete repo-cache volume declaration (#741) 2025-08-18 11:29:38 -07:00
mbecker20
134d1697e9 include backups path in env / yaml 2025-08-18 10:46:20 -07:00
mbecker20
3094d0036a edit cli docs 2025-08-17 21:00:04 -07:00
mbecker20
ee5fd55cdb first server commented out in default config 2025-08-17 18:39:27 -07:00
mbecker20
0ca126ff23 fix broken docs links before publish 2025-08-17 18:21:01 -07:00
Maxwell Becker
2fa9d9ecce 1.19.0 (#722)
* start 1.18.5

* prevent empty additional permission check (ie for new resources)

* dev-2

* bump rust to 1.88

* tweaks

* repo based stack commit happens from core repo cache rather than on server to simplify

* clippy auto fix

* clippy lints periphery

* clippy fix komodo_client

* dev-3

* emphasize ferret version pinning

* bump svi with PR fix

* dev-4

* webhook disabled early return

* Fix missing alert types for whitelist

* add "ScheduleRun"

* fix status cache not cleaning on resource delete

* dev-5

* forgot to pipe through poll in previous refactor

* refetch given in ms

* fix configure build extra args

* reorder resource sync config

* Implement ability to run actions at startup (#664)

* Implement ability to run actions at startup

* run post-startup actions after server is listening

* startup use action query

* fmt

* Fix Google Login enabled message (#668)

- it was showing "Github Login" instead of "Google Login"

* Allow CIDR ranges in Allowed IPs (#666)

* Allow CIDR ranges in Allowed IPs

* Catch mixed IPv4/IPv6 mappings that are probably intended to match

* forgiving vec

* dev-6

* forgiving vec log. allowed ips docs

* server stats UI: move current disk breakdown above charts

* searchable container stats, toggle collaple container / disk sections

* Add Clear repo cache method

* fix execute usage docs

* Komodo managed env-file should take precedence in all cases (ie come last in env file list)

* tag include unused flag for future use

* combine users page search

* util backup / restore

* refactor backup/restore duplication

* cleanup restore

* core image include util binary

* dev-7

* back to LinesCodec

* dev-8

* clean up

* clean up logs

* rename to komodo-util

* dev-9

* enable_fance_toml

* dev-10 enable fancy toml

* add user agent to oidc requests (#701)

Co-authored-by: eleith <online-github@eleith.com>

* fmt

* use database library

* clippy lint

* consolidate and standardize cli

* dev-11

* dev-12 implement backup using cli

* dev-13 logs

* command variant fields need to be #[arg]

* tweak cli

* gen client

* fix terminal reconnect issue

* rename cli to `km`

* tweaks for the cli logs

* wait for enter on --yes empty println

* fix --yes

* dev-15

* bump deps

* update croner to latest, use static parser

* dev-16

* cli execute polls updates until complete before logging

* remove repo cache mount

* cli nice

* /backup -> /backups

* dev-17 config loading preserves CONFIG_PATHS precedence

* update dockerfile default docker cli config keywords

* dev-18

* support .kmignore

* add ignores log

* Implement automatic backup pruning, default 14 backups before prune

* db copy / restore uses idempotent upsert

* cli update variable - "km set var VAR value"

* improve cli initial logs

* time the executions

* implement update for most resources

* dev 20

* add update page

* dev 21 support cli update link

* dev-22 test the deploy

* dev-23 use indexmap

* install-cli.py

* Frontend mobile fixes (#714)

* Allow ResourcePageHeader items to wrap

* Allow CardHeader items to wrap

* Increase z-index of sticky TableHeader, fixes #690

* Remove fixed widths from ActionButton, let them flex more to fit more layouts

* Make Section scroll overflow

* Remove grid class from Tabs, seems to prevent them from overflowing at small sizes

* deploy 1.18.5-dev-24

* auto version increment and deploy

* cli: profiles support aliases and merge on top of Default (root) config

* fix page set titles

* rust 1.89 and improve config logs

* skip serializing for proper merge

* fix clippy lints re 1.89

* remove layouts overflow-x-scroll

* deploy 1.18.5-dev-25

* 1.89 docker images not ready yet

* km cfg -a (print all profiles)

* include commit variables

* skip serializing profiles when empty

* skip serialize default db / log configs

* km cfg --debug print mode

* correct defaults for CLI and only can pass restore folder from cli arg

* some more skip serialization

* db restore / copy index optional

* add runfile command aliases

* remove second schedule updating loop, can causes some schedules to be missed

* deploy 1.18.5-dev-26

* add log when target db indexing disabled

* cli: user password reset, update user super admin

* Add manual network interface configuration for multi-NIC Docker environments (#719)

* Add iproute2 to debian-debs

* feat: Add manual network interface configuration for multi-NIC support

Complete implementation of manual interface configuration:
- Add internet_interface config option
- Implement manual gateway routing
- Add NET_ADMIN capability requirement
- Clean up codebase changes

* fix: Update internet interface handling for multi-NIC support

* refactor: Enhance error messages and logging in networking module

* refactor: Simplify interface argument handling and improve logging in network configuration and cleanup

* refactor(network): simplify startup integration and improve error handling

- Move config access and error handling into network::configure_internet_gateway()
- Simplify startup.rs to single function call without parameters
- Remove redundant check_network_privileges() function
- Improve error handling by checking actual command output instead of pre-validation
- Better separation of concerns between startup and network modules

Addresses feedback from PR discussion:
https://github.com/moghtech/komodo/pull/719#discussion_r2261542921

* fix(config): update default internet interface setting
Addresses feedback from PR discussion:
https://github.com/moghtech/komodo/pull/719#discussion_r2261552279

* fix(config): remove custom default for internet interface in CoreConfig

* move mod.rs -> network.rs
Addresses feedback from PR discussion:
https://github.com/moghtech/komodo/pull/719#discussion_r2261558332

* add internet interface example

* docs(build-images): document multi-platform builds with Docker Buildx (#721)

* docs(build-images): add multi-platform buildx guide to builders.md

* docs(build-images): add multi-platform buildx guide and clarify platform selection in Komodo UI Extra Args field

* move to 1.19.0

* core support reading from multiple config files

* config support yaml

* deploy 1.19.0-dev-1

* deploy 1.19.0-dev-2

* add default komodo cli config

* better config merge with base

* no need to panic if empty config paths

* improve km --help

* prog on cli docs

* tweak cli docs

* tweak doc

* split the runfile commands

* update docsite deps

* km ps initial

* km ls

* list resource apis

* km con inspect

* deploy 1.19.0-dev-3

* fix: need serde default

* dev-4 fix container parsing issue

* tweak

* use include-based file finding for much faster discovery

* just move to standard config dir .config/komodo/komodo.cli.*

* update fe w/ new contianer info minimal serialization

* add links to table names

* deploy 1.19.0-dev-5

* links in tables

* backend for Action arguments

* deploy 1.19.0-dev-6

* deploy 1.19.0-dev-7

* deploy 1.19.0-dev-8

* no space at front of KeyValue default args

* webhook branch / body optional

* The incoming arguments

* deploy 1.19.0-dev-9

* con -> cn

* add config -> cf alias

* .kmignore

* .peripheryinclude

* outdated

* optional links, configurable table format

* table_format -> table_borders

* get types

* include docsite in yarn install

* update runnables command in docs

* tweak

* improve km ls only show important stuff

* Add BackupCoreDatabase

* deploy 1.19.0-dev-10

* backup command needs "--yes"

* deploy 1.19.0-dev-11

* update rustc 1.89.0

* cli tweak

* try chef

* Fix chef (after dependencies)

* try other compile command

* fix

* fix comment

* cleanup stats page

* ensure database backup procedure

* UI allow configure Backup Core Database in Procedures

* procedure description

* deploy 1.19.0-dev-12

* deploy 1.19.0-dev-13

* GlobalAutoUpdate

* deploy 1.19.0-dev-14

* default tags and global auto update procedure

* deploy 1.19.0-dev-15

* trim the default procedure descriptions

* deploy 1.19.0-dev-16

* in "system" theme, also poll for updates to the theme based on time.

* Add next run to Action / Procedure column

* km ls support filter by templates

* fix procedure toml serialization when params = {}

* deploy 1.19.0-dev-17

* KOMODO_INIT_ADMIN_USERNAME

* KOMODO_FIRST_SERVER_NAME

* add server.config.external_address for use with links

* deploy 1.19.0-dev-18

* improve auto prune

* fix system theme auto update

* deploy 1.19.0-dev-19

* rename auth/CreateLocalUser -> SignUpLocalUser. Add write/CreateLocalUser for in-ui initialization.

* deploy 1.19.0-dev-20

* UI can handle multiple active logins

* deploy 1.19.0-dev-21

* fix

* add logout function

* fix oauth redirect

* fix multi user exchange token function

* default external address

* just Add

* style account switcher

* backup and restore docs

* rework docsite file / sidebar structure, start auto update docs

* auto update docs

* tweak

* fix doc links

* only pull / update running stacks / deployments images

* deploy 1.19.0-dev-22

* deploy 1.19.0-dev-23

* fix #737

* community docs

* add BackupCoreDatabase link to docs

* update ferret v2 update guide using komodo-cli

* fix data table headers overlapping topbar

* don't alert when deploying

* CommitSync returns Update

* deploy 1.19.0-dev-24

* trim the decoded branch

* action uses file contents deserializer

* deploy 1.19.0-dev-25

* remove Toml from action args format

* clarify External Address purpose

* Fix podman compatibility in `get_container_stats` (#739)

* Add podman compability for querying stats

Podman and docker stats differ in results in significant ways but this filter change they will output the same stats

* syntax fix

* feat(dashboard): display CPU, memory, and disk usage on server cards (#729)

* feat: mini-stats-card: Expose Server CPU , Memory, Disk Usage to Dashboard View

* comment: resolved

* Feat: fix overflow card , DRY stats-mini, add unreachable mini stats

* lint: fix

* deploy 1.19.0-dev-26

* 1.19.0

* linux, macos container install

* cli main config

---------

Co-authored-by: Brian Bradley <brian.bradley.p@gmail.com>
Co-authored-by: Daniel <daniel.barabasa@gmail.com>
Co-authored-by: eleith <eleith@users.noreply.github.com>
Co-authored-by: eleith <online-github@eleith.com>
Co-authored-by: Sam Edwards <sam@samedwards.ca>
Co-authored-by: Marcel Pfennig <82059270+MP-Tool@users.noreply.github.com>
Co-authored-by: itsmesid <693151+arevindh@users.noreply.github.com>
Co-authored-by: mbecker20 <max@mogh.tech>
Co-authored-by: Rhyn <Rhyn@users.noreply.github.com>
Co-authored-by: Anh Nguyen <tuananh131001@gmail.com>
2025-08-17 17:25:45 -07:00
Maxwell Becker
118ae9b92c 1.18.4 (#604)
* update easy deps

* update otel deps

* implement template in types + update resource meta

* ts types

* dev-2

* dev-3 default template query is include

* Toggle resource is template in resource header

* dev-4 support CopyServer

* gen ts

* style template selector in New Resource menu

* fix new menu show 0

* add template market in omni search bar

* fix some dynamic import behavior

* template badge on dashboard

* dev-5

* standardize interpolation methods with nice api

* core use new interpolation methods

* refactor git usage

* dev-6 refactor interpolation / git methods

* fix pull stack passed replacers

*  new types

* remove redundant interpolation for build secret args

* clean up periphery docker client

* dev-7 include ports in container summary, see if they actually come through

* show container ports in container table

* refresh processes without tasks (more efficient)

* dev-8 keep container stats cache, include with ContainerListItem

* gen types

* display more container ports

* dev-9 fix repo clone when repo doesn't exist initially

* Add ports display to more spots

* fix function name

* add Periphery full container stats api, may be used later

* server container stats list

* dev-10

* 1.18.4 release

* Use reset instead of invalidate to fix GetUser spam on token expiry (#618)

---------

Co-authored-by: Jacky Fong <hello@huzky.dev>
2025-06-24 16:32:39 -07:00
Luke
2205a81e79 Update webhooks.md (#611) 2025-06-20 11:56:05 -07:00
mbecker20
e2280f38df fix: allow Build / Repo add Attach permission 2025-06-16 00:40:24 -07:00
Maxwell Becker
545196d7eb 1.18.3 (#603)
* start 1.18.3 branch

* git::pull will fetch before checkout

* dev-2

* 1.18.3 quick release
2025-06-15 23:45:50 -07:00
Maxwell Becker
23f8ecc1d9 1.18.2 (#591)
* feat: add maintenance window management to suppress alerts during planned activities (#550)

* feat: add scheduled maintenance windows to server configuration

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

* chore: enhance maintenance windows with types and permission improvements

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

* feat: restore alert buffer system to prevent noise

* fix yarn fe

* fix the merge with new alerting changes

* move alert buffer handle out of loop

* nit

* fix server version changes

* unneeded buffer clear

---------

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

* set version 1.18.2

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

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

* add profile to scopes and account for username already taken

* search through server docker lists

* move maintenance stuff

* refactor maintenance schedules to have more toml compatible structure

* daily schedule type use struct

* add timezone to core info response

* frontend can build with new maintenance types

* Action monaco expose KomodoClient to init another client

* flatten out the nested enum

* update maintenance schedule types

* dev-3

* implement maintenance windows on alerters

* dev-4

* add IanaTimezone enum

* typeshare timezone enum

* maintenance modes almost done on servers AND alerters

* maintenance schedules working

* remove mention of migrator

* Procedure / Action schedule timezone selector

* improve timezone selector to display configure core TZ

* dev-5

* refetch core version

* add version to server list item info

* add periphery version in server table

* dev-6

* capitalize Unknown server status in cache

* handle unknown version case

* set server table sizes

* default resource_poll_interval 1-hr

* ensure parent folder exists before cloning

* document Build Attach permission

* git actions return absolute path

* stack linked repos

* resource toml replace linked_repo id with name

* validate incoming linked repo

* add linked repo to stack list item info

* stack list item info resolved linked repo information

* configure linked repo stack

* to repo links

* dev-7

* sync: replace linked repo with name for execute compare

* obscure provider tokens in table view

* clean up stack write w/ refactor

* Resource Sync / Build start support Repo attach

* add stack clone path config

* Builds + syncs can link to repos

* dev-9

* update ts

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

* add linked repo UI for builds / syncs

* fix commit linked repo sync

* include linked repo syncs

* correct Sync / Build config mode

* dev-12 fix resource sync inclusion w/ linked_repo

* remove unneed sync commit todo!()

* fix other config.repo.is_empty issues

* replace ids in all to toml exports

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

* fix fe for linked repo cases

* consolidate linked repo config component

* fix resource sync commit behavior

* dev 17

* Build uses Pull or Clone api to setup build source

* capitalize Clone Repo stage

* mount PullOrCloneRepo

* dev-19

* Expand supported container names and also avoid unnecessary name formatting

* dev-20

* add periphery /terminal/execute/container api

* periphery client execute_container_exec method

* implement execute container, deployment, stack exec

* gen types

* execute container exec method

* clean up client / fix fe

* enumerate exec ts methods for each resource type

* fix and gen ts client

* fix FE use connect_exec

* add url log when terminal ws fail to connect

* ts client server allow terminal.js

* FE preload terminal.js / .d.ts

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

* update docs on attach perms

* 1.18.2

---------

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

* improve stack header / all resource links

* disable build config selector

* clean up deployment header

* update build header

* builder header

* update repo header

* start adding repo links from api

* implement list item repo link

* clean up fe

* gen client

* repo links across the board

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

* add runnables-cli link in runfile

* improve frontend first load time through some code splitting

* add services count to stack header

* fix repo on pull

* Add dedicated Deploying state to Deployments and Stacks

* move predeploy script before compose config (#584)

* Periphery / core version mismatch check / red text

* move builders / alerts out of sidebar, into settings

* remove force push

* list schedules api

* dev-1

* actually dev-3

* fix action

* filter none procedures

* fix schedule api

* dev-5

* basic schedules page

* prog on schedule page

* simplify schedule

* use name to sort target

* add resource tags to schedule

* Schedule page working

* dev-6

* remove schedule table type column

* reorder schedule table

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

* 1.18.1

---------

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

* remove ignored for sqlite

* tweak

* mongo copier

* 1.17.6

* primary name is ferretdb option

* give doc counts

* fmt

* print document count

* komodo util versioned seperately

* add copy startup sleep

* FerretDB v2 upgrade guide

* tweak docs

* tweak

* tweak

* add link to upgrade guide for ferretdb v1 users

* fix copy batch size

* multi arch util setup

* util use workspace version

* clarify behavior re root_directory

* finished copying database log

* update to rust:1.87.0

* fix: reset rename editor on navigate

* loosen naming restrictions for most resource types

* added support for ntfy email forwarding (#493)

* fix alerter email option docs

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

* more granular permissions

* fix initial fe type errors

* fix the new perm typing

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

* frontend should convey / respect the perms

* use IndexSet for SpecificPermission

* finish IndexSet

* match regex or wildcard resource  name pattern

* gen ts client

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

* user group backend "everyone" support

* bump to 1.18.0 for significant permissioning changes

* ts 1.18.0

* permissions FE in prog

* FE permissions assignment working

* user group all map uses ordered IndexMap for consistency

* improve user group toml and fix execute bug

* URL encode names in webhook urls

* UI support configure 'everyone' User Group

* sync handle toggling user group everyone

* user group table show everyone enabled

* sync will update user group "everyone"

* Inspect Deployment / Stack containers directly

* fix InspectStackContainer container name

* Deployment / stack service inspect

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

* fix compose down not capitalized

* don't use tabs

* more descriptive permission table titles

* different localstorage for permissions show all

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

* fix container inspect

* fix list container undefined error

* prcesses list gated UI

* remove localstorage on permission table expansion

* fix ug sync handling of all zero permissions

* pretty log startup config

* implement actually pretty logging initial config

* fix user permissions when api returns string

* fix container info table

* util based on bullseye-slim

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

* container tab permissions reversed

* reorder pretty logging stuff to be together

* update docs with permissioning info

* tweak docs

* update roadmap

---------

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

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

* update roadmap

* configure the shell used to start the pty

* start on ExecuteTerminal api

* Rename resources less hidden - click on name in header

* update deps

* execute terminal

* BatchPullStack

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

* add --reload for cached deps

* type execute terminal response as AsyncIterable

* execute terminal client api

* KOMODO_EXIT_CODE

* Early exit without code

* action configurable deno dep reload

* remove ServerTemplate resource

* kept disabled

* rework exec terminal command wrapper

* debug: print lines in start sentinel loop

* edit debug / remove ref

* echo

* line compare

* log lengths

* use printf again

* check char compare

* leading \n

* works with leading \n

* extra \n after START_OF_OUTPUT

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

* isolate post-db startup procedures

* clean up server templates

* disable websocket reconnect from core config

* change periphery ssl enabled to default to true

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

* disable terminals while allowing container exec

* disable_container_exec in default config

* update ws reconnect implementation

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

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

* add core terminal exec method

* terminal typescript client method

* terminals WIP

* backend for pty

* add ts responses

* about wire everything

* add new blog

* credit Skyfay

* working

* regen lock

* 1.17.4-dev-1

* pty history

* replace the test terminal impl with websocket (pty)

* create api and improve frontend

* fix fe

* terminals

* disable terminal api on periphery

* implement write level terminal perms

* remove unneeded

* fix clippy

* delete unneeded

* fix waste cpu cycles

* set TERM and COLORTERM for shell environment

* fix xterm scrolling behavior

* starship promp in periphery container terminal

* kill all terminals on periphery shutdown signal

* improve starship config and enable ssl in compose

* use same scrollTop setter

* fix periphery container distribution link

* support custom command / args to init terminal

* allow fully configurable init command

* docker exec into container

* add permissioning for container exec

* add starship to core container

* add delete all terminals

* dev-2

* finished gen client

* core need curl

* hide Terminal trigger if disabled

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

* rust 1.86.0

* config periphery directories easier with PERIPHERY_ROOT_DIRECTORY

* schedule backend

* fix config switch toggling through disabled

* procedure schedule working

* implement schedules for actions

* update schedule immediately after last run

* improve config update logs using toml diffs backend

* improve the config update logs with TOML diff view

* add schedule alerting

* version 1.17.2

* Set TZ in core env

* dev-1

* better term signal labels

* sync configurable pending alert send

* fix monaco editor height on larger screen

* poll update until complete on client

update lib

* add logger.pretty option for both core and periphery

* fix pretty

* configure schedule alert

* configure failure alert

* dev-3

* 1.17.2

* fmt

* added pushover alerter (#421)

* fix up pushover

* fix some clippy

---------

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

* fix js client docs

* js client should be type: module

* click table tags to toggle tag filter

* git token helper early return when empty provider

* reorder Stack fields

* action support interpolation doc

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

* GetDeploymentsSummary (#386)

* added GetDockerContainersSummary endpoint in rust api

* typescript stuff

* more autogenned typescript stuff

* fixed comments to be in line with actual behaviour

* fixed ReadResponse for GetDockerContainersSummary

* I64 -> u32 for response types

* more accurate error context

* backend for build files on host / ui defined

* core api supports non repo based build

* Ntfy as Alerter (#404)

* add ntfy alerter

* add ntfy alerter

* add ntfy alerter

---------

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

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

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

* Spelling: overide -> override

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

* clean up ntfy alerter

* clean up ResourceSyncConfig

* update build cache after create / update

* refresh stack cache log

* Build UI Defined / file on host frontend

* update clap + rustls

* don't cleanup build files

* clean up dockerfile full path

* update BuildListItemInfo + UI table

* add Other Resources page

* add 5 second ws reconnection timeout

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

* Make listener address configurable

* Make listener address configurable for periphery

* rename listener_address -> bind_ip

---------

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

add new ec2 instance types

clean up testing config

document the libraries a bit

clean up main

update sysinfo and otel

update client resolver 3.0

resolver v3 prog

clean up gitignore

implement periphery resolver v3

clean up

core read api v3

more prog

execute api

missing apis

compiling

1.16.13

work on more granular traits

prog on crud

* fmt

* format

* resource2 not really a benefit

* axum to 0.8

* bump aws deps

* just make it 1.17.0

* clean up cors

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

* add entities / message for test alerter

* test alert implementation

* rust 1.84.0

* axum update :param to {param} syntax

* fix last axum updates

* Add test alerter button

* higher quality / colored icons

* komodo-logo

* simplify network stats

* rename Test Alerter button

* escape incoming sync backslashes (BREAKING)

* clean up rust client websocket subscription

* finish oidc comment

* show update available stack table

* update available deployment table

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

* Update repo path handling in commit fetching

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

* feat: use optional name and path in GetLatestCommit

* review: don't use optional for name

* review: use helper

* review: remove redundant to_string()

* 1.17.0-dev

* feature: add post_deploy command (#288)

* feature: add post_deploy command

* review: do not run post_deploy if deploy failed

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

* feature: interpolate secrets in custom alerter

* fix rust warning

* review: sanitize errors

* review: sanitize error message

* Remove .git from remote_url (#299)

Remove .git from remote_url

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

* mbecker20 -> moghtech

* remove example from cargo toml workspace

* dev-1

* fix login screen logo

* more legible favicon

* fix new compose images

* docs new organization

* typescript subscribe_to_update_websocket

* add donate button docsite

* add config save button in desktop sidebar navigator

* add save button to config bottom

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

* feature: allow docker image text to overflow in table

* review: use break-words

* wip: revert line break in css file

* feature: update devcontainer node release

* improve First Login docs

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

* requery alerts more often

* improve update indicator style and also put on home screen

* Add all services stack log

* 1.17.0-dev-2

* fix api name chnage

* choose which stack services to include in logs

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

* feature: improve tables quick actions on mobile

* review: fix gap4

* review: use flex-wrap

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

* Fix unclear ComposePull log re #244

* use komodo_client.subscribe_to_update_websocket, and click indicator to reconnect

* dev-3

* ServerTemplate description

* improve WriteComposeContentsToHost instrument fields

* give server stat charts labels

* filters wrap

* show provider usernames from config file

* Stack: Fix git repo new compose file initialization

* init sync file new repo

* set branch on git init folder

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

* Improve resource sync Execute / Pending view selector

* standardize running commands with interpolation / output sanitizations

* fix all clippy lints

* fix rand

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

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

* ResourceSync state resolution refinement

* make sure parent directories exist whenever writing files

* don't prune images if server not enabled

* update most deps

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

* dev-4

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

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

* auto update all service deploy option

* dev-5 fix the stack service executions

* clean up service_args

* rust 1.85

* store sync edits on localstorage

* stack edits on localstorage and show last deployed config

* add yarn install to runfile

* Fix actions when core on https

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

* rust 2024 and fmt

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

* update .devcontainer / dev docs for updated runfile

* use png in topbar logo, svg quality sometimes bad

* OIDC: Support PKCE auth (secret optional)

* update docs on OIDC and client secret

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

* add KOMODO_LOCK_LOGIN_CREDENTIALS_FOR in config doc

* update deps

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

* use jsonwebtoken

* improve variable value table overflow

* colored tags

* fix sync summary count ok

* default new tag colors to grey

* soften tag opacity a bit

* Update config.tsx (#358)

* isolate stacks / deployments with pending updates

* update some deps

* use Tooltip component instead of HoverCard for mobile compatibility

* batch Build builds

* link to typescript client in the intro

* add link to main docs from client docs

* doc tweaks

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

* remove unnecessary explicit network

* periphery.compose.yaml

* clean up periphery compose

* add link to config

* update periphery container compose config

* rust 1.85.1

* update sync docs

* 1.17.0

---------

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

Add network usage graph and current status

Change network graphs to use network interface from drop down menu

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

Working setup with a working builder

remove changes to these dockerfile

remove lock changes

* change network hashmap to Vector

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

* PR requested changes applied

* Change net_ingress_bytes and egress to network_ingress_bytes egress respectively

* final gen-client types

---------

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

* Komodo interp in ui compose file

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

* Pull image buttons don't need safety dialog

* WIP crosscompile

* rename

* entrypoint

* fix copy

* remove example/* from workspace

* add targets

* multiarch pkg config

* use specific COPY

* update deps

* multiarch build command

* pre compile deps

* cross compile

* enable-linger

* remove spammed log when server doesn't have docker

* add multiarch.Dockerfile

* fix casing

* fix tag

* try not let COPY fail

* try

* ARG TARGETPLATFORM

* use /app for consistency

* try

* delete cross-compile approach

* add multiarch core build

* multiarch Deno

* single arch multi arch

* typeshare cli note

* new typeshare

* remove note about aarch64 image

* test configs

* fix config file headers

* binaries dockerfile

* update cargo build

* docs

* simple

* just simple

* use -p

* add configurable binaries tag

* add multi-arch

* allow copy to fail

* fix binary paths

* frontend Dockerfiel

* use dedicated static frontend build

* auto retry getting instance state from aws

* retry 5 times

* cleanup

* simplify binary build

* try alpine and musl

* install alpine deps

* back to debian, try rustls

* move fully to rustls

* single arch builds using single binary image

* default IMAGE_TAG

* cleanup

* try caching deps

* single arch add frontend build

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

* back to simple

* comment dockerfile

* add select options prop, render checkboxes if present

* add allowSelectedIf to enable / disable rows where necessary

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

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

* selected resources hook, start deployment batch execute component

* add deployment group actions

* add deployment group actions

* add default (empty) group actions for other resources

* fix checkbox header styles

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

* don't disable row selection for deployments table

* don't need id for groupactions

* add group actions to resources page

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

* re-implement group action list using dropdown menu

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

* add loading indicator

* gap betwen new resource and group actions

* refactor group actions

* remove "Batch" from action labels

* add group actions for relevant resources

* fix hardcode

* add selectOptions to relevant tables

* select by name not id

* expect selected to be names

* add note re selection state init for future reference

* multi select working nicely for all resources

* configure server health check timeout

* config message

* refresh processes remove dead processes

* simplify the build args

* default timeout seconds 3

---------

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

* action only log completion correctly

* add containers to omni search

* periphery build use --push

* use --password-stdin to login

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

* scrolling / capturing monaco editors

* deployed services has correct image

* serde default services for backward compat

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

* periphery image pull api

* Add Pull apis

* Add PullStack / PullDeployment

* improve init deploy from container

* stacks + deployments update_available source

* Fix deploy / destroy stack service

* updates available indicator

* add poll for updates and auto update options

* use interval to handle waiting between resource refresh

* stack auto update deploy whole stack

* format

* clean up the docs

* update available alerts

* update alerting format

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

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

* Fix references to __ALL__
2024-11-01 14:33:41 -07:00
mbecker20
2a1270dd74 webhook check will return better status codex 2024-11-01 15:57:36 -04:00
Maxwell Becker
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 Becker
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 Becker
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 Becker
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 Becker
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 Foxx
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 Becker
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 Becker
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 Becker
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 Becker
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 Foxx
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 Wyatt
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 Becker
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 Becker
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 Becker
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 Becker
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 Becker
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 Becker
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 Becker
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
Maxwell Becker
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 Lugo
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 Lugo
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
Jérémy
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 Becker
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 Becker
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 Becker
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 Becker
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 Becker
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 Becker
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 Becker
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
Maxwell Becker
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 Becker
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 Becker
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 Becker
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
mbecker20
5dee16a100 0.3.3 add default term signal and timeout to deployment 2023-05-01 08:28:12 +00:00
mbecker20
35f3bcdf2f update core version 2023-05-01 03:05:51 +00:00
mbecker20
130ca8e1f1 bump versions to 0.3.2 2023-05-01 01:53:58 +00:00
mbecker20
ced4c21688 update monitor client to 0.3.1 2023-05-01 01:41:23 +00:00
mbecker20
6ec7078024 custom termination signals 2023-04-30 06:52:27 +00:00
mbecker20
b28d8f2506 update frontend types 2023-04-30 03:26:01 +00:00
mbecker20
c88a9291a0 support auto redeploy and custom stop signals 2023-04-30 00:10:59 +00:00
mbecker20
1e82d19306 build summary defaults to time view 2023-04-21 16:34:03 +00:00
mbecker20
dd87e50cb2 build stats summary 2023-04-21 08:52:17 +00:00
mbecker20
4c8f96a30f build stats card 2023-04-21 08:08:15 +00:00
mbecker20
c4f45e05f1 finish build stats api 2023-04-21 08:08:01 +00:00
mbecker20
6aa382c7c1 finish build stats api 2023-04-20 16:34:14 +00:00
mbecker20
ccb9f059e6 get build stats api 2023-04-20 07:34:49 +00:00
mbecker20
1cdcea0771 start on route to get daily build stats (time, count) 2023-04-19 07:02:21 +00:00
mbecker20
88dda0de80 update rename deployment to check whether deployment has repo attached, and if so, reclone it to account for name change. 2023-04-19 06:44:58 +00:00
mbecker20
30ed99e2b0 publish monitor client 0.3.1 with Readme 2023-04-18 07:56:30 +00:00
mbecker20
e5953b7541 monitor client readme 2023-04-18 07:55:05 +00:00
mbecker20
1f9d01c59f new home servers png 2023-04-18 06:31:23 +00:00
mbecker20
cc5210a3d8 fix server children add new button 2023-04-18 06:17:42 +00:00
mbecker20
26559e2d3b delete builds screenshots 2023-04-18 03:20:20 +00:00
mbecker20
7eeddb300f add link to screenshots docsite 2023-04-18 02:59:37 +00:00
mbecker20
1e01bae16b add screenshots to monitor readme 2023-04-18 02:47:46 +00:00
mbecker20
87c03924e5 remove second universal search 2023-04-18 02:46:46 +00:00
mbecker20
f0998b1d43 add universal search screenshot 2023-04-18 02:42:21 +00:00
mbecker20
1995a04244 add screenshots 2023-04-18 02:38:07 +00:00
mbecker20
420fe6bcd5 add build time to version selector 2023-04-17 08:26:34 +00:00
mbecker20
d4e26c0553 fix docker repo reference 2023-04-16 19:33:28 +00:00
mbecker20
5f5e7cb45e add note about oauth 2023-04-16 07:44:34 +00:00
beckerinj
8aa0304738 core setup doc 2023-04-16 03:32:06 -04:00
beckerinj
8ec98c33a4 first user is auto enabled and made admin 2023-04-16 03:23:30 -04:00
beckerinj
2667182ca3 update core example config 2023-04-16 02:49:00 -04:00
mbecker20
1cd0018b93 0.3.0 check whether pre_build / on_clone / on_pull are non empty before running 2023-04-14 22:58:10 +00:00
beckerinj
359789ee29 update aws sdk version 2023-04-14 12:52:52 -04:00
mbecker20
e79c860c0f make update hover 2023-04-14 15:06:05 +00:00
mbecker20
765f53f30e types doc 2023-04-14 06:14:33 +00:00
beckerinj
3c3c21d7f5 move header to top. redesign build 2023-04-14 01:57:10 -04:00
mbecker20
eb700cb500 improve search functionality 2023-04-14 03:53:21 +00:00
beckerinj
b3b723a717 build show unknown if builds[id] cannot be found 2023-04-13 11:19:51 -04:00
beckerinj
555c230d2e update menu stays open through updates 2023-04-13 03:07:31 -04:00
beckerinj
adf4b97aef lots of api docs 2023-04-13 02:23:40 -04:00
beckerinj
32c38d796b docs 2023-04-12 18:19:13 -04:00
beckerinj
c8829e15ed get most of servers api docs 2023-04-10 17:58:52 -04:00
beckerinj
453df417d0 finish deployment api doc 2023-04-10 04:39:47 -04:00
mbecker20
02a7741a9c don't show group editing ui if user doesn't have permissions 2023-04-09 22:55:30 +00:00
mbecker20
96fc5b0ca8 group page / edit work with non admin users 2023-04-09 22:38:36 +00:00
mbecker20
b13e624a66 support manage user permissions on groups 2023-04-09 19:24:07 +00:00
beckerinj
6a8f66f272 work on deployment api docs 2023-04-09 02:15:08 -04:00
mbecker20
0c638a08fd fix problems with build config page breaking 2023-04-07 20:15:21 +00:00
mbecker20
b07f8af8e5 deployment extra args fix as well 2023-04-07 19:50:16 +00:00
mbecker20
3bbb2a985f extra args frontend needs to account for when they don't exist 2023-04-07 19:50:06 +00:00
beckerinj
afdf71c545 work on api docs 2023-04-07 11:12:44 -04:00
beckerinj
8de8d2df9a work on API docs 2023-04-06 01:03:01 -04:00
mbecker20
1dffdbddc2 sort by ts increasing 2023-04-05 18:40:12 +00:00
mbecker20
11fff633b0 don't use $mod in stats query 2023-04-05 18:17:56 +00:00
mbecker20
61bc44d1f4 add hover class to home tree build 2023-04-04 21:18:13 +00:00
beckerinj
e8fabb8cfa Update index.mdx 2023-04-03 16:40:15 -04:00
mbecker20
7a50885847 exit group view with Escape in addition to ArrowLeft 2023-04-03 16:35:44 +00:00
beckerinj
6239da45f4 delete old docs and provide link to docsite 2023-04-03 11:28:50 -04:00
mbecker20
af597eb3c7 clean up builds 2023-04-03 05:55:37 +00:00
mbecker20
d66cda068c connect up delete group 2023-04-03 05:52:58 +00:00
beckerinj
91fcd07c1c make group behavior more sensible frontned 2023-04-03 01:50:45 -04:00
mbecker20
85aa470da1 publish docs 2023-04-03 02:48:00 +00:00
mbecker20
6f0d5f37a5 update home page description for sso 2023-04-02 22:30:37 +00:00
beckerinj
1b4d604404 deploymentBranch 2023-04-02 18:22:04 -04:00
beckerinj
a7f6cbe0b9 small docsite fixes 2023-04-02 12:44:35 -04:00
beckerinj
9cf28bf123 improve docs 2023-04-02 04:15:06 -04:00
beckerinj
c92e04294a monitor docs site working 2023-04-02 04:03:01 -04:00
beckerinj
36f059b455 docsite 2023-04-02 01:40:56 -04:00
mbecker20
4aac301852 0.2.14 only try to merge files, filter nested directories 2023-04-01 19:35:04 +00:00
mbecker20
b375708bbd 0.2.13 support config directories 2023-04-01 19:02:43 +00:00
mbecker20
10b6a9482b update aws sdk verison and implement merge_config_files 2023-04-01 07:06:17 +00:00
mbecker20
84d45c5df8 0.2.12 fix docker build command interp 2023-03-31 18:06:18 +00:00
mbecker20
c6559814b1 frontend for docker build extra args and use buildx 2023-03-31 17:31:39 +00:00
mbecker20
c8c080183f remove publish for cli 2023-03-31 17:04:15 +00:00
mbecker20
597b67f799 0.2.11 support buildx and arbitrary extra args 2023-03-31 17:03:38 +00:00
mbecker20
ec52d5f422 support docker buildx build and passing arbitrary extra args 2023-03-31 16:57:02 +00:00
mbecker20
34806304d6 add center menu title bottom border and adjust copy menu 2023-03-31 05:41:35 +00:00
beckerinj
87953d5495 menu padding 2rem 2023-03-31 01:27:17 -04:00
beckerinj
b6c7c80c95 full width input for copy menu 2023-03-31 01:26:19 -04:00
beckerinj
77e568d5c3 small 2023-03-27 12:41:59 -04:00
mbecker20
699fc51cf7 link to build if click on image deployment header 2023-03-27 15:30:11 +00:00
mbecker20
21029c90b7 info page on stats page 2023-03-27 05:13:12 +00:00
mbecker20
6b0530eb7f brush up server stats page 2023-03-26 23:15:58 +00:00
beckerinj
f7061c7225 toggle to show absolutes for mem and disk stat graphs 2023-03-26 18:47:21 -04:00
mbecker20
750f698369 updates page 2023-03-26 02:20:39 +00:00
mbecker20
ec5ef42298 add max height / scrolling to copy menu target selector 2023-03-24 00:45:47 +00:00
beckerinj
46820b0044 increase the tab title padding 2023-03-23 20:36:31 -04:00
beckerinj
425a6648f7 improve summary styling 2023-03-23 03:13:19 -04:00
mbecker20
349fc297ce 0.2.10 add renaming functionality 2023-03-22 20:33:26 +00:00
mbecker20
5ad87c03ed show none when none 2023-03-22 07:16:04 +00:00
mbecker20
d16006f28f improve design 2023-03-22 07:03:28 +00:00
beckerinj
7f0452a5f5 improve pie chart home page 2023-03-22 02:59:29 -04:00
mbecker20
c605b2f6fc implement pie chart summary 2023-03-22 06:41:57 +00:00
beckerinj
6c2d8a8494 unnecessary import 2023-03-21 23:10:01 -07:00
mbecker20
874691f729 add a pie chart component 2023-03-21 09:44:00 +00:00
beckerinj
cdf702e17d orange 2023-03-21 00:52:25 -07:00
mbecker20
25fdb32627 rename deployments 2023-03-19 08:14:54 +00:00
mbecker20
e976ea0a3a improve the behavior 2023-03-17 20:55:37 +00:00
mbecker20
34e6b4fc69 rename server working 2023-03-17 20:40:19 +00:00
mbecker20
a2d77567b3 dont need to 'to_monitor_name' servers 2023-03-15 07:35:54 +00:00
mbecker20
ecb460f9b5 add rename deployment to monitor client 2023-03-14 20:15:27 +00:00
mbecker20
63444b089c rename deployment func 2023-03-12 23:36:20 +00:00
mbecker20
c787984b77 initialize mongo with builder 2023-03-12 22:03:31 +00:00
mbecker20
bf3d03e801 fix problem of repeated query for docker accounts, secrets, etc 2023-03-12 05:07:55 +00:00
mbecker20
bc2e69b975 use resource to load stuff 2023-03-12 03:45:49 +00:00
mbecker20
7b94fcf3da 0.2.9 finish implement secret helpers on frontend 2023-03-12 00:48:18 +00:00
mbecker20
9cf03b8b88 add route to get available secret keys 2023-03-12 00:16:03 +00:00
mbecker20
a288edcf61 0.2.8 implement secret interpolation on builds and deployments 2023-03-11 23:34:17 +00:00
mbecker20
89cc18ad37 update tokio version 2023-03-10 19:27:40 +00:00
mbecker20
ffa3b671e1 change default alerting thresholds 2023-03-09 07:08:38 +00:00
beckerinj
f32eeb413b add label to home sort by 2023-03-08 16:10:23 -05:00
mbecker20
b5a5103cfc move core dockerfile 2023-03-08 18:26:42 +00:00
mbecker20
c5697e59f3 delete sample file 2023-03-08 18:24:15 +00:00
mbecker20
f030667ff4 update image in deployment header as well 2023-03-07 17:41:00 +00:00
mbecker20
e9fef5d97c change get_deployment_deployed_version to 'unknown' if not known 2023-03-07 17:39:59 +00:00
beckerinj
f5818ac7ea actually return image 2023-03-07 12:37:45 -05:00
mbecker20
c85ab4110d show derived image is container.image is sha256: 2023-03-07 16:30:59 +00:00
mbecker20
9690ea35b8 make description text area larger 2023-03-07 08:44:31 +00:00
mbecker20
6300c8011b fix modify global user permissions operator - make operator the admin, instead of the target 2023-03-06 17:09:12 +00:00
mbecker20
97f582b381 customizable page title 2023-03-06 02:07:08 +00:00
mbecker20
5135a9c228 show server name under deployment on admin user manage page 2023-03-06 01:46:40 +00:00
mbecker20
b7d1212a82 make resources links in account page 2023-03-05 21:50:15 +00:00
mbecker20
7d9d0a9fc4 add view of resources you can access on account page 2023-03-05 21:42:11 +00:00
beckerinj
ed9aef4321 add resources to account page 2023-03-05 16:33:40 -05:00
mbecker20
0aa638bdf4 only do daily update if servers not empty 2023-03-05 20:19:06 +00:00
mbecker20
0ec39d793d one page to view all permissions for user 2023-03-05 09:24:59 +00:00
mbecker20
5579ba869c v0.2.7 remove passkeys from periphery startup log 2023-03-03 17:27:42 +00:00
mbecker20
210940038c hide passkeys on periphery startup config log 2023-03-03 17:24:28 +00:00
mbecker20
98a1a60362 /home/ubuntu/example 2023-03-03 08:15:17 +00:00
mbecker20
86cf9116ba update builds and deployments docs with link to file paths doc 2023-03-03 08:09:41 +00:00
mbecker20
8b2defe0d9 add doc about file paths 2023-03-03 07:58:09 +00:00
mbecker20
50b14b3ce5 0.2.6 store ami name instead of ami_id (because the id has to change sometimes) 2023-03-03 07:11:55 +00:00
mbecker20
1bfb17cb5d handle setting default ami id correctly 2023-03-02 21:32:22 +00:00
mbecker20
b90acb66c7 0.2.5 stop leaking github token 2023-03-02 21:25:46 +00:00
mbecker20
7648b0dd10 don't let github access token leak when clone fails 2023-03-02 21:21:07 +00:00
mbecker20
2d69f1791a default builds to use aws config on create 2023-03-02 17:11:23 +00:00
mbecker20
5ba887095a allow select "none" for docker organization 2023-03-02 17:00:40 +00:00
mbecker20
19b7405562 show organization immediately if it exists on build 2023-03-01 23:20:54 +00:00
mbecker20
f5c5f734e1 clean deployment / build config before update 2023-03-01 21:18:40 +00:00
mbecker20
8d1639bcaf fix build permissions 2023-03-01 10:18:58 +00:00
mbecker20
e2446af00e remove print 2023-03-01 10:12:26 +00:00
mbecker20
1b39aaaa38 implement description 2023-03-01 09:46:50 +00:00
mbecker20
5a2a1a3d98 0.2.4: add description and update description 2023-03-01 08:13:07 +00:00
mbecker20
39eceb745b v0.2.2: configure docker organizations for builds 2023-03-01 07:18:49 +00:00
beckerinj
4c1ec5db33 edit permissions.md 2023-02-28 02:26:57 -05:00
beckerinj
8b68b9481e permissions.md 2023-02-28 02:21:46 -05:00
beckerinj
14843f83c6 add core setup link to table of contents 2023-02-28 01:56:05 -05:00
beckerinj
e67d87e885 even 2023-02-28 01:53:54 -05:00
beckerinj
7d4d865d58 elaborate on networks 2023-02-28 01:53:04 -05:00
beckerinj
1e4aaff23c if to is 2023-02-28 01:37:43 -05:00
beckerinj
df3f4a5f4a improve builds.md 2023-02-28 01:36:35 -05:00
beckerinj
1f8557300d fix type 2023-02-28 01:33:55 -05:00
beckerinj
bf17d705f0 fix typo 2023-02-28 01:33:09 -05:00
beckerinj
0d24b792c6 container lifetime management 2023-02-28 01:30:43 -05:00
mbecker20
fb61e36417 remove download log button, its kind of unsafe if the log is long 2023-02-28 06:18:42 +00:00
beckerinj
c39869d2f8 deployments.md 2023-02-28 01:18:11 -05:00
mbecker20
750e0274da #example 2023-02-28 05:54:43 +00:00
beckerinj
a9d37ab667 add placeholders to show to to pass env 2023-02-28 00:52:09 -05:00
mbecker20
eacb549d5e update core config example with github_webhook_base_url 2023-02-28 05:08:05 +00:00
mbecker20
ce7cb8fe45 improve confirm menu with copy button 2023-02-28 04:58:39 +00:00
mbecker20
f9fe4e32b4 restyle builds and deployments 2023-02-28 04:24:15 +00:00
mbecker20
2c9fc2bad4 always show docker account 2023-02-28 03:57:35 +00:00
mbecker20
94949291c2 fix notifications, add dynamic listener url 2023-02-28 03:41:25 +00:00
beckerinj
2944ba6ef9 cli v0.2.3 2023-02-27 22:18:04 -05:00
beckerinj
997e68a31d dynamic github webhook base url 2023-02-27 22:17:37 -05:00
beckerinj
bfb9d9e34d add periphery version in builder connected logs 2023-02-27 21:46:43 -05:00
mbecker20
3b9219b586 fix updates selector style 2023-02-27 05:55:33 +00:00
mbecker20
7bf2a88ab1 finish build args section 2023-02-27 05:53:56 +00:00
mbecker20
d21ed093dc fix build args gap 2023-02-27 05:52:13 +00:00
mbecker20
6e89671e91 switch cli build and build args build config 2023-02-27 05:47:10 +00:00
beckerinj
ee1128a666 Update builds.md 2023-02-27 00:45:16 -05:00
beckerinj
63b5deecd7 Update servers.md 2023-02-27 00:44:01 -05:00
mbecker20
f4f97ce1a7 finish builds / servers 2023-02-27 05:42:22 +00:00
mbecker20
a666df099f use image on deployment container 2023-02-26 06:55:31 +00:00
mbecker20
21dd0ee072 cli should be 0.2.2 2023-02-26 06:48:38 +00:00
mbecker20
bd2a1d4236 v0.2.1 merge multiple config files 2023-02-26 06:25:26 +00:00
mbecker20
7acdbcfd8f improve updates selector - add class 2023-02-25 22:34:37 +00:00
mbecker20
58514c5c93 fix height of builder config when no builder type chosen 2023-02-25 22:07:05 +00:00
mbecker20
580e800923 fix clap args with - 2023-02-23 23:14:02 +00:00
mbecker20
29f6b19f33 cli 0.2.0. fix starting mongo when no existing container present 2023-02-23 22:46:17 +00:00
mbecker20
e090247723 fix error when user doesn't have access to build on deployment 2023-02-23 08:00:55 +00:00
mbecker20
1374c26cd8 0.2.0 cleanup 2023-02-23 07:28:11 +00:00
mbecker20
5467b40b2e fix to git clone <TOKEN> splice 2023-02-23 07:22:32 +00:00
mbecker20
165b9012da improve users responsiveness 2023-02-23 07:17:18 +00:00
mbecker20
22630f665e update the manage users page 2023-02-23 07:09:33 +00:00
mbecker20
3d867084ba log poll default to false 2023-02-23 06:44:55 +00:00
mbecker20
171dd2d9e0 remove menu animation, change builder type to selector 2023-02-23 06:38:36 +00:00
mbecker20
9709239f88 build version h2 2023-02-22 22:45:05 +00:00
mbecker20
60d457b285 improve deployment in tree and display deployed version in header 2023-02-22 22:27:54 +00:00
mbecker20
8b1d4793a7 0.1.17 support building with ec2 instances 2023-02-22 21:05:03 +00:00
mbecker20
f2166c8435 configure aws config on builds 2023-02-22 20:49:56 +00:00
mbecker20
07d723a748 more prog on frontend, some api etc 2023-02-22 06:39:32 +00:00
mbecker20
b36f485287 put server / aws build on build header 2023-02-21 23:05:49 +00:00
mbecker20
a121ae0828 begin frontend refactor for ephemeral build support 2023-02-21 18:11:43 +00:00
mbecker20
e2b5a02008 building works 2023-02-21 05:22:26 +00:00
mbecker20
575aa62625 update versions to 0.1.16 2023-02-21 04:32:41 +00:00
mbecker20
ac88a2c4ed testing and fixes for aws build 2023-02-21 04:27:30 +00:00
mbecker20
f1dcb71a8a poll periphery on build instance to ensure connectivity before moving on 2023-02-20 22:56:22 +00:00
mbecker20
30d04bc201 support building on epheral ec2 2023-02-20 09:41:15 +00:00
mbecker20
33a00bb1a2 poll when instance running 2023-02-20 04:55:44 +00:00
mbecker20
ccca44ea89 start working on build instance spawn on aws 2023-02-20 01:19:07 +00:00
mbecker20
ae5f36fe51 0.1.15 use passkey for addition core - periphery auth layer 2023-02-17 18:29:13 +00:00
beckerinj
69ce1e4f36 start adding passkey auth to core periphery communication 2023-02-17 12:53:09 -05:00
mbecker20
6e444b9032 temp remove deployment state polling 2023-02-14 18:33:32 +00:00
beckerinj
73eff72da4 work on docs 2023-02-13 11:16:27 -05:00
beckerinj
698e3c214b opinionated 2023-02-12 14:48:28 -05:00
beckerinj
9da77667dc add next page 2023-02-12 04:14:06 -05:00
beckerinj
c30793fb8f to 2023-02-12 04:12:49 -05:00
beckerinj
84fdaab24d close intro better 2023-02-12 04:10:10 -05:00
beckerinj
cbd67bb609 back to table 2023-02-12 04:05:35 -05:00
beckerinj
00f58e9008 architecture 2023-02-12 03:59:26 -05:00
beckerinj
7738fab351 intro 2023-02-12 03:57:55 -05:00
beckerinj
06e8f6589b links 2023-02-12 00:55:14 -05:00
beckerinj
57d9287724 start docs 2023-02-12 00:51:05 -05:00
mbecker20
2cc65595ee log only polls when container is running 2023-02-11 20:29:25 +00:00
mbecker20
3dd2b97873 add polling for deployment state and logs 2023-02-11 20:17:13 +00:00
mbecker20
3c805ebbf7 fix deployment action build A tag styling 2023-02-11 19:36:36 +00:00
beckerinj
a854160018 Update periphery.config.example.toml 2023-02-08 13:40:00 -05:00
mbecker20
a99d9e5969 make update menu wider 2023-02-08 05:56:09 +00:00
mbecker20
813b6c1182 confirm menu 2023-02-08 05:37:27 +00:00
mbecker20
2958f9589b deployment / build delete wont fail if server disabled / unreachable. 2023-02-08 05:37:12 +00:00
beckerinj
69b4e26176 ConfirmMenuButton 2023-02-07 23:49:27 -05:00
beckerinj
78b00f139d delete server side effects 2023-02-07 23:23:46 -05:00
beckerinj
dc1e8de851 widen env 2023-02-07 23:23:18 -05:00
mbecker20
3187b335a3 cli 0.1.20 2023-02-07 20:12:20 +00:00
beckerinj
54b5a2b420 add logrotate to mongo startup 2023-02-07 15:10:36 -05:00
beckerinj
14c6bd00a8 use the scroller class for LogContainer 2023-02-02 16:14:11 -05:00
mbecker20
e9c3646450 0.1.14 fix deployment builder defaults 2023-01-30 17:45:34 +00:00
mbecker20
4f20257479 0.1.13 improve Builder structs with defaults 2023-01-30 17:24:03 +00:00
beckerinj
65749991de 0.1.12 MonitorClient::new_from_env 2023-01-30 11:50:48 -05:00
mbecker20
237a1d802d handle stuff when server disabled / unreachable 2023-01-28 09:03:00 +00:00
mbecker20
e4336f19f3 default copy server to curr server 2023-01-25 23:08:31 +00:00
mbecker20
c895e5e67f fix selector search behavior when using itemMap 2023-01-25 23:01:11 +00:00
mbecker20
4e4e210736 implement build / deployment copy 2023-01-25 22:53:27 +00:00
beckerinj
09dfc8faa3 cli 0.1.19 add Restart=on-failure to service unit file 2023-01-25 14:58:54 -05:00
mbecker20
3c4f77cc78 0.1.11 custom DockerRunArgs Default implementation 2023-01-25 06:29:09 +00:00
mbecker20
c86880ccdb v0.1.10 make default --network host 2023-01-25 04:01:01 +00:00
beckerinj
9db26a3037 sort the servers 2023-01-24 22:33:02 -05:00
mbecker20
711d27e15e change notification formatting 2023-01-25 02:49:24 +00:00
mbecker20
e1d53598e6 historical stats default 500 length 2023-01-25 02:41:31 +00:00
beckerinj
f871bc3e03 auto refresh stats and color red in error 2023-01-24 02:46:39 -05:00
mbecker20
69b359ce4a fix loading more updates when operation selected 2023-01-23 00:24:25 +00:00
mbecker20
0dd914b6e4 wrap summary items 2023-01-22 23:08:33 +00:00
beckerinj
f03d0b8930 add links in updates and headers 2023-01-22 17:50:35 -05:00
mbecker20
a672c1cba3 fix update summary 2023-01-22 09:38:28 +00:00
beckerinj
233d9dab33 filter updates by operation 2023-01-22 04:29:46 -05:00
mbecker20
f5ac23834d fix to summary 2023-01-21 09:09:30 +00:00
mbecker20
03bc43b04e improve monitoring 2023-01-21 09:02:37 +00:00
mbecker20
fdbd2a2181 improve updates home, add server, add group 2023-01-21 06:25:49 +00:00
mbecker20
ae9fe5c424 fix open unexpected 2023-01-21 04:55:28 +00:00
mbecker20
7de10b7277 serverchildren 2023-01-21 04:49:30 +00:00
mbecker20
8c1f4d7786 fix parse show_builds 2023-01-20 05:21:26 +00:00
beckerinj
f3e84b52c6 try to fix updates 2023-01-20 00:12:47 -05:00
beckerinj
d64c46f44d fixes 2023-01-19 23:42:26 -05:00
beckerinj
a6a58a25be server info tab 2023-01-19 23:36:38 -05:00
mbecker20
37046ddbd8 add back home screen summary / updates 2023-01-20 02:48:22 +00:00
beckerinj
6f697d292a show builds on deployment page 2023-01-19 21:05:53 -05:00
beckerinj
4098c6b487 create api secret frontend 2023-01-19 00:10:06 -05:00
mbecker20
d2844b6558 enable scroll again 2023-01-18 09:34:46 +00:00
mbecker20
59eb63bcda 0.1.9 add 15-sec to Timelength options 2023-01-18 09:15:36 +00:00
mbecker20
7842ac5c45 need to add timelength here too 2023-01-18 09:01:25 +00:00
mbecker20
ba7b6db5b0 update async timing util to 0.1.14 2023-01-18 08:55:50 +00:00
mbecker20
3648a6efd4 handle permissions 2023-01-18 06:22:13 +00:00
mbecker20
f96ee3f1e8 fix permissions access for actions 2023-01-18 05:03:38 +00:00
mbecker20
f0a9b4f5a6 update async_timing_util to 0.1.13 2023-01-18 04:51:31 +00:00
beckerinj
f01c159f38 Merge branch 'responsive' 2023-01-17 23:27:18 -05:00
beckerinj
79c587c892 improve frontend responsive behavior 2023-01-17 23:26:58 -05:00
mbecker20
9f4a1625ff attach version to deployment update 2023-01-17 04:39:37 +00:00
mbecker20
3b331e1df7 take timestamp upon recv current stat 2023-01-17 03:53:30 +00:00
mbecker20
fb067c15ff element should be func 2023-01-16 23:29:57 +00:00
mbecker20
f4bba8febc improve sizes of some inputs 2023-01-16 23:02:21 +00:00
mbecker20
bf442f5cdf timeout on health check 1 second 2023-01-16 22:49:33 +00:00
mbecker20
447e41c0af add health check before all requests to peripheries 2023-01-16 22:38:10 +00:00
mbecker20
5901992d80 cli 0.1.16 dont install periphery from crates by default 2023-01-16 21:46:37 +00:00
mbecker20
13e8ad93e2 cli 0.1.15 fix --yes param to skip manual enter 2023-01-16 21:37:32 +00:00
mbecker20
66daa1ac17 cli 0.1.14 gen systemd file pointing to bin in .monitor 2023-01-16 21:31:15 +00:00
beckerinj
3622394751 move deployment permissions to top level tabs like others 2023-01-16 14:27:29 -05:00
mbecker20
53ab799d9b I guess the process filter is kind of usable now 2023-01-16 03:30:56 +00:00
mbecker20
5d0563968b fix build actions height 2023-01-16 02:33:57 +00:00
beckerinj
6e9b692328 fix currentstats mostly 2023-01-15 21:17:55 -05:00
beckerinj
de7ddd2224 improve historical stats and add server 2023-01-15 20:46:03 -05:00
beckerinj
db82a2d44e improve page layouts 2023-01-15 06:06:08 -05:00
beckerinj
ee46c86106 log on daily update 2023-01-14 23:21:59 -05:00
beckerinj
756490bc0c dont sort groups procedures 2023-01-14 23:15:38 -05:00
beckerinj
a483283352 implement groups create delete add remove 2023-01-14 23:12:33 -05:00
beckerinj
399e4bfd40 move stats to http poll 2023-01-14 03:59:17 -05:00
beckerinj
d1df2818b5 move current stats to polling http 2023-01-14 03:58:20 -05:00
mbecker20
5438ca87fb don't log stats object 2023-01-13 05:58:00 +00:00
mbecker20
20a2a660a0 Merge pull request #4 from mbecker20/next
Next
2023-01-13 00:43:46 -05:00
mbecker20
f9d234908b Merge branch 'main' into next 2023-01-13 00:42:14 -05:00
beckerinj
56b21f0d9e fix core historical stats query filter: specify server_id 2023-01-12 17:08:09 -05:00
mbecker20
d604b28d91 filter charts on undefined 2023-01-12 20:20:42 +00:00
mbecker20
d227f2c311 fix login css width to not overflow 2023-01-12 19:58:11 +00:00
mbecker20
a1acac1479 remove width 100vw in app css class 2023-01-12 19:43:28 +00:00
mbecker20
805ebe55ce smaller stats bars 2023-01-12 05:51:32 +00:00
mbecker20
3f478fefdf 0.1.8 add process start time 2023-01-12 05:07:13 +00:00
mbecker20
809be61e9c cli 0.1.13 periphery systemd needed to run 'loginctl enable-linger $USER' 2023-01-12 04:42:29 +00:00
mbecker20
cfa3dd537d cli 0.1.12 specify whether to --add-host to core container 2023-01-12 04:14:25 +00:00
mbecker20
511efdc113 cli 0.1.11 fix docker run. dont fail when there is no existing container 2023-01-12 00:24:41 +00:00
mbecker20
34a6ac3be3 cli 0.1.10 pass --yes or -y to skip requirement for user to press enter 2023-01-11 22:43:26 +00:00
mbecker20
1674f066ea cli 0.1.9 try fix to periphery start systemd 2023-01-11 22:22:57 +00:00
mbecker20
572560d5b2 cli 0.1.8 start periphery with systemd 2023-01-11 22:08:16 +00:00
mbecker20
34822d6d19 periphery 0.1.6 2023-01-11 19:29:21 +00:00
mbecker20
6655cb43b5 cli 0.1.7 pass repo dir to periphery gen_config 2023-01-11 10:45:07 +00:00
mbecker20
7060926854 add cpu freq and avg system load 2023-01-11 10:03:48 +00:00
mbecker20
a19c339857 show sys info and, load, cpu freq on stats 2023-01-11 09:58:23 +00:00
mbecker20
2cc86a8f21 0.1.6 2023-01-11 09:12:53 +00:00
mbecker20
5adbba61ec increase temp threshold to 85% of critical 2023-01-11 08:51:18 +00:00
mbecker20
d0540669b4 core and periphery 0.1.5 2023-01-11 08:44:16 +00:00
mbecker20
4fb3eb12ba 0.1.5 2023-01-11 08:42:09 +00:00
mbecker20
f885398d7c 0.1.4 across all crates 2023-01-11 06:52:02 +00:00
mbecker20
bda5a93ea4 helpers 0.1.3 2023-01-11 06:29:40 +00:00
mbecker20
713d7680c1 helpers 0.1.3 update types 2023-01-11 06:28:48 +00:00
mbecker20
3b574341f2 types 0.1.4 fix extra_args update diff 2023-01-11 06:22:25 +00:00
mbecker20
664a585bdd frontend for extra args 2023-01-11 06:21:44 +00:00
mbecker20
0a0bf55204 periphery 0.1.3 update types and helpers 2023-01-11 05:27:13 +00:00
mbecker20
b3405a8ab9 periphery 0.1.3 update types and helpers version 2023-01-11 05:25:16 +00:00
mbecker20
7693993d18 deployment can have extra args 2023-01-11 05:20:26 +00:00
mbecker20
89e16ef715 types 0.1.3 pass any extra args to docker run command 2023-01-11 05:08:35 +00:00
mbecker20
199a0e0517 remove unused mount filter 2023-01-11 04:25:48 +00:00
mbecker20
ca80fb4d28 periphery 0.1.2 remove disk filter 2023-01-10 17:04:50 +00:00
mbecker20
143d855ad7 cli v0.1.2 start core container with --add-host option 2023-01-10 16:53:34 +00:00
mbecker20
aaea572c4f update tokio version 2023-01-10 09:41:10 +00:00
mbecker20
41d86e1f34 fix monitor client 2023-01-10 09:38:05 +00:00
mbecker20
00bae80410 client 0.1.2 2023-01-10 09:30:17 +00:00
mbecker20
ef0522b200 monitor cli 0.1.1 2023-01-10 09:29:07 +00:00
mbecker20
f61f38be7e periphery v0.1.1 2023-01-10 09:00:48 +00:00
mbecker20
120877bfb5 helpers v0.1.1 2023-01-10 08:59:12 +00:00
mbecker20
3587818775 types v0.1.2 2023-01-10 08:57:45 +00:00
mbecker20
1f2cf5a848 show system procs 2023-01-10 08:54:25 +00:00
mbecker20
d9e4e5b390 system stats views working 2023-01-10 08:32:16 +00:00
mbecker20
4f965cde2d improve charts, historical and current graphs 2023-01-10 06:37:10 +00:00
mbecker20
662df40aaf current stats page 2023-01-10 02:56:22 +00:00
mbecker20
b41486fb20 realtime current stats heatbar 2023-01-09 07:47:00 +00:00
mbecker20
6acbc19c6a more accurate comp temp sort 2023-01-09 06:34:02 +00:00
mbecker20
01dfdd03fe sort the components and processes 2023-01-09 06:30:56 +00:00
mbecker20
5dc335595e core will auto prune images daily on servers with feature enabled 2023-01-09 05:53:20 +00:00
mbecker20
7c5cd1553d show build_build as just build in update menu, and fix build listener url 2023-01-09 05:39:22 +00:00
mbecker20
c78c89da37 pull image before remove container to minimize downtime 2023-01-09 05:24:37 +00:00
mbecker20
f9f3b276c8 fix default config file 2023-01-09 05:16:30 +00:00
mbecker20
e982f5a6f2 periphery systemd service file 2023-01-09 05:13:13 +00:00
mbecker20
ba0f76fd59 periphery as a service 2023-01-09 05:11:30 +00:00
mbecker20
e26769a787 get periphery version route 2023-01-09 03:54:16 +00:00
mbecker20
cffe3b1428 add permissions map to action 2023-01-09 02:12:35 +00:00
mbecker20
82f9076f3e spawn command handler 2023-01-09 01:36:12 +00:00
mbecker20
80438e7a74 run arbitrary command on periphery 2023-01-09 01:34:15 +00:00
mbecker20
8453f9ac5f handle build version 2023-01-09 00:02:35 +00:00
mbecker20
a9834a9997 fix to build last_built_at parsing 2023-01-08 08:31:36 +00:00
mbecker20
772aca95f9 fix to docker build command parsing 2023-01-08 08:25:05 +00:00
mbecker20
e1b1ee3f8d cli point to correct image 2023-01-08 08:13:06 +00:00
mbecker20
91b640cd90 disk io 2023-01-08 08:09:12 +00:00
mbecker20
47ed7d8d5f lazy load stats 2023-01-08 07:52:30 +00:00
mbecker20
af948edbea make standalone stats page 2023-01-08 07:50:47 +00:00
mbecker20
e18cd2eebb select build version for monitor build 2023-01-08 06:11:34 +00:00
mbecker20
2e5f2d11b4 add more info on stats page 2023-01-08 03:08:45 +00:00
mbecker20
2906eaf5f5 server stats page 2023-01-08 02:54:59 +00:00
mbecker20
4bc8fc4b25 fix peirphery client not passing through disks query 2023-01-07 23:46:09 +00:00
mbecker20
487bf515ba spawn threads to complete tasks requested over http 2023-01-07 21:45:57 +00:00
mbecker20
9378954551 improve frontend config design 2023-01-07 09:44:44 +00:00
mbecker20
6b366fb0e2 get stats api 2023-01-07 08:01:44 +00:00
mbecker20
a5af235cef start on github webhook listener 2023-01-05 08:41:58 +00:00
mbecker20
b0b991115b user permission management working 2023-01-05 06:55:41 +00:00
mbecker20
f2db575c70 implement owners management 2023-01-05 06:33:14 +00:00
mbecker20
5628d92b3c add status to update menu 2023-01-05 05:36:23 +00:00
mbecker20
ba994e80f6 make simple tabs to use on home screen 2023-01-05 05:30:55 +00:00
mbecker20
db9f633e89 container log should be GET method 2023-01-05 05:20:33 +00:00
mbecker20
7aaececa87 deployment page loads 2023-01-05 05:14:25 +00:00
mbecker20
1c6f31cfea finish deployment refactor 2023-01-05 05:09:06 +00:00
mbecker20
88f46a04f0 add deployment page 2023-01-05 05:06:01 +00:00
mbecker20
8b0fd2326f add cli publish task 2023-01-05 03:17:09 +00:00
mbecker20
86af789dd3 cli periphery seems to be working 2023-01-05 03:16:37 +00:00
mbecker20
243f54ec11 use cli to start periphery as daemon 2023-01-05 02:32:21 +00:00
mbecker20
4cc1f4458e start up periphery with cli 2023-01-05 02:22:15 +00:00
mbecker20
823a735ef7 prep monitor_periphery for publish 2023-01-05 02:13:22 +00:00
mbecker20
7782412c9b helpers use crates.io monitor_types 2023-01-05 02:09:41 +00:00
mbecker20
ccd7d715b9 add publish helpers task 2023-01-05 02:07:44 +00:00
mbecker20
a192b142d4 update helpers to be on crates.io 2023-01-05 02:07:10 +00:00
mbecker20
277d5fe662 update licenses and descriptions 2023-01-05 02:03:00 +00:00
mbecker20
44b9eb462e fix monitor_types license 2023-01-05 02:01:09 +00:00
mbecker20
680bb55321 start prep monitor_periphery for crates.io 2023-01-05 01:57:55 +00:00
mbecker20
33e0568782 cli to start up periphery as daemon 2023-01-05 01:55:00 +00:00
mbecker20
2b1b88ce3c periphery doesn't log any secret values on startup 2023-01-05 01:20:19 +00:00
mbecker20
bd02b9e281 fix build and deploy 2023-01-05 01:04:10 +00:00
mbecker20
85c5b146d4 fix builds getting stuck. update core dockerfile 2023-01-04 23:26:12 +00:00
mbecker20
ac3d17ce82 fix updates pagination 2023-01-04 22:41:48 +00:00
mbecker20
f6f94303a5 put id on SystemStatsRecord 2023-01-04 08:26:13 +00:00
mbecker20
53cf644008 record system processes with stats 2023-01-04 08:25:17 +00:00
mbecker20
3dfeb65aab prune system stats 2023-01-04 08:22:02 +00:00
mbecker20
c90e91d78b finish stat collection and slack alerts, daily update. async mutex 2023-01-04 08:01:25 +00:00
mbecker20
17a5d624d9 implement some stats recording. clean up Tab comp 2023-01-03 06:50:23 +00:00
mbecker20
ef8f75b3e2 begin recording system stats 2023-01-02 10:44:48 +00:00
mbecker20
da83afc6f4 stats interval is timelength 2023-01-02 10:18:54 +00:00
mbecker20
d77d07cb52 attach id of deleted item to delete update 2023-01-02 09:43:40 +00:00
mbecker20
161ac34afa fix Operation for delete build update 2023-01-02 09:34:42 +00:00
mbecker20
e9685ce71a add build page 2023-01-02 09:09:32 +00:00
mbecker20
bdf4e9d6c6 pass icon dir in frontend env 2023-01-02 08:31:39 +00:00
mbecker20
5e205efd29 add user management features 2023-01-02 08:15:43 +00:00
mbecker20
a491f206c9 create channel to handle stats stream in client 2023-01-02 07:16:55 +00:00
mbecker20
a6d655b42e add usernames cache to state 2023-01-02 06:33:18 +00:00
mbecker20
f5fa676db6 builds and deployments under server 2023-01-02 05:58:22 +00:00
mbecker20
8b16aff3bf implement home, servers, implement container log passthrough 2023-01-02 05:28:38 +00:00
mbecker20
3006aa7fa3 basically get server page 2023-01-02 00:44:20 +00:00
mbecker20
d5861274de add get_username method on client 2023-01-01 21:18:24 +00:00
mbecker20
a927571e29 add get_username to client 2023-01-01 20:51:57 +00:00
mbecker20
9998debb96 get username api 2023-01-01 20:40:01 +00:00
mbecker20
956805603e get home screen working pretty much 2023-01-01 08:00:08 +00:00
mbecker20
4a4bf197a9 home screen update views 2023-01-01 02:15:29 +00:00
mbecker20
0845dc04e1 summary 2022-12-31 21:42:58 +00:00
mbecker20
6104e5aa5a add summary component 2022-12-31 11:17:57 +00:00
mbecker20
1d717dff7a remove core ws stats logs 2022-12-31 10:28:29 +00:00
mbecker20
c2702869c1 test the stats ws connection with monitor client 2022-12-31 10:26:37 +00:00
mbecker20
58a5accd83 link up the stats websocket 2022-12-31 09:42:41 +00:00
mbecker20
de937707e1 Periphery config repo dir parse as PathBuf 2022-12-31 06:23:07 +00:00
mbecker20
0ceecee604 a bunch of stuff 2022-12-31 05:42:44 +00:00
beckerinj
5d0c14201d start on processes in stats 2022-12-30 12:36:13 -05:00
mbecker20
d0d6271d8f frontend topbar search 2022-12-30 07:49:30 +00:00
mbecker20
90884904de kind of get search working 2022-12-30 06:23:09 +00:00
mbecker20
b042494b44 get system component temps etc 2022-12-30 05:56:16 +00:00
mbecker20
80ced82131 implement configurable login options 2022-12-30 05:05:45 +00:00
mbecker20
f74faaecf8 google oauth working 2022-12-29 09:09:04 +00:00
mbecker20
3b6d3af7bb implement google oauth 2022-12-29 07:59:50 +00:00
mbecker20
15662c951d topbar ws indicator and userPermissionsOnEntity 2022-12-28 06:08:09 +00:00
mbecker20
de8c59bb61 clean up the log for unauthorized ip access 2022-12-27 05:51:06 +00:00
mbecker20
9cde83de0e update gen_config in cli to support adding allowed_ips 2022-12-27 05:41:14 +00:00
mbecker20
0a5256cbd7 periphery allowed ips 2022-12-27 05:03:24 +00:00
mbecker20
99ce35ce1d put container state on list deployment return, including NotCreated and Unknown 2022-12-26 23:18:17 +00:00
mbecker20
4d3feb52c7 make sure user has execute permissions on resources in procedure 2022-12-26 22:39:21 +00:00
mbecker20
eb79e8726d fix type import issue 2022-12-26 22:21:08 +00:00
mbecker20
3c055544cc move Timelength to local types for typeshare 2022-12-26 22:14:57 +00:00
mbecker20
0d4118dac8 add crud for groups at all client levels 2022-12-26 19:55:33 +00:00
mbecker20
dac9c05a89 change default periphery stats polling rate to 5-sec 2022-12-26 19:07:32 +00:00
mbecker20
1dbc2909e2 start making crud for groups 2022-12-26 06:32:36 +00:00
mbecker20
6fc4a0627c running as daemon seems to work! 2022-12-26 06:24:38 +00:00
mbecker20
4a99ccc1a9 run periphery as daemon 2022-12-25 22:59:03 +00:00
mbecker20
46dc663385 create the Group type to group all the resources (including groups for nesting 2022-12-25 06:20:22 +00:00
mbecker20
d2a3a6cddf refactor types lib into split files for easier finding of things 2022-12-25 05:46:21 +00:00
mbecker20
0d140b420e periphery runs fine lcoally 2022-12-24 06:20:25 +00:00
mbecker20
6be463475c change update send channel to tokio::sync::broadcast 2022-12-24 06:10:13 +00:00
mbecker20
858cead89d rename stats_interval to stats_polling_rate 2022-12-24 05:28:10 +00:00
mbecker20
ba439d6f5f improve periphery stats with modify interval, modify user updates, frontend state 2022-12-23 05:41:06 +00:00
mbecker20
9c09906bdd implement get updates list 2022-12-22 07:05:18 +00:00
mbecker20
90173a30e7 add topbar to FE and some state hooks 2022-12-22 01:06:42 +00:00
mbecker20
902bf0c981 frontend login guard and page, deployment / build copy route 2022-12-21 06:21:23 +00:00
mbecker20
5ac3239f4f pull deployment in clients 2022-12-21 05:13:25 +00:00
mbecker20
3a0fa36d19 pull deployment repo 2022-12-21 05:10:17 +00:00
mbecker20
3b0f919baa implement correct busy logic 2022-12-21 04:45:27 +00:00
mbecker20
b2226efd59 implement a not good busy guard 2022-12-20 07:45:25 +00:00
mbecker20
a0beb27072 small refactor on the ts client 2022-12-19 07:32:47 +00:00
mbecker20
5cd68191fc implement a lot of run procedure 2022-12-19 00:59:00 +00:00
mbecker20
b9978f531a fill out the ts client 2022-12-18 08:10:14 +00:00
mbecker20
11a4379fb6 add additional typescript type gen from core 2022-12-18 06:00:23 +00:00
mbecker20
090c4a6dc7 generate typescript types in frontend 2022-12-18 05:44:41 +00:00
mbecker20
0b3bd899fe move to string timestamps 2022-12-18 05:42:58 +00:00
mbecker20
0e913ac420 add shared util components 2022-12-18 03:23:50 +00:00
mbecker20
3e5df637fd add frontend public 2022-12-18 02:51:05 +00:00
mbecker20
81e96861d7 only show size of / and paths including "external" 2022-12-18 02:41:36 +00:00
mbecker20
6284a48a3a some consistency fixes 2022-12-18 00:14:46 +00:00
mbecker20
c0854c7923 port 8000 2022-12-17 23:35:40 +00:00
mbecker20
b4c98820d9 cli | replace ~ with env var HOME in path 2022-12-17 23:07:48 +00:00
beckerinj
1cbb7d6bfd fix test 2022-12-17 01:27:38 -05:00
beckerinj
be58cec1c9 add start stop remove container 2022-12-17 01:27:06 -05:00
beckerinj
839af15311 create procedure and update mungos 2022-12-16 02:50:07 -05:00
beckerinj
a985139f9c add procedures to api and add functions in monitor_client 2022-12-15 00:50:07 -05:00
beckerinj
7719c17227 outline functions on state to work with procedures 2022-12-15 00:24:57 -05:00
beckerinj
b92111c7c2 leverage build args to build slim / full periphery 2022-12-14 23:59:17 -05:00
beckerinj
a3ceef26f8 cli specify restart mode 2022-12-14 17:58:35 -05:00
beckerinj
e31515f6fd get cli working again 2022-12-14 12:55:02 -05:00
beckerinj
38be2c53e7 add color and remove cli deps 2022-12-14 03:09:01 -05:00
beckerinj
1d64040129 fill out cli scripts 2022-12-13 02:53:58 -05:00
beckerinj
ae9a1cb89e create full server / deployment / build 2022-12-13 02:18:44 -05:00
beckerinj
9858a7a5ce start mongo cli 2022-12-12 02:48:36 -05:00
beckerinj
e5dec41b49 slim and full periphery. updated stats type 2022-12-12 00:46:27 -05:00
beckerinj
b98f4a6ec1 add better error handing 2022-12-11 13:05:37 -05:00
beckerinj
ba431638b7 gen periphery config with cli 2022-12-11 12:42:53 -05:00
beckerinj
ffeca7658a fill out monitor client permissions and secret calls 2022-12-11 12:29:48 -05:00
beckerinj
daf06a33f8 start on cli - generate core config 2022-12-11 04:15:19 -05:00
beckerinj
e81b6a14de list + prune images networks containers 2022-12-11 02:19:52 -05:00
beckerinj
c79469af30 go back to copy . . 2022-12-10 23:37:10 -05:00
beckerinj
d761e49a64 only copy what each dockerfile needs to improve caching 2022-12-10 23:12:59 -05:00
beckerinj
0f865be593 first user auto enabled and admined 2022-12-10 20:15:23 -05:00
beckerinj
4130bc1726 add created_at and updated_at fields 2022-12-10 19:14:21 -05:00
beckerinj
5559c3de42 handle version in deployment from build 2022-12-10 18:43:18 -05:00
beckerinj
1884693ca6 add basic container stats to core get / list deployment 2022-12-10 18:16:43 -05:00
beckerinj
1eb707a759 get docker compose testing setup working 2022-12-10 15:55:41 -05:00
beckerinj
91246b7078 add routes to update build at id. build stops if pre_build fails. 2022-12-10 00:44:56 -05:00
beckerinj
31dd47aac8 create and test deploy 2022-12-08 00:29:34 -05:00
beckerinj
644a3c795e build_build 2022-12-07 19:55:41 -05:00
beckerinj
521976e57e remove all DbExtension 2022-12-07 15:42:57 -05:00
beckerinj
f1df4f36df move actions to actions folder as fn impl on State 2022-12-07 03:51:58 -05:00
beckerinj
e8e566ea62 add end ts to reclone update 2022-12-06 01:53:33 -05:00
beckerinj
b0a865ddb8 change clone repo update status to complete 2022-12-06 01:50:13 -05:00
beckerinj
937b271686 update things, reclone 2022-12-06 01:31:13 -05:00
beckerinj
c4a533bc4b cleanup 2022-12-05 03:13:59 -05:00
beckerinj
16aa58482f update build 2022-12-05 02:58:04 -05:00
beckerinj
1674825a94 apply Diff to all the updatable structs 2022-12-04 20:41:51 -05:00
beckerinj
a382b4d589 delete repo when deleting build 2022-12-04 04:36:36 -05:00
beckerinj
22a8845c53 clean up old stats 2022-12-04 04:11:27 -05:00
beckerinj
34b60c755b container stats 2022-12-04 01:59:47 -05:00
beckerinj
38d7126959 create test bin for testing system with monitor client 2022-12-03 02:25:34 -05:00
beckerinj
00a89ccb48 implement secret creating and deleting 2022-12-02 02:40:44 -05:00
beckerinj
4e518d90ad made login route for secret usage 2022-12-02 00:22:04 -05:00
beckerinj
0d4bacd892 fix description monitor client 2022-12-01 23:17:03 -05:00
beckerinj
d876634899 add client readme and publish client task 2022-12-01 23:14:07 -05:00
beckerinj
8c8b4ea488 create permissioned delete routes 2022-12-01 22:58:22 -05:00
beckerinj
209f6ebb87 update types package 2022-12-01 01:34:25 -05:00
beckerinj
15faefff93 delete deployment 2022-12-01 01:32:54 -05:00
beckerinj
bf80b34e8e rename monitor_types and update axum version 2022-12-01 00:33:00 -05:00
beckerinj
8c87102116 update target 2022-11-29 01:39:17 -05:00
beckerinj
500e180241 update status enum 2022-11-24 23:39:00 -08:00
beckerinj
20997cf5ac implement get server stats on core 2022-11-24 22:30:03 -08:00
beckerinj
96127b52e6 add docker build tasks 2022-11-24 13:32:50 -08:00
beckerinj
192363bb64 get periphery docker build working 2022-11-24 13:30:08 -08:00
beckerinj
8d7b0625b0 permissions update returns message 2022-11-24 13:05:45 -08:00
beckerinj
47717760b2 move permissions admin check to early return 2022-11-24 13:00:15 -08:00
beckerinj
9c5d2da199 refactor permissions a bit to reduce duplication 2022-11-24 12:59:06 -08:00
beckerinj
556f9d7d96 finish update permissions route for builds and deployments 2022-11-24 12:54:54 -08:00
beckerinj
e5bd6493f5 basic add permissions 2022-11-24 00:54:54 -08:00
beckerinj
7a4cf8be2d delete update "entity_type" 2022-11-23 12:31:17 -08:00
beckerinj
4a7c0008e0 health check? 2022-11-23 13:13:32 -05:00
beckerinj
d15d2ca56b plan to inject arbitrary string secrets defined in periphery config into deployment configuration 2022-11-21 19:35:25 -05:00
beckerinj
a91fd11844 improve permissions code 2022-11-21 00:02:39 -05:00
beckerinj
37d5606eb3 list servers 2022-11-20 23:46:36 -05:00
beckerinj
56269b2fcf add dockerfiles and add version tags to docker builds 2022-11-20 22:24:59 -05:00
beckerinj
abc1ad4356 create for server / deployment 2022-11-20 20:14:16 -05:00
beckerinj
052756140b broadcast updates to clients, and implement create build 2022-11-20 19:52:23 -05:00
beckerinj
6f03bd5827 something 2022-11-16 02:35:43 -05:00
beckerinj
437052b58b start implementing the core websocket 2022-11-15 02:40:57 -05:00
beckerinj
e35d3473c7 add start / end ts, in progress to update 2022-11-14 02:22:13 -05:00
beckerinj
f48703aef7 renome config example folder 2022-11-14 01:05:48 -05:00
beckerinj
393e202102 move config files to dedicated folder 2022-11-14 01:05:26 -05:00
beckerinj
809732e7f3 build route 2022-11-13 23:29:12 -05:00
beckerinj
a7abca2038 implement docker build 2022-11-13 23:19:41 -05:00
beckerinj
475f6774bf implement more periphery routes + periphery client 2022-11-13 02:14:36 -05:00
beckerinj
d4a775025f implement clone build / deployment repo 2022-11-13 00:01:53 -05:00
beckerinj
21c8fac0a4 configs 2022-11-12 19:35:28 -05:00
beckerinj
d0051016fc dont really need the json example files 2022-11-12 01:36:27 -05:00
beckerinj
b495593b59 add accounts route in periphery 2022-11-11 17:40:57 -05:00
beckerinj
aaa8d7e97d get accounts on builder 2022-11-11 17:34:53 -05:00
beckerinj
af126c70a6 add a bunch of secret / config examples. begin builder 2022-11-11 17:15:29 -05:00
beckerinj
1a4e3fa55a pass core config as toml 2022-11-11 16:00:57 -05:00
mbecker20
e740128baf parse toml secrets 2022-11-11 15:08:29 -05:00
beckerinj
fdf5626a93 fmt 2022-11-11 11:51:10 -05:00
beckerinj
40c62234bb write the basic git clone function 2022-11-11 04:11:27 -05:00
beckerinj
b96a6cf7c2 implement container stuff in periphery client 2022-11-09 01:52:21 -05:00
beckerinj
6728507914 retrieve system stats 2022-11-09 01:28:33 -05:00
beckerinj
f679fa2c0c start looking at stats 2022-11-08 02:48:59 -05:00
beckerinj
ced8aa661b begin implement periphery docker functions 2022-11-08 02:37:37 -05:00
beckerinj
b936ff3e36 work on periphery deploy client 2022-11-07 00:26:06 -05:00
beckerinj
841b458f05 start implementing the periphery client 2022-11-06 22:50:01 -05:00
mbecker20
fd13240134 attach userid and is_admin to request 2022-11-06 20:16:17 -05:00
mbecker20
c70917382f implement auth extension 2022-11-06 20:05:51 -05:00
beckerinj
8d2ea8ae87 prog2 2022-11-06 04:57:29 -05:00
beckerinj
d4a91f7240 connect up some auth routing 2022-11-06 04:30:00 -05:00
beckerinj
4ba8c0f380 prog 2022-11-06 04:15:02 -05:00
beckerinj
cb9b482340 start local login implement 2022-11-06 02:57:28 -05:00
beckerinj
797f33e0eb restructure and work on docker client 2022-11-05 22:22:27 -04:00
beckerinj
6a4991354b clients galore 2022-11-05 03:26:58 -04:00
beckerinj
336646c280 pretty much fill out the types 2022-10-10 23:30:53 -04:00
beckerinj
2664ba939e define some types 2022-10-10 20:55:46 -04:00
beckerinj
47c3680d8c add bollard to deps 2022-09-25 17:43:21 -04:00
beckerinj
f281f68a8f scafolding for next 2022-09-25 17:27:06 -04:00
beckerinj
1852ee5216 delete everything for next version 2022-09-25 16:59:59 -04:00
1056 changed files with 174197 additions and 27402 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
rustflags = ["-Wunused-crate-dependencies"]

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:

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
.devcontainer/postCreate.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
cargo install typeshare-cli

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

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

18
.gitignore vendored
View File

@@ -1,7 +1,13 @@
.DS_Store
target
node_modules
build
secrets
bundle.*
monitor_mount
.env*
dist
deno.lock
.env
.env.development
.DS_Store
.idea
/frontend/build
/lib/ts_client/build
.dev

1
.kminclude Normal file
View File

@@ -0,0 +1 @@
.dev

8
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"vadimcn.vscode-lldb",
"denoland.vscode-deno"
]
}

View File

@@ -1,60 +0,0 @@
{
"service": {
"scope": "typescript",
"prefix": "plugin",
"body": [
"import { FastifyInstance } from \"fastify\";",
"import fp from \"fastify-plugin\";",
"",
"const ${1:$TM_FILENAME_BASE} = fp((app: FastifyInstance, _: {}, done: () => void) => {",
"\t${0}",
"\tdone();",
"});",
"",
"export default ${1:$TM_FILENAME_BASE};"
]
},
"schema": {
"scope": "typescript",
"prefix": "schema",
"body": [
"import { FastifyInstance } from \"fastify\";",
"import fp from \"fastify-plugin\";",
"import { Schema } from \"mongoose\";",
"import model from \"../../util/model\";",
"",
"const ${2:$TM_FILENAME_BASE} = fp((app: FastifyInstance, _: {}, done: () => void) => {",
"\tconst schema = new Schema({",
"\t\t${0}",
"\t});",
"\t",
"\tapp.decorate(\"${2:$TM_FILENAME_BASE}\", model(app, \"${1}\", schema));",
"\t",
"\tdone();",
"});",
"",
"export default ${2:$TM_FILENAME_BASE};"
]
},
"get-auth": {
"scope": "typescript",
"prefix": "get-auth",
"body": [
"app.get(\"/${1}\", { onRequest: [app.auth, app.userEnabled] }, async (req, res) => {",
"\t${0}",
"});"
]
},
"post-auth": {
"scope": "typescript",
"prefix": "post-auth",
"body": [
"app.post(\"/${1}\", { onRequest: [app.auth, app.userEnabled] }, async (req, res) => {",
"\t${0}",
"});"
]
}
}

View File

@@ -1,21 +0,0 @@
{
"ink-comp": {
"scope": "typescriptreact,javascriptreact",
"prefix": "ink-comp",
"body": [
"import React from \"react\";",
"import { Box } from \"ink\";",
"",
"const ${1:$TM_FILENAME_BASE} = ({}: {}) => {",
"\treturn (",
"\t\t<Box>",
"\t\t\t${0}",
"\t\t</Box>",
"\t);",
"}",
"",
"export default ${1:$TM_FILENAME_BASE};"
]
},
}

25
.vscode/resolver.code-snippets vendored Normal file
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})",
"}"
]
}
}

View File

@@ -1,3 +0,0 @@
{
"npm.exclude": "**/monitor/**"
}

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 { Component, createContext, useContext } from \"solid-js\";",
"",
"const value = () => {",
"\treturn {};",
"}",
"",
"export type Value = ReturnType<typeof value>;",
"",
"const context = createContext<Value>();",
"",
"export const Provider: Component<{}> = (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
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,179 @@
{
"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": []
},
]
}

6423
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

136
Cargo.toml Normal file
View File

@@ -0,0 +1,136 @@
[workspace]
resolver = "2"
members = [
"bin/*",
"lib/*",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "1.19.3"
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" }
environment = { path = "lib/environment" }
interpolate = { path = "lib/interpolate" }
formatting = { path = "lib/formatting" }
database = { path = "lib/database" }
response = { path = "lib/response" }
command = { path = "lib/command" }
config = { path = "lib/config" }
logger = { path = "lib/logger" }
cache = { path = "lib/cache" }
git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.5.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"
async_timing_util = "1.0.0"
partial_derive2 = "0.4.3"
derive_variants = "1.0.0"
mongo_indexed = "2.0.2"
resolver_api = "3.0.0"
toml_pretty = "1.2.0"
mungos = "3.2.2"
svi = "1.2.0"
# ASYNC
reqwest = { version = "0.12.23", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
tokio = { version = "1.47.1", features = ["full"] }
tokio-util = { version = "0.7.16", features = ["io", "codec"] }
tokio-stream = { version = "0.1.17", features = ["sync"] }
pin-project-lite = "0.2.16"
futures = "0.3.31"
futures-util = "0.3.31"
arc-swap = "1.7.1"
# SERVER
tokio-tungstenite = { version = "0.27.0", features = ["rustls-tls-native-roots"] }
axum-extra = { version = "0.10.1", features = ["typed-header"] }
tower-http = { version = "0.6.6", features = ["fs", "cors"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
# SER/DE
ipnetwork = { version = "0.21.1", features = ["serde"] }
indexmap = { version = "2.11.0", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
strum = { version = "0.27.2", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
serde_yaml_ng = "0.10.0"
serde_json = "1.0.143"
serde_qs = "0.15.0"
toml = "0.9.5"
# ERROR
anyhow = "1.0.99"
thiserror = "2.0.16"
# LOGGING
opentelemetry-otlp = { version = "0.30.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.30.0"
tracing-opentelemetry = "0.31.0"
opentelemetry = "0.30.0"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.45", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.18.0", features = ["v4", "fast-rng", "serde"] }
jsonwebtoken = { version = "9.3.1", default-features = false }
openidconnect = "4.0.1"
urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.17.1"
base64 = "0.22.1"
rustls = "0.23.31"
hmac = "0.12.1"
sha2 = "0.10.9"
rand = "0.9.2"
hex = "0.4.3"
# SYSTEM
portable-pty = "0.9.0"
bollard = "0.19.2"
sysinfo = "0.37.0"
# CLOUD
aws-config = "1.8.5"
aws-sdk-ec2 = "1.161.0"
aws-credential-types = "1.2.5"
## CRON
english-to-cron = "0.1.6"
chrono-tz = "0.10.4"
chrono = "0.4.41"
croner = "3.0.0"
# MISC
async-compression = { version = "0.4.28", features = ["tokio", "gzip"] }
derive_builder = "0.20.2"
comfy-table = "7.1.4"
typeshare = "1.0.4"
octorust = "0.10.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.2"
bytes = "1.10.1"
shell-escape = "0.1.5"

30
bin/binaries.Dockerfile Normal file
View File

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

View File

@@ -0,0 +1,34 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture.
## Uses chef for dependency caching to help speed up back-to-back builds.
FROM lukemathwalker/cargo-chef:latest-rust-1.89.0-bullseye AS chef
WORKDIR /builder
# Plan just the RECIPE to see if things have changed
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /builder/recipe.json recipe.json
# Build JUST dependencies - cached layer
RUN cargo chef cook --release --recipe-path recipe.json
# NOW copy again (this time into builder) and build app
COPY . .
RUN \
cargo build --release --bin core && \
cargo build --release --bin periphery && \
cargo build --release --bin km
# Copy just the binaries to scratch image
FROM scratch
COPY --from=builder /builder/target/release/core /core
COPY --from=builder /builder/target/release/periphery /periphery
COPY --from=builder /builder/target/release/km /km
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

36
bin/cli/Cargo.toml Normal file
View File

@@ -0,0 +1,36 @@
[package]
name = "komodo_cli"
description = "Command line tool for Komodo"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
[[bin]]
name = "km"
path = "src/main.rs"
[dependencies]
# local
environment_file.workspace = true
komodo_client.workspace = true
database.workspace = true
config.workspace = true
logger.workspace = true
# external
futures-util.workspace = true
comfy-table.workspace = true
serde_json.workspace = true
serde_qs.workspace = true
wildcard.workspace = true
tracing.workspace = true
colored.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
chrono.workspace = true
tokio.workspace = true
serde.workspace = true
clap.workspace = true
envy.workspace = true

118
bin/cli/README.md Normal file
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.

24
bin/cli/aio.Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM rust:1.89.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/cli ./bin/cli
# Compile bin
RUN cargo build -p komodo_cli --release
# Copy binaries to distroless base
FROM gcr.io/distroless/cc
COPY --from=builder /builder/target/release/km /usr/local/bin/km
ENV KOMODO_CLI_CONFIG_PATHS="/config"
CMD [ "km" ]
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo CLI"
LABEL org.opencontainers.image.licenses=GPL-3.0

View File

@@ -0,0 +1,135 @@
# Copy Database Utility
Copy the Komodo database contents between running, mongo-compatible databases.
Can be used to move between MongoDB / FerretDB, or upgrade from FerretDB v1 to v2.
```yaml
services:
copy_database:
image: ghcr.io/moghtech/komodo-cli
command: km database copy -y
environment:
KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@source:27017
KOMODO_DATABASE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
KOMODO_CLI_DATABASE_TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@target:27017
KOMODO_CLI_DATABASE_TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
```
## FerretDB v2 Update Guide
Up to Komodo 1.17.5, users who wanted to use Postgres / Sqlite were instructed to deploy FerretDB v1.
Now that v2 is out however, v1 will go largely unsupported. Users are recommended to migrate to v2 for
the best performance and ongoing support / updates, however the internal data structures
have changed and this cannot be done in-place.
Also note that FerretDB v2 no longer supports Sqlite, and only supports
a [customized Postgres distribution](https://docs.ferretdb.io/installation/documentdb/docker/).
Nonetheless, it remains a solid option for hosts which [do not support mongo](https://github.com/moghtech/komodo/issues/59).
Also note, the same basic process outlined below can also be used to move between MongoDB and FerretDB, just replace FerretDB v2
with the database you wish to move to.
### **Step 1**: *Add* the new database to the top of your existing Komodo compose file.
**Don't forget to also add the new volumes.**
```yaml
## In Komodo compose.yaml
services:
postgres2:
# Recommended: Pin to a specific version
# https://github.com/FerretDB/documentdb/pkgs/container/postgres-documentdb
image: ghcr.io/ferretdb/postgres-documentdb
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
# ports:
# - 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${KOMODO_DB_USERNAME}
POSTGRES_PASSWORD: ${KOMODO_DB_PASSWORD}
POSTGRES_DB: postgres # Do not change
ferretdb2:
# Recommended: Pin to a specific version
# https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb
image: ghcr.io/ferretdb/ferretdb
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
depends_on:
- postgres2
# ports:
# - 27017:27017
volumes:
- ferretdb-state:/state
environment:
FERRETDB_POSTGRESQL_URL: postgres://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@postgres2:5432/postgres
...(unchanged)
volumes:
...(unchanged)
postgres-data:
ferretdb-state:
```
### **Step 2**: *Add* the database copy utility to Komodo compose file.
The SOURCE_URI points to the existing database, ie the old FerretDB v1, and it depends
on whether it was deployed using Postgres or Sqlite. The example below uses the Postgres one,
but if you use Sqlite it should just be something like `mongodb://ferretdb:27017`.
```yaml
## In Komodo compose.yaml
services:
...(new database)
copy_database:
image: ghcr.io/moghtech/komodo-cli
command: km database copy -y
environment:
KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN
KOMODO_DATABASE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
KOMODO_CLI_DATABASE_TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb2:27017
KOMODO_CLI_DATABASE_TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo}
...(unchanged)
```
### **Step 3**: *Compose Up* the new additions
Run `docker compose -p komodo --env-file compose.env -f xxxxx.compose.yaml up -d`, filling in the name of your compose.yaml.
This will start up both the old and new database, and copy the data to the new one.
Wait a few moments for the `copy_database` service to finish. When it exits,
confirm the logs show the data was moved successfully, and move on to the next step.
### **Step 4**: Point Komodo Core to the new database
In your Komodo compose.yaml, first *comment out* the `copy_database` service and old ferretdb v1 service/s.
Then update the `core` service environment to point to `ferretdb2`.
```yaml
services:
...
core:
...(unchanged)
environment:
KOMODO_DATABASE_ADDRESS: ferretdb2:27017
KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME}
KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD}
```
### **Step 5**: Final *Compose Up*
Repeat the same `docker compose` command as before to apply the changes, and then try navigating to your Komodo web page.
If it works, congrats, **you are done**. You can clean up the compose file if you would like, removing the old volumes etc.
If it does not work, check the logs for any obvious issues, and if necessary you can undo the previous steps
to go back to using the previous database.

View File

@@ -0,0 +1,29 @@
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG 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 debian:bullseye-slim
WORKDIR /app
## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
COPY --from=x86_64 /km /app/arch/linux/amd64
COPY --from=aarch64 /km /app/arch/linux/arm64
ARG TARGETPLATFORM
RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/arch
ENV KOMODO_CLI_CONFIG_PATHS="/config"
CMD [ "km" ]
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo CLI"
LABEL org.opencontainers.image.licenses=GPL-3.0

4
bin/cli/runfile.toml Normal file
View File

@@ -0,0 +1,4 @@
[install-cli]
alias = "ic"
description = "installs the komodo-cli, available on the command line as 'km'"
cmd = "cargo install --path ."

View File

@@ -0,0 +1,18 @@
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
# This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries
FROM gcr.io/distroless/cc
COPY --from=binaries /km /usr/local/bin/km
ENV KOMODO_CLI_CONFIG_PATHS="/config"
CMD [ "km" ]
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo CLI"
LABEL org.opencontainers.image.licenses=GPL-3.0

View File

@@ -0,0 +1,312 @@
use std::collections::{HashMap, HashSet};
use anyhow::Context;
use colored::Colorize;
use comfy_table::{Attribute, Cell, Color};
use futures_util::{
FutureExt, TryStreamExt, stream::FuturesUnordered,
};
use komodo_client::{
api::read::{
InspectDockerContainer, ListAllDockerContainers, ListServers,
},
entities::{
config::cli::args::container::{
Container, ContainerCommand, InspectContainer,
},
docker::{
self,
container::{ContainerListItem, ContainerStateStatusEnum},
},
},
};
use crate::{
command::{
PrintTable, clamp_sha, matches_wildcards, parse_wildcards,
print_items,
},
config::cli_config,
};
pub async fn handle(container: &Container) -> anyhow::Result<()> {
match &container.command {
None => list_containers(container).await,
Some(ContainerCommand::Inspect(inspect)) => {
inspect_container(inspect).await
}
}
}
async fn list_containers(
Container {
all,
down,
links,
reverse,
containers: names,
images,
networks,
servers,
format,
command: _,
}: &Container,
) -> anyhow::Result<()> {
let client = super::komodo_client().await?;
let (server_map, containers) = tokio::try_join!(
client
.read(ListServers::default())
.map(|res| res.map(|res| res
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllDockerContainers {
servers: Default::default()
}),
)?;
// (Option<Server Name>, Container)
let containers = containers.into_iter().map(|c| {
let server = if let Some(server_id) = c.server_id.as_ref()
&& let Some(server) = server_map.get(server_id)
{
server
} else {
return (None, c);
};
(Some(server.name.as_str()), c)
});
let names = parse_wildcards(names);
let servers = parse_wildcards(servers);
let images = parse_wildcards(images);
let networks = parse_wildcards(networks);
let mut containers = containers
.into_iter()
.filter(|(server_name, c)| {
let state_check = if *all {
true
} else if *down {
!matches!(c.state, ContainerStateStatusEnum::Running)
} else {
matches!(c.state, ContainerStateStatusEnum::Running)
};
let network_check = matches_wildcards(
&networks,
&c.network_mode
.as_deref()
.map(|n| vec![n])
.unwrap_or_default(),
) || matches_wildcards(
&networks,
&c.networks.iter().map(String::as_str).collect::<Vec<_>>(),
);
state_check
&& network_check
&& matches_wildcards(&names, &[c.name.as_str()])
&& matches_wildcards(
&servers,
&server_name
.as_deref()
.map(|i| vec![i])
.unwrap_or_default(),
)
&& matches_wildcards(
&images,
&c.image.as_deref().map(|i| vec![i]).unwrap_or_default(),
)
})
.collect::<Vec<_>>();
containers.sort_by(|(a_s, a), (b_s, b)| {
a.state
.cmp(&b.state)
.then(a.name.cmp(&b.name))
.then(a_s.cmp(b_s))
.then(a.network_mode.cmp(&b.network_mode))
.then(a.image.cmp(&b.image))
});
if *reverse {
containers.reverse();
}
print_items(containers, *format, *links)?;
Ok(())
}
pub async fn inspect_container(
inspect: &InspectContainer,
) -> anyhow::Result<()> {
let client = super::komodo_client().await?;
let (server_map, mut containers) = tokio::try_join!(
client
.read(ListServers::default())
.map(|res| res.map(|res| res
.into_iter()
.map(|s| (s.id.clone(), s))
.collect::<HashMap<_, _>>())),
client.read(ListAllDockerContainers {
servers: Default::default()
}),
)?;
containers.iter_mut().for_each(|c| {
let Some(server_id) = c.server_id.as_ref() else {
return;
};
let Some(server) = server_map.get(server_id) else {
c.server_id = Some(String::from("Unknown"));
return;
};
c.server_id = Some(server.name.clone());
});
let names = [inspect.container.to_string()];
let names = parse_wildcards(&names);
let servers = parse_wildcards(&inspect.servers);
let mut containers = containers
.into_iter()
.filter(|c| {
matches_wildcards(&names, &[c.name.as_str()])
&& matches_wildcards(
&servers,
&c.server_id
.as_deref()
.map(|i| vec![i])
.unwrap_or_default(),
)
})
.map(|c| async move {
client
.read(InspectDockerContainer {
container: c.name,
server: c.server_id.context("No server...")?,
})
.await
})
.collect::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await?;
containers.sort_by(|a, b| a.name.cmp(&b.name));
match containers.len() {
0 => {
println!(
"{}: Did not find any containers matching '{}'",
"INFO".green(),
inspect.container.bold()
);
}
1 => {
println!("{}", serialize_container(inspect, &containers[0])?);
}
_ => {
let containers = containers
.iter()
.map(|c| serialize_container(inspect, c))
.collect::<anyhow::Result<Vec<_>>>()?
.join("\n");
println!("{containers}");
}
}
Ok(())
}
fn serialize_container(
inspect: &InspectContainer,
container: &docker::container::Container,
) -> anyhow::Result<String> {
let res = if inspect.state {
serde_json::to_string_pretty(&container.state)
} else if inspect.mounts {
serde_json::to_string_pretty(&container.mounts)
} else if inspect.host_config {
serde_json::to_string_pretty(&container.host_config)
} else if inspect.config {
serde_json::to_string_pretty(&container.config)
} else if inspect.network_settings {
serde_json::to_string_pretty(&container.network_settings)
} else {
serde_json::to_string_pretty(container)
}
.context("Failed to serialize items to JSON")?;
Ok(res)
}
// (Option<Server Name>, Container)
impl PrintTable for (Option<&'_ str>, ContainerListItem) {
fn header(links: bool) -> &'static [&'static str] {
if links {
&[
"Container",
"State",
"Server",
"Ports",
"Networks",
"Image",
"Link",
]
} else {
&["Container", "State", "Server", "Ports", "Networks", "Image"]
}
}
fn row(self, links: bool) -> Vec<Cell> {
let color = match self.1.state {
ContainerStateStatusEnum::Running => Color::Green,
ContainerStateStatusEnum::Paused => Color::DarkYellow,
ContainerStateStatusEnum::Empty => Color::Grey,
_ => Color::Red,
};
let mut networks = HashSet::new();
if let Some(network) = self.1.network_mode {
networks.insert(network);
}
for network in self.1.networks {
networks.insert(network);
}
let mut networks = networks.into_iter().collect::<Vec<_>>();
networks.sort();
let mut ports = self
.1
.ports
.into_iter()
.flat_map(|p| p.public_port.map(|p| p.to_string()))
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
ports.sort();
let ports = if ports.is_empty() {
Cell::new("")
} else {
Cell::new(format!(":{}", ports.join(", :")))
};
let image = self.1.image.as_deref().unwrap_or("Unknown");
let mut res = vec![
Cell::new(self.1.name.clone()).add_attribute(Attribute::Bold),
Cell::new(self.1.state.to_string())
.fg(color)
.add_attribute(Attribute::Bold),
Cell::new(self.0.unwrap_or("Unknown")),
ports,
Cell::new(networks.join(", ")),
Cell::new(clamp_sha(image)),
];
if !links {
return res;
}
let link = if let Some(server_id) = self.1.server_id {
format!(
"{}/servers/{server_id}/container/{}",
cli_config().host,
self.1.name
)
} else {
String::new()
};
res.push(Cell::new(link));
res
}
}

View File

@@ -0,0 +1,320 @@
use std::path::Path;
use anyhow::Context;
use colored::Colorize;
use komodo_client::entities::{
config::cli::args::database::DatabaseCommand, optional_string,
};
use crate::{command::sanitize_uri, config::cli_config};
pub async fn handle(command: &DatabaseCommand) -> anyhow::Result<()> {
match command {
DatabaseCommand::Backup { yes, .. } => backup(*yes).await,
DatabaseCommand::Restore {
restore_folder,
index,
yes,
..
} => restore(restore_folder.as_deref(), *index, *yes).await,
DatabaseCommand::Prune { yes, .. } => prune(*yes).await,
DatabaseCommand::Copy { yes, index, .. } => {
copy(*index, *yes).await
}
}
}
async fn backup(yes: bool) -> anyhow::Result<()> {
let config = cli_config();
println!(
"\n🦎 {} Database {} Utility 🦎",
"Komodo".bold(),
"Backup".green().bold()
);
println!(
"\n{}\n",
" - Backup all database contents to gzip compressed files."
.dimmed()
);
if let Some(uri) = optional_string(&config.database.uri) {
println!("{}: {}", " - Source URI".dimmed(), sanitize_uri(&uri));
}
if let Some(address) = optional_string(&config.database.address) {
println!("{}: {address}", " - Source Address".dimmed());
}
if let Some(username) = optional_string(&config.database.username) {
println!("{}: {username}", " - Source Username".dimmed());
}
println!(
"{}: {}\n",
" - Source Db Name".dimmed(),
config.database.db_name,
);
println!(
"{}: {:?}",
" - Backups Folder".dimmed(),
config.backups_folder
);
if config.max_backups == 0 {
println!(
"{}{}",
" - Backup pruning".dimmed(),
"disabled".red().dimmed()
);
} else {
println!("{}: {}", " - Max Backups".dimmed(), config.max_backups);
}
crate::command::wait_for_enter("start backup", yes)?;
let db = database::init(&config.database).await?;
database::utils::backup(&db, &config.backups_folder).await?;
// Early return if backup pruning disabled
if config.max_backups == 0 {
return Ok(());
}
// Know that new backup was taken successfully at this point,
// safe to prune old backup folders
prune_inner().await
}
async fn restore(
restore_folder: Option<&Path>,
index: bool,
yes: bool,
) -> anyhow::Result<()> {
let config = cli_config();
println!(
"\n🦎 {} Database {} Utility 🦎",
"Komodo".bold(),
"Restore".purple().bold()
);
println!(
"\n{}\n",
" - Restores database contents from gzip compressed files."
.dimmed()
);
if let Some(uri) = optional_string(&config.database_target.uri) {
println!("{}: {}", " - Target URI".dimmed(), sanitize_uri(&uri));
}
if let Some(address) =
optional_string(&config.database_target.address)
{
println!("{}: {address}", " - Target Address".dimmed());
}
if let Some(username) =
optional_string(&config.database_target.username)
{
println!("{}: {username}", " - Target Username".dimmed());
}
println!(
"{}: {}",
" - Target Db Name".dimmed(),
config.database_target.db_name,
);
if !index {
println!(
"{}: {}",
" - Target Db Indexing".dimmed(),
"DISABLED".red(),
);
}
println!(
"\n{}: {:?}",
" - Backups Folder".dimmed(),
config.backups_folder
);
if let Some(restore_folder) = restore_folder {
println!("{}: {restore_folder:?}", " - Restore Folder".dimmed());
}
crate::command::wait_for_enter("start restore", yes)?;
let db = if index {
database::Client::new(&config.database_target).await?.db
} else {
database::init(&config.database_target).await?
};
database::utils::restore(
&db,
&config.backups_folder,
restore_folder,
)
.await
}
async fn prune(yes: bool) -> anyhow::Result<()> {
let config = cli_config();
println!(
"\n🦎 {} Database {} Utility 🦎",
"Komodo".bold(),
"Backup Prune".cyan().bold()
);
println!(
"\n{}\n",
" - Prunes database backup folders when greater than the configured amount."
.dimmed()
);
println!(
"{}: {:?}",
" - Backups Folder".dimmed(),
config.backups_folder
);
if config.max_backups == 0 {
println!(
"{}{}",
" - Backup pruning".dimmed(),
"disabled".red().dimmed()
);
} else {
println!("{}: {}", " - Max Backups".dimmed(), config.max_backups);
}
// Early return if backup pruning disabled
if config.max_backups == 0 {
info!(
"Backup pruning is disabled, enabled using 'max_backups' (KOMODO_CLI_MAX_BACKUPS)"
);
return Ok(());
}
crate::command::wait_for_enter("start backup prune", yes)?;
prune_inner().await
}
async fn prune_inner() -> anyhow::Result<()> {
let config = cli_config();
let mut backups_dir =
match tokio::fs::read_dir(&config.backups_folder)
.await
.context("Failed to read backups folder for prune")
{
Ok(backups_dir) => backups_dir,
Err(e) => {
warn!("{e:#}");
return Ok(());
}
};
let mut backup_folders = Vec::new();
loop {
match backups_dir.next_entry().await {
Ok(Some(entry)) => {
let Ok(metadata) = entry.metadata().await else {
continue;
};
if metadata.is_dir() {
backup_folders.push(entry.path());
}
}
Ok(None) => break,
Err(_) => {
continue;
}
}
}
// Ordered from oldest -> newest
backup_folders.sort();
let max_backups = config.max_backups as usize;
let backup_folders_len = backup_folders.len();
// Early return if under the backup count threshold
if backup_folders_len <= max_backups {
info!("No backups to prune");
return Ok(());
}
let to_delete =
&backup_folders[..(backup_folders_len - max_backups)];
info!("Pruning old backups: {to_delete:?}");
for path in to_delete {
if let Err(e) =
tokio::fs::remove_dir_all(path).await.with_context(|| {
format!("Failed to delete backup folder at {path:?}")
})
{
warn!("{e:#}");
}
}
Ok(())
}
async fn copy(index: bool, yes: bool) -> anyhow::Result<()> {
let config = cli_config();
println!(
"\n🦎 {} Database {} Utility 🦎",
"Komodo".bold(),
"Copy".blue().bold()
);
println!(
"\n{}\n",
" - Copies database contents to another database.".dimmed()
);
if let Some(uri) = optional_string(&config.database.uri) {
println!("{}: {}", " - Source URI".dimmed(), sanitize_uri(&uri));
}
if let Some(address) = optional_string(&config.database.address) {
println!("{}: {address}", " - Source Address".dimmed());
}
if let Some(username) = optional_string(&config.database.username) {
println!("{}: {username}", " - Source Username".dimmed());
}
println!(
"{}: {}\n",
" - Source Db Name".dimmed(),
config.database.db_name,
);
if let Some(uri) = optional_string(&config.database_target.uri) {
println!("{}: {}", " - Target URI".dimmed(), sanitize_uri(&uri));
}
if let Some(address) =
optional_string(&config.database_target.address)
{
println!("{}: {address}", " - Target Address".dimmed());
}
if let Some(username) =
optional_string(&config.database_target.username)
{
println!("{}: {username}", " - Target Username".dimmed());
}
println!(
"{}: {}",
" - Target Db Name".dimmed(),
config.database_target.db_name,
);
if !index {
println!(
"{}: {}",
" - Target Db Indexing".dimmed(),
"DISABLED".red(),
);
}
crate::command::wait_for_enter("start copy", yes)?;
let source_db = database::init(&config.database).await?;
let target_db = if index {
database::Client::new(&config.database_target).await?.db
} else {
database::init(&config.database_target).await?
};
database::utils::copy(&source_db, &target_db).await
}

View File

@@ -0,0 +1,572 @@
use std::time::Duration;
use colored::Colorize;
use futures_util::{StreamExt, stream::FuturesUnordered};
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchExecutionResponseItem, Execution,
},
entities::{resource_link, update::Update},
};
use crate::config::cli_config;
enum ExecutionResult {
Single(Box<Update>),
Batch(BatchExecutionResponse),
}
pub async fn handle(
execution: &Execution,
yes: bool,
) -> 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::BatchPullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RestartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::UnpauseStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StopStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunStackService(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::TestAlerter(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::SendAlert(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::ClearRepoCache(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BackupCoreDatabase(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::GlobalAutoUpdate(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
}
super::wait_for_enter("run execution", yes)?;
info!("Running Execution...");
let client = super::komodo_client().await?;
let res = match execution.clone() {
Execution::RunAction(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunAction(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::RunProcedure(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunProcedure(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::RunBuild(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchRunBuild(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::CancelBuild(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::Deploy(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeploy(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::PullDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyDeployment(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDestroyDeployment(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::CloneRepo(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchCloneRepo(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::PullRepo(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchPullRepo(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::BuildRepo(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchBuildRepo(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::CancelRepoBuild(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyContainer(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StartAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopAllContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneContainers(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteNetwork(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneNetworks(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteImage(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneImages(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeleteVolume(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneVolumes(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneDockerBuilders(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneBuildx(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PruneSystem(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RunSync(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::CommitSync(request) => client
.write(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DeployStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeployStack(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::DeployStackIfChanged(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDeployStackIfChanged(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::PullStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchPullStack(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::StartStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::RestartStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::PauseStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::UnpauseStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::StopStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::DestroyStack(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchDestroyStack(request) => {
client.execute(request).await.map(ExecutionResult::Batch)
}
Execution::RunStackService(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::TestAlerter(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::SendAlert(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::ClearRepoCache(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BackupCoreDatabase(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::GlobalAutoUpdate(request) => client
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::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)) => {
poll_update_until_complete(&update).await
}
Ok(ExecutionResult::Batch(updates)) => {
let mut handles = updates
.iter()
.map(|update| async move {
match update {
BatchExecutionResponseItem::Ok(update) => {
poll_update_until_complete(update).await
}
BatchExecutionResponseItem::Err(e) => {
error!("{e:#?}");
Ok(())
}
}
})
.collect::<FuturesUnordered<_>>();
while let Some(res) = handles.next().await {
match res {
Ok(()) => {}
Err(e) => {
error!("{e:#?}");
}
}
}
Ok(())
}
Err(e) => {
error!("{e:#?}");
Ok(())
}
}
}
async fn poll_update_until_complete(
update: &Update,
) -> anyhow::Result<()> {
let link = if update.id.is_empty() {
let (resource_type, id) = update.target.extract_variant_id();
resource_link(&cli_config().host, resource_type, id)
} else {
format!("{}/updates/{}", cli_config().host, update.id)
};
println!("Link: '{}'", link.bold());
let client = super::komodo_client().await?;
let timer = tokio::time::Instant::now();
let update = client.poll_update_until_complete(&update.id).await?;
if update.success {
println!(
"FINISHED in {}: {}",
format!("{:.1?}", timer.elapsed()).bold(),
"EXECUTION SUCCESSFUL".green(),
);
} else {
eprintln!(
"FINISHED in {}: {}",
format!("{:.1?}", timer.elapsed()).bold(),
"EXECUTION FAILED".red(),
);
}
Ok(())
}

1171
bin/cli/src/command/list.rs Normal file

File diff suppressed because it is too large Load Diff

181
bin/cli/src/command/mod.rs Normal file
View File

@@ -0,0 +1,181 @@
use std::io::Read;
use anyhow::{Context, anyhow};
use chrono::TimeZone;
use colored::Colorize;
use comfy_table::{Attribute, Cell, Table};
use komodo_client::{
KomodoClient,
entities::config::cli::{CliTableBorders, args::CliFormat},
};
use serde::Serialize;
use tokio::sync::OnceCell;
use wildcard::Wildcard;
use crate::config::cli_config;
pub mod container;
pub mod database;
pub mod execute;
pub mod list;
pub mod update;
async fn komodo_client() -> anyhow::Result<&'static KomodoClient> {
static KOMODO_CLIENT: OnceCell<KomodoClient> =
OnceCell::const_new();
KOMODO_CLIENT
.get_or_try_init(|| async {
let config = cli_config();
let (Some(key), Some(secret)) =
(&config.cli_key, &config.cli_secret)
else {
return Err(anyhow!(
"Must provide both cli_key and cli_secret"
));
};
KomodoClient::new(&config.host, key, secret)
.with_healthcheck()
.await
})
.await
}
fn wait_for_enter(
press_enter_to: &str,
skip: bool,
) -> anyhow::Result<()> {
if skip {
println!();
return Ok(());
}
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(())
}
/// Sanitizes uris of the form:
/// `protocol://username:password@address`
fn sanitize_uri(uri: &str) -> String {
// protocol: `mongodb`
// credentials_address: `username:password@address`
let Some((protocol, credentials_address)) = uri.split_once("://")
else {
// If no protocol, return as-is
return uri.to_string();
};
// credentials: `username:password`
let Some((credentials, address)) =
credentials_address.split_once('@')
else {
// If no credentials, return as-is
return uri.to_string();
};
match credentials.split_once(':') {
Some((username, _)) => {
format!("{protocol}://{username}:*****@{address}")
}
None => {
format!("{protocol}://*****@{address}")
}
}
}
fn print_items<T: PrintTable + Serialize>(
items: Vec<T>,
format: CliFormat,
links: bool,
) -> anyhow::Result<()> {
match format {
CliFormat::Table => {
let mut table = Table::new();
let preset = {
use comfy_table::presets::*;
match cli_config().table_borders {
None | Some(CliTableBorders::Horizontal) => {
UTF8_HORIZONTAL_ONLY
}
Some(CliTableBorders::Vertical) => UTF8_FULL_CONDENSED,
Some(CliTableBorders::Inside) => UTF8_NO_BORDERS,
Some(CliTableBorders::Outside) => UTF8_BORDERS_ONLY,
Some(CliTableBorders::All) => UTF8_FULL,
}
};
table.load_preset(preset).set_header(
T::header(links)
.iter()
.map(|h| Cell::new(h).add_attribute(Attribute::Bold)),
);
for item in items {
table.add_row(item.row(links));
}
println!("{table}");
}
CliFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&items)
.context("Failed to serialize items to JSON")?
);
}
}
Ok(())
}
trait PrintTable {
fn header(links: bool) -> &'static [&'static str];
fn row(self, links: bool) -> Vec<Cell>;
}
fn parse_wildcards(items: &[String]) -> Vec<Wildcard<'_>> {
items
.iter()
.flat_map(|i| {
Wildcard::new(i.as_bytes()).inspect_err(|e| {
warn!("Failed to parse wildcard: {i} | {e:?}")
})
})
.collect::<Vec<_>>()
}
fn matches_wildcards(
wildcards: &[Wildcard<'_>],
items: &[&str],
) -> bool {
if wildcards.is_empty() {
return true;
}
items.iter().any(|item| {
wildcards.iter().any(|wc| wc.is_match(item.as_bytes()))
})
}
fn format_timetamp(ts: i64) -> anyhow::Result<String> {
let ts = chrono::Local
.timestamp_millis_opt(ts)
.single()
.context("Invalid ts")?
.format("%m/%d %H:%M:%S")
.to_string();
Ok(ts)
}
fn clamp_sha(maybe_sha: &str) -> String {
if maybe_sha.starts_with("sha256:") {
maybe_sha[0..20].to_string() + "..."
} else {
maybe_sha.to_string()
}
}
// fn text_link(link: &str, text: &str) -> String {
// format!("\x1b]8;;{link}\x07{text}\x1b]8;;\x07")
// }

View File

@@ -0,0 +1,43 @@
use komodo_client::entities::{
build::PartialBuildConfig,
config::cli::args::update::UpdateCommand,
deployment::PartialDeploymentConfig, repo::PartialRepoConfig,
server::PartialServerConfig, stack::PartialStackConfig,
sync::PartialResourceSyncConfig,
};
mod resource;
mod user;
mod variable;
pub async fn handle(command: &UpdateCommand) -> anyhow::Result<()> {
match command {
UpdateCommand::Build(update) => {
resource::update::<PartialBuildConfig>(update).await
}
UpdateCommand::Deployment(update) => {
resource::update::<PartialDeploymentConfig>(update).await
}
UpdateCommand::Repo(update) => {
resource::update::<PartialRepoConfig>(update).await
}
UpdateCommand::Server(update) => {
resource::update::<PartialServerConfig>(update).await
}
UpdateCommand::Stack(update) => {
resource::update::<PartialStackConfig>(update).await
}
UpdateCommand::Sync(update) => {
resource::update::<PartialResourceSyncConfig>(update).await
}
UpdateCommand::Variable {
name,
value,
secret,
yes,
} => variable::update(name, value, *secret, *yes).await,
UpdateCommand::User { username, command } => {
user::update(username, command).await
}
}
}

View File

@@ -0,0 +1,152 @@
use anyhow::Context;
use colored::Colorize;
use komodo_client::{
api::write::{
UpdateBuild, UpdateDeployment, UpdateRepo, UpdateResourceSync,
UpdateServer, UpdateStack,
},
entities::{
build::PartialBuildConfig,
config::cli::args::update::UpdateResource,
deployment::PartialDeploymentConfig, repo::PartialRepoConfig,
server::PartialServerConfig, stack::PartialStackConfig,
sync::PartialResourceSyncConfig,
},
};
use serde::{Serialize, de::DeserializeOwned};
pub async fn update<
T: std::fmt::Debug + Serialize + DeserializeOwned + ResourceUpdate,
>(
UpdateResource {
resource,
update,
yes,
}: &UpdateResource,
) -> anyhow::Result<()> {
println!("\n{}: Update {}\n", "Mode".dimmed(), T::resource_type());
println!(" - {}: {resource}", "Name".dimmed());
let config = serde_qs::from_str::<T>(update)
.context("Failed to deserialize config")?;
match serde_json::to_string_pretty(&config) {
Ok(config) => {
println!(" - {}: {config}", "Update".dimmed());
}
Err(_) => {
println!(" - {}: {config:#?}", "Update".dimmed());
}
}
crate::command::wait_for_enter("update resource", *yes)?;
config.apply(resource).await
}
pub trait ResourceUpdate {
fn resource_type() -> &'static str;
async fn apply(self, resource: &str) -> anyhow::Result<()>;
}
impl ResourceUpdate for PartialBuildConfig {
fn resource_type() -> &'static str {
"Build"
}
async fn apply(self, resource: &str) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
client
.write(UpdateBuild {
id: resource.to_string(),
config: self,
})
.await
.context("Failed to update build config")?;
Ok(())
}
}
impl ResourceUpdate for PartialDeploymentConfig {
fn resource_type() -> &'static str {
"Deployment"
}
async fn apply(self, resource: &str) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
client
.write(UpdateDeployment {
id: resource.to_string(),
config: self,
})
.await
.context("Failed to update deployment config")?;
Ok(())
}
}
impl ResourceUpdate for PartialRepoConfig {
fn resource_type() -> &'static str {
"Repo"
}
async fn apply(self, resource: &str) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
client
.write(UpdateRepo {
id: resource.to_string(),
config: self,
})
.await
.context("Failed to update repo config")?;
Ok(())
}
}
impl ResourceUpdate for PartialServerConfig {
fn resource_type() -> &'static str {
"Server"
}
async fn apply(self, resource: &str) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
client
.write(UpdateServer {
id: resource.to_string(),
config: self,
})
.await
.context("Failed to update server config")?;
Ok(())
}
}
impl ResourceUpdate for PartialStackConfig {
fn resource_type() -> &'static str {
"Stack"
}
async fn apply(self, resource: &str) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
client
.write(UpdateStack {
id: resource.to_string(),
config: self,
})
.await
.context("Failed to update stack config")?;
Ok(())
}
}
impl ResourceUpdate for PartialResourceSyncConfig {
fn resource_type() -> &'static str {
"Sync"
}
async fn apply(self, resource: &str) -> anyhow::Result<()> {
let client = crate::command::komodo_client().await?;
client
.write(UpdateResourceSync {
id: resource.to_string(),
config: self,
})
.await
.context("Failed to update sync config")?;
Ok(())
}
}

View File

@@ -0,0 +1,122 @@
use anyhow::Context;
use colored::Colorize;
use database::mungos::mongodb::bson::doc;
use komodo_client::entities::{
config::{
cli::args::{CliEnabled, update::UpdateUserCommand},
empty_or_redacted,
},
optional_string,
};
use crate::{command::sanitize_uri, config::cli_config};
pub async fn update(
username: &str,
command: &UpdateUserCommand,
) -> anyhow::Result<()> {
match command {
UpdateUserCommand::Password {
password,
unsanitized,
yes,
} => {
update_password(username, password, *unsanitized, *yes).await
}
UpdateUserCommand::SuperAdmin { enabled, yes } => {
update_super_admin(username, *enabled, *yes).await
}
}
}
async fn update_password(
username: &str,
password: &str,
unsanitized: bool,
yes: bool,
) -> anyhow::Result<()> {
println!("\n{}: Update Password\n", "Mode".dimmed());
println!(" - {}: {username}", "Username".dimmed());
if unsanitized {
println!(" - {}: {password}", "Password".dimmed());
} else {
println!(
" - {}: {}",
"Password".dimmed(),
empty_or_redacted(password)
);
}
crate::command::wait_for_enter("update password", yes)?;
info!("Updating password...");
let db = database::Client::new(&cli_config().database).await?;
let user = db
.users
.find_one(doc! { "username": username })
.await
.context("Failed to query database for user")?
.context("No user found with given username")?;
db.set_user_password(&user, password).await?;
info!("Password updated ✅");
Ok(())
}
async fn update_super_admin(
username: &str,
super_admin: CliEnabled,
yes: bool,
) -> anyhow::Result<()> {
let config = cli_config();
println!("\n{}: Update Super Admin\n", "Mode".dimmed());
println!(" - {}: {username}", "Username".dimmed());
println!(" - {}: {super_admin}\n", "Super Admin".dimmed());
if let Some(uri) = optional_string(&config.database.uri) {
println!("{}: {}", " - Source URI".dimmed(), sanitize_uri(&uri));
}
if let Some(address) = optional_string(&config.database.address) {
println!("{}: {address}", " - Source Address".dimmed());
}
if let Some(username) = optional_string(&config.database.username) {
println!("{}: {username}", " - Source Username".dimmed());
}
println!(
"{}: {}",
" - Source Db Name".dimmed(),
config.database.db_name,
);
crate::command::wait_for_enter("update super admin", yes)?;
info!("Updating super admin...");
let db = database::Client::new(&config.database).await?;
// Make sure the user exists first before saying it is successful.
let user = db
.users
.find_one(doc! { "username": username })
.await
.context("Failed to query database for user")?
.context("No user found with given username")?;
let super_admin: bool = super_admin.into();
db.users
.update_one(
doc! { "username": user.username },
doc! { "$set": { "super_admin": super_admin } },
)
.await
.context("Failed to update user super admin on db")?;
info!("Super admin updated ✅");
Ok(())
}

View File

@@ -0,0 +1,70 @@
use anyhow::Context;
use colored::Colorize;
use komodo_client::api::{
read::GetVariable,
write::{
CreateVariable, UpdateVariableIsSecret, UpdateVariableValue,
},
};
pub async fn update(
name: &str,
value: &str,
secret: Option<bool>,
yes: bool,
) -> anyhow::Result<()> {
println!("\n{}: Update Variable\n", "Mode".dimmed());
println!(" - {}: {name}", "Name".dimmed());
println!(" - {}: {value}", "Value".dimmed());
if let Some(secret) = secret {
println!(" - {}: {secret}", "Is Secret".dimmed());
}
crate::command::wait_for_enter("update variable", yes)?;
let client = crate::command::komodo_client().await?;
let Ok(existing) = client
.read(GetVariable {
name: name.to_string(),
})
.await
else {
// Create the variable
client
.write(CreateVariable {
name: name.to_string(),
value: value.to_string(),
is_secret: secret.unwrap_or_default(),
description: Default::default(),
})
.await
.context("Failed to create variable")?;
info!("Variable created ✅");
return Ok(());
};
client
.write(UpdateVariableValue {
name: name.to_string(),
value: value.to_string(),
})
.await
.context("Failed to update variable 'value'")?;
info!("Variable 'value' updated ✅");
let Some(secret) = secret else { return Ok(()) };
if secret != existing.is_secret {
client
.write(UpdateVariableIsSecret {
name: name.to_string(),
is_secret: secret,
})
.await
.context("Failed to update variable 'is_secret'")?;
info!("Variable 'is_secret' updated to {secret} ✅");
}
Ok(())
}

274
bin/cli/src/config.rs Normal file
View File

@@ -0,0 +1,274 @@
use std::{path::PathBuf, sync::OnceLock};
use anyhow::Context;
use clap::Parser;
use colored::Colorize;
use environment_file::maybe_read_item_from_file;
use komodo_client::entities::{
config::{
DatabaseConfig,
cli::{
CliConfig, Env,
args::{CliArgs, Command, Execute, database::DatabaseCommand},
},
},
logger::LogConfig,
};
pub fn cli_args() -> &'static CliArgs {
static CLI_ARGS: OnceLock<CliArgs> = OnceLock::new();
CLI_ARGS.get_or_init(CliArgs::parse)
}
pub fn cli_env() -> &'static Env {
static CLI_ARGS: OnceLock<Env> = OnceLock::new();
CLI_ARGS.get_or_init(|| {
match envy::from_env()
.context("Failed to parse Komodo CLI environment")
{
Ok(env) => env,
Err(e) => {
panic!("{e:?}");
}
}
})
}
pub fn cli_config() -> &'static CliConfig {
static CLI_CONFIG: OnceLock<CliConfig> = OnceLock::new();
CLI_CONFIG.get_or_init(|| {
let args = cli_args();
let env = cli_env().clone();
let config_paths = args
.config_path
.clone()
.unwrap_or(env.komodo_cli_config_paths);
let debug_startup =
args.debug_startup.unwrap_or(env.komodo_cli_debug_startup);
if debug_startup {
println!(
"{}: Komodo CLI version: {}",
"DEBUG".cyan(),
env!("CARGO_PKG_VERSION").blue().bold()
);
println!(
"{}: {}: {config_paths:?}",
"DEBUG".cyan(),
"Config Paths".dimmed(),
);
}
let config_keywords = args
.config_keyword
.clone()
.unwrap_or(env.komodo_cli_config_keywords);
let config_keywords = config_keywords
.iter()
.map(String::as_str)
.collect::<Vec<_>>();
if debug_startup {
println!(
"{}: {}: {config_keywords:?}",
"DEBUG".cyan(),
"Config File Keywords".dimmed(),
);
}
let mut unparsed_config = (config::ConfigLoader {
paths: &config_paths
.iter()
.map(PathBuf::as_path)
.collect::<Vec<_>>(),
match_wildcards: &config_keywords,
include_file_name: ".kminclude",
merge_nested: env.komodo_cli_merge_nested_config,
extend_array: env.komodo_cli_extend_config_arrays,
debug_print: debug_startup,
})
.load::<serde_json::Map<String, serde_json::Value>>()
.expect("failed at parsing config from paths");
let init_parsed_config = serde_json::from_value::<CliConfig>(
serde_json::Value::Object(unparsed_config.clone()),
)
.context("Failed to parse config")
.unwrap();
let (host, key, secret) = match &args.command {
Command::Execute(Execute {
host, key, secret, ..
}) => (host.clone(), key.clone(), secret.clone()),
_ => (None, None, None),
};
let backups_folder = match &args.command {
Command::Database {
command: DatabaseCommand::Backup { backups_folder, .. },
} => backups_folder.clone(),
Command::Database {
command: DatabaseCommand::Restore { backups_folder, .. },
} => backups_folder.clone(),
_ => None,
};
let (uri, address, username, password, db_name) =
match &args.command {
Command::Database {
command:
DatabaseCommand::Copy {
uri,
address,
username,
password,
db_name,
..
},
} => (
uri.clone(),
address.clone(),
username.clone(),
password.clone(),
db_name.clone(),
),
_ => (None, None, None, None, None),
};
let profile = args
.profile
.as_ref()
.or(init_parsed_config.default_profile.as_ref());
let unparsed_config = if let Some(profile) = profile
&& !profile.is_empty()
{
// Find the profile config,
// then merge it with the Default config.
let serde_json::Value::Array(profiles) = unparsed_config
.remove("profile")
.context("Config has no profiles, but a profile is required")
.unwrap()
else {
panic!("`config.profile` is not array");
};
let Some(profile_config) = profiles.into_iter().find(|p| {
let Ok(parsed) =
serde_json::from_value::<CliConfig>(p.clone())
else {
return false;
};
&parsed.config_profile == profile
|| parsed
.config_aliases
.iter()
.any(|alias| alias == profile)
}) else {
panic!("No profile matching '{profile}' was found.");
};
let serde_json::Value::Object(profile_config) = profile_config
else {
panic!("Profile config is not Object type.");
};
config::merge_config(
unparsed_config,
profile_config.clone(),
env.komodo_cli_merge_nested_config,
env.komodo_cli_extend_config_arrays,
)
.unwrap_or(profile_config)
} else {
unparsed_config
};
let config = serde_json::from_value::<CliConfig>(
serde_json::Value::Object(unparsed_config),
)
.context("Failed to parse final config")
.unwrap();
let config_profile = if config.config_profile.is_empty() {
String::from("None")
} else {
config.config_profile
};
CliConfig {
config_profile,
config_aliases: config.config_aliases,
default_profile: config.default_profile,
table_borders: env
.komodo_cli_table_borders
.or(config.table_borders),
host: host
.or(env.komodo_cli_host)
.or(env.komodo_host)
.unwrap_or(config.host),
cli_key: key.or(env.komodo_cli_key).or(config.cli_key),
cli_secret: secret
.or(env.komodo_cli_secret)
.or(config.cli_secret),
backups_folder: backups_folder
.or(env.komodo_cli_backups_folder)
.unwrap_or(config.backups_folder),
max_backups: env
.komodo_cli_max_backups
.unwrap_or(config.max_backups),
database_target: DatabaseConfig {
uri: uri
.or(env.komodo_cli_database_target_uri)
.unwrap_or(config.database_target.uri),
address: address
.or(env.komodo_cli_database_target_address)
.unwrap_or(config.database_target.address),
username: username
.or(env.komodo_cli_database_target_username)
.unwrap_or(config.database_target.username),
password: password
.or(env.komodo_cli_database_target_password)
.unwrap_or(config.database_target.password),
db_name: db_name
.or(env.komodo_cli_database_target_db_name)
.unwrap_or(config.database_target.db_name),
app_name: config.database_target.app_name,
},
database: DatabaseConfig {
uri: maybe_read_item_from_file(
env.komodo_database_uri_file,
env.komodo_database_uri,
)
.unwrap_or(config.database.uri),
address: env
.komodo_database_address
.unwrap_or(config.database.address),
username: maybe_read_item_from_file(
env.komodo_database_username_file,
env.komodo_database_username,
)
.unwrap_or(config.database.username),
password: maybe_read_item_from_file(
env.komodo_database_password_file,
env.komodo_database_password,
)
.unwrap_or(config.database.password),
db_name: env
.komodo_database_db_name
.unwrap_or(config.database.db_name),
app_name: config.database.app_name,
},
cli_logging: LogConfig {
level: env
.komodo_cli_logging_level
.unwrap_or(config.cli_logging.level),
stdio: env
.komodo_cli_logging_stdio
.unwrap_or(config.cli_logging.stdio),
pretty: env
.komodo_cli_logging_pretty
.unwrap_or(config.cli_logging.pretty),
location: false,
otlp_endpoint: env
.komodo_cli_logging_otlp_endpoint
.unwrap_or(config.cli_logging.otlp_endpoint),
opentelemetry_service_name: env
.komodo_cli_logging_opentelemetry_service_name
.unwrap_or(config.cli_logging.opentelemetry_service_name),
},
profile: config.profile,
}
})
}

72
bin/cli/src/main.rs Normal file
View File

@@ -0,0 +1,72 @@
#[macro_use]
extern crate tracing;
use anyhow::Context;
use komodo_client::entities::config::cli::args;
use crate::config::cli_config;
mod command;
mod config;
async fn app() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
logger::init(&config::cli_config().cli_logging)?;
let args = config::cli_args();
let env = config::cli_env();
let debug_load =
args.debug_startup.unwrap_or(env.komodo_cli_debug_startup);
match &args.command {
args::Command::Config {
all_profiles,
unsanitized,
} => {
let mut config = if *unsanitized {
cli_config().clone()
} else {
cli_config().sanitized()
};
if !*all_profiles {
config.profile = Default::default();
}
if debug_load {
println!("\n{config:#?}");
} else {
println!(
"\nCLI Config {}",
serde_json::to_string_pretty(&config)
.context("Failed to serialize config for pretty print")?
);
}
Ok(())
}
args::Command::Container(container) => {
command::container::handle(container).await
}
args::Command::Inspect(inspect) => {
command::container::inspect_container(inspect).await
}
args::Command::List(list) => command::list::handle(list).await,
args::Command::Execute(args) => {
command::execute::handle(&args.execution, args.yes).await
}
args::Command::Update { command } => {
command::update::handle(command).await
}
args::Command::Database { command } => {
command::database::handle(command).await
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut term_signal = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
)?;
tokio::select! {
res = tokio::spawn(app()) => res?,
_ = term_signal.recv() => Ok(()),
}
}

83
bin/core/Cargo.toml Normal file
View File

@@ -0,0 +1,83 @@
[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
interpolate.workspace = true
formatting.workspace = true
database.workspace = true
response.workspace = true
command.workspace = true
config.workspace = true
logger.workspace = true
cache.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }
async_timing_util.workspace = true
partial_derive2.workspace = true
derive_variants.workspace = true
resolver_api.workspace = true
toml_pretty.workspace = true
slack.workspace = true
svi.workspace = true
# external
aws-credential-types.workspace = true
tokio-tungstenite.workspace = true
english-to-cron.workspace = true
openidconnect.workspace = true
jsonwebtoken.workspace = true
axum-server.workspace = true
urlencoding.workspace = true
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_ng.workspace = true
typeshare.workspace = true
chrono-tz.workspace = true
indexmap.workspace = true
octorust.workspace = true
wildcard.workspace = true
arc-swap.workspace = true
colored.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
futures.workspace = true
nom_pem.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
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

61
bin/core/aio.Dockerfile Normal file
View File

@@ -0,0 +1,61 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.89.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
COPY ./bin/cli ./bin/cli
# Compile app
RUN cargo build -p komodo_core --release && \
cargo build -p komodo_cli --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
COPY ./bin/core/starship.toml /starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Setup an application directory
WORKDIR /app
# Copy
COPY ./config/core.config.toml /config/.default.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=core-builder /builder/target/release/km /usr/local/bin/km
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
ENV KOMODO_CLI_CONFIG_PATHS="/config"
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
CMD [ "core" ]
# 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

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

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

View File

@@ -0,0 +1,59 @@
## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile).
## Sets up the necessary runtime container dependencies for Komodo Core.
## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG FRONTEND_IMAGE=ghcr.io/moghtech/komodo-frontend:latest
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
# This is required to work with COPY --from
FROM ${X86_64_BINARIES} AS x86_64
FROM ${AARCH64_BINARIES} AS aarch64
FROM ${FRONTEND_IMAGE} AS frontend
# Final Image
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
WORKDIR /app
ARG TARGETPLATFORM
# Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM.
COPY --from=x86_64 /core /app/core/linux/amd64
COPY --from=aarch64 /core /app/core/linux/arm64
RUN mv /app/core/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/core
# Same for util
COPY --from=x86_64 /km /app/km/linux/amd64
COPY --from=aarch64 /km /app/km/linux/arm64
RUN mv /app/km/${TARGETPLATFORM} /usr/local/bin/km && rm -r /app/km
# Copy default config / static frontend / deno binary
COPY ./config/core.config.toml /config/.default.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
ENV KOMODO_CLI_CONFIG_PATHS="/config"
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
CMD [ "core" ]
# 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

View File

@@ -0,0 +1,48 @@
## 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
COPY ./bin/core/starship.toml /starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Copy
COPY ./config/core.config.toml /config/.default.config.toml
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=binaries /core /usr/local/bin/core
COPY --from=binaries /km /usr/local/bin/km
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# 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
ENV KOMODO_CLI_CONFIG_PATHS="/config"
# This ensures any `komodo.cli.*` takes precedence over the Core `/config/*config.*`
ENV KOMODO_CLI_CONFIG_KEYWORDS="*config.*,*komodo.cli*.*"
CMD [ "core" ]
# 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

View File

@@ -0,0 +1,306 @@
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::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | **{name}**{region} | Periphery version now matches Core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | **{name}**{region} | Version mismatch detected ⚠️\nPeriphery: **{server_version}** | Core: **{core_version}**\n{link}"
)
}
}
}
AlertData::ServerUnreachable {
id,
name,
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::Custom { message, details } => {
format!(
"{level} | {message}{}",
if details.is_empty() {
format_args!("")
} else {
format_args!("\n{details}")
}
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
send_message(&url_interpolated, &content)
.await
.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
.collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {sanitized_error}"
))
})?;
}
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,
}

488
bin/core/src/alert/mod.rs Normal file
View File

@@ -0,0 +1,488 @@
use ::slack::types::Block;
use anyhow::{Context, anyhow};
use database::mungos::{find::find_collect, mongodb::bson::doc};
use derive_variants::ExtractVariant;
use futures::future::join_all;
use interpolate::Interpolator;
use komodo_client::entities::{
ResourceTargetVariant,
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
alerter::*,
deployment::DeploymentState,
komodo_timestamp,
stack::StackState,
};
use tracing::Instrument;
use crate::helpers::query::get_variables_and_secrets;
use crate::helpers::{
maintenance::is_in_maintenance, query::VariablesAndSecrets,
};
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_to_alerters(&alerters, alert));
join_all(handles).await;
}
.instrument(span)
.await
}
#[instrument(level = "debug")]
async fn send_alert_to_alerters(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(());
}
if is_in_maintenance(
&alerter.config.maintenance_windows,
komodo_timestamp(),
) {
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, email }) => {
ntfy::send_alert(url, email.as_deref(), alert)
.await
.with_context(|| {
format!(
"Failed to send alert to ntfy Alerter {}",
alerter.name
)
})
}
AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {
pushover::send_alert(url, alert).await.with_context(|| {
format!(
"Failed to send alert to Pushover Alerter {}",
alerter.name
)
})
}
}
}
#[instrument(level = "debug")]
async fn send_custom_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
let res = reqwest::Client::new()
.post(url_interpolated)
.json(alert)
.send()
.await
.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
.collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with request: {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 {
komodo_client::entities::resource_link(
&core_config().host,
resource_type,
id,
)
}
/// Standard message content format
/// used by Ntfy, Pushover.
fn standard_alert_content(alert: &Alert) -> String {
let level = fmt_level(alert.level);
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::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {name}{region} | Periphery version now matches Core version ✅\n{link}"
)
}
_ => {
format!(
"{level} | {name}{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}\n{link}"
)
}
}
}
AlertData::ServerUnreachable {
id,
name,
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!("\nerror: {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_state = fmt_docker_container_state(to);
format!(
"📦Deployment {name} is now {to_state}\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_state = fmt_stack_state(to);
format!(
"🥞 Stack {name} is now {to_state}\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_str = images.join(", ");
format!(
"⬆ Stack {name} was updated automatically ⏫\nserver: {server_name}\n{images_label}: {images_str}\n{link}",
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminate 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::Custom { message, details } => {
format!(
"{level} | {message}{}",
if details.is_empty() {
format_args!("")
} else {
format_args!("\n{details}")
}
)
}
AlertData::None {} => Default::default(),
}
}

View File

@@ -0,0 +1,56 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
email: Option<&str>,
alert: &Alert,
) -> anyhow::Result<()> {
let content = standard_alert_content(alert);
if !content.is_empty() {
send_message(url, email, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
email: Option<&str>,
content: String,
) -> anyhow::Result<()> {
let mut request = http_client()
.post(url)
.header("Title", "ntfy Alert")
.body(content);
if let Some(email) = email {
request = request.header("X-Email", email);
}
let response =
request.send().await.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("ntfy alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to ntfy | {status} | failed to get response text"
)
})?;
Err(anyhow!(
"Failed to send message to ntfy | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}

View File

@@ -0,0 +1,55 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let content = standard_alert_content(alert);
if !content.is_empty() {
send_message(url, content).await?;
}
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 | {status} | failed to get response text"
)
})?;
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)
}

493
bin/core/src/alert/slack.rs Normal file
View File

@@ -0,0 +1,493 @@
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::ServerVersionMismatch {
id,
name,
region,
server_version,
core_version,
} => {
let region = fmt_region(region);
let text = match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} | Periphery version now matches Core version ✅"
)
}
_ => {
format!(
"{level} | *{name}*{region} | Version mismatch detected ⚠️\nPeriphery: {server_version} | Core: {core_version}"
)
}
};
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
];
(text, blocks.into())
}
AlertData::ServerUnreachable {
id,
name,
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::Custom { message, details } => {
let text = format!("{level} | {message}");
let blocks =
vec![Block::header(text.clone()), Block::section(details)];
(text, blocks.into())
}
AlertData::None {} => Default::default(),
};
if !text.is_empty() {
let VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut url_interpolated = url.to_string();
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_string(&mut url_interpolated)?;
let slack = ::slack::Client::new(url_interpolated);
slack.send_message(text, blocks).await.map_err(|e| {
let replacers = interpolator
.secret_replacers
.into_iter()
.collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {sanitized_error}"
))
})?;
}
Ok(())
}

158
bin/core/src/api/auth.rs Normal file
View File

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

View File

@@ -0,0 +1,423 @@
use std::{
collections::HashSet,
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use anyhow::Context;
use command::run_komodo_command;
use config::merge_objects;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use interpolate::Interpolator;
use komodo_client::{
api::{
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
FileFormat, JsonObject,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
update::Update,
user::action_user,
},
parsers::parse_key_value_list,
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
alert::send_alerts,
api::{execute::ExecuteRequest, user::UserArgs},
config::core_config,
helpers::{
query::{VariablesAndSecrets, get_variables_and_secrets},
random_string,
update::update_update,
},
permission::get_check_permissions,
resource::refresh_action_state_cache,
state::{action_states, db_client},
};
use super::ExecuteArgs;
impl super::BatchExecute for BatchRunAction {
type Resource = Action;
fn single_request(action: String) -> ExecuteRequest {
ExecuteRequest::RunAction(RunAction {
action,
args: Default::default(),
})
}
}
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 = get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Execute.into(),
)
.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 default_args = parse_action_arguments(
&action.config.arguments,
action.config.arguments_format,
)
.context("Failed to parse default Action arguments")?;
let args = merge_objects(
default_args,
self.args.unwrap_or_default(),
true,
true,
)
.context("Failed to merge request args with default args")?;
let args = serde_json::to_string(&args)
.context("Failed to serialize action run arguments")?;
let CreateApiKeyResponse { key, secret } = CreateApiKey {
name: update.id.clone(),
expires: 0,
}
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await?;
let contents = &mut action.config.file_contents;
// Wrap the file contents in the execution context.
*contents = full_contents(contents, &args, &key, &secret);
let replacers =
interpolate(contents, &mut update, key.clone(), secret.clone())
.await?
.into_iter()
.collect::<Vec<_>>();
let 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 reload = if action.config.reload_deno_deps {
" --reload"
} else {
""
};
let mut res = run_komodo_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!(
"deno run --allow-all{https_cert_flag}{reload} {}",
path.display()
),
)
.await;
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
.replace(&key, "<ACTION_API_KEY>");
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
.replace(&secret, "<ACTION_API_SECRET>");
cleanup_run(file + ".js", &path).await;
if let Err(e) = (DeleteApiKey { key })
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await
{
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,
database::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 VariablesAndSecrets {
variables,
mut secrets,
} = get_variables_and_secrets().await?;
secrets.insert(String::from("ACTION_API_KEY"), key);
secrets.insert(String::from("ACTION_API_SECRET"), secret);
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator
.interpolate_string(contents)?
.push_logs(&mut update.logs);
Ok(interpolator.secret_replacers)
}
fn full_contents(
contents: &str,
// Pre-serialized to JSON string.
args: &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, Types }} 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 ARGS = {args};
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(error);
}}
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
}
})
}
fn parse_action_arguments(
args: &str,
format: FileFormat,
) -> anyhow::Result<JsonObject> {
match format {
FileFormat::KeyValue => {
let args = parse_key_value_list(args)
.context("Failed to parse args as key value list")?
.into_iter()
.map(|(k, v)| (k, serde_json::Value::String(v)))
.collect();
Ok(args)
}
FileFormat::Toml => toml::from_str(args)
.context("Failed to parse Toml to Action args"),
FileFormat::Yaml => serde_yaml_ng::from_str(args)
.context("Failed to parse Yaml to action args"),
FileFormat::Json => serde_json::from_str(args)
.context("Failed to parse Json to action args"),
}
}

View File

@@ -0,0 +1,149 @@
use anyhow::{Context, anyhow};
use formatting::format_serror;
use futures::{TryStreamExt, stream::FuturesUnordered};
use komodo_client::{
api::execute::{SendAlert, TestAlerter},
entities::{
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
alerter::Alerter,
komodo_timestamp,
permission::PermissionLevel,
},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use crate::{
alert::send_alert_to_alerter, helpers::update::update_update,
permission::get_check_permissions, resource::list_full_for_user,
};
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for TestAlerter {
#[instrument(name = "TestAlerter", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> Result<Self::Response, Self::Error> {
let alerter = get_check_permissions::<Alerter>(
&self.alerter,
user,
PermissionLevel::Execute.into(),
)
.await?;
let mut update = update.clone();
if !alerter.config.enabled {
update.push_error_log(
"Test Alerter",
String::from(
"Alerter is disabled. Enable the Alerter to send alerts.",
),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
level: SeverityLevel::Ok,
target: update.target.clone(),
data: AlertData::Test {
id: alerter.id.clone(),
name: alerter.name.clone(),
},
resolved_ts: Some(ts),
};
if let Err(e) = send_alert_to_alerter(&alerter, &alert).await {
update.push_error_log("Test Alerter", format_serror(&e.into()));
} else {
update.push_simple_log("Test Alerter", String::from("Alert sent successfully. It should be visible at your alerting destination."));
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
//
impl Resolve<ExecuteArgs> for SendAlert {
#[instrument(name = "SendAlert", 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 alerters = list_full_for_user::<Alerter>(
Default::default(),
user,
PermissionLevel::Execute.into(),
&[],
)
.await?
.into_iter()
.filter(|a| {
a.config.enabled
&& (self.alerters.is_empty()
|| self.alerters.contains(&a.name)
|| self.alerters.contains(&a.id))
&& (a.config.alert_types.is_empty()
|| a.config.alert_types.contains(&AlertDataVariant::Custom))
})
.collect::<Vec<_>>();
if alerters.is_empty() {
return Err(anyhow!(
"Could not find any valid alerters to send to, this required Execute permissions on the Alerter"
).status_code(StatusCode::BAD_REQUEST));
}
let mut update = update.clone();
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
level: self.level,
target: update.target.clone(),
data: AlertData::Custom {
message: self.message,
details: self.details,
},
resolved_ts: Some(ts),
};
update.push_simple_log(
"Send alert",
serde_json::to_string_pretty(&alert)
.context("Failed to serialize alert to JSON")?,
);
if let Err(e) = alerters
.iter()
.map(|alerter| send_alert_to_alerter(alerter, &alert))
.collect::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await
{
update.push_error_log("Send Error", format_serror(&e.into()));
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}

View File

@@ -0,0 +1,659 @@
use std::{
collections::{HashMap, HashSet},
future::IntoFuture,
time::Duration,
};
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::update_one_by_id,
find::find_collect,
mongodb::{
bson::{doc, to_bson, to_document},
options::FindOneOptions,
},
};
use formatting::format_serror;
use futures::future::join_all;
use interpolate::Interpolator;
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
RunBuild,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
build::{Build, BuildConfig},
builder::{Builder, BuilderConfig},
deployment::DeploymentState,
komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
update::{Log, Update},
user::auto_redeploy_user,
},
};
use periphery_client::api;
use resolver_api::Resolve;
use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
helpers::{
build_git_token,
builder::{cleanup_builder_instance, get_builder_periphery},
channel::build_cancel_channel,
query::{
VariablesAndSecrets, get_deployment_state,
get_variables_and_secrets,
},
registry_token,
update::{init_execution_update, update_update},
},
permission::get_check_permissions,
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 = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
)
.await?;
let mut repo = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&build.config.linked_repo)
.await?
.into()
} else {
None
};
let VariablesAndSecrets {
mut variables,
secrets,
} = get_variables_and_secrets().await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
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 =
build_git_token(&mut build, repo.as_mut()).await?;
let registry_tokens =
validate_account_extract_registry_tokens(&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 interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator.interpolate_build(&mut build)?;
if let Some(repo) = repo.as_mut() {
interpolator.interpolate_repo(repo)?;
}
interpolator.push_logs(&mut update.logs);
interpolator.secret_replacers
} else {
Default::default()
};
let commit_message = if !build.config.files_on_host
&& (!build.config.repo.is_empty()
|| !build.config.linked_repo.is_empty())
{
// PULL OR CLONE REPO
let res = tokio::select! {
res = periphery
.request(api::git::PullOrCloneRepo {
args: repo.as_ref().map(Into::into).unwrap_or((&build).into()),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
on_clone: None,
on_pull: None,
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.res.logs);
update.commit_hash =
res.res.commit_hash.unwrap_or_default().to_string();
res.res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("Failed build at clone repo | {e:#}");
update.push_error_log(
"Clone Repo",
format_serror(&e.context("Failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
Some(commit_message)
} else {
None
};
if all_logs_success(&update.logs) {
// RUN BUILD
let res = tokio::select! {
res = periphery
.request(api::build::Build {
build: build.clone(),
repo,
registry_tokens,
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,
database::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,
database::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 = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
)
.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.id)
.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_tokens(
Build {
config: BuildConfig { image_registry, .. },
..
}: &Build,
// Maps (domain, account) -> token
) -> serror::Result<Vec<(String, String, String)>> {
let mut res = HashMap::with_capacity(image_registry.capacity());
for (domain, account) in image_registry
.iter()
.map(|r| (r.domain.as_str(), r.account.as_str()))
// This ensures uniqueness / prevents redundant logins
.collect::<HashSet<_>>()
{
if domain.is_empty() {
continue;
}
if account.is_empty() {
return Err(
anyhow!(
"Must attach account to use registry provider {domain}"
)
.into(),
);
}
let Some(registry_token) = registry_token(domain, account).await.with_context(
|| format!("Failed to get registry token in call to db. Stopping run. | {domain} | {account}"),
)? else {
continue;
};
res.insert(
(domain.to_string(), account.to_string()),
registry_token,
);
}
Ok(
res
.into_iter()
.map(|((domain, account), token)| (domain, account, token))
.collect(),
)
}

View File

@@ -0,0 +1,719 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use cache::TimeoutCache;
use formatting::format_serror;
use interpolate::Interpolator;
use komodo_client::{
api::execute::*,
entities::{
Version,
build::{Build, ImageRegistryConfig},
deployment::{
Deployment, DeploymentImage, extract_registry_domain,
},
get_image_names, komodo_timestamp, optional_string,
permission::PermissionLevel,
server::Server,
update::{Log, Update},
user::User,
},
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{
periphery_client,
query::{VariablesAndSecrets, get_variables_and_secrets},
registry_token,
update::update_update,
},
monitor::update_cache_for_server,
permission::get_check_permissions,
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 = get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Execute.into(),
)
.await?;
if deployment.config.server_id.is_empty() {
return Err(anyhow!("Deployment has no Server configured"));
}
let server =
resource::get::<Server>(&deployment.config.server_id).await?;
if !server.config.enabled {
return Err(anyhow!("Attached Server is not enabled"));
}
Ok((deployment, server))
}
impl Resolve<ExecuteArgs> for Deploy {
#[instrument(name = "Deploy", skip(user, update), fields(user_id = user.id, update_id = update.id))]
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_names = get_image_names(&build);
let image_name = image_names
.first()
.context("No image name could be created")
.context("Failed to create image name")?;
let version = if version.is_none() {
build.config.version
} else {
*version
};
let version_str = version.to_string();
// Potentially add the build image_tag postfix
let version_str = if build.config.image_tag.is_empty() {
version_str
} else {
format!("{version_str}-{}", build.config.image_tag)
};
// replace image with corresponding build image.
deployment.config.image = DeploymentImage::Image {
image: format!("{image_name}:{version_str}"),
};
let first_registry = build
.config
.image_registry
.first()
.unwrap_or(ImageRegistryConfig::static_default());
if first_registry.domain.is_empty() {
(version, None)
} else {
let ImageRegistryConfig {
domain, account, ..
} = first_registry;
if deployment.config.image_registry_account.is_empty() {
deployment.config.image_registry_account =
account.to_string();
}
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 VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator
.interpolate_deployment(&mut deployment)?
.push_logs(&mut update.logs);
interpolator.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_names = get_image_names(&build);
let image_name = image_names
.first()
.context("No image name could be created")
.context("Failed to create image name")?;
let version = if version.is_none() {
build.config.version.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}");
let first_registry = build
.config
.image_registry
.first()
.unwrap_or(ImageRegistryConfig::static_default());
if first_registry.domain.is_empty() {
(image, None, None)
} else {
let ImageRegistryConfig {
domain, account, ..
} = first_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)
}
}

View File

@@ -0,0 +1,319 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use command::run_komodo_command;
use database::mungos::{find::find_collect, mongodb::bson::doc};
use formatting::{bold, format_serror};
use komodo_client::{
api::execute::{
BackupCoreDatabase, ClearRepoCache, GlobalAutoUpdate,
},
entities::{
deployment::DeploymentState, server::ServerState,
stack::StackState,
},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use tokio::sync::Mutex;
use crate::{
api::execute::{
ExecuteArgs, pull_deployment_inner, pull_stack_inner,
},
config::core_config,
helpers::update::update_update,
state::{
db_client, deployment_status_cache, server_status_cache,
stack_status_cache,
},
};
/// Makes sure the method can only be called once at a time
fn clear_repo_cache_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(Default::default)
}
impl Resolve<ExecuteArgs> for ClearRepoCache {
#[instrument(
name = "ClearRepoCache",
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> {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::UNAUTHORIZED),
);
}
let _lock = clear_repo_cache_lock()
.try_lock()
.context("Clear already in progress...")?;
let mut update = update.clone();
let mut contents =
tokio::fs::read_dir(&core_config().repo_directory)
.await
.context("Failed to read repo cache directory")?;
loop {
let path = match contents
.next_entry()
.await
.context("Failed to read contents at path")
{
Ok(Some(contents)) => contents.path(),
Ok(None) => break,
Err(e) => {
update.push_error_log(
"Read Directory",
format_serror(&e.into()),
);
continue;
}
};
if path.is_dir() {
match tokio::fs::remove_dir_all(&path)
.await
.context("Failed to clear contents at path")
{
Ok(_) => {}
Err(e) => {
update.push_error_log(
"Clear Directory",
format_serror(&e.into()),
);
}
};
}
}
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
//
/// Makes sure the method can only be called once at a time
fn backup_database_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(Default::default)
}
impl Resolve<ExecuteArgs> for BackupCoreDatabase {
#[instrument(
name = "BackupCoreDatabase",
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> {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::UNAUTHORIZED),
);
}
let _lock = backup_database_lock()
.try_lock()
.context("Backup already in progress...")?;
let mut update = update.clone();
update_update(update.clone()).await?;
let res = run_komodo_command(
"Backup Core Database",
None,
"km database backup --yes",
)
.await;
update.logs.push(res);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
//
/// Makes sure the method can only be called once at a time
fn global_update_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(Default::default)
}
impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
#[instrument(
name = "GlobalAutoUpdate",
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> {
if !user.admin {
return Err(
anyhow!("This method is admin only.")
.status_code(StatusCode::UNAUTHORIZED),
);
}
let _lock = global_update_lock()
.try_lock()
.context("Global update already in progress...")?;
let mut update = update.clone();
update_update(update.clone()).await?;
// This is all done in sequence because there is no rush,
// the pulls / deploys happen spaced out to ease the load on system.
let servers = find_collect(&db_client().servers, None, None)
.await
.context("Failed to query for servers from database")?;
let query = doc! {
"$or": [
{ "config.poll_for_updates": true },
{ "config.auto_update": true }
]
};
let (stacks, repos) = tokio::try_join!(
find_collect(&db_client().stacks, query.clone(), None),
find_collect(&db_client().repos, None, None)
)
.context("Failed to query for resources from database")?;
let server_status_cache = server_status_cache();
let stack_status_cache = stack_status_cache();
// Will be edited later at update.logs[0]
update.push_simple_log("Auto Pull", String::new());
for stack in stacks {
let Some(status) = stack_status_cache.get(&stack.id).await
else {
continue;
};
// Only pull running stacks.
if !matches!(status.curr.state, StackState::Running) {
continue;
}
if let Some(server) =
servers.iter().find(|s| s.id == stack.config.server_id)
// This check is probably redundant along with running check
// but shouldn't hurt
&& server_status_cache
.get(&server.id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
{
let name = stack.name.clone();
let repo = if stack.config.linked_repo.is_empty() {
None
} else {
let Some(repo) =
repos.iter().find(|r| r.id == stack.config.linked_repo)
else {
update.push_error_log(
&format!("Pull Stack {name}"),
format!(
"Did not find any Repo matching {}",
stack.config.linked_repo
),
);
continue;
};
Some(repo.clone())
};
if let Err(e) =
pull_stack_inner(stack, Vec::new(), server, repo, None)
.await
{
update.push_error_log(
&format!("Pull Stack {name}"),
format_serror(&e.into()),
);
} else {
if !update.logs[0].stdout.is_empty() {
update.logs[0].stdout.push('\n');
}
update.logs[0]
.stdout
.push_str(&format!("Pulled Stack {}", bold(name)));
}
}
}
let deployment_status_cache = deployment_status_cache();
let deployments =
find_collect(&db_client().deployments, query, None)
.await
.context("Failed to query for deployments from database")?;
for deployment in deployments {
let Some(status) =
deployment_status_cache.get(&deployment.id).await
else {
continue;
};
// Only pull running deployments.
if !matches!(status.curr.state, DeploymentState::Running) {
continue;
}
if let Some(server) =
servers.iter().find(|s| s.id == deployment.config.server_id)
// This check is probably redundant along with running check
// but shouldn't hurt
&& server_status_cache
.get(&server.id)
.await
.map(|s| matches!(s.state, ServerState::Ok))
.unwrap_or_default()
{
let name = deployment.name.clone();
if let Err(e) =
pull_deployment_inner(deployment, server).await
{
update.push_error_log(
&format!("Pull Deployment {name}"),
format_serror(&e.into()),
);
} else {
if !update.logs[0].stdout.is_empty() {
update.logs[0].stdout.push('\n');
}
update.logs[0].stdout.push_str(&format!(
"Pulled Deployment {}",
bold(name)
));
}
}
}
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}

View File

@@ -0,0 +1,347 @@
use std::{pin::Pin, time::Instant};
use anyhow::Context;
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use axum_extra::{TypedHeader, headers::ContentType};
use database::mungos::by_id::find_one_by_id;
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::*,
entities::{
Operation,
permission::PermissionLevel,
update::{Log, Update},
user::User,
},
};
use resolver_api::Resolve;
use response::JsonString;
use serde::{Deserialize, Serialize};
use serde_json::json;
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 maintenance;
mod procedure;
mod repo;
mod server;
mod stack;
mod sync;
use super::Variant;
pub use {
deployment::pull_deployment_inner, stack::pull_stack_inner,
};
pub struct ExecuteArgs {
pub user: User,
pub update: Update,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, 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),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
BatchPullStack(BatchPullStack),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
RunStackService(RunStackService),
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
PullDeployment(PullDeployment),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// ==== BUILD ====
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
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),
// ==== ALERTER ====
TestAlerter(TestAlerter),
SendAlert(SendAlert),
// ==== SYNC ====
RunSync(RunSync),
// ==== MAINTENANCE ====
ClearRepoCache(ClearRepoCache),
BackupCoreDatabase(BackupCoreDatabase),
GlobalAutoUpdate(GlobalAutoUpdate),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let req: ExecuteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
) -> 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))
}
#[typeshare(serialized_as = "Update")]
type BoxUpdate = Box<Update>;
pub enum ExecutionResult {
Single(BoxUpdate),
/// 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.
// This ensures no double update created if Cancel is called more than once for the same request.
build::validate_cancel_build(&request).await?;
repo::validate_cancel_repo_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?,
));
}
// Spawn a task for the execution which continues
// running after this method returns.
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
// Spawns another task to monitor the first for failures,
// and add the log to Update about it (which primary task can't do because it errored out)
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 {
// Nothing to do if update was never actually created,
// which is the case when the id is empty.
if update_id.is_empty() {
return Ok(());
}
let mut update =
find_one_by_id(&db_client().updates, &update_id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
}
.await;
if let Err(e) = res {
warn!(
"failed to update update with task error log | {e:#}"
);
}
}
});
Ok(ExecutionResult::Single(update.into()))
})
}
#[instrument(
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,
PermissionLevel::Execute.into(),
&[],
)
.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)
}

View File

@@ -0,0 +1,170 @@
use std::pin::Pin;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use formatting::{Color, bold, colored, format_serror, muted};
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
komodo_timestamp,
permission::PermissionLevel,
procedure::Procedure,
update::Update,
user::User,
},
};
use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
alert::send_alerts,
helpers::{procedure::execute_procedure, update::update_update},
permission::get_check_permissions,
resource::refresh_procedure_state_cache,
state::{action_states, db_client},
};
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 = get_check_permissions::<Procedure>(
&procedure,
&user,
PermissionLevel::Execute.into(),
)
.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,
database::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)
})
}

View File

@@ -0,0 +1,730 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::update_one_by_id,
mongodb::{
bson::{doc, to_document},
options::FindOneOptions,
},
};
use formatting::format_serror;
use interpolate::Interpolator;
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 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, periphery_client,
query::{VariablesAndSecrets, get_variables_and_secrets},
update::update_update,
},
permission::get_check_permissions,
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 = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.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,
on_clone: repo.config.on_clone.into(),
on_pull: repo.config.on_pull.into(),
skip_secret_interp: repo.config.skip_secret_interp,
replacers: secret_replacers.into_iter().collect(),
})
.await
{
Ok(res) => 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_repo_update_return(update).await
}
}
impl super::BatchExecute for BatchPullRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::PullRepo(PullRepo { 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 = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.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,
on_pull: repo.config.on_pull.into(),
skip_secret_interp: repo.config.skip_secret_interp,
replacers: secret_replacers.into_iter().collect(),
})
.await
{
Ok(res) => {
update.commit_hash = res.res.commit_hash.unwrap_or_default();
res.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_repo_update_return(update).await
}
}
#[instrument(skip_all, fields(update_id = update.id))]
async fn handle_repo_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,
database::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 = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.await?;
if repo.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to BuildRepo").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,
on_clone: repo.config.on_clone.into(),
on_pull: repo.config.on_pull.into(),
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.res.logs);
update.commit_hash = res.res.commit_hash.unwrap_or_default();
res.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,
database::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,
database::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 = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.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 VariablesAndSecrets { variables, secrets } =
get_variables_and_secrets().await?;
let mut interpolator =
Interpolator::new(Some(&variables), &secrets);
interpolator
.interpolate_repo(repo)?
.push_logs(&mut update.logs);
Ok(interpolator.secret_replacers)
} else {
Ok(Default::default())
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,542 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::update_one_by_id,
mongodb::bson::{doc, oid::ObjectId},
};
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,
stack::Stack,
sync::ResourceSync,
update::{Log, Update},
user::sync_user,
},
};
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{
all_resources::AllResourcesById, query::get_id_to_tags,
update::update_update,
},
permission::get_check_permissions,
state::{action_states, db_client},
sync::{
ResourceSyncTrait,
deploy::{
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 = get_check_permissions::<entities::sync::ResourceSync>(
&sync,
user,
PermissionLevel::Execute.into(),
)
.await?;
let repo = if !sync.config.files_on_host
&& !sync.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&sync.config.linked_repo)
.await?
.into()
} else {
None
};
// get the action state for the sync (or insert default).
let action_state = action_states()
.resource_sync
.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, repo.as_ref())
.await
.context("failed to get remote resources")?;
update.logs.extend(logs);
update_update(update.clone()).await?;
if !file_errors.is_empty() {
return Err(
anyhow!("Found file errors. Cannot execute sync.").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::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,
})
.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,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let stack_deltas = if sync.config.include_resources {
get_updates_for_execution::<Stack>(
resources.stacks,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let deployment_deltas = if sync.config.include_resources {
get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let build_deltas = if sync.config.include_resources {
get_updates_for_execution::<Build>(
resources.builds,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let repo_deltas = if sync.config.include_resources {
get_updates_for_execution::<Repo>(
resources.repos,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let procedure_deltas = if sync.config.include_resources {
get_updates_for_execution::<Procedure>(
resources.procedures,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let action_deltas = if sync.config.include_resources {
get_updates_for_execution::<Action>(
resources.actions,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let builder_deltas = if sync.config.include_resources {
get_updates_for_execution::<Builder>(
resources.builders,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let alerter_deltas = if sync.config.include_resources {
get_updates_for_execution::<Alerter>(
resources.alerters,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let resource_sync_deltas = if sync.config.include_resources {
get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let (
variables_to_create,
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,
)
.await?
} else {
Default::default()
};
if deploy_cache.is_empty()
&& resource_sync_deltas.no_changes()
&& server_deltas.no_changes()
&& deployment_deltas.no_changes()
&& stack_deltas.no_changes()
&& build_deltas.no_changes()
&& builder_deltas.no_changes()
&& alerter_deltas.no_changes()
&& repo_deltas.no_changes()
&& procedure_deltas.no_changes()
&& action_deltas.no_changes()
&& user_groups_to_create.is_empty()
&& user_groups_to_update.is_empty()
&& user_groups_to_delete.is_empty()
&& 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,
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);
}
}

11
bin/core/src/api/mod.rs Normal file
View File

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

View File

@@ -0,0 +1,147 @@
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,
permission::get_check_permissions,
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(
get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetActionActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ActionActionState> {
let action = get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.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,
PermissionLevel::Read.into(),
&[],
)
.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)
}
}

View File

@@ -0,0 +1,88 @@
use anyhow::Context;
use database::mungos::{
by_id::find_one_by_id,
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use komodo_client::{
api::read::{
GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,
},
entities::{
deployment::Deployment, server::Server, stack::Stack,
sync::ResourceSync,
},
};
use resolver_api::Resolve;
use crate::{
config::core_config, permission::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")?,
)
}
}

View File

@@ -0,0 +1,105 @@
use anyhow::Context;
use database::mongo_indexed::Document;
use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
alerter::{Alerter, AlerterListItem},
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAlerter {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Alerter> {
Ok(
get_check_permissions::<Alerter>(
&self.alerter,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&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)
}
}

View File

@@ -0,0 +1,386 @@
use std::collections::{HashMap, HashSet};
use anyhow::Context;
use async_timing_util::unix_timestamp_ms;
use database::mungos::{
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use futures::TryStreamExt;
use komodo_client::{
api::read::*,
entities::{
Operation,
build::{Build, BuildActionState, BuildListItem, BuildState},
config::core::CoreConfig,
permission::PermissionLevel,
update::UpdateStatus,
},
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
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(
get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetBuildActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<BuildActionState> {
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.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,
PermissionLevel::Read.into(),
&[],
)
.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 = get_check_permissions::<Build>(
&build,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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 = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
)
.await?;
if build.config.git_provider != "github.com"
|| build.config.repo.is_empty()
{
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
enabled: false,
});
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(GetBuildWebhookEnabledResponse {
managed: true,
enabled: true,
});
}
}
Ok(GetBuildWebhookEnabledResponse {
managed: true,
enabled: false,
})
}
}

View File

@@ -0,0 +1,105 @@
use anyhow::Context;
use database::mongo_indexed::Document;
use database::mungos::mongodb::bson::doc;
use komodo_client::{
api::read::*,
entities::{
builder::{Builder, BuilderListItem},
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuilder {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Builder> {
Ok(
get_check_permissions::<Builder>(
&self.builder,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&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)
}
}

View File

@@ -0,0 +1,367 @@
use std::{cmp, collections::HashSet};
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
deployment::{
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
},
docker::container::{Container, ContainerStats},
permission::PermissionLevel,
server::{Server, ServerState},
update::Log,
},
};
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use crate::{
helpers::{periphery_client, query::get_all_tags},
permission::get_check_permissions,
resource,
state::{
action_states, deployment_status_cache, server_status_cache,
},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Deployment> {
Ok(
get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let deployments = if only_update_available {
deployments
.into_iter()
.filter(|deployment| deployment.info.update_available)
.collect()
} else {
deployments
};
Ok(deployments)
}
}
impl Resolve<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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetDeploymentContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentContainerResponse> {
let deployment = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
let status = deployment_status_cache()
.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, .. },
..
} = get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read.logs(),
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerLog {
name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.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, .. },
..
} = get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read.logs(),
)
.await?;
if server_id.is_empty() {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerLogSearch {
name,
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectDeploymentContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let InspectDeploymentContainer { deployment } = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if server_id.is_empty() {
return Err(
anyhow!(
"Cannot inspect deployment, not attached to any server"
)
.into(),
);
}
let server = resource::get::<Server>(&server_id).await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.request(InspectContainer { name })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetDeploymentStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ContainerStats> {
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
if server_id.is_empty() {
return Err(
anyhow!("deployment has no server attached").into(),
);
}
let server = resource::get::<Server>(&server_id).await?;
let res = periphery_client(&server)?
.request(api::container::GetContainerStats { name })
.await
.context("failed to get stats from periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetDeploymentActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<DeploymentActionState> {
let deployment = get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.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,
PermissionLevel::Read.into(),
&[],
)
.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,
PermissionLevel::Read.into(),
&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)
}
}

View File

@@ -0,0 +1,587 @@
use std::{collections::HashSet, sync::OnceLock, time::Instant};
use anyhow::{Context, anyhow};
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
build::Build,
builder::{Builder, BuilderConfig},
config::{DockerRegistry, GitProvider},
permission::PermissionLevel,
repo::Repo,
server::Server,
sync::ResourceSync,
user::User,
},
};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::auth_request, config::core_config, helpers::periphery_client,
resource,
};
use super::Variant;
mod action;
mod alert;
mod alerter;
mod build;
mod builder;
mod deployment;
mod permission;
mod procedure;
mod provider;
mod repo;
mod schedule;
mod server;
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),
GetPermission(GetPermission),
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),
// ==== SCHEDULE ====
ListSchedules(ListSchedules),
// ==== SERVER ====
GetServersSummary(GetServersSummary),
GetServer(GetServer),
GetServerState(GetServerState),
GetPeripheryVersion(GetPeripheryVersion),
GetServerActionState(GetServerActionState),
GetHistoricalServerStats(GetHistoricalServerStats),
ListServers(ListServers),
ListFullServers(ListFullServers),
InspectDockerContainer(InspectDockerContainer),
GetResourceMatchingContainer(GetResourceMatchingContainer),
GetContainerLog(GetContainerLog),
SearchContainerLog(SearchContainerLog),
InspectDockerNetwork(InspectDockerNetwork),
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
ListDockerNetworks(ListDockerNetworks),
ListDockerImages(ListDockerImages),
ListDockerVolumes(ListDockerVolumes),
ListComposeProjects(ListComposeProjects),
ListTerminals(ListTerminals),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
ListSystemProcesses(ListSystemProcesses),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackLog(GetStackLog),
SearchStackLog(SearchStackLog),
InspectStackContainer(InspectStackContainer),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== DEPLOYMENT ====
GetDeploymentsSummary(GetDeploymentsSummary),
GetDeployment(GetDeployment),
GetDeploymentContainer(GetDeploymentContainer),
GetDeploymentActionState(GetDeploymentActionState),
GetDeploymentStats(GetDeploymentStats),
GetDeploymentLog(GetDeploymentLog),
SearchDeploymentLog(SearchDeploymentLog),
InspectDeploymentContainer(InspectDeploymentContainer),
ListDeployments(ListDeployments),
ListFullDeployments(ListFullDeployments),
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
// ==== BUILD ====
GetBuildsSummary(GetBuildsSummary),
GetBuild(GetBuild),
GetBuildActionState(GetBuildActionState),
GetBuildMonthlyStats(GetBuildMonthlyStats),
ListBuildVersions(ListBuildVersions),
GetBuildWebhookEnabled(GetBuildWebhookEnabled),
ListBuilds(ListBuilds),
ListFullBuilds(ListFullBuilds),
ListCommonBuildExtraArgs(ListCommonBuildExtraArgs),
// ==== REPO ====
GetReposSummary(GetReposSummary),
GetRepo(GetRepo),
GetRepoActionState(GetRepoActionState),
GetRepoWebhooksEnabled(GetRepoWebhooksEnabled),
ListRepos(ListRepos),
ListFullRepos(ListFullRepos),
// ==== SYNC ====
GetResourceSyncsSummary(GetResourceSyncsSummary),
GetResourceSync(GetResourceSync),
GetResourceSyncActionState(GetResourceSyncActionState),
GetSyncWebhooksEnabled(GetSyncWebhooksEnabled),
ListResourceSyncs(ListResourceSyncs),
ListFullResourceSyncs(ListFullResourceSyncs),
// ==== 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),
// ==== VARIABLE ====
GetVariable(GetVariable),
ListVariables(ListVariables),
// ==== PROVIDER ====
GetGitProviderAccount(GetGitProviderAccount),
ListGitProviderAccounts(ListGitProviderAccounts),
GetDockerRegistryAccount(GetDockerRegistryAccount),
ListDockerRegistryAccounts(ListDockerRegistryAccounts),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: ReadRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
#[instrument(name = "ReadHandler", level = "debug", skip(user), fields(user_id = user.id))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ReadRequest>,
) -> serror::Result<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,
disable_websocket_reconnect: config.disable_websocket_reconnect,
enable_fancy_toml: config.enable_fancy_toml,
github_webhook_owners: config
.github_webhook_app
.installations
.iter()
.map(|i| i.namespace.to_string())
.collect(),
timezone: config.timezone.clone(),
}
})
}
impl Resolve<ReadArgs> for GetCoreInfo {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetCoreInfoResponse> {
Ok(core_info().clone())
}
}
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,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[]
),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[]
),
)?;
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);
}
}
}

View File

@@ -0,0 +1,71 @@
use anyhow::{Context, anyhow};
use database::mungos::{find::find_collect, mongodb::bson::doc};
use komodo_client::{
api::read::{
GetPermission, GetPermissionResponse, ListPermissions,
ListPermissionsResponse, ListUserTargetPermissions,
ListUserTargetPermissionsResponse,
},
entities::permission::PermissionLevel,
};
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 GetPermission {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPermissionResponse> {
if user.admin {
return Ok(PermissionLevel::Write.all());
}
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)
}
}

View File

@@ -0,0 +1,145 @@
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,
permission::get_check_permissions,
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(
get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&[],
)
.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 = get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.procedure
.get(&procedure.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}

View File

@@ -0,0 +1,115 @@
use anyhow::{Context, anyhow};
use database::mongo_indexed::{Document, doc};
use database::mungos::{
by_id::find_one_by_id, find::find_collect,
mongodb::options::FindOptions,
};
use komodo_client::api::read::*;
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)
}
}

View File

@@ -0,0 +1,258 @@
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,
permission::get_check_permissions,
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(
get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetRepoActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<RepoActionState> {
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.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,
PermissionLevel::Read.into(),
&[],
)
.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 = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
)
.await?;
if repo.config.git_provider != "github.com"
|| repo.config.repo.is_empty()
{
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
clone_enabled: false,
pull_enabled: false,
build_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let clone_url =
format!("{host}/listener/github/repo/{}/clone", repo.id);
let pull_url =
format!("{host}/listener/github/repo/{}/pull", repo.id);
let build_url =
format!("{host}/listener/github/repo/{}/build", repo.id);
let mut clone_enabled = false;
let mut pull_enabled = false;
let mut build_enabled = false;
for webhook in webhooks {
if !webhook.active {
continue;
}
if webhook.config.url == clone_url {
clone_enabled = true
}
if webhook.config.url == pull_url {
pull_enabled = true
}
if webhook.config.url == build_url {
build_enabled = true
}
}
Ok(GetRepoWebhooksEnabledResponse {
managed: true,
clone_enabled,
pull_enabled,
build_enabled,
})
}
}

View File

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

View File

@@ -0,0 +1,878 @@
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 database::mungos::{
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
komodo_timestamp,
permission::PermissionLevel,
server::{
Server, ServerActionState, ServerListItem, ServerState,
TerminalInfo,
},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
},
};
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, get_system_info},
},
permission::get_check_permissions,
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,
PermissionLevel::Read.into(),
&[],
)
.await?;
let core_version = env!("CARGO_PKG_VERSION");
let mut res = GetServersSummaryResponse::default();
for server in servers {
res.total += 1;
match server.info.state {
ServerState::Ok => {
// Check for version mismatch
let has_version_mismatch = !server.info.version.is_empty()
&& server.info.version != "Unknown"
&& server.info.version != core_version;
if has_version_mismatch {
res.warning += 1;
} else {
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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let version = server_status_cache()
.get(&server.id)
.await
.map(|s| s.version.clone())
.unwrap_or(String::from("unknown"));
Ok(GetPeripheryVersionResponse { version })
}
}
impl Resolve<ReadArgs> for GetServer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Server> {
Ok(
get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetServerState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerStateResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.server
.get(&server.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetSystemInformation {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SystemInformation> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
get_system_info(&server).await.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for GetSystemStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSystemStatsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let status =
server_status_cache().get(&server.id).await.with_context(
|| format!("did not find status for server at {}", server.id),
)?;
let stats = status
.stats
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.processes(),
)
.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 = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.into(),
)
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(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,
PermissionLevel::Read.into(),
&[],
)
.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,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetDockerContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.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 = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.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 = get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
)
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
// first check deployments
if let Ok(deployment) =
resource::get::<Deployment>(&self.container).await
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
});
}
// then check stacks
let stacks =
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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect image: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)?
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot get image history: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
.into(),
);
}
let res = periphery_client(&server)?
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(projects) = &cache.projects {
Ok(projects.clone())
} else {
Ok(Vec::new())
}
}
}
#[derive(Default)]
struct TerminalCacheItem {
list: Vec<TerminalInfo>,
ttl: i64,
}
const TERMINAL_CACHE_TIMEOUT: i64 = 30_000;
#[derive(Default)]
struct TerminalCache(
std::sync::Mutex<
HashMap<String, Arc<tokio::sync::Mutex<TerminalCacheItem>>>,
>,
);
impl TerminalCache {
fn get_or_insert(
&self,
server_id: String,
) -> Arc<tokio::sync::Mutex<TerminalCacheItem>> {
if let Some(cached) =
self.0.lock().unwrap().get(&server_id).cloned()
{
return cached;
}
let to_cache =
Arc::new(tokio::sync::Mutex::new(TerminalCacheItem::default()));
self.0.lock().unwrap().insert(server_id, to_cache.clone());
to_cache
}
}
fn terminals_cache() -> &'static TerminalCache {
static TERMINALS: OnceLock<TerminalCache> = OnceLock::new();
TERMINALS.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for ListTerminals {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListTerminalsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
let cache = terminals_cache().get_or_insert(server.id.clone());
let mut cache = cache.lock().await;
if self.fresh || komodo_timestamp() > cache.ttl {
cache.list = periphery_client(&server)?
.request(periphery_client::api::terminal::ListTerminals {})
.await
.context("Failed to get fresh terminal list")?;
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
Ok(cache.list.clone())
} else {
Ok(cache.list.clone())
}
}
}

View File

@@ -0,0 +1,461 @@
use std::collections::HashSet;
use anyhow::{Context, anyhow};
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
docker::container::Container,
permission::PermissionLevel,
server::{Server, ServerState},
stack::{Stack, StackActionState, StackListItem, StackState},
},
};
use periphery_client::api::{
compose::{GetComposeLog, GetComposeLogSearch},
container::InspectContainer,
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::{periphery_client, query::get_all_tags},
permission::get_check_permissions,
resource,
stack::get_stack_and_server,
state::{
action_states, github_client, server_status_cache,
stack_status_cache,
},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetStack {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Stack> {
Ok(
get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListStackServices {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListStackServicesResponse> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.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.logs(),
true,
)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLog {
project: stack.project_name(false),
services,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?;
Ok(res)
}
}
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.logs(),
true,
)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLogSearch {
project: stack.project_name(false),
services,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectStackContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let InspectStackContainer { stack, service } = self;
let stack = get_check_permissions::<Stack>(
&stack,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if stack.config.server_id.is_empty() {
return Err(
anyhow!("Cannot inspect stack, not attached to any server")
.into(),
);
}
let server =
resource::get::<Server>(&stack.config.server_id).await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let services = &stack_status_cache()
.get(&stack.id)
.await
.unwrap_or_default()
.curr
.services;
let Some(name) = services
.iter()
.find(|s| s.service == service)
.and_then(|s| s.container.as_ref().map(|c| c.name.clone()))
else {
return Err(anyhow!(
"No service found matching '{service}'. Was the stack last deployed manually?"
).into());
};
let res = periphery_client(&server)?
.request(InspectContainer { name })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonStackExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?;
let stacks = if only_update_available {
stacks
.into_iter()
.filter(|stack| {
stack
.info
.services
.iter()
.any(|service| service.update_available)
})
.collect()
} else {
stacks
};
Ok(stacks)
}
}
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> 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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetStackActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<StackActionState> {
let stack = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.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,
PermissionLevel::Read.into(),
&[],
)
.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 = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
)
.await?;
if stack.config.git_provider != "github.com"
|| stack.config.repo.is_empty()
{
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
}
let mut split = stack.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
deploy_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let refresh_url =
format!("{host}/listener/github/stack/{}/refresh", stack.id);
let deploy_url =
format!("{host}/listener/github/stack/{}/deploy", stack.id);
let mut refresh_enabled = false;
let mut deploy_enabled = false;
for webhook in webhooks {
if webhook.active && webhook.config.url == refresh_url {
refresh_enabled = true
}
if webhook.active && webhook.config.url == deploy_url {
deploy_enabled = true
}
}
Ok(GetStackWebhooksEnabledResponse {
managed: true,
refresh_enabled,
deploy_enabled,
})
}
}

View File

@@ -0,0 +1,244 @@
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,
permission::get_check_permissions,
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(
get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
)
.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,
PermissionLevel::Read.into(),
&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,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetResourceSyncActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSyncActionState> {
let sync = get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
)
.await?;
let action_state = action_states()
.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,
PermissionLevel::Read.into(),
&[],
)
.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 = get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.git_provider != "github.com"
|| sync.config.repo.is_empty()
{
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
}
let mut split = sync.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
refresh_enabled: false,
sync_enabled: false,
});
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let refresh_url =
format!("{host}/listener/github/sync/{}/refresh", sync.id);
let sync_url =
format!("{host}/listener/github/sync/{}/sync", sync.id);
let mut refresh_enabled = false;
let mut sync_enabled = false;
for webhook in webhooks {
if webhook.active && webhook.config.url == refresh_url {
refresh_enabled = true
}
if webhook.active && webhook.config.url == sync_url {
sync_enabled = true
}
}
Ok(GetSyncWebhooksEnabledResponse {
managed: true,
refresh_enabled,
sync_enabled,
})
}
}

View File

@@ -0,0 +1,33 @@
use anyhow::Context;
use database::mongo_indexed::doc;
use database::mungos::{
find::find_collect, mongodb::options::FindOptions,
};
use komodo_client::{
api::read::{GetTag, ListTags},
entities::tag::Tag,
};
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)
}
}

View File

@@ -0,0 +1,516 @@
use anyhow::Context;
use database::mungos::find::find_collect;
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, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, user::User,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::{
get_all_tags, get_id_to_tags, get_user_user_group_ids,
},
permission::get_check_permissions,
resource,
state::db_client,
sync::{
toml::{ToToml, convert_resource},
user_groups::{convert_user_groups, user_group_to_toml},
variables::variable_to_toml,
},
};
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_full_for_user::<Alerter>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Alerter(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Builder>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Builder(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Server>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Stack>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Deployment>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Build>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Build(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Repo>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Repo(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Procedure>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Action>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
// These will already be filtered by [ExportResourcesToToml]
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
Ok(targets)
}
impl Resolve<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 id_to_tags = get_id_to_tags(None).await?;
let ReadArgs { user } = args;
for target in targets {
match target {
ResourceTarget::Alerter(id) => {
let mut alerter = get_check_permissions::<Alerter>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Alerter::replace_ids(&mut alerter);
res.alerters.push(convert_resource::<Alerter>(
alerter,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
vec![],
&id_to_tags,
))
}
}
ResourceTarget::Server(id) => {
let mut server = get_check_permissions::<Server>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Server::replace_ids(&mut server);
res.servers.push(convert_resource::<Server>(
server,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Builder(id) => {
let mut builder = get_check_permissions::<Builder>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Builder::replace_ids(&mut builder);
res.builders.push(convert_resource::<Builder>(
builder,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Build(id) => {
let mut build = get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Build::replace_ids(&mut build);
res.builds.push(convert_resource::<Build>(
build,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Deployment(id) => {
let mut deployment = get_check_permissions::<Deployment>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Deployment::replace_ids(&mut deployment);
res.deployments.push(convert_resource::<Deployment>(
deployment,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Repo(id) => {
let mut repo = get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Repo::replace_ids(&mut repo);
res.repos.push(convert_resource::<Repo>(
repo,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Stack(id) => {
let mut stack = get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Stack::replace_ids(&mut stack);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
vec![],
&id_to_tags,
))
}
ResourceTarget::Procedure(id) => {
let mut procedure = get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Procedure::replace_ids(&mut procedure);
res.procedures.push(convert_resource::<Procedure>(
procedure,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::Action(id) => {
let mut action = get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Action::replace_ids(&mut action);
res.actions.push(convert_resource::<Action>(
action,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::System(_) => continue,
};
}
add_user_groups(user_groups, &mut res, 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,
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, &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 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_to_toml(variable)?);
}
for user_group in resources.user_groups {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str(&user_group_to_toml(user_group)?);
}
Ok(toml)
}

View File

@@ -0,0 +1,314 @@
use std::collections::HashMap;
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::find_one_by_id,
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{
ResourceTarget,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
stack::Stack,
sync::ResourceSync,
update::{Update, UpdateListItem},
user::User,
},
};
use resolver_api::Resolve;
use crate::{
config::core_config,
permission::{get_check_permissions, get_resource_ids_for_user},
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 = get_resource_ids_for_user::<Server>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Server", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Server" });
let deployment_query =
get_resource_ids_for_user::<Deployment>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Deployment", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
let stack_query = get_resource_ids_for_user::<Stack>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Stack", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let build_query = get_resource_ids_for_user::<Build>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Build", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Build" });
let repo_query = get_resource_ids_for_user::<Repo>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Repo", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let procedure_query =
get_resource_ids_for_user::<Procedure>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Procedure", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query = get_resource_ids_for_user::<Action>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Action", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query = get_resource_ids_for_user::<Builder>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Builder", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query = get_resource_ids_for_user::<Alerter>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Alerter", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let resource_sync_query = get_resource_ids_for_user::<
ResourceSync,
>(user)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let mut query = self.query.unwrap_or_default();
query.extend(doc! {
"$or": [
server_query,
deployment_query,
stack_query,
build_query,
repo_query,
procedure_query,
action_query,
alerter_query,
builder_query,
resource_sync_query,
]
});
query.into()
};
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) => {
get_check_permissions::<Server>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Deployment(id) => {
get_check_permissions::<Deployment>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Build(id) => {
get_check_permissions::<Build>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Repo(id) => {
get_check_permissions::<Repo>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Builder(id) => {
get_check_permissions::<Builder>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Alerter(id) => {
get_check_permissions::<Alerter>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Procedure(id) => {
get_check_permissions::<Procedure>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Action(id) => {
get_check_permissions::<Action>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::ResourceSync(id) => {
get_check_permissions::<ResourceSync>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
ResourceTarget::Stack(id) => {
get_check_permissions::<Stack>(
id,
user,
PermissionLevel::Read.into(),
)
.await?;
}
}
Ok(update)
}
}

View File

@@ -0,0 +1,137 @@
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::find_one_by_id,
find::find_collect,
mongodb::{bson::doc, options::FindOptions},
};
use komodo_client::{
api::read::{
FindUser, FindUserResponse, GetUsername, GetUsernameResponse,
ListApiKeys, ListApiKeysForServiceUser,
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
ListUsers, ListUsersResponse,
},
entities::user::{UserConfig, admin_service_user},
};
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)
}
}

View File

@@ -0,0 +1,60 @@
use std::str::FromStr;
use anyhow::Context;
use database::mungos::{
find::find_collect,
mongodb::{
bson::{Document, doc, oid::ObjectId},
options::FindOptions,
},
};
use komodo_client::api::read::*;
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)
}
}

View File

@@ -0,0 +1,53 @@
use anyhow::Context;
use database::mongo_indexed::doc;
use database::mungos::{
find::find_collect, mongodb::options::FindOptions,
};
use komodo_client::api::read::*;
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)
}
}

View File

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

213
bin/core/src/api/user.rs Normal file
View File

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

View File

@@ -0,0 +1,70 @@
use komodo_client::{
api::write::*,
entities::{
action::Action, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, 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, .. } = get_check_permissions::<Action>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.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?)
}
}

View File

@@ -0,0 +1,76 @@
use komodo_client::{
api::write::*,
entities::{
alerter::Alerter, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, 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, .. } = get_check_permissions::<Alerter>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.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?)
}
}

View File

@@ -0,0 +1,735 @@
use std::{path::PathBuf, str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use database::mongo_indexed::doc;
use database::mungos::mongodb::bson::to_document;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
FileContents, NoData, Operation, RepoExecutionArgs,
all_logs_success,
build::{Build, BuildInfo, PartialBuildConfig},
builder::{Builder, BuilderConfig},
config::core::CoreConfig,
permission::PermissionLevel,
repo::Repo,
server::ServerState,
update::Update,
},
};
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},
},
permission::get_check_permissions,
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, .. } = get_check_permissions::<Build>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.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 = get_check_permissions::<Build>(
&self.build,
&args.user,
PermissionLevel::Write.into(),
)
.await?;
if !build.config.files_on_host
&& build.config.repo.is_empty()
&& build.config.linked_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 repo_args: RepoExecutionArgs = if !build
.config
.files_on_host
&& !build.config.linked_repo.is_empty()
{
(&crate::resource::get::<Repo>(&build.config.linked_repo).await?)
.into()
} else {
(&build).into()
};
let root = repo_args.unique_path(&core_config().repo_directory)?;
repo_args.destination = Some(root.display().to_string());
let build_path = build
.config
.build_path
.parse::<PathBuf>()
.context("Invalid build path")?;
let dockerfile_path = build
.config
.dockerfile_path
.parse::<PathBuf>()
.context("Invalid dockerfile path")?;
let full_path = root.join(&build_path).join(&dockerfile_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize dockerfile parent directory {parent:?}"
)
})?;
}
let access_token = if let Some(account) = &repo_args.account {
git_token(&repo_args.provider, account, |https| repo_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", repo_args.provider),
)?
} else {
None
};
// Ensure the folder is initialized as git repo.
// This allows a new file to be committed on a branch that may not exist.
if !root.join(".git").exists() {
git::init_folder_as_repo(
&root,
&repo_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);
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
&core_config().repo_directory,
access_token,
)
.await
.context("Failed to pull latest changes before commit")
{
Ok((res, _)) => update.logs.extend(res.logs),
Err(e) => {
update.push_error_log("Pull Repo", format_serror(&e.into()));
update.finalize();
return Ok(update);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})
{
update
.push_error_log("Write Dockerfile", format_serror(&e.into()));
} else {
update.push_simple_log(
"Write Dockerfile",
format!("File written to {full_path:?}"),
);
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
let commit_res = git::commit_file(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&branch,
)
.await;
update.logs.extend(commit_res.logs);
if let Err(e) = (RefreshBuildCache { build: build.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
impl Resolve<WriteArgs> for RefreshBuildCache {
#[instrument(
name = "RefreshBuildCache",
level = "debug",
skip(user)
)]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// build should be able to do this.
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
)
.await?;
let repo = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&build.config.linked_repo)
.await?
.into()
} else {
None
};
let (
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
) = if build.config.files_on_host {
// =============
// FILES ON HOST
// =============
match get_on_host_dockerfile(&build).await {
Ok(FileContents { path, contents }) => {
(Some(path), Some(contents), None, None, None)
}
Err(e) => {
(None, None, Some(format_serror(&e.into())), None, None)
}
}
} else if let Some(repo) = &repo {
let Some(res) = get_git_remote(&build, repo.into()).await?
else {
// Nothing to do here
return Ok(NoData {});
};
res
} else if !build.config.repo.is_empty() {
let Some(res) = get_git_remote(&build, (&build).into()).await?
else {
// Nothing to do here
return Ok(NoData {});
};
res
} else {
// =============
// UI BASED FILE
// =============
(None, None, None, None, None)
};
let info = BuildInfo {
last_built_at: build.info.last_built_at,
built_hash: build.info.built_hash,
built_message: build.info.built_message,
built_contents: build.info.built_contents,
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
};
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
}
async fn get_git_remote(
build: &Build,
mut clone_args: RepoExecutionArgs,
) -> anyhow::Result<
Option<(
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
)>,
> {
if clone_args.provider.is_empty() {
// Nothing to do here
return Ok(None);
}
let config = core_config();
let repo_path = clone_args.unique_path(&config.repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
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 (res, _) = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
)
.await
.context("failed to clone build repo")?;
let relative_path = PathBuf::from_str(&build.config.build_path)
.context("Invalid build path")?
.join(&build.config.dockerfile_path);
let full_path = repo_path.join(&relative_path);
let (contents, error) =
match fs::read_to_string(&full_path).await.with_context(|| {
format!("Failed to read dockerfile contents at {full_path:?}")
}) {
Ok(contents) => (Some(contents), None),
Err(e) => (None, Some(format_serror(&e.into()))),
};
Ok(Some((
Some(relative_path.display().to_string()),
contents,
error,
res.commit_hash,
res.commit_message,
)))
}
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<CreateBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let WriteArgs { user } = args;
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
)
.await?;
if build.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if build.config.webhook_secret.is_empty() {
webhook_secret
} else {
&build.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo, &request)
.await
.context("failed to create webhook")?;
if !build.config.webhook_enabled {
UpdateBuild {
id: build.id,
config: PartialBuildConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update build to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteBuildWebhook {
#[instrument(name = "DeleteBuildWebhook", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let build = get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
)
.await?;
if build.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if build.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't delete webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
split.next().context("Build repo has no repo after the /")?;
let github_repos = github.repos();
let webhooks = github_repos
.list_all_webhooks(owner, repo)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = format!("{host}/listener/github/build/{}", build.id);
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
}

View File

@@ -0,0 +1,76 @@
use komodo_client::{
api::write::*,
entities::{
builder::Builder, permission::PermissionLevel, update::Update,
},
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, 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, .. } = get_check_permissions::<Builder>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.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?)
}
}

View File

@@ -0,0 +1,263 @@
use anyhow::{Context, anyhow};
use database::mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use komodo_client::{
api::write::*,
entities::{
Operation,
deployment::{
Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode,
},
docker::container::RestartPolicyNameEnum,
komodo_timestamp,
permission::PermissionLevel,
server::{Server, ServerState},
to_container_compatible_name,
update::Update,
},
};
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},
},
permission::get_check_permissions,
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, .. } =
get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect().attach(),
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let container = periphery_client(&server)?
.request(InspectContainer {
name: self.name.clone(),
})
.await
.context("Failed to inspect container")?;
let mut config = PartialDeploymentConfig {
server_id: server.id.into(),
..Default::default()
};
if let Some(container_config) = container.config {
config.image = container_config
.image
.map(|image| DeploymentImage::Image { image });
config.command = container_config.cmd.join(" ").into();
config.environment = container_config
.env
.into_iter()
.map(|env| format!(" {env}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.labels = container_config
.labels
.into_iter()
.map(|(key, val)| format!(" {key}: {val}"))
.collect::<Vec<_>>()
.join("\n")
.into();
}
if let Some(host_config) = container.host_config {
config.volumes = host_config
.binds
.into_iter()
.map(|bind| format!(" {bind}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.network = host_config.network_mode;
config.ports = host_config
.port_bindings
.into_iter()
.filter_map(|(container, mut host)| {
let host = host.pop()?.host_port?;
Some(format!(" {host}:{}", container.replace("/tcp", "")))
})
.collect::<Vec<_>>()
.join("\n")
.into();
config.restart = host_config.restart_policy.map(|restart| {
match restart.name {
RestartPolicyNameEnum::Always => RestartMode::Always,
RestartPolicyNameEnum::No
| RestartPolicyNameEnum::Empty => RestartMode::NoRestart,
RestartPolicyNameEnum::UnlessStopped => {
RestartMode::UnlessStopped
}
RestartPolicyNameEnum::OnFailure => RestartMode::OnFailure,
}
});
}
Ok(
resource::create::<Deployment>(&self.name, config, user)
.await?,
)
}
}
impl Resolve<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 = get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.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_container_compatible_name(&self.name);
let container_state =
get_deployment_state(&deployment.id).await?;
if container_state == DeploymentState::Unknown {
return Err(
anyhow!(
"Cannot rename Deployment when container status is unknown"
)
.into(),
);
}
let mut update =
make_update(&deployment, Operation::RenameDeployment, user);
update_one_by_id(
&db_client().deployments,
&deployment.id,
database::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)
}
}

View File

@@ -0,0 +1,256 @@
use std::time::Instant;
use anyhow::Context;
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::write::*, entities::user::User};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::auth::auth_request;
use super::Variant;
mod action;
mod alerter;
mod build;
mod builder;
mod deployment;
mod permissions;
mod procedure;
mod provider;
mod repo;
mod resource;
mod server;
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 ====
CreateLocalUser(CreateLocalUser),
UpdateUserUsername(UpdateUserUsername),
UpdateUserPassword(UpdateUserPassword),
DeleteUser(DeleteUser),
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER GROUP ====
CreateUserGroup(CreateUserGroup),
RenameUserGroup(RenameUserGroup),
DeleteUserGroup(DeleteUserGroup),
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
SetEveryoneUserGroup(SetEveryoneUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
UpdateUserBasePermissions(UpdateUserBasePermissions),
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
// ==== RESOURCE ====
UpdateResourceMeta(UpdateResourceMeta),
// ==== SERVER ====
CreateServer(CreateServer),
CopyServer(CopyServer),
DeleteServer(DeleteServer),
UpdateServer(UpdateServer),
RenameServer(RenameServer),
CreateNetwork(CreateNetwork),
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
// ==== STACK ====
CreateStack(CreateStack),
CopyStack(CopyStack),
DeleteStack(DeleteStack),
UpdateStack(UpdateStack),
RenameStack(RenameStack),
WriteStackFileContents(WriteStackFileContents),
RefreshStackCache(RefreshStackCache),
CreateStackWebhook(CreateStackWebhook),
DeleteStackWebhook(DeleteStackWebhook),
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
CopyDeployment(CopyDeployment),
CreateDeploymentFromContainer(CreateDeploymentFromContainer),
DeleteDeployment(DeleteDeployment),
UpdateDeployment(UpdateDeployment),
RenameDeployment(RenameDeployment),
// ==== 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),
// ==== 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),
// ==== TAG ====
CreateTag(CreateTag),
DeleteTag(DeleteTag),
RenameTag(RenameTag),
UpdateTagColor(UpdateTagColor),
// ==== 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))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: WriteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<WriteRequest>,
) -> serror::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let res = tokio::spawn(task(req_id, request, user))
.await
.context("failure in spawned task");
res?
}
#[instrument(
name = "WriteRequest",
skip(user, request),
fields(
user_id = user.id,
request = format!("{:?}", request.extract_variant())
)
)]
async fn task(
req_id: Uuid,
request: WriteRequest,
user: User,
) -> serror::Result<axum::response::Response> {
info!("/write request | user: {}", user.username);
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)
}

View File

@@ -0,0 +1,444 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::{find_one_by_id, update_one_by_id},
mongodb::{
bson::{Document, doc, oid::ObjectId, to_bson},
options::UpdateOptions,
},
};
use komodo_client::{
api::write::*,
entities::{
ResourceTarget, ResourceTargetVariant,
permission::{UserTarget, UserTargetVariant},
},
};
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> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let UpdateUserBasePermissions {
user_id,
enabled,
create_servers,
create_builds,
} = self;
let user = find_one_by_id(&db_client().users, &user_id)
.await
.context("failed to query mongo for user")?
.context("did not find user with given id")?;
if user.super_admin {
return Err(
anyhow!(
"Cannot use this method to update super admins permissions"
)
.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,
database::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> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let Self {
user_target,
resource_type,
permission,
} = self;
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if user.admin {
return Err(
anyhow!(
"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 filter = doc! { "_id": id };
let field = format!("all.{resource_type}");
let set =
to_bson(&permission).context("permission is not Bson")?;
let update = doc! { "$set": { &field: &set } };
match user_target_variant {
UserTargetVariant::User => {
db_client()
.users
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {set} on db")
})?;
}
UserTargetVariant::UserGroup => {
db_client()
.user_groups
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {set} 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> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let UpdatePermissionOnTarget {
user_target,
resource_target,
permission,
} = self;
// Some extra checks relevant if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if !user.enabled {
return Err(anyhow!("user not enabled").into());
}
if user.admin {
return Err(
anyhow!(
"cannot use this method to update other admins permissions"
)
.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());
let specific = to_bson(&permission.specific)
.context("permission.specific is not valid Bson")?;
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.level.as_ref(),
"specific": specific
}
},
)
.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::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))
}
}
}

View File

@@ -0,0 +1,80 @@
use komodo_client::{
api::write::*,
entities::{
permission::PermissionLevel, procedure::Procedure, update::Update,
},
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, 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, .. } =
get_check_permissions::<Procedure>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.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?)
}
}

View File

@@ -0,0 +1,410 @@
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},
mongodb::bson::{doc, to_document},
};
use komodo_client::{
api::write::*,
entities::{
Operation, ResourceTarget,
provider::{DockerRegistryAccount, GitProviderAccount},
},
};
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
&& domain.is_empty()
{
return Err(
anyhow!("cannot update git provider with empty domain")
.into(),
);
}
if let Some(username) = &self.account.username
&& 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
&& domain.is_empty()
{
return Err(
anyhow!(
"cannot update docker registry account with empty domain"
)
.into(),
);
}
if let Some(username) = &self.account.username
&& 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)
}
}

View File

@@ -0,0 +1,454 @@
use anyhow::{Context, anyhow};
use database::mongo_indexed::doc;
use database::mungos::{
by_id::update_one_by_id, mongodb::bson::to_document,
};
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
NoData, Operation, RepoExecutionArgs,
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
repo::{PartialRepoConfig, Repo, RepoInfo},
server::Server,
to_path_compatible_name,
update::{Log, Update},
},
};
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},
},
permission::get_check_permissions,
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, .. } = get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.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 = get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.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_path_compatible_name(&self.name);
let mut update = make_update(&repo, Operation::RenameRepo, user);
update_one_by_id(
&db_client().repos,
&repo.id,
database::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_path_compatible_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 = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
)
.await?;
if repo.config.git_provider.is_empty()
|| repo.config.repo.is_empty()
{
// Nothing to do
return Ok(NoData {});
}
let mut clone_args: RepoExecutionArgs = (&repo).into();
let repo_path =
clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
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 (res, _) = git::pull_or_clone(
clone_args,
&core_config().repo_directory,
access_token,
)
.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: res.commit_hash,
latest_message: res.commit_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 = get_check_permissions::<Repo>(
&self.repo,
&args.user,
PermissionLevel::Write.into(),
)
.await?;
if repo.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
webhook_secret,
..
} = core_config();
let webhook_secret = if repo.config.webhook_secret.is_empty() {
webhook_secret
} else {
&repo.config.webhook_secret
};
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
RepoWebhookAction::Pull => {
format!("{host}/listener/github/repo/{}/pull", repo.id)
}
RepoWebhookAction::Build => {
format!("{host}/listener/github/repo/{}/build", repo.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
return Ok(NoData {});
}
}
// Now good to create the webhook
let request = ReposCreateWebhookRequest {
active: Some(true),
config: Some(ReposCreateWebhookRequestConfig {
url,
secret: webhook_secret.to_string(),
content_type: String::from("json"),
insecure_ssl: None,
digest: Default::default(),
token: Default::default(),
}),
events: vec![String::from("push")],
name: String::from("web"),
};
github_repos
.create_webhook(owner, repo_name, &request)
.await
.context("failed to create webhook")?;
if !repo.config.webhook_enabled {
UpdateRepo {
id: repo.id,
config: PartialRepoConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update repo to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteRepoWebhook {
#[instrument(name = "DeleteRepoWebhook", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Write.into(),
)
.await?;
if repo.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if repo.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
split.next().context("Repo repo has no repo after the /")?;
let github_repos = github.repos();
// First make sure the webhook isn't already created (inactive ones are ignored)
let webhooks = github_repos
.list_all_webhooks(owner, repo_name)
.await
.context("failed to list all webhooks on repo")?
.body;
let CoreConfig {
host,
webhook_base_url,
..
} = core_config();
let host = if webhook_base_url.is_empty() {
host
} else {
webhook_base_url
};
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
RepoWebhookAction::Pull => {
format!("{host}/listener/github/repo/{}/pull", repo.id)
}
RepoWebhookAction::Build => {
format!("{host}/listener/github/repo/{}/build", repo.id)
}
};
for webhook in webhooks {
if webhook.active && webhook.config.url == url {
github_repos
.delete_webhook(owner, repo_name, webhook.id)
.await
.context("failed to delete webhook")?;
return Ok(NoData {});
}
}
// No webhook to delete, all good
Ok(NoData {})
}
}

View File

@@ -0,0 +1,68 @@
use anyhow::anyhow;
use komodo_client::{
api::write::{UpdateResourceMeta, UpdateResourceMetaResponse},
entities::{
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment, procedure::Procedure,
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
},
};
use resolver_api::Resolve;
use crate::resource::{self, ResourceMetaUpdate};
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateResourceMeta {
#[instrument(name = "UpdateResourceMeta", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<UpdateResourceMetaResponse> {
let meta = ResourceMetaUpdate {
description: self.description,
template: self.template,
tags: self.tags,
};
match self.target {
ResourceTarget::System(_) => {
return Err(
anyhow!("cannot update meta of System resource target")
.into(),
);
}
ResourceTarget::Server(id) => {
resource::update_meta::<Server>(&id, meta, args).await?;
}
ResourceTarget::Deployment(id) => {
resource::update_meta::<Deployment>(&id, meta, args).await?;
}
ResourceTarget::Build(id) => {
resource::update_meta::<Build>(&id, meta, args).await?;
}
ResourceTarget::Repo(id) => {
resource::update_meta::<Repo>(&id, meta, args).await?;
}
ResourceTarget::Builder(id) => {
resource::update_meta::<Builder>(&id, meta, args).await?;
}
ResourceTarget::Alerter(id) => {
resource::update_meta::<Alerter>(&id, meta, args).await?;
}
ResourceTarget::Procedure(id) => {
resource::update_meta::<Procedure>(&id, meta, args).await?;
}
ResourceTarget::Action(id) => {
resource::update_meta::<Action>(&id, meta, args).await?;
}
ResourceTarget::ResourceSync(id) => {
resource::update_meta::<ResourceSync>(&id, meta, args)
.await?;
}
ResourceTarget::Stack(id) => {
resource::update_meta::<Stack>(&id, meta, args).await?;
}
}
Ok(UpdateResourceMetaResponse {})
}
}

View File

@@ -0,0 +1,203 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
NoData, Operation,
permission::PermissionLevel,
server::Server,
to_docker_compatible_name,
update::{Update, UpdateStatus},
},
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{
periphery_client,
update::{add_update, make_update, update_update},
},
permission::get_check_permissions,
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 CopyServer {
#[instrument(name = "CopyServer", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
let Server { config, .. } = get_check_permissions::<Server>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
Ok(
resource::create::<Server>(&self.name, config.into(), 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 = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.into(),
)
.await?;
let periphery = periphery_client(&server)?;
let mut update =
make_update(&server, Operation::CreateNetwork, user);
update.status = UpdateStatus::InProgress;
update.id = add_update(update.clone()).await?;
match periphery
.request(api::network::CreateNetwork {
name: to_docker_compatible_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)
}
}
impl Resolve<WriteArgs> for CreateTerminal {
#[instrument(name = "CreateTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::CreateTerminal {
name: self.name,
command: self.command,
recreate: self.recreate,
})
.await
.context("Failed to create terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteTerminal {
#[instrument(name = "DeleteTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteTerminal {
terminal: self.terminal,
})
.await
.context("Failed to delete terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteAllTerminals {
#[instrument(name = "DeleteAllTerminals", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteAllTerminals {})
.await
.context("Failed to delete all terminals on periphery")?;
Ok(NoData {})
}
}

View File

@@ -0,0 +1,157 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::find_one_by_id,
mongodb::bson::{doc, oid::ObjectId},
};
use komodo_client::{
api::{user::CreateApiKey, write::*},
entities::{
komodo_timestamp,
user::{User, UserConfig},
},
};
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 {})
}
}

View File

@@ -0,0 +1,783 @@
use std::path::PathBuf;
use anyhow::{Context, anyhow};
use database::mungos::mongodb::bson::{doc, to_document};
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
FileContents, NoData, Operation, RepoExecutionArgs,
all_logs_success,
config::core::CoreConfig,
permission::PermissionLevel,
repo::Repo,
server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo},
update::Update,
user::stack_user,
},
};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::api::compose::{
GetComposeContentsOnHost, GetComposeContentsOnHostResponse,
WriteComposeContentsToHost,
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::{
periphery_client,
query::get_server_with_state,
stack_git_token,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
stack::{
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, .. } = get_check_permissions::<Stack>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.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 stack = get_check_permissions::<Stack>(
&stack,
user,
PermissionLevel::Write.into(),
)
.await?;
if !stack.config.files_on_host
&& stack.config.repo.is_empty()
&& stack.config.linked_repo.is_empty()
{
return Err(anyhow!(
"Stack is not configured to use Files on Host, Git Repo, or Linked 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);
if stack.config.files_on_host {
write_stack_file_contents_on_host(
stack, file_path, contents, update,
)
.await
} else {
write_stack_file_contents_git(
stack,
&file_path,
&contents,
&user.username,
update,
)
.await
}
}
}
async fn write_stack_file_contents_on_host(
stack: Stack,
file_path: String,
contents: String,
mut update: Update,
) -> serror::Result<Update> {
if stack.config.server_id.is_empty() {
return Err(anyhow!(
"Cannot write file, Files on host Stack has not configured a Server"
).into());
}
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
if state != ServerState::Ok {
return Err(
anyhow!(
"Cannot write file when server is unreachable or disabled"
)
.into(),
);
}
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()),
);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
// Finish with a cache refresh
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)
}
async fn write_stack_file_contents_git(
mut stack: Stack,
file_path: &str,
contents: &str,
username: &str,
mut update: Update,
) -> serror::Result<Update> {
let mut repo = if !stack.config.linked_repo.is_empty() {
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
let git_token = stack_git_token(&mut stack, repo.as_mut()).await?;
let mut repo_args: RepoExecutionArgs = if let Some(repo) = &repo {
repo.into()
} else {
(&stack).into()
};
let root = repo_args.unique_path(&core_config().repo_directory)?;
repo_args.destination = Some(root.display().to_string());
let file_path = stack
.config
.run_directory
.parse::<PathBuf>()
.context("Run directory is not a valid path")?
.join(file_path);
let full_path =
root.join(&file_path).components().collect::<PathBuf>();
if let Some(parent) = full_path.parent() {
tokio::fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize stack file 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() {
git::init_folder_as_repo(
&root,
&repo_args,
git_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);
}
}
// Save this for later -- repo_args moved next.
let branch = repo_args.branch.clone();
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
repo_args,
&core_config().repo_directory,
git_token,
)
.await
.context("Failed to pull latest changes before commit")
{
Ok((res, _)) => update.logs.extend(res.logs),
Err(e) => {
update.push_error_log("Pull Repo", format_serror(&e.into()));
update.finalize();
return Ok(update);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) = tokio::fs::write(&full_path, &contents)
.await
.with_context(|| {
format!(
"Failed to write compose file contents to {full_path:?}"
)
})
{
update.push_error_log("Write File", format_serror(&e.into()));
} else {
update.push_simple_log(
"Write File",
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!("{username}: Write Stack File"),
&root,
&file_path,
&branch,
)
.await;
update.logs.extend(commit_res.logs);
// Finish with a cache refresh
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 = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Execute.into(),
)
.await?;
let repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
let file_contents_empty = stack.config.file_contents.is_empty();
let repo_empty =
stack.config.repo.is_empty() && repo.as_ref().is_none();
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.all_file_dependencies(),
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 {
// Don't include additional files in service parsing
if !stack.is_compose_file(&contents.path) {
continue;
}
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,
repo.as_ref(),
Some(&mut missing_files),
)
.await?;
let project_name = stack.project_name(true);
let mut services = Vec::new();
for contents in &remote_contents {
// Don't include additional files in service parsing
if !stack.is_compose_file(&contents.path) {
continue;
}
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")?;
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 = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write.into(),
)
.await?;
if stack.config.repo.is_empty() {
return Err(
anyhow!("No repo configured, can't create webhook").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 = get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write.into(),
)
.await?;
if stack.config.git_provider != "github.com" {
return Err(
anyhow!("Can only manage github.com repo webhooks").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

View File

@@ -0,0 +1,128 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::{delete_one_by_id, update_one_by_id},
mongodb::bson::{doc, oid::ObjectId},
};
use komodo_client::{
api::write::{CreateTag, DeleteTag, RenameTag, UpdateTagColor},
entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, stack::Stack, sync::ResourceSync, tag::Tag,
},
};
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: self.color.unwrap_or_default(),
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::<Stack>(&self.id),
resource::remove_tag_from_all::<Deployment>(&self.id),
resource::remove_tag_from_all::<Build>(&self.id),
resource::remove_tag_from_all::<Repo>(&self.id),
resource::remove_tag_from_all::<Procedure>(&self.id),
resource::remove_tag_from_all::<Action>(&self.id),
resource::remove_tag_from_all::<ResourceSync>(&self.id),
resource::remove_tag_from_all::<Builder>(&self.id),
resource::remove_tag_from_all::<Alerter>(&self.id),
)?;
delete_one_by_id(&db_client().tags, &self.id, None).await?;
Ok(tag)
}
}

View File

@@ -0,0 +1,233 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms;
use database::{
hash_password,
mungos::mongodb::bson::{doc, oid::ObjectId},
};
use komodo_client::{
api::write::*,
entities::{
NoData,
user::{User, UserConfig},
},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCodeError;
use crate::{config::core_config, state::db_client};
use super::WriteArgs;
//
impl Resolve<WriteArgs> for CreateLocalUser {
#[instrument(name = "CreateLocalUser", skip(admin, self), fields(admin_id = admin.id, username = self.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<CreateLocalUserResponse> {
if !admin.admin {
return Err(
anyhow!("This method is admin-only.")
.status_code(StatusCode::UNAUTHORIZED),
);
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty.").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.").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 ts = unix_timestamp_ms() as i64;
let hashed_password = hash_password(self.password)?;
let mut user = User {
id: Default::default(),
username: self.username,
enabled: true,
admin: false,
super_admin: false,
create_server_permissions: false,
create_build_permissions: false,
updated_at: ts,
last_update_view: 0,
recents: Default::default(),
all: Default::default(),
config: UserConfig::Local {
password: hashed_password,
},
};
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();
user.sanitize();
Ok(user)
}
}
//
impl Resolve<WriteArgs> for UpdateUserUsername {
#[instrument(name = "UpdateUserUsername", skip(user), fields(user_id = user.id))]
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());
}
if ObjectId::from_str(&self.username).is_ok() {
return Err(
anyhow!("Username cannot be valid ObjectId").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 {
#[instrument(name = "UpdateUserPassword", skip(user, self), fields(user_id = user.id))]
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(),
);
}
}
db_client().set_user_password(user, &self.password).await?;
Ok(NoData {})
}
}
//
impl Resolve<WriteArgs> for DeleteUser {
#[instrument(name = "DeleteUser", skip(admin), fields(user = self.user))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<DeleteUserResponse> {
if !admin.admin {
return Err(
anyhow!("This method is admin-only.")
.status_code(StatusCode::UNAUTHORIZED),
);
}
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")?;
// Also remove user id from all user groups
if let Err(e) = db
.user_groups
.update_many(doc! {}, doc! { "$pull": { "users": &user.id } })
.await
{
warn!("Failed to remove deleted user from user groups | {e:?}");
};
Ok(user)
}
}

View File

@@ -0,0 +1,279 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, anyhow};
use database::mungos::{
by_id::{delete_one_by_id, find_one_by_id, update_one_by_id},
find::find_collect,
mongodb::bson::{doc, oid::ObjectId},
};
use komodo_client::{
api::write::*,
entities::{komodo_timestamp, user_group::UserGroup},
};
use resolver_api::Resolve;
use crate::state::db_client;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateUserGroup {
#[instrument(name = "CreateUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let user_group = UserGroup {
name: self.name,
id: Default::default(),
everyone: Default::default(),
users: Default::default(),
all: Default::default(),
updated_at: komodo_timestamp(),
};
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 {
#[instrument(name = "RenameUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
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 {
#[instrument(name = "DeleteUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let 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 {
#[instrument(name = "AddUserToUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let filter = match ObjectId::from_str(&self.user) {
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 {
#[instrument(name = "RemoveUserFromUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let filter = match ObjectId::from_str(&self.user) {
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 {
#[instrument(name = "SetUsersInUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let 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)
}
}
impl Resolve<WriteArgs> for SetEveryoneUserGroup {
#[instrument(name = "SetEveryoneUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &self.user_group },
};
db.user_groups
.update_one(
filter.clone(),
doc! { "$set": { "everyone": self.everyone } },
)
.await
.context("failed to set everyone on user group")?;
let res = db
.user_groups
.find_one(filter)
.await
.context("failed to query db for UserGroups")?
.context("no user group with given id")?;
Ok(res)
}
}

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