Compare commits

...

105 Commits

Author SHA1 Message Date
mbecker20
51b16434a5 docs new organization 2025-02-08 19:21:31 -08:00
mbecker20
f49d73dba3 fix new compose images 2025-02-08 19:21:19 -08:00
mbecker20
62723fa32f more legible favicon 2025-02-08 19:18:04 -08:00
mbecker20
d1d17626b2 fix login screen logo 2025-02-08 19:00:08 -08:00
mbecker20
312f51e50b dev-1 2025-02-08 18:45:32 -08:00
mbecker20
d68ea5cf45 remove example from cargo toml workspace 2025-02-08 18:45:32 -08:00
mbecker20
71fc0bba97 mbecker20 -> moghtech 2025-02-08 18:45:32 -08:00
Maxwell Becker
1a7a0299de Remove .git from remote_url (#299)
Remove .git from remote_url

Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>
2025-02-08 18:45:32 -08:00
unsync
cd59da100f feature: interpolate secrets in custom alerter (#289)
* feature: interpolate secrets in custom alerter

* fix rust warning

* review: sanitize errors

* review: sanitize error message
2025-02-08 18:45:32 -08:00
unsync
5a8ed8b81d feature: add post_deploy command (#288)
* feature: add post_deploy command

* review: do not run post_deploy if deploy failed
2025-02-08 18:45:32 -08:00
mbecker20
4c9479a8bc 1.17.0-dev 2025-02-08 18:45:32 -08:00
unsync
19aa5fb260 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()
2025-02-08 18:45:32 -08:00
mbecker20
75c0c967ac update available deployment table 2025-02-08 18:45:32 -08:00
mbecker20
8832a05ffe show update available stack table 2025-02-08 18:45:32 -08:00
mbecker20
7598978cd1 finish oidc comment 2025-02-08 18:45:32 -08:00
mbecker20
b868ad5794 clean up rust client websocket subscription 2025-02-08 18:45:32 -08:00
mbecker20
be6a0db511 escape incoming sync backslashes (BREAKING) 2025-02-08 18:45:32 -08:00
mbecker20
c360289984 rename Test Alerter button 2025-02-08 18:45:32 -08:00
mbecker20
23abdb85b0 simplify network stats 2025-02-08 18:45:32 -08:00
mbecker20
b510d6dc41 komodo-logo 2025-02-08 18:45:31 -08:00
mbecker20
f29c60a6e9 higher quality / colored icons 2025-02-08 18:45:31 -08:00
mbecker20
ee33fc98d9 Add test alerter button 2025-02-08 18:45:31 -08:00
mbecker20
729ffc9c3c fix last axum updates 2025-02-08 18:45:31 -08:00
mbecker20
395930094d axum update :param to {param} syntax 2025-02-08 18:45:31 -08:00
mbecker20
79a16c6f22 rust 1.84.0 2025-02-08 18:45:31 -08:00
mbecker20
4e4aa8c567 test alert implementation 2025-02-08 18:45:31 -08:00
mbecker20
bfbcd33c1a add entities / message for test alerter 2025-02-08 18:45:31 -08:00
mbecker20
01e2e0be11 the komodo env file should be highest priority over additional files 2025-02-08 18:45:31 -08:00
mbecker20
2e6e409b85 clean up cors 2025-02-08 18:45:31 -08:00
mbecker20
30e87b6aff just make it 1.17.0 2025-02-08 18:45:31 -08:00
mbecker20
8423a53106 bump aws deps 2025-02-08 18:45:31 -08:00
mbecker20
9e6c122313 axum to 0.8 2025-02-08 18:45:31 -08:00
mbecker20
d03d0b3b78 resource2 not really a benefit 2025-02-08 18:45:31 -08:00
mbecker20
1bf76f1b57 format 2025-02-08 18:45:31 -08:00
mbecker20
7815639aeb fmt 2025-02-08 18:45:31 -08:00
mbecker20
af9fbf9667 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
2025-02-08 18:45:31 -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
499 changed files with 47651 additions and 18752 deletions

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: ../test.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": "18.18.0"
},
"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

View File

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

8
.gitignore vendored
View File

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

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"
]
}

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": []
},
]
}

1811
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,36 @@
[workspace]
resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
members = [
"bin/*",
"lib/*",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "1.15.7"
version = "1.17.0-dev-1"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
repository = "https://github.com/mbecker20/komodo"
repository = "https://github.com/moghtech/komodo"
homepage = "https://komo.do"
[patch.crates-io]
# komodo_client = { path = "client/core/rs" }
[workspace.dependencies]
# LOCAL
# komodo_client = "1.15.6"
komodo_client = { path = "client/core/rs" }
periphery_client = { path = "client/periphery/rs" }
environment_file = { path = "lib/environment_file" }
formatting = { path = "lib/formatting" }
response = { path = "lib/response" }
command = { path = "lib/command" }
logger = { path = "lib/logger" }
cache = { path = "lib/cache" }
git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.4.6", default-features = false }
slack = { version = "0.2.0", package = "slack_client_rs" }
serror = { version = "0.5.0", default-features = false }
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
merge_config_files = "0.1.5"
@@ -35,58 +38,59 @@ async_timing_util = "1.0.0"
partial_derive2 = "0.4.3"
derive_variants = "1.0.0"
mongo_indexed = "2.0.1"
resolver_api = "1.1.1"
resolver_api = "3.0.0"
toml_pretty = "1.1.2"
mungos = "1.1.0"
svi = "1.0.1"
# ASYNC
reqwest = { version = "0.12.8", features = ["json"] }
tokio = { version = "1.38.1", features = ["full"] }
tokio-util = "0.7.12"
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "1.43.0", features = ["full"] }
tokio-util = "0.7.13"
futures = "0.3.31"
futures-util = "0.3.31"
# SERVER
axum-extra = { version = "0.9.4", features = ["typed-header"] }
tower-http = { version = "0.6.1", features = ["fs", "cors"] }
axum-server = { version = "0.7.1", features = ["tls-openssl"] }
axum = { version = "0.7.7", features = ["ws", "json"] }
tokio-tungstenite = "0.24.0"
axum-extra = { version = "0.10.0", features = ["typed-header"] }
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
tokio-tungstenite = "0.26.1"
# SER/DE
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.217", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
serde_json = "1.0.128"
serde_json = "1.0.135"
serde_yaml = "0.9.34"
toml = "0.8.19"
# ERROR
anyhow = "1.0.89"
thiserror = "1.0.64"
anyhow = "1.0.95"
thiserror = "2.0.11"
# LOGGING
opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.18", features = ["json"] }
opentelemetry-semantic-conventions = "0.25.0"
tracing-opentelemetry = "0.26.0"
opentelemetry-otlp = "0.25.0"
opentelemetry = "0.25.0"
tracing = "0.1.40"
opentelemetry-otlp = { version = "0.27.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.27.0"
tracing-opentelemetry = "0.28.0"
opentelemetry = "0.27.1"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.20", features = ["derive"] }
clap = { version = "4.5.26", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] }
uuid = { version = "1.12.0", features = ["v4", "fast-rng", "serde"] }
openidconnect = "3.5.0"
urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.15.1"
bcrypt = "0.16.0"
base64 = "0.22.1"
rustls = "0.23.21"
hmac = "0.12.1"
sha2 = "0.10.8"
rand = "0.8.5"
@@ -94,18 +98,19 @@ jwt = "0.16.0"
hex = "0.4.3"
# SYSTEM
bollard = "0.17.1"
sysinfo = "0.32.0"
bollard = "0.18.1"
sysinfo = "0.33.1"
# CLOUD
aws-config = "1.5.8"
aws-sdk-ec2 = "1.77.0"
aws-config = "1.5.13"
aws-sdk-ec2 = "1.101.0"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.3"
octorust = "0.7.0"
typeshare = "1.0.4"
octorust = "0.9.0"
dashmap = "6.1.0"
colored = "2.1.0"
regex = "1.11.0"
bson = "2.13.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.1"
bson = "2.13.0"

27
bin/binaries.Dockerfile Normal file
View File

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

View File

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

View File

@@ -1,13 +1,21 @@
use std::time::Duration;
use colored::Colorize;
use komodo_client::api::execute::Execution;
use komodo_client::{
api::execute::{BatchExecutionResponse, Execution},
entities::update::Update,
};
use crate::{
helpers::wait_for_enter,
state::{cli_args, komodo_client},
};
pub enum ExecutionResult {
Single(Update),
Batch(BatchExecutionResponse),
}
pub async fn run(execution: Execution) -> anyhow::Result<()> {
if matches!(execution, Execution::None(_)) {
println!("Got 'none' execution. Doing nothing...");
@@ -21,18 +29,36 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
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())
}
@@ -51,15 +77,27 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
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())
}
@@ -129,12 +167,24 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CommitSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -153,6 +203,12 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::TestAlerter(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -165,138 +221,246 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
info!("Running Execution...");
let res = match execution {
Execution::RunProcedure(request) => {
komodo_client().execute(request).await
}
Execution::RunBuild(request) => {
komodo_client().execute(request).await
}
Execution::CancelBuild(request) => {
komodo_client().execute(request).await
}
Execution::Deploy(request) => {
komodo_client().execute(request).await
}
Execution::StartDeployment(request) => {
komodo_client().execute(request).await
}
Execution::RestartDeployment(request) => {
komodo_client().execute(request).await
}
Execution::PauseDeployment(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseDeployment(request) => {
komodo_client().execute(request).await
}
Execution::StopDeployment(request) => {
komodo_client().execute(request).await
}
Execution::DestroyDeployment(request) => {
komodo_client().execute(request).await
}
Execution::CloneRepo(request) => {
komodo_client().execute(request).await
}
Execution::PullRepo(request) => {
komodo_client().execute(request).await
}
Execution::BuildRepo(request) => {
komodo_client().execute(request).await
}
Execution::CancelRepoBuild(request) => {
komodo_client().execute(request).await
}
Execution::StartContainer(request) => {
komodo_client().execute(request).await
}
Execution::RestartContainer(request) => {
komodo_client().execute(request).await
}
Execution::PauseContainer(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseContainer(request) => {
komodo_client().execute(request).await
}
Execution::StopContainer(request) => {
komodo_client().execute(request).await
}
Execution::DestroyContainer(request) => {
komodo_client().execute(request).await
}
Execution::StartAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::RestartAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::PauseAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::StopAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::PruneContainers(request) => {
komodo_client().execute(request).await
}
Execution::DeleteNetwork(request) => {
komodo_client().execute(request).await
}
Execution::PruneNetworks(request) => {
komodo_client().execute(request).await
}
Execution::DeleteImage(request) => {
komodo_client().execute(request).await
}
Execution::PruneImages(request) => {
komodo_client().execute(request).await
}
Execution::DeleteVolume(request) => {
komodo_client().execute(request).await
}
Execution::PruneVolumes(request) => {
komodo_client().execute(request).await
}
Execution::PruneDockerBuilders(request) => {
komodo_client().execute(request).await
}
Execution::PruneBuildx(request) => {
komodo_client().execute(request).await
}
Execution::PruneSystem(request) => {
komodo_client().execute(request).await
}
Execution::RunSync(request) => {
komodo_client().execute(request).await
}
Execution::DeployStack(request) => {
komodo_client().execute(request).await
}
Execution::DeployStackIfChanged(request) => {
komodo_client().execute(request).await
}
Execution::StartStack(request) => {
komodo_client().execute(request).await
}
Execution::RestartStack(request) => {
komodo_client().execute(request).await
}
Execution::PauseStack(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseStack(request) => {
komodo_client().execute(request).await
}
Execution::StopStack(request) => {
komodo_client().execute(request).await
}
Execution::DestroyStack(request) => {
komodo_client().execute(request).await
}
Execution::RunAction(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchRunAction(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::RunProcedure(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchRunProcedure(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::RunBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchRunBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::CancelBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::Deploy(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDeploy(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::PullDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::CloneRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchCloneRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::PullRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchPullRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::BuildRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchBuildRepo(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::CancelRepoBuild(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DestroyContainer(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopAllContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneContainers(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DeleteNetwork(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneNetworks(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DeleteImage(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneImages(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DeleteVolume(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneVolumes(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneDockerBuilders(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneBuildx(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PruneSystem(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RunSync(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::CommitSync(request) => komodo_client()
.write(request)
.await
.map(ExecutionResult::Single),
Execution::DeployStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDeployStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::DeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::PullStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StartStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::RestartStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::PauseStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::UnpauseStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::StopStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::DestroyStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::BatchDestroyStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
Execution::TestAlerter(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);
@@ -308,7 +472,12 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
};
match res {
Ok(update) => println!("\n{}: {update:#?}", "SUCCESS".green()),
Ok(ExecutionResult::Single(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Ok(ExecutionResult::Batch(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Err(e) => println!("{}\n\n{e:#?}", "ERROR".red()),
}

View File

@@ -40,7 +40,9 @@ pub fn komodo_client() -> &'static KomodoClient {
creds
}
};
futures::executor::block_on(KomodoClient::new(url, key, secret))
.expect("failed to initialize Komodo client")
futures::executor::block_on(
KomodoClient::new(url, key, secret).with_healthcheck(),
)
.expect("failed to initialize Komodo client")
})
}

View File

@@ -19,7 +19,10 @@ komodo_client = { workspace = true, features = ["mongo"] }
periphery_client.workspace = true
environment_file.workspace = true
formatting.workspace = true
response.workspace = true
command.workspace = true
logger.workspace = true
cache.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }
@@ -47,15 +50,17 @@ serde_json.workspace = true
serde_yaml.workspace = true
typeshare.workspace = true
octorust.workspace = true
wildcard.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
futures.workspace = true
nom_pem.workspace = true
anyhow.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
bcrypt.workspace = true
base64.workspace = true
rustls.workspace = true
tokio.workspace = true
serde.workspace = true
regex.workspace = true

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

@@ -0,0 +1,55 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.84.1-bullseye AS core-builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
COPY ./lib ./lib
COPY ./client/core/rs ./client/core/rs
COPY ./client/periphery ./client/periphery
COPY ./bin/core ./bin/core
# Compile app
RUN cargo build -p komodo_core --release
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Setup an application directory
WORKDIR /app
# Copy
COPY ./config/core.config.toml /config/config.toml
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=core-builder /builder/target/release/core /usr/local/bin/core
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Set $DENO_DIR and preload external Deno deps
ENV DENO_DIR=/action-cache/deno
RUN mkdir /action-cache && \
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
# Hint at the port
EXPOSE 9120
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
ENTRYPOINT [ "core" ]

View File

@@ -1,45 +0,0 @@
## This one produces smaller images,
## but alpine uses `musl` instead of `glibc`.
## This makes it take longer / more resources to build,
## and may negatively affect runtime performance.
# Build Core
FROM rust:1.81.0-alpine AS core-builder
WORKDIR /builder
RUN apk update && apk --no-cache add musl-dev openssl-dev openssl-libs-static
COPY . .
RUN cargo build -p komodo_core --release
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link @komodo/client && yarn && yarn build
# Final Image
FROM alpine:3.20
# Install Deps
RUN apk update && apk add --no-cache --virtual .build-deps \
openssl ca-certificates git git-lfs
# Setup an application directory
WORKDIR /app
# Copy
COPY ./config/core.config.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /app
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
# Hint at the port
EXPOSE 9120
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
# Using ENTRYPOINT allows cli args to be passed, eg using "command" in docker compose.
ENTRYPOINT [ "/app/core" ]

View File

@@ -1,39 +0,0 @@
# Build Core
FROM rust:1.81.0-bullseye AS core-builder
WORKDIR /builder
COPY . .
RUN cargo build -p komodo_core --release
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link @komodo/client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Setup an application directory
WORKDIR /app
# Copy
COPY ./config/core.config.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /app
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
# Hint at the port
EXPOSE 9120
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
ENTRYPOINT [ "/app/core" ]

View File

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

View File

@@ -0,0 +1,44 @@
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
## Sets up the necessary runtime container dependencies for Komodo Core.
ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
# This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries
# Build Frontend
FROM node:20.12-alpine AS frontend-builder
WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link komodo_client && yarn && yarn build
FROM debian:bullseye-slim
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Copy
COPY ./config/core.config.toml /config/config.toml
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=binaries /core /usr/local/bin/core
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Set $DENO_DIR and preload external Deno deps
ENV DENO_DIR=/action-cache/deno
RUN mkdir /action-cache && \
cd /action-cache && \
deno install jsr:@std/yaml jsr:@std/toml
# Hint at the port
EXPOSE 9120
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0
CMD [ "core" ]

View File

@@ -11,6 +11,12 @@ pub async fn send_alert(
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter **{name}** is **working**\n{link}"
)
}
AlertData::ServerUnreachable {
id,
name,
@@ -22,7 +28,7 @@ pub async fn send_alert(
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | *{name}*{region} is now *reachable*\n{link}"
"{level} | **{name}**{region} is now **reachable**\n{link}"
)
}
SeverityLevel::Critical => {
@@ -31,7 +37,7 @@ pub async fn send_alert(
.map(|e| format!("\n**error**: {e:#?}"))
.unwrap_or_default();
format!(
"{level} | *{name}*{region} is *unreachable* ❌\n{link}{err}"
"{level} | **{name}**{region} is **unreachable**\n{link}{err}"
)
}
_ => unreachable!(),
@@ -46,7 +52,7 @@ pub async fn send_alert(
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*\n{link}"
"{level} | **{name}**{region} cpu usage at **{percentage:.1}%**\n{link}"
)
}
AlertData::ServerMem {
@@ -60,7 +66,7 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾\n\nUsing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
"{level} | **{name}**{region} memory usage at **{percentage:.1}%** 💾\n\nUsing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
AlertData::ServerDisk {
@@ -75,7 +81,7 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* 💿\nmount point: `{path:?}`\nusing *{used_gb:.1} GiB* / *{total_gb:.1} GiB*\n{link}"
"{level} | **{name}**{region} disk usage at **{percentage:.1}%** 💿\nmount point: `{path:?}`\nusing **{used_gb:.1} GiB** / **{total_gb:.1} GiB**\n{link}"
)
}
AlertData::ContainerStateChange {
@@ -88,7 +94,27 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to = fmt_docker_container_state(to);
format!("📦 Deployment *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
format!("📦 Deployment **{name}** is now **{to}**\nserver: **{server_name}**\nprevious: **{from}**\n{link}")
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!("⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}")
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!("⬆ Deployment **{name}** was updated automatically ⏫\nserver: **{server_name}**\nimage: **{image}**\n{link}")
}
AlertData::StackStateChange {
id,
@@ -100,28 +126,52 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to = fmt_stack_state(to);
format!("🥞 Stack *{name}* is now {to}\nserver: {server_name}\nprevious: {from}\n{link}")
format!("🥞 Stack **{name}** is now {to}\nserver: **{server_name}**\nprevious: **{from}**\n{link}")
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!("⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}")
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
format!("⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}")
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!("{level} | Failed to terminated AWS builder instance\ninstance id: *{instance_id}*\n{message}")
format!("{level} | Failed to terminated AWS builder instance\ninstance id: **{instance_id}**\n{message}")
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on *{name}*\n{link}"
"{level} | Pending resource sync updates on **{name}**\n{link}"
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!("{level} | Build *{name}* failed\nversion: v{version}\n{link}")
format!("{level} | Build **{name}** failed\nversion: **v{version}**\n{link}")
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for *{name}* failed\n{link}")
format!("{level} | Repo build for **{name}** failed\n{link}")
}
AlertData::None {} => Default::default(),
};

View File

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

View File

@@ -7,6 +7,22 @@ pub async fn send_alert(
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let (text, blocks): (_, Option<_>) = match &alert.data {
AlertData::Test { id, name } => {
let text = format!(
"{level} | If you see this message, then Alerter *{name}* is *working*"
);
let blocks = vec![
Block::header(level),
Block::section(format!(
"If you see this message, then Alerter *{name}* is *working*"
)),
Block::section(resource_link(
ResourceTargetVariant::Alerter,
id,
)),
];
(text, blocks.into())
}
AlertData::ServerUnreachable {
id,
name,
@@ -182,7 +198,7 @@ pub async fn send_alert(
..
} => {
let to = fmt_docker_container_state(to);
let text = format!("📦 Container *{name}* is now {to}");
let text = format!("📦 Container *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
@@ -195,6 +211,48 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_name,
server_id: _server_id,
image,
} => {
let text =
format!("⬆ Deployment *{name}* has an update available");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
)),
];
(text, blocks.into())
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_name,
server_id: _server_id,
image,
} => {
let text =
format!("⬆ Deployment *{name}* was updated automatically ⏫");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
)),
];
(text, blocks.into())
}
AlertData::StackStateChange {
name,
server_name,
@@ -204,11 +262,56 @@ pub async fn send_alert(
..
} => {
let to = fmt_stack_state(to);
let text = format!("🥞 Stack *{name}* is now {to}");
let text = format!("🥞 Stack *{name}* is now *{to}*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: {server_name}\nprevious: {from}",
"server: *{server_name}*\nprevious: *{from}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
)),
];
(text, blocks.into())
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_name,
server_id: _server_id,
service,
image,
} => {
let text = format!("⬆ Stack *{name}* has an update available");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nservice: *{service}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
)),
];
(text, blocks.into())
}
AlertData::StackAutoUpdated {
id,
name,
server_name,
server_id: _server_id,
images,
} => {
let text =
format!("⬆ Stack *{name}* was updated automatically ⏫");
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\n{images_label}: *{images}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
@@ -233,8 +336,9 @@ pub async fn send_alert(
(text, blocks.into())
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let text =
format!("{level} | Pending resource sync updates on {name}");
let text = format!(
"{level} | Pending resource sync updates on *{name}*"
);
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
@@ -252,20 +356,21 @@ pub async fn send_alert(
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"build id: *{id}*\nbuild name: *{name}*\nversion: v{version}",
"build name: *{name}*\nversion: *v{version}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Build,
id,
)),
Block::section(resource_link(ResourceTargetVariant::Build, id))
];
(text, blocks.into())
}
AlertData::RepoBuildFailed { id, name } => {
let text =
format!("{level} | Repo build for {name} has failed");
format!("{level} | Repo build for *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"repo id: *{id}*\nrepo name: *{name}*",
)),
Block::section(format!("repo name: *{name}*",)),
Block::section(resource_link(
ResourceTargetVariant::Repo,
id,

View File

@@ -1,13 +1,12 @@
use std::{sync::OnceLock, time::Instant};
use anyhow::anyhow;
use axum::{http::HeaderMap, routing::post, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::auth::*, entities::user::User};
use reqwest::StatusCode;
use resolver_api::{derive::Resolver, Resolve, Resolver};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serror::{AddStatusCode, Json};
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -20,13 +19,21 @@ use crate::{
},
config::core_config,
helpers::query::get_user,
state::{jwt_client, State},
state::jwt_client,
};
pub struct AuthArgs {
pub headers: HeaderMap,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[resolver_target(State)]
#[resolver_args(HeaderMap)]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[args(AuthArgs)]
#[response(Response)]
#[error(serror::Error)]
#[variant_derive(Debug)]
#[serde(tag = "type", content = "params")]
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
pub enum AuthRequest {
@@ -66,27 +73,20 @@ pub fn router() -> Router {
async fn handler(
headers: HeaderMap,
Json(request): Json<AuthRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!("/auth request {req_id} | METHOD: {}", request.req_type());
let res = State.resolve_request(request, headers).await.map_err(
|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
},
debug!(
"/auth request {req_id} | METHOD: {:?}",
request.extract_variant()
);
let res = request.resolve(&AuthArgs { headers }).await;
if let Err(e) = &res {
debug!("/auth request {req_id} | error: {e:#}");
debug!("/auth request {req_id} | error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/auth request {req_id} | resolve time: {elapsed:?}");
Ok((
TypedHeader(ContentType::json()),
res.status_code(StatusCode::UNAUTHORIZED)?,
))
res.map(|res| res.0)
}
fn login_options_reponse() -> &'static GetLoginOptionsResponse {
@@ -112,38 +112,34 @@ fn login_options_reponse() -> &'static GetLoginOptionsResponse {
})
}
impl Resolve<GetLoginOptions, HeaderMap> for State {
impl Resolve<AuthArgs> for GetLoginOptions {
#[instrument(name = "GetLoginOptions", level = "debug", skip(self))]
async fn resolve(
&self,
_: GetLoginOptions,
_: HeaderMap,
) -> anyhow::Result<GetLoginOptionsResponse> {
self,
_: &AuthArgs,
) -> serror::Result<GetLoginOptionsResponse> {
Ok(*login_options_reponse())
}
}
impl Resolve<ExchangeForJwt, HeaderMap> for State {
impl Resolve<AuthArgs> for ExchangeForJwt {
#[instrument(name = "ExchangeForJwt", level = "debug", skip(self))]
async fn resolve(
&self,
ExchangeForJwt { token }: ExchangeForJwt,
_: HeaderMap,
) -> anyhow::Result<ExchangeForJwtResponse> {
let jwt = jwt_client().redeem_exchange_token(&token).await?;
let res = ExchangeForJwtResponse { jwt };
Ok(res)
self,
_: &AuthArgs,
) -> serror::Result<ExchangeForJwtResponse> {
let jwt = jwt_client().redeem_exchange_token(&self.token).await?;
Ok(ExchangeForJwtResponse { jwt })
}
}
impl Resolve<GetUser, HeaderMap> for State {
impl Resolve<AuthArgs> for GetUser {
#[instrument(name = "GetUser", level = "debug", skip(self))]
async fn resolve(
&self,
GetUser {}: GetUser,
headers: HeaderMap,
) -> anyhow::Result<User> {
let user_id = get_user_id_from_headers(&headers).await?;
get_user(&user_id).await
self,
AuthArgs { headers }: &AuthArgs,
) -> serror::Result<User> {
let user_id = get_user_id_from_headers(headers).await?;
Ok(get_user(&user_id).await?)
}
}

View File

@@ -0,0 +1,332 @@
use std::{
collections::HashSet,
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use anyhow::Context;
use command::run_komodo_command;
use komodo_client::{
api::{
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
action::Action, config::core::CoreConfig,
permission::PermissionLevel, update::Update, user::action_user,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
api::{execute::ExecuteRequest, user::UserArgs},
config::core_config,
helpers::{
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_string,
},
query::get_variables_and_secrets,
random_string,
update::update_update,
},
resource::{self, refresh_action_state_cache},
state::{action_states, db_client},
};
use super::ExecuteArgs;
impl super::BatchExecute for BatchRunAction {
type Resource = Action;
fn single_request(action: String) -> ExecuteRequest {
ExecuteRequest::RunAction(RunAction { action })
}
}
impl Resolve<ExecuteArgs> for BatchRunAction {
#[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for RunAction {
#[instrument(name = "RunAction", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut action = resource::get_check_permissions::<Action>(
&self.action,
&user,
PermissionLevel::Execute,
)
.await?;
// get the action state for the action (or insert default).
let action_state = action_states()
.action
.get_or_insert_default(&action.id)
.await;
// This will set action state back to default when dropped.
// Will also check to ensure action not already busy before updating.
let _action_guard =
action_state.update(|state| state.running = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let CreateApiKeyResponse { key, secret } = CreateApiKey {
name: update.id.clone(),
expires: 0,
}
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await?;
let contents = &mut action.config.file_contents;
// Wrap the file contents in the execution context.
*contents = full_contents(contents, &key, &secret);
let replacers =
interpolate(contents, &mut update, key.clone(), secret.clone())
.await?
.into_iter()
.collect::<Vec<_>>();
let file = format!("{}.ts", random_string(10));
let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent).await;
}
fs::write(&path, contents).await.with_context(|| {
format!("Failed to write action file to {path:?}")
})?;
let mut res = run_komodo_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!("deno run --allow-all {}", path.display()),
false,
)
.await;
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
.replace(&key, "<ACTION_API_KEY>");
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
.replace(&secret, "<ACTION_API_SECRET>");
cleanup_run(file + ".js", &path).await;
if let Err(e) = (DeleteApiKey { key })
.resolve(&UserArgs {
user: action_user().to_owned(),
})
.await
{
warn!(
"Failed to delete API key after action execution | {:#}",
e.error
);
};
update.logs.push(res);
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with update_update.
// The Err case of to_document should be unreachable,
// but will fail to update cache in that case.
if let Ok(update_doc) = to_document(&update) {
let _ = update_one_by_id(
&db_client().updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_action_state_cache().await;
}
update_update(update.clone()).await?;
Ok(update)
}
}
async fn interpolate(
contents: &mut String,
update: &mut Update,
key: String,
secret: String,
) -> serror::Result<HashSet<(String, String)>> {
let mut vars_and_secrets = get_variables_and_secrets().await?;
vars_and_secrets
.secrets
.insert(String::from("ACTION_API_KEY"), key);
vars_and_secrets
.secrets
.insert(String::from("ACTION_API_SECRET"), secret);
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
contents,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(update, &global_replacers, &secret_replacers);
Ok(secret_replacers)
}
fn full_contents(contents: &str, key: &str, secret: &str) -> String {
let CoreConfig {
port, ssl_enabled, ..
} = core_config();
let protocol = if *ssl_enabled { "https" } else { "http" };
let base_url = format!("{protocol}://localhost:{port}");
format!(
"import {{ KomodoClient }} from '{base_url}/client/lib.js';
import * as __YAML__ from 'jsr:@std/yaml';
import * as __TOML__ from 'jsr:@std/toml';
const YAML = {{
stringify: __YAML__.stringify,
parse: __YAML__.parse,
parseAll: __YAML__.parseAll,
parseDockerCompose: __YAML__.parse,
}}
const TOML = {{
stringify: __TOML__.stringify,
parse: __TOML__.parse,
parseResourceToml: __TOML__.parse,
parseCargoToml: __TOML__.parse,
}}
const komodo = KomodoClient('{base_url}', {{
type: 'api-key',
params: {{ key: '{key}', secret: '{secret}' }}
}});
async function main() {{
{contents}
console.log('🦎 Action completed successfully 🦎');
}}
main()
.catch(error => {{
console.error('🚨 Action exited early with errors 🚨')
if (error.status !== undefined && error.result !== undefined) {{
console.error('Status:', error.status);
console.error(JSON.stringify(error.result, null, 2));
}} else {{
console.error(JSON.stringify(error, null, 2));
}}
Deno.exit(1)
}});"
)
}
/// Cleans up file at given path.
/// ALSO if $DENO_DIR is set,
/// will clean up the generated file matching "file"
async fn cleanup_run(file: String, path: &Path) {
if let Err(e) = fs::remove_file(path).await {
warn!(
"Failed to delete action file after action execution | {e:#}"
);
}
// If $DENO_DIR is set (will be in container),
// will clean up the generated file matching "file" (NOT under path)
let Some(deno_dir) = deno_dir() else {
return;
};
delete_file(deno_dir.join("gen/file"), file).await;
}
fn deno_dir() -> Option<&'static Path> {
static DENO_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
DENO_DIR
.get_or_init(|| {
let deno_dir = std::env::var("DENO_DIR").ok()?;
PathBuf::from_str(&deno_dir).ok()
})
.as_deref()
}
/// file is just the terminating file path,
/// it may be nested multiple folder under path,
/// this will find the nested file and delete it.
/// Assumes the file is only there once.
fn delete_file(
dir: PathBuf,
file: String,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send>>
{
Box::pin(async move {
let Ok(mut dir) = fs::read_dir(dir).await else {
return false;
};
// Collect the nested folders for recursing
// only after checking all the files in directory.
let mut folders = Vec::<PathBuf>::new();
while let Ok(Some(entry)) = dir.next_entry().await {
let Ok(meta) = entry.metadata().await else {
continue;
};
if meta.is_file() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
if name == file {
if let Err(e) = fs::remove_file(entry.path()).await {
warn!(
"Failed to clean up generated file after action execution | {e:#}"
);
};
return true;
}
} else {
folders.push(entry.path());
}
}
if folders.len() == 1 {
// unwrap ok, folders definitely is not empty
let folder = folders.pop().unwrap();
delete_file(folder, file).await
} else {
// Check folders with file.clone
for folder in folders {
if delete_file(folder, file.clone()).await {
return true;
}
}
false
}
})
}

View File

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

View File

@@ -4,7 +4,10 @@ use anyhow::{anyhow, Context};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::{CancelBuild, Deploy, RunBuild},
api::execute::{
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
RunBuild,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
@@ -14,7 +17,7 @@ use komodo_client::{
komodo_timestamp,
permission::PermissionLevel,
update::{Log, Update},
user::{auto_redeploy_user, User},
user::auto_redeploy_user,
},
};
use mungos::{
@@ -46,20 +49,39 @@ use crate::{
update::{init_execution_update, update_update},
},
resource::{self, refresh_build_state_cache},
state::{action_states, db_client, State},
state::{action_states, db_client},
};
use super::ExecuteRequest;
use super::{ExecuteArgs, ExecuteRequest};
impl Resolve<RunBuild, (User, Update)> for State {
#[instrument(name = "RunBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
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,
RunBuild { build }: RunBuild,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for RunBuild {
#[instrument(name = "RunBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut build = resource::get_check_permissions::<Build>(
&build,
&self.build,
&user,
PermissionLevel::Execute,
)
@@ -67,7 +89,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
let mut vars_and_secrets = get_variables_and_secrets().await?;
if build.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to RunBuild"));
return Err(anyhow!("Must attach builder to RunBuild").into());
}
// get the action state for the build (or insert default).
@@ -82,6 +104,9 @@ impl Resolve<RunBuild, (User, Update)> for State {
if build.config.auto_increment_version {
build.config.version.increment();
}
let mut update = update.clone();
update.version = build.config.version;
update_update(update.clone()).await?;
@@ -387,7 +412,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
});
}
Ok(update)
Ok(update.clone())
}
}
@@ -397,7 +422,7 @@ async fn handle_early_return(
build_id: String,
build_name: String,
is_cancel: bool,
) -> anyhow::Result<Update> {
) -> serror::Result<Update> {
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -435,10 +460,9 @@ async fn handle_early_return(
send_alerts(&[alert]).await
});
}
Ok(update)
Ok(update.clone())
}
#[instrument(skip_all)]
pub async fn validate_cancel_build(
request: &ExecuteRequest,
) -> anyhow::Result<()> {
@@ -485,15 +509,14 @@ pub async fn validate_cancel_build(
Ok(())
}
impl Resolve<CancelBuild, (User, Update)> for State {
#[instrument(name = "CancelBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for CancelBuild {
#[instrument(name = "CancelBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
CancelBuild { build }: CancelBuild,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let build = resource::get_check_permissions::<Build>(
&build,
&self.build,
&user,
PermissionLevel::Execute,
)
@@ -507,9 +530,11 @@ impl Resolve<CancelBuild, (User, Update)> for State {
.and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default()
{
return Err(anyhow!("Build is not building."));
return Err(anyhow!("Build is not building.").into());
}
let mut update = update.clone();
update.push_simple_log(
"cancel triggered",
"the build cancel has been triggered",
@@ -573,16 +598,13 @@ async fn handle_post_build_redeploy(build_id: &str) {
let user = auto_redeploy_user().to_owned();
let res = async {
let update = init_execution_update(&req, &user).await?;
State
.resolve(
Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
stop_time: None,
},
(user, update),
)
.await
Deploy {
deployment: deployment.id.clone(),
stop_signal: None,
stop_time: None,
}
.resolve(&ExecuteArgs { user, update })
.await
}
.await;
Some((deployment.id.clone(), res))
@@ -596,7 +618,10 @@ async fn handle_post_build_redeploy(build_id: &str) {
continue;
};
if let Err(e) = res {
warn!("failed post build redeploy for deployment {id}: {e:#}");
warn!(
"failed post build redeploy for deployment {id}: {:#}",
e.error
);
}
}
}
@@ -616,14 +641,17 @@ async fn validate_account_extract_registry_token(
},
..
}: &Build,
) -> anyhow::Result<Option<String>> {
) -> serror::Result<Option<String>> {
if domain.is_empty() {
return Ok(None);
}
if account.is_empty() {
return Err(anyhow!(
"Must attach account to use registry provider {domain}"
));
return Err(
anyhow!(
"Must attach account to use registry provider {domain}"
)
.into(),
);
}
let registry_token = registry_token(domain, account).await.with_context(

View File

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

View File

@@ -1,18 +1,22 @@
use std::time::Instant;
use std::{pin::Pin, time::Instant};
use anyhow::{anyhow, Context};
use anyhow::Context;
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::*,
entities::{
update::{Log, Update},
user::User,
Operation,
},
};
use mungos::by_id::find_one_by_id;
use resolver_api::{derive::Resolver, Resolver};
use resolver_api::Resolve;
use response::JsonString;
use serde::{Deserialize, Serialize};
use serror::Json;
use typeshare::typeshare;
@@ -21,9 +25,12 @@ use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::update::{init_execution_update, update_update},
state::{db_client, State},
resource::{list_full_for_user_using_pattern, KomodoResource},
state::db_client,
};
mod action;
mod alerter;
mod build;
mod deployment;
mod procedure;
@@ -33,13 +40,23 @@ mod server_template;
mod stack;
mod sync;
pub use {
deployment::pull_deployment_inner, stack::pull_stack_inner,
};
pub struct ExecuteArgs {
pub user: User,
pub update: Update,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[variant_derive(Debug)]
#[resolver_target(State)]
#[resolver_args((User, Update))]
#[args(ExecuteArgs)]
#[response(JsonString)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
pub enum ExecuteRequest {
// ==== SERVER ====
@@ -67,39 +84,58 @@ pub enum ExecuteRequest {
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
PullDeployment(PullDeployment),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// ==== BUILD ====
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
CancelBuild(CancelBuild),
// ==== REPO ====
CloneRepo(CloneRepo),
BatchCloneRepo(BatchCloneRepo),
PullRepo(PullRepo),
BatchPullRepo(BatchPullRepo),
BuildRepo(BuildRepo),
BatchBuildRepo(BatchBuildRepo),
CancelRepoBuild(CancelRepoBuild),
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
// ==== ACTION ====
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer),
// ==== ALERTER ====
TestAlerter(TestAlerter),
// ==== SYNC ====
RunSync(RunSync),
}
@@ -113,50 +149,88 @@ pub fn router() -> Router {
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
) -> serror::Result<Json<Update>> {
let req_id = Uuid::new_v4();
) -> 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))
}
// need to validate no cancel is active before any update is created.
build::validate_cancel_build(&request).await?;
pub enum ExecutionResult {
Single(Update),
/// The batch contents will be pre serialized here
Batch(String),
}
let update = init_execution_update(&request, &user).await?;
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();
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
// need to validate no cancel is active before any update is created.
build::validate_cancel_build(&request).await?;
tokio::spawn({
let update_id = update.id.clone();
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!("/execute request {req_id} task error: {e:#}",);
Log::error("task error", format_serror(&e.into()))
}
Err(e) => {
warn!("/execute request {req_id} spawn error: {e:?}",);
Log::error("spawn error", format!("{e:#?}"))
}
_ => return,
};
let res = async {
let mut update =
find_one_by_id(&db_client().updates, &update_id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
}
.await;
let update = init_execution_update(&request, &user).await?;
if let Err(e) = res {
warn!("failed to update update with task error log | {e:#}");
}
// This will be the case for the Batch exections,
// they don't have their own updates.
// The batch calls also call "inner_handler" themselves,
// and in their case will spawn tasks, so that isn't necessary
// here either.
if update.operation == Operation::None {
return Ok(ExecutionResult::Batch(
task(req_id, request, user, update).await?,
));
}
});
Ok(Json(update))
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
tokio::spawn({
let update_id = update.id.clone();
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!("/execute request {req_id} task error: {e:#}",);
Log::error("task error", format_serror(&e.into()))
}
Err(e) => {
warn!("/execute request {req_id} spawn error: {e:?}",);
Log::error("spawn error", format!("{e:#?}"))
}
_ => return,
};
let res = async {
let mut update =
find_one_by_id(&db_client().updates, &update_id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
}
.await;
if let Err(e) = res {
warn!(
"failed to update update with task error log | {e:#}"
);
}
}
});
Ok(ExecutionResult::Single(update))
})
}
#[instrument(
@@ -177,15 +251,14 @@ async fn task(
info!("/execute request {req_id} | user: {}", user.username);
let timer = Instant::now();
let res = State
.resolve_request(request, (user, update))
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
let res = match request.resolve(&ExecuteArgs { user, update }).await
{
Err(e) => Err(e.error),
Ok(JsonString::Err(e)) => Err(
anyhow::Error::from(e).context("failed to serialize response"),
),
Ok(JsonString::Ok(res)) => Ok(res),
};
if let Err(e) = &res {
warn!("/execute request {req_id} error: {e:#}");
@@ -196,3 +269,40 @@ async fn task(
res
}
trait BatchExecute {
type Resource: KomodoResource;
fn single_request(name: String) -> ExecuteRequest;
}
async fn batch_execute<E: BatchExecute>(
pattern: &str,
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let resources = list_full_for_user_using_pattern::<E::Resource>(
pattern,
Default::default(),
user,
&[],
)
.await?;
let futures = resources.into_iter().map(|resource| {
let user = user.clone();
async move {
inner_handler(E::single_request(resource.name.clone()), user)
.await
.map(|r| {
let ExecutionResult::Single(update) = r else {
unreachable!()
};
update
})
.map_err(|e| BatchExecutionResponseItemErr {
name: resource.name,
error: e.into(),
})
.into()
}
});
Ok(join_all(futures).await)
}

View File

@@ -2,7 +2,9 @@ use std::pin::Pin;
use formatting::{bold, colored, format_serror, muted, Color};
use komodo_client::{
api::execute::RunProcedure,
api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
@@ -15,17 +17,41 @@ use tokio::sync::Mutex;
use crate::{
helpers::{procedure::execute_procedure, update::update_update},
resource::{self, refresh_procedure_state_cache},
state::{action_states, db_client, State},
state::{action_states, db_client},
};
impl Resolve<RunProcedure, (User, Update)> for State {
#[instrument(name = "RunProcedure", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
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,
RunProcedure { procedure }: RunProcedure,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
resolve_inner(procedure, user, update).await
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?,
)
}
}

View File

@@ -3,7 +3,7 @@ use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{anyhow, Context};
use formatting::format_serror;
use komodo_client::{
api::execute::*,
api::{execute::*, write::RefreshRepoCache},
entities::{
alert::{Alert, AlertData, SeverityLevel},
builder::{Builder, BuilderConfig},
@@ -12,7 +12,6 @@ use komodo_client::{
repo::Repo,
server::Server,
update::{Log, Update},
user::User,
},
};
use mungos::{
@@ -28,6 +27,7 @@ use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
api::write::WriteArgs,
helpers::{
builder::{cleanup_builder_instance, get_builder_periphery},
channel::repo_cancel_channel,
@@ -42,21 +42,40 @@ use crate::{
update::update_update,
},
resource::{self, refresh_repo_state_cache},
state::{action_states, db_client, State},
state::{action_states, db_client},
};
use super::ExecuteRequest;
use super::{ExecuteArgs, ExecuteRequest};
impl Resolve<CloneRepo, (User, Update)> for State {
#[instrument(name = "CloneRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
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,
CloneRepo { repo }: CloneRepo,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for CloneRepo {
#[instrument(name = "CloneRepo", skip( user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
@@ -70,10 +89,11 @@ impl Resolve<CloneRepo, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.cloning = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
if repo.config.server_id.is_empty() {
return Err(anyhow!("repo has no server attached"));
return Err(anyhow!("repo has no server attached").into());
}
let git_token = git_token(
@@ -123,19 +143,50 @@ impl Resolve<CloneRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = (RefreshRepoCache { repo: repo.id })
.resolve(&WriteArgs { user: user.clone() })
.await
.map_err(|e| e.error)
.context("Failed to refresh repo cache")
{
update.push_error_log(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}
impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(name = "PullRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl super::BatchExecute for BatchPullRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<ExecuteArgs> for BatchPullRepo {
#[instrument(name = "BatchPullRepo", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
PullRepo { repo }: PullRepo,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(&self.pattern, &user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for PullRepo {
#[instrument(name = "PullRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&repo,
&self.repo,
&user,
PermissionLevel::Execute,
)
@@ -150,10 +201,12 @@ impl Resolve<PullRepo, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
if repo.config.server_id.is_empty() {
return Err(anyhow!("repo has no server attached"));
return Err(anyhow!("repo has no server attached").into());
}
let git_token = git_token(
@@ -207,6 +260,18 @@ impl Resolve<PullRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = (RefreshRepoCache { repo: repo.id })
.resolve(&WriteArgs { user: user.clone() })
.await
.map_err(|e| e.error)
.context("Failed to refresh repo cache")
{
update.push_error_log(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}
@@ -214,7 +279,7 @@ impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(skip_all, fields(update_id = update.id))]
async fn handle_server_update_return(
update: Update,
) -> anyhow::Result<Update> {
) -> serror::Result<Update> {
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
@@ -249,22 +314,41 @@ async fn update_last_pulled_time(repo_name: &str) {
}
}
impl Resolve<BuildRepo, (User, Update)> for State {
#[instrument(name = "BuildRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
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,
BuildRepo { repo }: BuildRepo,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for BuildRepo {
#[instrument(name = "BuildRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
if repo.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to BuildRepo"));
return Err(anyhow!("Must attach builder to BuildRepo").into());
}
// get the action state for the repo (or insert default).
@@ -276,6 +360,7 @@ impl Resolve<BuildRepo, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.building = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let git_token = git_token(
@@ -353,7 +438,8 @@ impl Resolve<BuildRepo, (User, Update)> for State {
return handle_builder_early_return(
update, repo.id, repo.name, false,
)
.await;
.await
.map_err(Into::into);
}
};
@@ -471,7 +557,7 @@ async fn handle_builder_early_return(
repo_id: String,
repo_name: String,
is_cancel: bool,
) -> anyhow::Result<Update> {
) -> serror::Result<Update> {
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -559,16 +645,15 @@ pub async fn validate_cancel_repo_build(
Ok(())
}
impl Resolve<CancelRepoBuild, (User, Update)> for State {
#[instrument(name = "CancelRepoBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for CancelRepoBuild {
#[instrument(name = "CancelRepoBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
CancelRepoBuild { repo }: CancelRepoBuild,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
&self.repo,
user,
PermissionLevel::Execute,
)
.await?;
@@ -581,9 +666,11 @@ impl Resolve<CancelRepoBuild, (User, Update)> for State {
.and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default()
{
return Err(anyhow!("Repo is not building."));
return Err(anyhow!("Repo is not building.").into());
}
let mut update = update.clone();
update.push_simple_log(
"cancel triggered",
"the repo build cancel has been triggered",

View File

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

View File

@@ -7,51 +7,51 @@ use komodo_client::{
server::PartialServerConfig,
server_template::{ServerTemplate, ServerTemplateConfig},
update::Update,
user::User,
},
};
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
cloud::{
aws::ec2::launch_ec2_instance, hetzner::launch_hetzner_server,
},
helpers::update::update_update,
resource,
state::{db_client, State},
state::db_client,
};
impl Resolve<LaunchServer, (User, Update)> for State {
#[instrument(name = "LaunchServer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for LaunchServer {
#[instrument(name = "LaunchServer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
LaunchServer {
name,
server_template,
}: LaunchServer,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
// validate name isn't already taken by another server
if db_client()
.servers
.find_one(doc! {
"name": &name
"name": &self.name
})
.await
.context("failed to query db for servers")?
.is_some()
{
return Err(anyhow!("name is already taken"));
return Err(anyhow!("name is already taken").into());
}
let template = resource::get_check_permissions::<ServerTemplate>(
&server_template,
&user,
&self.server_template,
user,
PermissionLevel::Execute,
)
.await?;
let mut update = update.clone();
update.push_simple_log(
"launching server",
format!("{:#?}", template.config),
@@ -63,24 +63,24 @@ impl Resolve<LaunchServer, (User, Update)> for State {
let region = config.region.clone();
let use_https = config.use_https;
let port = config.port;
let instance = match launch_ec2_instance(&name, config).await
{
Ok(instance) => instance,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch aws instance\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
let instance =
match launch_ec2_instance(&self.name, config).await {
Ok(instance) => instance,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch aws instance\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
update.push_simple_log(
"launch server",
format!(
"successfully launched server {name} on ip {}",
instance.ip
"successfully launched server {} on ip {}",
self.name, instance.ip
),
);
let protocol = if use_https { "https" } else { "http" };
@@ -95,24 +95,24 @@ impl Resolve<LaunchServer, (User, Update)> for State {
let datacenter = config.datacenter;
let use_https = config.use_https;
let port = config.port;
let server = match launch_hetzner_server(&name, config).await
{
Ok(server) => server,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch hetzner server\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
let server =
match launch_hetzner_server(&self.name, config).await {
Ok(server) => server,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch hetzner server\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
update.push_simple_log(
"launch server",
format!(
"successfully launched server {name} on ip {}",
server.ip
"successfully launched server {} on ip {}",
self.name, server.ip
),
);
let protocol = if use_https { "https" } else { "http" };
@@ -125,7 +125,13 @@ impl Resolve<LaunchServer, (User, Update)> for State {
}
};
match self.resolve(CreateServer { name, config }, user).await {
match (CreateServer {
name: self.name,
config,
})
.resolve(&WriteArgs { user: user.clone() })
.await
{
Ok(server) => {
update.push_simple_log(
"create server",
@@ -136,7 +142,9 @@ impl Resolve<LaunchServer, (User, Update)> for State {
Err(e) => {
update.push_error_log(
"create server",
format_serror(&e.context("failed to create server").into()),
format_serror(
&e.error.context("failed to create server").into(),
),
);
}
};

View File

@@ -6,9 +6,9 @@ use komodo_client::{
api::{execute::*, write::RefreshStackCache},
entities::{
permission::PermissionLevel,
server::Server,
stack::{Stack, StackInfo},
update::Update,
user::User,
update::{Log, Update},
},
};
use mungos::mongodb::bson::{doc, to_document};
@@ -16,6 +16,7 @@ use periphery_client::api::compose::*;
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{
interpolate::{
add_interp_update_log,
@@ -29,23 +30,45 @@ use crate::{
},
monitor::update_cache_for_server,
resource,
stack::{
execute::execute_compose, get_stack_and_server,
services::extract_services_into_res,
},
state::{action_states, db_client, State},
stack::{execute::execute_compose, get_stack_and_server},
state::{action_states, db_client},
};
impl Resolve<DeployStack, (User, Update)> for State {
#[instrument(name = "DeployStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchDeployStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack {
stack,
service: None,
stop_time: None,
})
}
}
impl Resolve<ExecuteArgs> for BatchDeployStack {
#[instrument(name = "BatchDeployStack", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
DeployStack { stack, stop_time }: DeployStack,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for DeployStack {
#[instrument(name = "DeployStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (mut stack, server) = get_stack_and_server(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Execute,
true,
)
@@ -60,8 +83,17 @@ impl Resolve<DeployStack, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
if let Some(service) = &self.service {
update.logs.push(Log::simple(
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
let git_token = crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
@@ -85,6 +117,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.file_contents,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.environment,
@@ -113,6 +152,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut stack.config.post_deploy,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
@@ -127,6 +173,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
let ComposeUpResponse {
logs,
deployed,
services,
file_contents,
missing_files,
remote_errors,
@@ -135,7 +182,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
} = periphery_client(&server)?
.request(ComposeUp {
stack: stack.clone(),
service: None,
service: self.service,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
@@ -145,24 +192,11 @@ impl Resolve<DeployStack, (User, Update)> for State {
update.logs.extend(logs);
let update_info = async {
let latest_services = if !file_contents.is_empty() {
let mut services = Vec::new();
for contents in &file_contents {
if let Err(e) = extract_services_into_res(
&stack.project_name(true),
&contents.contents,
&mut services,
) {
update.push_error_log(
"extract services",
format_serror(&e.context(format!("Failed to extract stack services for compose file path {}. Things probably won't work correctly", contents.path)).into())
);
}
}
services
} else {
let latest_services = if services.is_empty() {
// maybe better to do something else here for services.
stack.info.latest_services.clone()
} else {
services
};
// This ensures to get the latest project name,
@@ -246,26 +280,49 @@ impl Resolve<DeployStack, (User, Update)> for State {
}
}
impl Resolve<DeployStackIfChanged, (User, Update)> for State {
impl super::BatchExecute for BatchDeployStackIfChanged {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack,
stop_time: None,
})
}
}
impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
#[instrument(name = "BatchDeployStackIfChanged", skip(user), fields(user_id = user.id))]
async fn resolve(
&self,
DeployStackIfChanged { stack, stop_time }: DeployStackIfChanged,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchDeployStackIfChanged>(
&self.pattern,
user,
)
.await?,
)
}
}
impl Resolve<ExecuteArgs> for DeployStackIfChanged {
#[instrument(name = "DeployStackIfChanged", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Execute,
)
.await?;
State
.resolve(
RefreshStackCache {
stack: stack.id.clone(),
},
user.clone(),
)
.await?;
RefreshStackCache {
stack: stack.id.clone(),
}
.resolve(&WriteArgs { user: user.clone() })
.await?;
let stack = resource::get::<Stack>(&stack.id).await?;
let changed = match (
&stack.info.deployed_contents,
@@ -292,6 +349,8 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
_ => false,
};
let mut update = update.clone();
if !changed {
update.push_simple_log(
"Diff compose files",
@@ -305,138 +364,240 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
// This is usually done in crate::helpers::update::init_execution_update.
update.id = add_update_without_send(&update).await?;
State
.resolve(
DeployStack {
stack: stack.name,
stop_time,
},
(user, update),
)
.await
}
}
impl Resolve<StartStack, (User, Update)> for State {
#[instrument(name = "StartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StartStack { stack, service }: StartStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
execute_compose::<StartStack>(
&stack,
service,
&user,
|state| state.starting = true,
DeployStack {
stack: stack.name,
service: None,
stop_time: self.stop_time,
}
.resolve(&ExecuteArgs {
user: user.clone(),
update,
(),
)
})
.await
}
}
impl Resolve<RestartStack, (User, Update)> for State {
#[instrument(name = "RestartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RestartStack { stack, service }: RestartStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
execute_compose::<RestartStack>(
&stack,
pub async fn pull_stack_inner(
mut stack: Stack,
service: Option<String>,
server: &Server,
update: Option<&mut Update>,
) -> anyhow::Result<ComposePullResponse> {
if let (Some(service), Some(update)) = (&service, update) {
update.logs.push(Log::simple(
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
let git_token = crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {}", stack.config.git_provider, stack.config.git_account),
)?;
let registry_token = crate::helpers::registry_token(
&stack.config.registry_provider,
&stack.config.registry_account,
).await.with_context(
|| format!("Failed to get registry token in call to db. Stopping run. | {} | {}", stack.config.registry_provider, stack.config.registry_account),
)?;
let res = periphery_client(server)?
.request(ComposePull {
stack,
service,
&user,
git_token,
registry_token,
})
.await?;
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(server).await;
Ok(res)
}
impl Resolve<ExecuteArgs> for PullStack {
#[instrument(name = "PullStack", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (stack, server) = get_stack_and_server(
&self.stack,
user,
PermissionLevel::Execute,
true,
)
.await?;
// get the action state for the stack (or insert default).
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
// Will check to ensure stack not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
update_update(update.clone()).await?;
let res = pull_stack_inner(
stack,
self.service,
&server,
Some(&mut update),
)
.await?;
update.logs.extend(res.logs);
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<ExecuteArgs> for StartStack {
#[instrument(name = "StartStack", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<StartStack>(
&self.stack,
self.service,
user,
|state| state.starting = true,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<ExecuteArgs> for RestartStack {
#[instrument(name = "RestartStack", skip(user, update), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<RestartStack>(
&self.stack,
self.service,
user,
|state| {
state.restarting = true;
},
update,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<PauseStack, (User, Update)> for State {
#[instrument(name = "PauseStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for PauseStack {
#[instrument(name = "PauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PauseStack { stack, service }: PauseStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<PauseStack>(
&stack,
service,
&user,
&self.stack,
self.service,
user,
|state| state.pausing = true,
update,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<UnpauseStack, (User, Update)> for State {
#[instrument(name = "UnpauseStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for UnpauseStack {
#[instrument(name = "UnpauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
UnpauseStack { stack, service }: UnpauseStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<UnpauseStack>(
&stack,
service,
&user,
&self.stack,
self.service,
user,
|state| state.unpausing = true,
update,
update.clone(),
(),
)
.await
.map_err(Into::into)
}
}
impl Resolve<StopStack, (User, Update)> for State {
#[instrument(name = "StopStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for StopStack {
#[instrument(name = "StopStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StopStack {
stack,
stop_time,
service,
}: StopStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<StopStack>(
&stack,
service,
&user,
&self.stack,
self.service,
user,
|state| state.stopping = true,
update,
stop_time,
update.clone(),
self.stop_time,
)
.await
.map_err(Into::into)
}
}
impl Resolve<DestroyStack, (User, Update)> for State {
#[instrument(name = "DestroyStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
DestroyStack {
impl super::BatchExecute for BatchDestroyStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
remove_orphans,
stop_time,
}: DestroyStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
execute_compose::<DestroyStack>(
&stack,
None,
&user,
|state| state.destroying = true,
update,
(stop_time, remove_orphans),
)
.await
service: None,
remove_orphans: false,
stop_time: None,
})
}
}
impl Resolve<ExecuteArgs> for BatchDestroyStack {
#[instrument(name = "BatchDestroyStack", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
.await
.map_err(Into::into)
}
}
impl Resolve<ExecuteArgs> for DestroyStack {
#[instrument(name = "DestroyStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
execute_compose::<DestroyStack>(
&self.stack,
self.service,
user,
|state| state.destroying = true,
update.clone(),
(self.stop_time, self.remove_orphans),
)
.await
.map_err(Into::into)
}
}

View File

@@ -6,6 +6,7 @@ use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{
self,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
@@ -19,7 +20,7 @@ use komodo_client::{
stack::Stack,
sync::ResourceSync,
update::{Log, Update},
user::{sync_user, User},
user::sync_user,
ResourceTargetVariant,
},
};
@@ -31,9 +32,10 @@ use mungos::{
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{query::get_id_to_tags, update::update_update},
resource::{self, refresh_resource_sync_state_cache},
state::{action_states, db_client, State},
state::{action_states, db_client},
sync::{
deploy::{
build_deploy_cache, deploy_from_cache, SyncDeployParams,
@@ -44,17 +46,19 @@ use crate::{
},
};
impl Resolve<RunSync, (User, Update)> for State {
#[instrument(name = "RunSync", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for RunSync {
#[instrument(name = "RunSync", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunSync {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let RunSync {
sync,
resource_type: match_resource_type,
resources: match_resources,
}: RunSync,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
} = self;
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, &user, PermissionLevel::Execute)
@@ -71,6 +75,8 @@ impl Resolve<RunSync, (User, Update)> for State {
let _action_guard =
action_state.update(|state| state.syncing = true)?;
let mut update = update.clone();
// Send update here for FE to recheck action state
update_update(update.clone()).await?;
@@ -89,7 +95,9 @@ impl Resolve<RunSync, (User, Update)> for State {
update_update(update.clone()).await?;
if !file_errors.is_empty() {
return Err(anyhow!("Found file errors. Cannot execute sync."));
return Err(
anyhow!("Found file errors. Cannot execute sync.").into(),
);
}
let resources = resources?;
@@ -126,6 +134,10 @@ impl Resolve<RunSync, (User, Update)> for State {
.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)
@@ -270,6 +282,17 @@ impl Resolve<RunSync, (User, Update)> for State {
&sync.config.match_tags,
)
.await?;
let (actions_to_create, actions_to_update, actions_to_delete) =
get_updates_for_execution::<Action>(
resources.actions,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (builders_to_create, builders_to_update, builders_to_delete) =
get_updates_for_execution::<Builder>(
resources.builders,
@@ -388,6 +411,9 @@ impl Resolve<RunSync, (User, Update)> for State {
&& procedures_to_create.is_empty()
&& procedures_to_update.is_empty()
&& procedures_to_delete.is_empty()
&& actions_to_create.is_empty()
&& actions_to_update.is_empty()
&& actions_to_delete.is_empty()
&& user_groups_to_create.is_empty()
&& user_groups_to_update.is_empty()
&& user_groups_to_delete.is_empty()
@@ -464,6 +490,15 @@ impl Resolve<RunSync, (User, Update)> for State {
)
.await,
);
maybe_extend(
&mut update.logs,
Action::execute_sync_updates(
actions_to_create,
actions_to_update,
actions_to_delete,
)
.await,
);
// Dependent on server
maybe_extend(
@@ -553,18 +588,21 @@ impl Resolve<RunSync, (User, Update)> for State {
)
}
if let Err(e) = State
.resolve(
RefreshResourceSyncPending { sync: sync.id },
sync_user().to_owned(),
)
if let Err(e) = (RefreshResourceSyncPending { sync: sync.id })
.resolve(&WriteArgs {
user: sync_user().to_owned(),
})
.await
{
warn!("failed to refresh sync {} after run | {e:#}", sync.name);
warn!(
"failed to refresh sync {} after run | {:#}",
sync.name, e.error
);
update.push_error_log(
"refresh sync",
format_serror(
&e.context("failed to refresh sync pending after run")
&e.error
.context("failed to refresh sync pending after run")
.into(),
),
);

View File

@@ -0,0 +1,137 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
action::{
Action, ActionActionState, ActionListItem, ActionState,
},
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{action_state_cache, action_states},
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetAction {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Action> {
Ok(
resource::get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListActions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ActionListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Action>(self.query, &user, &all_tags)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullActions {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullActionsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Action>(
self.query, &user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetActionActionState {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ActionActionState> {
let action = resource::get_check_permissions::<Action>(
&self.action,
&user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.action
.get(&action.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<ReadArgs> for GetActionsSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get actions from db")?;
let mut res = GetActionsSummaryResponse::default();
let cache = action_state_cache();
let action_states = action_states();
for action in actions {
res.total += 1;
match (
cache.get(&action.id).await.unwrap_or_default(),
action_states
.action
.get(&action.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.running => {
res.running += 1;
}
(ActionState::Ok, _) => res.ok += 1,
(ActionState::Failed, _) => res.failed += 1,
(ActionState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the running state, since that comes from action states
(ActionState::Running, _) => unreachable!(),
}
}
Ok(res)
}
}

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ use komodo_client::{
config::core::CoreConfig,
permission::PermissionLevel,
update::UpdateStatus,
user::User,
Operation,
},
};
@@ -22,56 +21,75 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{
action_states, build_state_cache, db_client, github_client, State,
action_states, build_state_cache, db_client, github_client,
},
};
impl Resolve<GetBuild, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetBuild {
async fn resolve(
&self,
GetBuild { build }: GetBuild,
user: User,
) -> anyhow::Result<Build> {
resource::get_check_permissions::<Build>(
&build,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Build> {
Ok(
resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read,
)
.await?,
)
.await
}
}
impl Resolve<ListBuilds, User> for State {
impl Resolve<ReadArgs> for ListBuilds {
async fn resolve(
&self,
ListBuilds { query }: ListBuilds,
user: User,
) -> anyhow::Result<Vec<BuildListItem>> {
resource::list_for_user::<Build>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Build>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ListFullBuilds, User> for State {
impl Resolve<ReadArgs> for ListFullBuilds {
async fn resolve(
&self,
ListFullBuilds { query }: ListFullBuilds,
user: User,
) -> anyhow::Result<ListFullBuildsResponse> {
resource::list_full_for_user::<Build>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullBuildsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Build>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<GetBuildActionState, User> for State {
impl Resolve<ReadArgs> for GetBuildActionState {
async fn resolve(
&self,
GetBuildActionState { build }: GetBuildActionState,
user: User,
) -> anyhow::Result<BuildActionState> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<BuildActionState> {
let build = resource::get_check_permissions::<Build>(
&build,
&user,
&self.build,
user,
PermissionLevel::Read,
)
.await?;
@@ -85,15 +103,15 @@ impl Resolve<GetBuildActionState, User> for State {
}
}
impl Resolve<GetBuildsSummary, User> for State {
impl Resolve<ReadArgs> for GetBuildsSummary {
async fn resolve(
&self,
GetBuildsSummary {}: GetBuildsSummary,
user: User,
) -> anyhow::Result<GetBuildsSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildsSummaryResponse> {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
&user,
user,
&[],
)
.await
.context("failed to get all builds")?;
@@ -132,16 +150,15 @@ impl Resolve<GetBuildsSummary, User> for State {
const ONE_DAY_MS: i64 = 86400000;
impl Resolve<GetBuildMonthlyStats, User> for State {
impl Resolve<ReadArgs> for GetBuildMonthlyStats {
async fn resolve(
&self,
GetBuildMonthlyStats { page }: GetBuildMonthlyStats,
_: User,
) -> anyhow::Result<GetBuildMonthlyStatsResponse> {
self,
_: &ReadArgs,
) -> serror::Result<GetBuildMonthlyStatsResponse> {
let curr_ts = unix_timestamp_ms() as i64;
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
let close_ts = next_day - page as i64 * 30 * ONE_DAY_MS;
let close_ts = next_day - self.page as i64 * 30 * ONE_DAY_MS;
let open_ts = close_ts - 30 * ONE_DAY_MS;
let mut build_updates = db_client()
@@ -189,21 +206,21 @@ fn ms_to_hour(duration: i64) -> f64 {
duration as f64 / MS_TO_HOUR_DIVISOR
}
impl Resolve<ListBuildVersions, User> for State {
impl Resolve<ReadArgs> for ListBuildVersions {
async fn resolve(
&self,
ListBuildVersions {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<BuildVersionResponseItem>> {
let ListBuildVersions {
build,
major,
minor,
patch,
limit,
}: ListBuildVersions,
user: User,
) -> anyhow::Result<Vec<BuildVersionResponseItem>> {
} = self;
let build = resource::get_check_permissions::<Build>(
&build,
&user,
user,
PermissionLevel::Read,
)
.await?;
@@ -246,15 +263,21 @@ impl Resolve<ListBuildVersions, User> for State {
}
}
impl Resolve<ListCommonBuildExtraArgs, User> for State {
impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
async fn resolve(
&self,
ListCommonBuildExtraArgs { query }: ListCommonBuildExtraArgs,
user: User,
) -> anyhow::Result<ListCommonBuildExtraArgsResponse> {
let builds = resource::list_full_for_user::<Build>(query, &user)
.await
.context("failed to get resources matching query")?;
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonBuildExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let builds = resource::list_full_for_user::<Build>(
self.query, user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -271,12 +294,11 @@ impl Resolve<ListCommonBuildExtraArgs, User> for State {
}
}
impl Resolve<GetBuildWebhookEnabled, User> for State {
impl Resolve<ReadArgs> for GetBuildWebhookEnabled {
async fn resolve(
&self,
GetBuildWebhookEnabled { build }: GetBuildWebhookEnabled,
user: User,
) -> anyhow::Result<GetBuildWebhookEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetBuildWebhookEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetBuildWebhookEnabledResponse {
managed: false,
@@ -285,8 +307,8 @@ impl Resolve<GetBuildWebhookEnabled, User> for State {
};
let build = resource::get_check_permissions::<Build>(
&build,
&user,
&self.build,
user,
PermissionLevel::Read,
)
.await?;

View File

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

View File

@@ -12,62 +12,81 @@ use komodo_client::{
permission::PermissionLevel,
server::Server,
update::Log,
user::User,
},
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::periphery_client,
helpers::{periphery_client, query::get_all_tags},
resource,
state::{action_states, deployment_status_cache, State},
state::{action_states, deployment_status_cache},
};
impl Resolve<GetDeployment, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetDeployment {
async fn resolve(
&self,
GetDeployment { deployment }: GetDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Deployment> {
Ok(
resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read,
)
.await?,
)
.await
}
}
impl Resolve<ListDeployments, User> for State {
impl Resolve<ReadArgs> for ListDeployments {
async fn resolve(
&self,
ListDeployments { query }: ListDeployments,
user: User,
) -> anyhow::Result<Vec<DeploymentListItem>> {
resource::list_for_user::<Deployment>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<DeploymentListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Deployment>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullDeployments, User> for State {
impl Resolve<ReadArgs> for ListFullDeployments {
async fn resolve(
&self,
ListFullDeployments { query }: ListFullDeployments,
user: User,
) -> anyhow::Result<ListFullDeploymentsResponse> {
resource::list_full_for_user::<Deployment>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullDeploymentsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Deployment>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<GetDeploymentContainer, User> for State {
impl Resolve<ReadArgs> for GetDeploymentContainer {
async fn resolve(
&self,
GetDeploymentContainer { deployment }: GetDeploymentContainer,
user: User,
) -> anyhow::Result<GetDeploymentContainerResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentContainerResponse> {
let deployment = resource::get_check_permissions::<Deployment>(
&deployment,
&user,
&self.deployment,
user,
PermissionLevel::Read,
)
.await?;
@@ -85,23 +104,23 @@ impl Resolve<GetDeploymentContainer, User> for State {
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<GetDeploymentLog, User> for State {
impl Resolve<ReadArgs> for GetDeploymentLog {
async fn resolve(
&self,
GetDeploymentLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let GetDeploymentLog {
deployment,
tail,
timestamps,
}: GetDeploymentLog,
user: User,
) -> anyhow::Result<Log> {
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
&deployment,
&user,
user,
PermissionLevel::Read,
)
.await?;
@@ -109,36 +128,37 @@ impl Resolve<GetDeploymentLog, User> for State {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)?
let res = periphery_client(&server)?
.request(api::container::GetContainerLog {
name,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<SearchDeploymentLog, User> for State {
impl Resolve<ReadArgs> for SearchDeploymentLog {
async fn resolve(
&self,
SearchDeploymentLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let SearchDeploymentLog {
deployment,
terms,
combinator,
invert,
timestamps,
}: SearchDeploymentLog,
user: User,
) -> anyhow::Result<Log> {
} = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
&deployment,
&user,
user,
PermissionLevel::Read,
)
.await?;
@@ -146,7 +166,7 @@ impl Resolve<SearchDeploymentLog, User> for State {
return Ok(Log::default());
}
let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)?
let res = periphery_client(&server)?
.request(api::container::GetContainerLogSearch {
name,
terms,
@@ -155,46 +175,48 @@ impl Resolve<SearchDeploymentLog, User> for State {
timestamps,
})
.await
.context("failed at call to periphery")
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<GetDeploymentStats, User> for State {
impl Resolve<ReadArgs> for GetDeploymentStats {
async fn resolve(
&self,
GetDeploymentStats { deployment }: GetDeploymentStats,
user: User,
) -> anyhow::Result<ContainerStats> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ContainerStats> {
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = resource::get_check_permissions::<Deployment>(
&deployment,
&user,
&self.deployment,
user,
PermissionLevel::Read,
)
.await?;
if server_id.is_empty() {
return Err(anyhow!("deployment has no server attached"));
return Err(
anyhow!("deployment has no server attached").into(),
);
}
let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)?
let res = periphery_client(&server)?
.request(api::container::GetContainerStats { name })
.await
.context("failed to get stats from periphery")
.context("failed to get stats from periphery")?;
Ok(res)
}
}
impl Resolve<GetDeploymentActionState, User> for State {
impl Resolve<ReadArgs> for GetDeploymentActionState {
async fn resolve(
&self,
GetDeploymentActionState { deployment }: GetDeploymentActionState,
user: User,
) -> anyhow::Result<DeploymentActionState> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<DeploymentActionState> {
let deployment = resource::get_check_permissions::<Deployment>(
&deployment,
&user,
&self.deployment,
user,
PermissionLevel::Read,
)
.await?;
@@ -208,15 +230,15 @@ impl Resolve<GetDeploymentActionState, User> for State {
}
}
impl Resolve<GetDeploymentsSummary, User> for State {
impl Resolve<ReadArgs> for GetDeploymentsSummary {
async fn resolve(
&self,
GetDeploymentsSummary {}: GetDeploymentsSummary,
user: User,
) -> anyhow::Result<GetDeploymentsSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentsSummaryResponse> {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
&user,
user,
&[],
)
.await
.context("failed to get deployments from db")?;
@@ -248,16 +270,21 @@ impl Resolve<GetDeploymentsSummary, User> for State {
}
}
impl Resolve<ListCommonDeploymentExtraArgs, User> for State {
impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
async fn resolve(
&self,
ListCommonDeploymentExtraArgs { query }: ListCommonDeploymentExtraArgs,
user: User,
) -> anyhow::Result<ListCommonDeploymentExtraArgsResponse> {
let deployments =
resource::list_full_for_user::<Deployment>(query, &user)
.await
.context("failed to get resources matching query")?;
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonDeploymentExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let deployments = resource::list_full_for_user::<Deployment>(
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();

View File

@@ -2,7 +2,6 @@ use std::{collections::HashSet, sync::OnceLock, time::Instant};
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use komodo_client::{
api::read::*,
entities::{
@@ -16,9 +15,8 @@ use komodo_client::{
ResourceTarget,
},
};
use resolver_api::{
derive::Resolver, Resolve, ResolveToString, Resolver,
};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serror::Json;
use typeshare::typeshare;
@@ -26,9 +24,10 @@ use uuid::Uuid;
use crate::{
auth::auth_request, config::core_config, helpers::periphery_client,
resource, state::State,
resource,
};
mod action;
mod alert;
mod alerter;
mod build;
@@ -38,7 +37,6 @@ mod permission;
mod procedure;
mod provider;
mod repo;
mod search;
mod server;
mod server_template;
mod stack;
@@ -50,15 +48,18 @@ mod user;
mod user_group;
mod variable;
pub struct ReadArgs {
pub user: User,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[resolver_target(State)]
#[resolver_args(User)]
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
#[args(ReadArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
enum ReadRequest {
#[to_string_resolver]
GetVersion(GetVersion),
#[to_string_resolver]
GetCoreInfo(GetCoreInfo),
ListSecrets(ListSecrets),
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
@@ -78,9 +79,6 @@ enum ReadRequest {
GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups),
// ==== SEARCH ====
FindResources(FindResources),
// ==== PROCEDURE ====
GetProceduresSummary(GetProceduresSummary),
GetProcedure(GetProcedure),
@@ -88,6 +86,13 @@ enum ReadRequest {
ListProcedures(ListProcedures),
ListFullProcedures(ListFullProcedures),
// ==== ACTION ====
GetActionsSummary(GetActionsSummary),
GetAction(GetAction),
GetActionActionState(GetActionActionState),
ListActions(ListActions),
ListFullActions(ListFullActions),
// ==== SERVER TEMPLATE ====
GetServerTemplate(GetServerTemplate),
GetServerTemplatesSummary(GetServerTemplatesSummary),
@@ -112,15 +117,10 @@ enum ReadRequest {
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
ListAllDockerContainers(ListAllDockerContainers),
#[to_string_resolver]
ListDockerContainers(ListDockerContainers),
#[to_string_resolver]
ListDockerNetworks(ListDockerNetworks),
#[to_string_resolver]
ListDockerImages(ListDockerImages),
#[to_string_resolver]
ListDockerVolumes(ListDockerVolumes),
#[to_string_resolver]
ListComposeProjects(ListComposeProjects),
// ==== DEPLOYMENT ====
@@ -204,11 +204,8 @@ enum ReadRequest {
GetAlert(GetAlert),
// ==== SERVER STATS ====
#[to_string_resolver]
GetSystemInformation(GetSystemInformation),
#[to_string_resolver]
GetSystemStats(GetSystemStats),
#[to_string_resolver]
ListSystemProcesses(ListSystemProcesses),
// ==== VARIABLE ====
@@ -232,54 +229,35 @@ pub fn router() -> Router {
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ReadRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> serror::Result<axum::response::Response> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!("/read request | user: {}", user.username);
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
let res = request.resolve(&ReadArgs { user }).await;
if let Err(e) = &res {
debug!("/read request {req_id} error: {e:#}");
debug!("/read request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/read request {req_id} | resolve time: {elapsed:?}");
Ok((TypedHeader(ContentType::json()), res?))
res.map(|res| res.0)
}
fn version() -> &'static String {
static VERSION: OnceLock<String> = OnceLock::new();
VERSION.get_or_init(|| {
serde_json::to_string(&GetVersionResponse {
impl Resolve<ReadArgs> for GetVersion {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetVersionResponse> {
Ok(GetVersionResponse {
version: env!("CARGO_PKG_VERSION").to_string(),
})
.context("failed to serialize GetVersionResponse")
.unwrap()
})
}
impl ResolveToString<GetVersion, User> for State {
async fn resolve_to_string(
&self,
GetVersion {}: GetVersion,
_: User,
) -> anyhow::Result<String> {
Ok(version().to_string())
}
}
fn core_info() -> &'static String {
static CORE_INFO: OnceLock<String> = OnceLock::new();
fn core_info() -> &'static GetCoreInfoResponse {
static CORE_INFO: OnceLock<GetCoreInfoResponse> = OnceLock::new();
CORE_INFO.get_or_init(|| {
let config = core_config();
let info = GetCoreInfoResponse {
GetCoreInfoResponse {
title: config.title.clone(),
monitoring_interval: config.monitoring_interval,
webhook_base_url: if config.webhook_base_url.is_empty() {
@@ -297,40 +275,36 @@ fn core_info() -> &'static String {
.iter()
.map(|i| i.namespace.to_string())
.collect(),
};
serde_json::to_string(&info)
.context("failed to serialize GetCoreInfoResponse")
.unwrap()
}
})
}
impl ResolveToString<GetCoreInfo, User> for State {
async fn resolve_to_string(
&self,
GetCoreInfo {}: GetCoreInfo,
_: User,
) -> anyhow::Result<String> {
Ok(core_info().to_string())
impl Resolve<ReadArgs> for GetCoreInfo {
async fn resolve(
self,
_: &ReadArgs,
) -> serror::Result<GetCoreInfoResponse> {
Ok(core_info().clone())
}
}
impl Resolve<ListSecrets, User> for State {
impl Resolve<ReadArgs> for ListSecrets {
async fn resolve(
&self,
ListSecrets { target }: ListSecrets,
_: User,
) -> anyhow::Result<ListSecretsResponse> {
self,
_: &ReadArgs,
) -> serror::Result<ListSecretsResponse> {
let mut secrets = core_config()
.secrets
.keys()
.cloned()
.collect::<HashSet<_>>();
if let Some(target) = target {
if let Some(target) = self.target {
let server_id = match target {
ResourceTarget::Server(id) => Some(id),
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => None,
BuilderConfig::Server(config) => Some(config.server_id),
BuilderConfig::Aws(config) => {
secrets.extend(config.secrets);
@@ -339,7 +313,9 @@ impl Resolve<ListSecrets, User> for State {
}
}
_ => {
return Err(anyhow!("target must be `Server` or `Builder`"))
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
)
}
};
if let Some(id) = server_id {
@@ -364,21 +340,21 @@ impl Resolve<ListSecrets, User> for State {
}
}
impl Resolve<ListGitProvidersFromConfig, User> for State {
impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
async fn resolve(
&self,
ListGitProvidersFromConfig { target }: ListGitProvidersFromConfig,
user: User,
) -> anyhow::Result<ListGitProvidersFromConfigResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListGitProvidersFromConfigResponse> {
let mut providers = core_config().git_providers.clone();
if let Some(target) = target {
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_git_providers_for_server(&mut providers, &id).await?;
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_git_providers_for_server(
&mut providers,
@@ -395,7 +371,9 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
}
}
_ => {
return Err(anyhow!("target must be `Server` or `Builder`"))
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
)
}
}
}
@@ -403,12 +381,18 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>(
Default::default(),
&user
&user,
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
&user,
&[]
),
resource::list_full_for_user::<Repo>(Default::default(), &user),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
&user
&user,
&[]
),
)?;
@@ -455,15 +439,14 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
}
}
impl Resolve<ListDockerRegistriesFromConfig, User> for State {
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn resolve(
&self,
ListDockerRegistriesFromConfig { target }: ListDockerRegistriesFromConfig,
_: User,
) -> anyhow::Result<ListDockerRegistriesFromConfigResponse> {
self,
_: &ReadArgs,
) -> serror::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone();
if let Some(target) = target {
if let Some(target) = self.target {
match target {
ResourceTarget::Server(id) => {
merge_docker_registries_for_server(&mut registries, &id)
@@ -471,6 +454,7 @@ impl Resolve<ListDockerRegistriesFromConfig, User> for State {
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_docker_registries_for_server(
&mut registries,
@@ -487,7 +471,9 @@ impl Resolve<ListDockerRegistriesFromConfig, User> for State {
}
}
_ => {
return Err(anyhow!("target must be `Server` or `Builder`"))
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
)
}
}
}
@@ -501,7 +487,7 @@ impl Resolve<ListDockerRegistriesFromConfig, User> for State {
async fn merge_git_providers_for_server(
providers: &mut Vec<GitProvider>,
server_id: &str,
) -> anyhow::Result<()> {
) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
.request(periphery_client::api::ListGitProviders {})
@@ -539,7 +525,7 @@ fn merge_git_providers(
async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>,
server_id: &str,
) -> anyhow::Result<()> {
) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)?
.request(periphery_client::api::ListDockerRegistries {})

View File

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

View File

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

View File

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

View File

@@ -5,61 +5,79 @@ use komodo_client::{
config::core::CoreConfig,
permission::PermissionLevel,
repo::{Repo, RepoActionState, RepoListItem, RepoState},
user::User,
},
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{action_states, github_client, repo_state_cache, State},
state::{action_states, github_client, repo_state_cache},
};
impl Resolve<GetRepo, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetRepo {
async fn resolve(
&self,
GetRepo { repo }: GetRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::get_check_permissions::<Repo>(
&repo,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Repo> {
Ok(
resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read,
)
.await?,
)
.await
}
}
impl Resolve<ListRepos, User> for State {
impl Resolve<ReadArgs> for ListRepos {
async fn resolve(
&self,
ListRepos { query }: ListRepos,
user: User,
) -> anyhow::Result<Vec<RepoListItem>> {
resource::list_for_user::<Repo>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<RepoListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Repo>(self.query, &user, &all_tags)
.await?,
)
}
}
impl Resolve<ListFullRepos, User> for State {
impl Resolve<ReadArgs> for ListFullRepos {
async fn resolve(
&self,
ListFullRepos { query }: ListFullRepos,
user: User,
) -> anyhow::Result<ListFullReposResponse> {
resource::list_full_for_user::<Repo>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullReposResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Repo>(
self.query, &user, &all_tags,
)
.await?,
)
}
}
impl Resolve<GetRepoActionState, User> for State {
impl Resolve<ReadArgs> for GetRepoActionState {
async fn resolve(
&self,
GetRepoActionState { repo }: GetRepoActionState,
user: User,
) -> anyhow::Result<RepoActionState> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<RepoActionState> {
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
&self.repo,
user,
PermissionLevel::Read,
)
.await?;
@@ -73,16 +91,18 @@ impl Resolve<GetRepoActionState, User> for State {
}
}
impl Resolve<GetReposSummary, User> for State {
impl Resolve<ReadArgs> for GetReposSummary {
async fn resolve(
&self,
GetReposSummary {}: GetReposSummary,
user: User,
) -> anyhow::Result<GetReposSummaryResponse> {
let repos =
resource::list_full_for_user::<Repo>(Default::default(), &user)
.await
.context("failed to get repos from db")?;
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetReposSummaryResponse> {
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
user,
&[],
)
.await
.context("failed to get repos from db")?;
let mut res = GetReposSummaryResponse::default();
@@ -126,12 +146,11 @@ impl Resolve<GetReposSummary, User> for State {
}
}
impl Resolve<GetRepoWebhooksEnabled, User> for State {
impl Resolve<ReadArgs> for GetRepoWebhooksEnabled {
async fn resolve(
&self,
GetRepoWebhooksEnabled { repo }: GetRepoWebhooksEnabled,
user: User,
) -> anyhow::Result<GetRepoWebhooksEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetRepoWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetRepoWebhooksEnabledResponse {
managed: false,
@@ -142,8 +161,8 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
};
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
&self.repo,
user,
PermissionLevel::Read,
)
.await?;

View File

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

View File

@@ -23,8 +23,8 @@ use komodo_client::{
Server, ServerActionState, ServerListItem, ServerState,
},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
user::User,
ResourceTarget,
},
};
@@ -39,25 +39,29 @@ use periphery_client::api::{
network::InspectNetwork,
volume::InspectVolume,
};
use resolver_api::{Resolve, ResolveToString};
use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
helpers::periphery_client,
helpers::{periphery_client, query::get_all_tags},
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache, State},
state::{action_states, db_client, server_status_cache},
};
impl Resolve<GetServersSummary, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetServersSummary {
async fn resolve(
&self,
GetServersSummary {}: GetServersSummary,
user: User,
) -> anyhow::Result<GetServersSummaryResponse> {
let servers =
resource::list_for_user::<Server>(Default::default(), &user)
.await?;
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServersSummaryResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
user,
&[],
)
.await?;
let mut res = GetServersSummaryResponse::default();
for server in servers {
res.total += 1;
@@ -77,15 +81,14 @@ impl Resolve<GetServersSummary, User> for State {
}
}
impl Resolve<GetPeripheryVersion, User> for State {
impl Resolve<ReadArgs> for GetPeripheryVersion {
async fn resolve(
&self,
req: GetPeripheryVersion,
user: User,
) -> anyhow::Result<GetPeripheryVersionResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPeripheryVersionResponse> {
let server = resource::get_check_permissions::<Server>(
&req.server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -98,50 +101,66 @@ impl Resolve<GetPeripheryVersion, User> for State {
}
}
impl Resolve<GetServer, User> for State {
impl Resolve<ReadArgs> for GetServer {
async fn resolve(
&self,
req: GetServer,
user: User,
) -> anyhow::Result<Server> {
resource::get_check_permissions::<Server>(
&req.server,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Server> {
Ok(
resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read,
)
.await?,
)
.await
}
}
impl Resolve<ListServers, User> for State {
impl Resolve<ReadArgs> for ListServers {
async fn resolve(
&self,
ListServers { query }: ListServers,
user: User,
) -> anyhow::Result<Vec<ServerListItem>> {
resource::list_for_user::<Server>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ServerListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Server>(self.query, &user, &all_tags)
.await?,
)
}
}
impl Resolve<ListFullServers, User> for State {
impl Resolve<ReadArgs> for ListFullServers {
async fn resolve(
&self,
ListFullServers { query }: ListFullServers,
user: User,
) -> anyhow::Result<ListFullServersResponse> {
resource::list_full_for_user::<Server>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullServersResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Server>(
self.query, &user, &all_tags,
)
.await?,
)
}
}
impl Resolve<GetServerState, User> for State {
impl Resolve<ReadArgs> for GetServerState {
async fn resolve(
&self,
GetServerState { server }: GetServerState,
user: User,
) -> anyhow::Result<GetServerStateResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerStateResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -156,15 +175,14 @@ impl Resolve<GetServerState, User> for State {
}
}
impl Resolve<GetServerActionState, User> for State {
impl Resolve<ReadArgs> for GetServerActionState {
async fn resolve(
&self,
GetServerActionState { server }: GetServerActionState,
user: User,
) -> anyhow::Result<ServerActionState> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ServerActionState> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -180,22 +198,22 @@ impl Resolve<GetServerActionState, User> for State {
// This protects the peripheries from spam requests
const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
type SystemInfoCache = Mutex<HashMap<String, Arc<(String, u128)>>>;
type SystemInfoCache =
Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;
fn system_info_cache() -> &'static SystemInfoCache {
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
OnceLock::new();
SYSTEM_INFO_CACHE.get_or_init(Default::default)
}
impl ResolveToString<GetSystemInformation, User> for State {
async fn resolve_to_string(
&self,
GetSystemInformation { server }: GetSystemInformation,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for GetSystemInformation {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SystemInformation> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -209,28 +227,26 @@ impl ResolveToString<GetSystemInformation, User> for State {
let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemInformation {})
.await?;
let res = serde_json::to_string(&stats)?;
lock.insert(
server.id,
(res.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
.into(),
);
res
stats
}
};
Ok(res)
}
}
impl ResolveToString<GetSystemStats, User> for State {
async fn resolve_to_string(
&self,
GetSystemStats { server }: GetSystemStats,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for GetSystemStats {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSystemStatsResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -242,28 +258,27 @@ impl ResolveToString<GetSystemStats, User> for State {
.stats
.as_ref()
.context("server stats not available")?;
let stats = serde_json::to_string(&stats)?;
Ok(stats)
Ok(stats.clone())
}
}
// This protects the peripheries from spam requests
const PROCESSES_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
type ProcessesCache = Mutex<HashMap<String, Arc<(String, u128)>>>;
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 ResolveToString<ListSystemProcesses, User> for State {
async fn resolve_to_string(
&self,
ListSystemProcesses { server }: ListSystemProcesses,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for ListSystemProcesses {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSystemProcessesResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -276,13 +291,12 @@ impl ResolveToString<ListSystemProcesses, User> for State {
let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemProcesses {})
.await?;
let res = serde_json::to_string(&stats)?;
lock.insert(
server.id,
(res.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY)
(stats.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY)
.into(),
);
res
stats
}
};
Ok(res)
@@ -291,19 +305,19 @@ impl ResolveToString<ListSystemProcesses, User> for State {
const STATS_PER_PAGE: i64 = 200;
impl Resolve<GetHistoricalServerStats, User> for State {
impl Resolve<ReadArgs> for GetHistoricalServerStats {
async fn resolve(
&self,
GetHistoricalServerStats {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetHistoricalServerStatsResponse> {
let GetHistoricalServerStats {
server,
granularity,
page,
}: GetHistoricalServerStats,
user: User,
) -> anyhow::Result<GetHistoricalServerStatsResponse> {
} = self;
let server = resource::get_check_permissions::<Server>(
&server,
&user,
user,
PermissionLevel::Read,
)
.await?;
@@ -344,15 +358,14 @@ impl Resolve<GetHistoricalServerStats, User> for State {
}
}
impl ResolveToString<ListDockerContainers, User> for State {
async fn resolve_to_string(
&self,
ListDockerContainers { server }: ListDockerContainers,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for ListDockerContainers {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerContainersResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -360,29 +373,30 @@ impl ResolveToString<ListDockerContainers, User> for State {
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
serde_json::to_string(containers)
.context("failed to serialize response")
Ok(containers.clone())
} else {
Ok(String::from("[]"))
Ok(Vec::new())
}
}
}
impl Resolve<ListAllDockerContainers, User> for State {
impl Resolve<ReadArgs> for ListAllDockerContainers {
async fn resolve(
&self,
ListAllDockerContainers { servers }: ListAllDockerContainers,
user: User,
) -> anyhow::Result<Vec<ContainerListItem>> {
let servers =
resource::list_for_user::<Server>(Default::default(), &user)
.await?
.into_iter()
.filter(|server| {
servers.is_empty()
|| servers.contains(&server.id)
|| servers.contains(&server.name)
});
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListAllDockerContainersResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
&user,
&[],
)
.await?
.into_iter()
.filter(|server| {
self.servers.is_empty()
|| self.servers.contains(&server.id)
|| self.servers.contains(&server.name)
});
let mut containers = Vec::<ContainerListItem>::new();
@@ -399,15 +413,14 @@ impl Resolve<ListAllDockerContainers, User> for State {
}
}
impl Resolve<InspectDockerContainer, User> for State {
impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
&self,
InspectDockerContainer { server, container }: InspectDockerContainer,
user: User,
) -> anyhow::Result<Container> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -415,67 +428,74 @@ impl Resolve<InspectDockerContainer, User> for State {
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
));
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
periphery_client(&server)?
.request(InspectContainer { name: container })
.await
let res = periphery_client(&server)?
.request(InspectContainer {
name: self.container,
})
.await?;
Ok(res)
}
}
const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<GetContainerLog, User> for State {
impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve(
&self,
GetContainerLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let GetContainerLog {
server,
container,
tail,
timestamps,
}: GetContainerLog,
user: User,
) -> anyhow::Result<Log> {
} = self;
let server = resource::get_check_permissions::<Server>(
&server,
&user,
user,
PermissionLevel::Read,
)
.await?;
periphery_client(&server)?
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")
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<SearchContainerLog, User> for State {
impl Resolve<ReadArgs> for SearchContainerLog {
async fn resolve(
&self,
SearchContainerLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let SearchContainerLog {
server,
container,
terms,
combinator,
invert,
timestamps,
}: SearchContainerLog,
user: User,
) -> anyhow::Result<Log> {
} = self;
let server = resource::get_check_permissions::<Server>(
&server,
&user,
user,
PermissionLevel::Read,
)
.await?;
periphery_client(&server)?
let res = periphery_client(&server)?
.request(periphery::container::GetContainerLogSearch {
name: container,
terms,
@@ -484,25 +504,25 @@ impl Resolve<SearchContainerLog, User> for State {
timestamps,
})
.await
.context("failed at call to periphery")
.context("failed at call to periphery")?;
Ok(res)
}
}
impl Resolve<GetResourceMatchingContainer, User> for State {
impl Resolve<ReadArgs> for GetResourceMatchingContainer {
async fn resolve(
&self,
GetResourceMatchingContainer { server, container }: GetResourceMatchingContainer,
user: User,
) -> anyhow::Result<GetResourceMatchingContainerResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceMatchingContainerResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
// first check deployments
if let Ok(deployment) =
resource::get::<Deployment>(&container).await
resource::get::<Deployment>(&self.container).await
{
return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(),
@@ -522,20 +542,21 @@ impl Resolve<GetResourceMatchingContainer, User> for State {
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(&container);
.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 {
@@ -549,15 +570,14 @@ impl Resolve<GetResourceMatchingContainer, User> for State {
}
}
impl ResolveToString<ListDockerNetworks, User> for State {
async fn resolve_to_string(
&self,
ListDockerNetworks { server }: ListDockerNetworks,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for ListDockerNetworks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerNetworksResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -565,23 +585,21 @@ impl ResolveToString<ListDockerNetworks, User> for State {
.get_or_insert_default(&server.id)
.await;
if let Some(networks) = &cache.networks {
serde_json::to_string(networks)
.context("failed to serialize response")
Ok(networks.clone())
} else {
Ok(String::from("[]"))
Ok(Vec::new())
}
}
}
impl Resolve<InspectDockerNetwork, User> for State {
impl Resolve<ReadArgs> for InspectDockerNetwork {
async fn resolve(
&self,
InspectDockerNetwork { server, network }: InspectDockerNetwork,
user: User,
) -> anyhow::Result<Network> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Network> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -589,26 +607,29 @@ impl Resolve<InspectDockerNetwork, User> for State {
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
));
return Err(
anyhow!(
"Cannot inspect network: server is {:?}",
cache.state
)
.into(),
);
}
periphery_client(&server)?
.request(InspectNetwork { name: network })
.await
let res = periphery_client(&server)?
.request(InspectNetwork { name: self.network })
.await?;
Ok(res)
}
}
impl ResolveToString<ListDockerImages, User> for State {
async fn resolve_to_string(
&self,
ListDockerImages { server }: ListDockerImages,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for ListDockerImages {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerImagesResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -616,23 +637,21 @@ impl ResolveToString<ListDockerImages, User> for State {
.get_or_insert_default(&server.id)
.await;
if let Some(images) = &cache.images {
serde_json::to_string(images)
.context("failed to serialize response")
Ok(images.clone())
} else {
Ok(String::from("[]"))
Ok(Vec::new())
}
}
}
impl Resolve<InspectDockerImage, User> for State {
impl Resolve<ReadArgs> for InspectDockerImage {
async fn resolve(
&self,
InspectDockerImage { server, image }: InspectDockerImage,
user: User,
) -> anyhow::Result<Image> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Image> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -640,26 +659,26 @@ impl Resolve<InspectDockerImage, User> for State {
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(anyhow!(
"Cannot inspect image: server is {:?}",
cache.state
));
return Err(
anyhow!("Cannot inspect image: server is {:?}", cache.state)
.into(),
);
}
periphery_client(&server)?
.request(InspectImage { name: image })
.await
let res = periphery_client(&server)?
.request(InspectImage { name: self.image })
.await?;
Ok(res)
}
}
impl Resolve<ListDockerImageHistory, User> for State {
impl Resolve<ReadArgs> for ListDockerImageHistory {
async fn resolve(
&self,
ListDockerImageHistory { server, image }: ListDockerImageHistory,
user: User,
) -> anyhow::Result<Vec<ImageHistoryResponseItem>> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ImageHistoryResponseItem>> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -667,26 +686,29 @@ impl Resolve<ListDockerImageHistory, User> for State {
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(anyhow!(
"Cannot get image history: server is {:?}",
cache.state
));
return Err(
anyhow!(
"Cannot get image history: server is {:?}",
cache.state
)
.into(),
);
}
periphery_client(&server)?
.request(ImageHistory { name: image })
.await
let res = periphery_client(&server)?
.request(ImageHistory { name: self.image })
.await?;
Ok(res)
}
}
impl ResolveToString<ListDockerVolumes, User> for State {
async fn resolve_to_string(
&self,
ListDockerVolumes { server }: ListDockerVolumes,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for ListDockerVolumes {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerVolumesResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -694,23 +716,21 @@ impl ResolveToString<ListDockerVolumes, User> for State {
.get_or_insert_default(&server.id)
.await;
if let Some(volumes) = &cache.volumes {
serde_json::to_string(volumes)
.context("failed to serialize response")
Ok(volumes.clone())
} else {
Ok(String::from("[]"))
Ok(Vec::new())
}
}
}
impl Resolve<InspectDockerVolume, User> for State {
impl Resolve<ReadArgs> for InspectDockerVolume {
async fn resolve(
&self,
InspectDockerVolume { server, volume }: InspectDockerVolume,
user: User,
) -> anyhow::Result<Volume> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Volume> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -718,26 +738,26 @@ impl Resolve<InspectDockerVolume, User> for State {
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(anyhow!(
"Cannot inspect volume: server is {:?}",
cache.state
));
return Err(
anyhow!("Cannot inspect volume: server is {:?}", cache.state)
.into(),
);
}
periphery_client(&server)?
.request(InspectVolume { name: volume })
.await
let res = periphery_client(&server)?
.request(InspectVolume { name: self.volume })
.await?;
Ok(res)
}
}
impl ResolveToString<ListComposeProjects, User> for State {
async fn resolve_to_string(
&self,
ListComposeProjects { server }: ListComposeProjects,
user: User,
) -> anyhow::Result<String> {
impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListComposeProjectsResponse> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Read,
)
.await?;
@@ -745,10 +765,9 @@ impl ResolveToString<ListComposeProjects, User> for State {
.get_or_insert_default(&server.id)
.await;
if let Some(projects) = &cache.projects {
serde_json::to_string(projects)
.context("failed to serialize response")
Ok(projects.clone())
} else {
Ok(String::from("[]"))
Ok(Vec::new())
}
}
}

View File

@@ -3,7 +3,6 @@ use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
user::User,
},
};
use mongo_indexed::Document;
@@ -11,52 +10,71 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
resource,
state::{db_client, State},
helpers::query::get_all_tags, resource, state::db_client,
};
impl Resolve<GetServerTemplate, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetServerTemplate {
async fn resolve(
&self,
GetServerTemplate { server_template }: GetServerTemplate,
user: User,
) -> anyhow::Result<GetServerTemplateResponse> {
resource::get_check_permissions::<ServerTemplate>(
&server_template,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerTemplateResponse> {
Ok(
resource::get_check_permissions::<ServerTemplate>(
&self.server_template,
user,
PermissionLevel::Read,
)
.await?,
)
.await
}
}
impl Resolve<ListServerTemplates, User> for State {
impl Resolve<ReadArgs> for ListServerTemplates {
async fn resolve(
&self,
ListServerTemplates { query }: ListServerTemplates,
user: User,
) -> anyhow::Result<ListServerTemplatesResponse> {
resource::list_for_user::<ServerTemplate>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListServerTemplatesResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<ServerTemplate>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullServerTemplates, User> for State {
impl Resolve<ReadArgs> for ListFullServerTemplates {
async fn resolve(
&self,
ListFullServerTemplates { query }: ListFullServerTemplates,
user: User,
) -> anyhow::Result<ListFullServerTemplatesResponse> {
resource::list_full_for_user::<ServerTemplate>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullServerTemplatesResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<ServerTemplate>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<GetServerTemplatesSummary, User> for State {
impl Resolve<ReadArgs> for GetServerTemplatesSummary {
async fn resolve(
&self,
GetServerTemplatesSummary {}: GetServerTemplatesSummary,
user: User,
) -> anyhow::Result<GetServerTemplatesSummaryResponse> {
let query = match resource::get_resource_ids_for_user::<
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerTemplatesSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
ServerTemplate,
>(&user)
.await?

View File

@@ -7,7 +7,6 @@ use komodo_client::{
config::core::CoreConfig,
permission::PermissionLevel,
stack::{Stack, StackActionState, StackListItem, StackState},
user::User,
},
};
use periphery_client::api::compose::{
@@ -17,36 +16,38 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::periphery_client,
helpers::{periphery_client, query::get_all_tags},
resource,
stack::get_stack_and_server,
state::{action_states, github_client, stack_status_cache, State},
state::{action_states, github_client, stack_status_cache},
};
impl Resolve<GetStack, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetStack {
async fn resolve(
&self,
GetStack { stack }: GetStack,
user: User,
) -> anyhow::Result<Stack> {
resource::get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Stack> {
Ok(
resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read,
)
.await?,
)
.await
}
}
impl Resolve<ListStackServices, User> for State {
impl Resolve<ReadArgs> for ListStackServices {
async fn resolve(
&self,
ListStackServices { stack }: ListStackServices,
user: User,
) -> anyhow::Result<ListStackServicesResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListStackServicesResponse> {
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Read,
)
.await?;
@@ -63,25 +64,21 @@ impl Resolve<ListStackServices, User> for State {
}
}
impl Resolve<GetStackServiceLog, User> for State {
impl Resolve<ReadArgs> for GetStackServiceLog {
async fn resolve(
&self,
GetStackServiceLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackServiceLogResponse> {
let GetStackServiceLog {
stack,
service,
tail,
timestamps,
}: GetStackServiceLog,
user: User,
) -> anyhow::Result<GetStackServiceLogResponse> {
let (stack, server) = get_stack_and_server(
&stack,
&user,
PermissionLevel::Read,
true,
)
.await?;
periphery_client(&server)?
} = self;
let (stack, server) =
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeServiceLog {
project: stack.project_name(false),
service,
@@ -89,31 +86,28 @@ impl Resolve<GetStackServiceLog, User> for State {
timestamps,
})
.await
.context("failed to get stack service log from periphery")
.context("failed to get stack service log from periphery")?;
Ok(res)
}
}
impl Resolve<SearchStackServiceLog, User> for State {
impl Resolve<ReadArgs> for SearchStackServiceLog {
async fn resolve(
&self,
SearchStackServiceLog {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchStackServiceLogResponse> {
let SearchStackServiceLog {
stack,
service,
terms,
combinator,
invert,
timestamps,
}: SearchStackServiceLog,
user: User,
) -> anyhow::Result<SearchStackServiceLogResponse> {
let (stack, server) = get_stack_and_server(
&stack,
&user,
PermissionLevel::Read,
true,
)
.await?;
periphery_client(&server)?
} = self;
let (stack, server) =
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeServiceLogSearch {
project: stack.project_name(false),
service,
@@ -123,19 +117,26 @@ impl Resolve<SearchStackServiceLog, User> for State {
timestamps,
})
.await
.context("failed to get stack service log from periphery")
.context("failed to get stack service log from periphery")?;
Ok(res)
}
}
impl Resolve<ListCommonStackExtraArgs, User> for State {
impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
async fn resolve(
&self,
ListCommonStackExtraArgs { query }: ListCommonStackExtraArgs,
user: User,
) -> anyhow::Result<ListCommonStackExtraArgsResponse> {
let stacks = resource::list_full_for_user::<Stack>(query, &user)
.await
.context("failed to get resources matching query")?;
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonStackExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks = resource::list_full_for_user::<Stack>(
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -152,15 +153,21 @@ impl Resolve<ListCommonStackExtraArgs, User> for State {
}
}
impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
async fn resolve(
&self,
ListCommonStackBuildExtraArgs { query }: ListCommonStackBuildExtraArgs,
user: User,
) -> anyhow::Result<ListCommonStackBuildExtraArgsResponse> {
let stacks = resource::list_full_for_user::<Stack>(query, &user)
.await
.context("failed to get resources matching query")?;
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListCommonStackBuildExtraArgsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks = resource::list_full_for_user::<Stack>(
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -177,35 +184,50 @@ impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
}
}
impl Resolve<ListStacks, User> for State {
impl Resolve<ReadArgs> for ListStacks {
async fn resolve(
&self,
ListStacks { query }: ListStacks,
user: User,
) -> anyhow::Result<Vec<StackListItem>> {
resource::list_for_user::<Stack>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<StackListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Stack>(self.query, user, &all_tags)
.await?,
)
}
}
impl Resolve<ListFullStacks, User> for State {
impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve(
&self,
ListFullStacks { query }: ListFullStacks,
user: User,
) -> anyhow::Result<ListFullStacksResponse> {
resource::list_full_for_user::<Stack>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullStacksResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<Stack>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<GetStackActionState, User> for State {
impl Resolve<ReadArgs> for GetStackActionState {
async fn resolve(
&self,
GetStackActionState { stack }: GetStackActionState,
user: User,
) -> anyhow::Result<StackActionState> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<StackActionState> {
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Read,
)
.await?;
@@ -219,15 +241,15 @@ impl Resolve<GetStackActionState, User> for State {
}
}
impl Resolve<GetStacksSummary, User> for State {
impl Resolve<ReadArgs> for GetStacksSummary {
async fn resolve(
&self,
GetStacksSummary {}: GetStacksSummary,
user: User,
) -> anyhow::Result<GetStacksSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStacksSummaryResponse> {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
&user,
user,
&[],
)
.await
.context("failed to get stacks from db")?;
@@ -252,12 +274,11 @@ impl Resolve<GetStacksSummary, User> for State {
}
}
impl Resolve<GetStackWebhooksEnabled, User> for State {
impl Resolve<ReadArgs> for GetStackWebhooksEnabled {
async fn resolve(
&self,
GetStackWebhooksEnabled { stack }: GetStackWebhooksEnabled,
user: User,
) -> anyhow::Result<GetStackWebhooksEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetStackWebhooksEnabledResponse {
managed: false,
@@ -267,8 +288,8 @@ impl Resolve<GetStackWebhooksEnabled, User> for State {
};
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Read,
)
.await?;

View File

@@ -8,63 +8,81 @@ use komodo_client::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
ResourceSyncState,
},
user::User,
},
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{
action_states, github_client, resource_sync_state_cache, State,
},
state::{action_states, github_client, resource_sync_state_cache},
};
impl Resolve<GetResourceSync, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for GetResourceSync {
async fn resolve(
&self,
GetResourceSync { sync }: GetResourceSync,
user: User,
) -> anyhow::Result<ResourceSync> {
resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
PermissionLevel::Read,
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSync> {
Ok(
resource::get_check_permissions::<ResourceSync>(
&self.sync,
&user,
PermissionLevel::Read,
)
.await?,
)
.await
}
}
impl Resolve<ListResourceSyncs, User> for State {
impl Resolve<ReadArgs> for ListResourceSyncs {
async fn resolve(
&self,
ListResourceSyncs { query }: ListResourceSyncs,
user: User,
) -> anyhow::Result<Vec<ResourceSyncListItem>> {
resource::list_for_user::<ResourceSync>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ResourceSyncListItem>> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<ResourceSync>(
self.query, &user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ListFullResourceSyncs, User> for State {
impl Resolve<ReadArgs> for ListFullResourceSyncs {
async fn resolve(
&self,
ListFullResourceSyncs { query }: ListFullResourceSyncs,
user: User,
) -> anyhow::Result<ListFullResourceSyncsResponse> {
resource::list_full_for_user::<ResourceSync>(query, &user).await
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullResourceSyncsResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<ResourceSync>(
self.query, &user, &all_tags,
)
.await?,
)
}
}
impl Resolve<GetResourceSyncActionState, User> for State {
impl Resolve<ReadArgs> for GetResourceSyncActionState {
async fn resolve(
&self,
GetResourceSyncActionState { sync }: GetResourceSyncActionState,
user: User,
) -> anyhow::Result<ResourceSyncActionState> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSyncActionState> {
let sync = resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
&self.sync,
user,
PermissionLevel::Read,
)
.await?;
@@ -78,16 +96,16 @@ impl Resolve<GetResourceSyncActionState, User> for State {
}
}
impl Resolve<GetResourceSyncsSummary, User> for State {
impl Resolve<ReadArgs> for GetResourceSyncsSummary {
async fn resolve(
&self,
GetResourceSyncsSummary {}: GetResourceSyncsSummary,
user: User,
) -> anyhow::Result<GetResourceSyncsSummaryResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceSyncsSummaryResponse> {
let resource_syncs =
resource::list_full_for_user::<ResourceSync>(
Default::default(),
&user,
user,
&[],
)
.await
.context("failed to get resource_syncs from db")?;
@@ -143,12 +161,11 @@ impl Resolve<GetResourceSyncsSummary, User> for State {
}
}
impl Resolve<GetSyncWebhooksEnabled, User> for State {
impl Resolve<ReadArgs> for GetSyncWebhooksEnabled {
async fn resolve(
&self,
GetSyncWebhooksEnabled { sync }: GetSyncWebhooksEnabled,
user: User,
) -> anyhow::Result<GetSyncWebhooksEnabledResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSyncWebhooksEnabledResponse> {
let Some(github) = github_client() else {
return Ok(GetSyncWebhooksEnabledResponse {
managed: false,
@@ -158,8 +175,8 @@ impl Resolve<GetSyncWebhooksEnabled, User> for State {
};
let sync = resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
&self.sync,
user,
PermissionLevel::Read,
)
.await?;

View File

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

View File

@@ -1,55 +1,56 @@
use std::collections::HashMap;
use anyhow::Context;
use komodo_client::{
api::read::{
ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
ExportResourcesToToml, ExportResourcesToTomlResponse,
GetUserGroup, ListUserTargetPermissions,
ListUserGroups,
},
entities::{
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
permission::{PermissionLevel, UserTarget},
procedure::Procedure,
repo::Repo,
resource::ResourceQuery,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
toml::{PermissionToml, ResourcesToml, UserGroupToml},
user::User,
ResourceTarget,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, permission::PermissionLevel,
procedure::Procedure, repo::Repo, resource::ResourceQuery,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, ResourceTarget,
},
};
use mungos::find::find_collect;
use resolver_api::Resolve;
use crate::{
helpers::query::{get_id_to_tags, get_user_user_group_ids},
helpers::query::{
get_all_tags, get_id_to_tags, get_user_user_group_ids,
},
resource,
state::{db_client, State},
state::db_client,
sync::{
toml::{convert_resource, ToToml, TOML_PRETTY_OPTIONS},
user_groups::convert_user_groups,
AllResourcesById,
},
};
impl Resolve<ExportAllResourcesToToml, User> for State {
use super::ReadArgs;
impl Resolve<ReadArgs> for ExportAllResourcesToToml {
async fn resolve(
&self,
ExportAllResourcesToToml { tags }: ExportAllResourcesToToml,
user: User,
) -> anyhow::Result<ExportAllResourcesToTomlResponse> {
self,
args: &ReadArgs,
) -> serror::Result<ExportAllResourcesToTomlResponse> {
let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if self.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let ReadArgs { user } = args;
targets.extend(
resource::list_for_user::<Alerter>(
ResourceQuery::builder().tags(tags.clone()).build(),
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -57,8 +58,9 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
);
targets.extend(
resource::list_for_user::<Builder>(
ResourceQuery::builder().tags(tags.clone()).build(),
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -66,35 +68,39 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
);
targets.extend(
resource::list_for_user::<Server>(
ResourceQuery::builder().tags(tags.clone()).build(),
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_for_user::<Deployment>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_for_user::<Stack>(
ResourceQuery::builder().tags(tags.clone()).build(),
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_for_user::<Build>(
ResourceQuery::builder().tags(tags.clone()).build(),
resource::list_for_user::<Deployment>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_for_user::<Build>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -102,8 +108,9 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
);
targets.extend(
resource::list_for_user::<Repo>(
ResourceQuery::builder().tags(tags.clone()).build(),
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -111,17 +118,29 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
);
targets.extend(
resource::list_for_user::<Procedure>(
ResourceQuery::builder().tags(tags.clone()).build(),
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(tags.clone()).build(),
resource::list_for_user::<Action>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -129,8 +148,9 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
);
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags.clone()).build(),
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -138,7 +158,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
let user_groups = if user.admin && tags.is_empty() {
let user_groups = if user.admin && self.tags.is_empty() {
find_collect(&db_client().user_groups, None, None)
.await
.context("failed to query db for user groups")?
@@ -149,32 +169,30 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
get_user_user_group_ids(&user.id).await?
};
self
.resolve(
ExportResourcesToToml {
targets,
user_groups,
include_variables: tags.is_empty(),
},
user,
)
.await
}
}
impl Resolve<ExportResourcesToToml, User> for State {
async fn resolve(
&self,
ExportResourcesToToml {
targets,
user_groups,
include_variables: self.tags.is_empty(),
}
.resolve(args)
.await
}
}
impl Resolve<ReadArgs> for ExportResourcesToToml {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<ExportResourcesToTomlResponse> {
let ExportResourcesToToml {
targets,
user_groups,
include_variables,
}: ExportResourcesToToml,
user: User,
) -> anyhow::Result<ExportResourcesToTomlResponse> {
} = self;
let mut res = ResourcesToml::default();
let all = AllResourcesById::load().await?;
let id_to_tags = get_id_to_tags(None).await?;
let ReadArgs { user } = args;
for target in targets {
match target {
ResourceTarget::Alerter(id) => {
@@ -331,11 +349,26 @@ impl Resolve<ExportResourcesToToml, User> for State {
&id_to_tags,
));
}
ResourceTarget::Action(id) => {
let mut action = resource::get_check_permissions::<Action>(
&id,
&user,
PermissionLevel::Read,
)
.await?;
Action::replace_ids(&mut action, &all);
res.actions.push(convert_resource::<Action>(
action,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::System(_) => continue,
};
}
add_user_groups(user_groups, &mut res, &all, &user)
add_user_groups(user_groups, &mut res, &all, args)
.await
.context("failed to add user groups")?;
@@ -365,124 +398,20 @@ async fn add_user_groups(
user_groups: Vec<String>,
res: &mut ResourcesToml,
all: &AllResourcesById,
user: &User,
args: &ReadArgs,
) -> anyhow::Result<()> {
let db = db_client();
let usernames = find_collect(&db.users, None, None)
.await?
let user_groups = ListUserGroups {}
.resolve(args)
.await
.map_err(|e| e.error)?
.into_iter()
.map(|user| (user.id, user.username))
.collect::<HashMap<_, _>>();
for user_group in user_groups {
let ug = State
.resolve(GetUserGroup { user_group }, user.clone())
.await?;
// this method is admin only, but we already know user can see user group if above does not return Err
let permissions = State
.resolve(
ListUserTargetPermissions {
user_target: UserTarget::UserGroup(ug.id),
},
User {
admin: true,
..Default::default()
},
)
.await?
.into_iter()
.map(|mut permission| {
match &mut permission.resource_target {
ResourceTarget::Build(id) => {
*id = all
.builds
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Builder(id) => {
*id = all
.builders
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Deployment(id) => {
*id = all
.deployments
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Server(id) => {
*id = all
.servers
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
*id = all
.repos
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
*id = all
.alerters
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Procedure(id) => {
*id = all
.procedures
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::ServerTemplate(id) => {
*id = all
.templates
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::ResourceSync(id) => {
*id = all
.syncs
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Stack(id) => {
*id = all
.stacks
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::System(_) => {}
}
PermissionToml {
target: permission.resource_target,
level: permission.level,
}
})
.collect();
res.user_groups.push(UserGroupToml {
name: ug.name,
users: ug
.users
.into_iter()
.filter_map(|user_id| usernames.get(&user_id).cloned())
.collect(),
all: ug.all,
permissions,
.filter(|ug| {
user_groups.contains(&ug.name) || user_groups.contains(&ug.id)
});
}
let mut ug = Vec::with_capacity(user_groups.size_hint().0);
convert_user_groups(user_groups, all, &mut ug).await?;
res.user_groups = ug.into_iter().map(|ug| ug.1).collect();
Ok(())
}
@@ -539,6 +468,14 @@ fn serialize_resources_toml(
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");

View File

@@ -4,6 +4,7 @@ use anyhow::{anyhow, Context};
use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
@@ -27,22 +28,19 @@ use mungos::{
};
use resolver_api::Resolve;
use crate::{
config::core_config,
resource,
state::{db_client, State},
};
use crate::{config::core_config, resource, state::db_client};
use super::ReadArgs;
const UPDATES_PER_PAGE: i64 = 100;
impl Resolve<ListUpdates, User> for State {
impl Resolve<ReadArgs> for ListUpdates {
async fn resolve(
&self,
ListUpdates { query, page }: ListUpdates,
user: User,
) -> anyhow::Result<ListUpdatesResponse> {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListUpdatesResponse> {
let query = if user.admin || core_config().transparent_mode {
query
self.query
} else {
let server_query =
resource::get_resource_ids_for_user::<Server>(&user)
@@ -104,6 +102,16 @@ impl Resolve<ListUpdates, User> for State {
})
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query =
resource::get_resource_ids_for_user::<Action>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Action", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query =
resource::get_resource_ids_for_user::<Builder>(&user)
.await?
@@ -124,29 +132,29 @@ impl Resolve<ListUpdates, User> for State {
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let server_template_query = resource::get_resource_ids_for_user::<ServerTemplate>(
&user,
)
.await?
.map(|ids| {
doc! {
"target.type": "ServerTemplate", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
let server_template_query =
resource::get_resource_ids_for_user::<ServerTemplate>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "ServerTemplate", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
let resource_sync_query = resource::get_resource_ids_for_user::<ResourceSync>(
&user,
)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let resource_sync_query =
resource::get_resource_ids_for_user::<ResourceSync>(
&user,
)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let mut query = query.unwrap_or_default();
let mut query = self.query.unwrap_or_default();
query.extend(doc! {
"$or": [
server_query,
@@ -155,6 +163,7 @@ impl Resolve<ListUpdates, User> for State {
build_query,
repo_query,
procedure_query,
action_query,
alerter_query,
builder_query,
server_template_query,
@@ -176,7 +185,7 @@ impl Resolve<ListUpdates, User> for State {
query,
FindOptions::builder()
.sort(doc! { "start_ts": -1 })
.skip(page as u64 * UPDATES_PER_PAGE as u64)
.skip(self.page as u64 * UPDATES_PER_PAGE as u64)
.limit(UPDATES_PER_PAGE)
.build(),
)
@@ -208,7 +217,7 @@ impl Resolve<ListUpdates, User> for State {
.collect::<Vec<_>>();
let next_page = if updates.len() == UPDATES_PER_PAGE as usize {
Some(page + 1)
Some(self.page + 1)
} else {
None
};
@@ -217,13 +226,12 @@ impl Resolve<ListUpdates, User> for State {
}
}
impl Resolve<GetUpdate, User> for State {
impl Resolve<ReadArgs> for GetUpdate {
async fn resolve(
&self,
GetUpdate { id }: GetUpdate,
user: User,
) -> anyhow::Result<Update> {
let update = find_one_by_id(&db_client().updates, &id)
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Update> {
let update = find_one_by_id(&db_client().updates, &self.id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
@@ -232,9 +240,9 @@ impl Resolve<GetUpdate, User> for State {
}
match &update.target {
ResourceTarget::System(_) => {
return Err(anyhow!(
"user must be admin to view system updates"
))
return Err(
anyhow!("user must be admin to view system updates").into(),
)
}
ResourceTarget::Server(id) => {
resource::get_check_permissions::<Server>(
@@ -292,6 +300,14 @@ impl Resolve<GetUpdate, User> for State {
)
.await?;
}
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
id,
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
id,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -6,7 +6,7 @@ use komodo_client::{
build::{Build, BuildInfo, PartialBuildConfig},
config::core::CoreConfig,
permission::PermissionLevel,
user::User,
update::Update,
CloneArgs, NoData,
},
};
@@ -21,78 +21,88 @@ use crate::{
config::core_config,
helpers::git_token,
resource,
state::{db_client, github_client, State},
state::{db_client, github_client},
};
impl Resolve<CreateBuild, User> for State {
#[instrument(name = "CreateBuild", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateBuild {
#[instrument(name = "CreateBuild", skip(user))]
async fn resolve(
&self,
CreateBuild { name, config }: CreateBuild,
user: User,
) -> anyhow::Result<Build> {
resource::create::<Build>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
Ok(
resource::create::<Build>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyBuild, User> for State {
#[instrument(name = "CopyBuild", skip(self, user))]
impl Resolve<WriteArgs> for CopyBuild {
#[instrument(name = "CopyBuild", skip(user))]
async fn resolve(
&self,
CopyBuild { name, id }: CopyBuild,
user: User,
) -> anyhow::Result<Build> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
let Build { mut config, .. } =
resource::get_check_permissions::<Build>(
&id,
&user,
&self.id,
user,
PermissionLevel::Write,
)
.await?;
// reset version to 0.0.0
config.version = Default::default();
resource::create::<Build>(&name, config.into(), &user).await
Ok(
resource::create::<Build>(&self.name, config.into(), user)
.await?,
)
}
}
impl Resolve<DeleteBuild, User> for State {
#[instrument(name = "DeleteBuild", skip(self, user))]
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,
DeleteBuild { id }: DeleteBuild,
user: User,
) -> anyhow::Result<Build> {
resource::delete::<Build>(&id, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
Ok(resource::update::<Build>(&self.id, self.config, user).await?)
}
}
impl Resolve<UpdateBuild, User> for State {
#[instrument(name = "UpdateBuild", skip(self, user))]
impl Resolve<WriteArgs> for RenameBuild {
#[instrument(name = "RenameBuild", skip(user))]
async fn resolve(
&self,
UpdateBuild { id, config }: UpdateBuild,
user: User,
) -> anyhow::Result<Build> {
resource::update::<Build>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Build>(&self.id, &self.name, user).await?)
}
}
impl Resolve<RefreshBuildCache, User> for State {
impl Resolve<WriteArgs> for RefreshBuildCache {
#[instrument(
name = "RefreshBuildCache",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
RefreshBuildCache { build }: RefreshBuildCache,
user: User,
) -> anyhow::Result<NoData> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// build should be able to do this.
let build = resource::get_check_permissions::<Build>(
&build,
&user,
&self.build,
user,
PermissionLevel::Execute,
)
.await?;
@@ -166,39 +176,44 @@ impl Resolve<RefreshBuildCache, User> for State {
}
}
impl Resolve<CreateBuildWebhook, User> for State {
#[instrument(name = "CreateBuildWebhook", skip(self, user))]
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
&self,
CreateBuildWebhook { build }: CreateBuildWebhook,
user: User,
) -> anyhow::Result<CreateBuildWebhookResponse> {
self,
args: &WriteArgs,
) -> serror::Result<CreateBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let WriteArgs { user } = args;
let build = resource::get_check_permissions::<Build>(
&build,
&user,
&self.build,
user,
PermissionLevel::Write,
)
.await?;
if build.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -259,64 +274,65 @@ impl Resolve<CreateBuildWebhook, User> for State {
.context("failed to create webhook")?;
if !build.config.webhook_enabled {
self
.resolve(
UpdateBuild {
id: build.id,
config: PartialBuildConfig {
webhook_enabled: Some(true),
..Default::default()
},
},
user,
)
.await
.context("failed to update build to enable webhook")?;
UpdateBuild {
id: build.id,
config: PartialBuildConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update build to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<DeleteBuildWebhook, User> for State {
#[instrument(name = "DeleteBuildWebhook", skip(self, user))]
impl Resolve<WriteArgs> for DeleteBuildWebhook {
#[instrument(name = "DeleteBuildWebhook", skip(user))]
async fn resolve(
&self,
DeleteBuildWebhook { build }: DeleteBuildWebhook,
user: User,
) -> anyhow::Result<DeleteBuildWebhookResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteBuildWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let build = resource::get_check_permissions::<Build>(
&build,
&self.build,
&user,
PermissionLevel::Write,
)
.await?;
if build.config.git_provider != "github.com" {
return Err(anyhow!(
"Can only manage github.com repo webhooks"
));
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if build.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't delete webhook"
));
return Err(
anyhow!("No repo configured, can't delete webhook").into(),
);
}
let mut split = build.config.repo.split('/');
let owner = split.next().context("Build repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =

View File

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

@@ -2,18 +2,21 @@ use anyhow::{anyhow, Context};
use komodo_client::{
api::write::*,
entities::{
deployment::{Deployment, DeploymentState},
deployment::{
Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode,
},
docker::container::RestartPolicyNameEnum,
komodo_timestamp,
permission::PermissionLevel,
server::Server,
server::{Server, ServerState},
to_komodo_name,
update::Update,
user::User,
Operation,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use periphery_client::api;
use periphery_client::api::{self, container::InspectContainer};
use resolver_api::Resolve;
use crate::{
@@ -23,70 +26,175 @@ use crate::{
update::{add_update, make_update},
},
resource,
state::{action_states, db_client, State},
state::{action_states, db_client, server_status_cache},
};
impl Resolve<CreateDeployment, User> for State {
#[instrument(name = "CreateDeployment", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateDeployment {
#[instrument(name = "CreateDeployment", skip(user))]
async fn resolve(
&self,
CreateDeployment { name, config }: CreateDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::create::<Deployment>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
resource::create::<Deployment>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyDeployment, User> for State {
#[instrument(name = "CopyDeployment", skip(self, user))]
impl Resolve<WriteArgs> for CopyDeployment {
#[instrument(name = "CopyDeployment", skip(user))]
async fn resolve(
&self,
CopyDeployment { name, id }: CopyDeployment,
user: User,
) -> anyhow::Result<Deployment> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let Deployment { config, .. } =
resource::get_check_permissions::<Deployment>(
&id,
&user,
&self.id,
user,
PermissionLevel::Write,
)
.await?;
resource::create::<Deployment>(&name, config.into(), &user).await
Ok(
resource::create::<Deployment>(
&self.name,
config.into(),
&user,
)
.await?,
)
}
}
impl Resolve<DeleteDeployment, User> for State {
#[instrument(name = "DeleteDeployment", skip(self, user))]
impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
#[instrument(name = "CreateDeploymentFromContainer", skip(user))]
async fn resolve(
&self,
DeleteDeployment { id }: DeleteDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::delete::<Deployment>(&id, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write,
)
.await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let container = periphery_client(&server)?
.request(InspectContainer {
name: self.name.clone(),
})
.await
.context("Failed to inspect container")?;
let mut config = PartialDeploymentConfig {
server_id: server.id.into(),
..Default::default()
};
if let Some(container_config) = container.config {
config.image = container_config
.image
.map(|image| DeploymentImage::Image { image });
config.command = container_config.cmd.join(" ").into();
config.environment = container_config
.env
.into_iter()
.map(|env| format!(" {env}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.labels = container_config
.labels
.into_iter()
.map(|(key, val)| format!(" {key}: {val}"))
.collect::<Vec<_>>()
.join("\n")
.into();
}
if let Some(host_config) = container.host_config {
config.volumes = host_config
.binds
.into_iter()
.map(|bind| format!(" {bind}"))
.collect::<Vec<_>>()
.join("\n")
.into();
config.network = host_config.network_mode;
config.ports = host_config
.port_bindings
.into_iter()
.filter_map(|(container, mut host)| {
let host = host.pop()?.host_port?;
Some(format!(" {host}:{}", container.replace("/tcp", "")))
})
.collect::<Vec<_>>()
.join("\n")
.into();
config.restart = host_config.restart_policy.map(|restart| {
match restart.name {
RestartPolicyNameEnum::Always => RestartMode::Always,
RestartPolicyNameEnum::No
| RestartPolicyNameEnum::Empty => RestartMode::NoRestart,
RestartPolicyNameEnum::UnlessStopped => {
RestartMode::UnlessStopped
}
RestartPolicyNameEnum::OnFailure => RestartMode::OnFailure,
}
});
}
Ok(
resource::create::<Deployment>(&self.name, config, &user)
.await?,
)
}
}
impl Resolve<UpdateDeployment, User> for State {
#[instrument(name = "UpdateDeployment", skip(self, user))]
impl Resolve<WriteArgs> for DeleteDeployment {
#[instrument(name = "DeleteDeployment", skip(args))]
async fn resolve(
&self,
UpdateDeployment { id, config }: UpdateDeployment,
user: User,
) -> anyhow::Result<Deployment> {
resource::update::<Deployment>(&id, config, &user).await
self,
args: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(resource::delete::<Deployment>(&self.id, args).await?)
}
}
impl Resolve<RenameDeployment, User> for State {
#[instrument(name = "RenameDeployment", skip(self, user))]
impl Resolve<WriteArgs> for UpdateDeployment {
#[instrument(name = "UpdateDeployment", skip(user))]
async fn resolve(
&self,
RenameDeployment { id, name }: RenameDeployment,
user: User,
) -> anyhow::Result<Update> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
resource::update::<Deployment>(&self.id, self.config, &user)
.await?,
)
}
}
impl Resolve<WriteArgs> for RenameDeployment {
#[instrument(name = "RenameDeployment", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let deployment = resource::get_check_permissions::<Deployment>(
&id,
&user,
&self.id,
user,
PermissionLevel::Write,
)
.await?;
@@ -102,14 +210,17 @@ impl Resolve<RenameDeployment, User> for State {
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&name);
let name = to_komodo_name(&self.name);
let container_state = get_deployment_state(&deployment).await?;
if container_state == DeploymentState::Unknown {
return Err(anyhow!(
"cannot rename deployment when container status is unknown"
));
return Err(
anyhow!(
"Cannot rename Deployment when container status is unknown"
)
.into(),
);
}
let mut update =
@@ -124,7 +235,7 @@ impl Resolve<RenameDeployment, User> for State {
None,
)
.await
.context("failed to update deployment name on db")?;
.context("Failed to update Deployment name on db")?;
if container_state != DeploymentState::NotDeployed {
let server =
@@ -135,20 +246,19 @@ impl Resolve<RenameDeployment, User> for State {
new_name: name.clone(),
})
.await
.context("failed to rename container on server")?;
.context("Failed to rename container on server")?;
update.logs.push(log);
}
update.push_simple_log(
"rename deployment",
"Rename Deployment",
format!(
"renamed deployment from {} to {}",
"Renamed Deployment from {} to {}",
deployment.name, name
),
);
update.finalize();
add_update(update.clone()).await?;
update.id = add_update(update.clone()).await?;
Ok(update)
}

View File

@@ -2,36 +2,37 @@ use anyhow::anyhow;
use komodo_client::{
api::write::{UpdateDescription, UpdateDescriptionResponse},
entities::{
alerter::Alerter, build::Build, builder::Builder,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, user::User, ResourceTarget,
sync::ResourceSync, ResourceTarget,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
use crate::resource;
impl Resolve<UpdateDescription, User> for State {
#[instrument(name = "UpdateDescription", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateDescription {
#[instrument(name = "UpdateDescription", skip(user))]
async fn resolve(
&self,
UpdateDescription {
target,
description,
}: UpdateDescription,
user: User,
) -> anyhow::Result<UpdateDescriptionResponse> {
match target {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateDescriptionResponse> {
match self.target {
ResourceTarget::System(_) => {
return Err(anyhow!(
"cannot update description of System resource target"
))
return Err(
anyhow!(
"cannot update description of System resource target"
)
.into(),
)
}
ResourceTarget::Server(id) => {
resource::update_description::<Server>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -39,7 +40,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::Deployment(id) => {
resource::update_description::<Deployment>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -47,7 +48,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::Build(id) => {
resource::update_description::<Build>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -55,7 +56,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::Repo(id) => {
resource::update_description::<Repo>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -63,7 +64,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::Builder(id) => {
resource::update_description::<Builder>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -71,7 +72,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::Alerter(id) => {
resource::update_description::<Alerter>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -79,7 +80,15 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::Procedure(id) => {
resource::update_description::<Procedure>(
&id,
&description,
&self.description,
&user,
)
.await?;
}
ResourceTarget::Action(id) => {
resource::update_description::<Action>(
&id,
&self.description,
&user,
)
.await?;
@@ -87,7 +96,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::ServerTemplate(id) => {
resource::update_description::<ServerTemplate>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -95,7 +104,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::ResourceSync(id) => {
resource::update_description::<ResourceSync>(
&id,
&description,
&self.description,
&user,
)
.await?;
@@ -103,7 +112,7 @@ impl Resolve<UpdateDescription, User> for State {
ResourceTarget::Stack(id) => {
resource::update_description::<Stack>(
&id,
&description,
&self.description,
&user,
)
.await?;

View File

@@ -1,18 +1,19 @@
use std::time::Instant;
use anyhow::{anyhow, Context};
use anyhow::Context;
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::write::*, entities::user::User};
use resolver_api::{derive::Resolver, Resolver};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::{auth::auth_request, state::State};
use crate::auth::auth_request;
mod action;
mod alerter;
mod build;
mod builder;
@@ -32,13 +33,18 @@ mod user;
mod user_group;
mod variable;
pub struct WriteArgs {
pub user: User,
}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)]
#[variant_derive(Debug)]
#[resolver_target(State)]
#[resolver_args(User)]
#[args(WriteArgs)]
#[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")]
pub enum WriteRequest {
// ==== USER ====
@@ -79,6 +85,7 @@ pub enum WriteRequest {
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
CopyDeployment(CopyDeployment),
CreateDeploymentFromContainer(CreateDeploymentFromContainer),
DeleteDeployment(DeleteDeployment),
UpdateDeployment(UpdateDeployment),
RenameDeployment(RenameDeployment),
@@ -88,6 +95,7 @@ pub enum WriteRequest {
CopyBuild(CopyBuild),
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
@@ -97,18 +105,21 @@ pub enum WriteRequest {
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== SERVER TEMPLATE ====
CreateServerTemplate(CreateServerTemplate),
CopyServerTemplate(CopyServerTemplate),
DeleteServerTemplate(DeleteServerTemplate),
UpdateServerTemplate(UpdateServerTemplate),
RenameServerTemplate(RenameServerTemplate),
// ==== REPO ====
CreateRepo(CreateRepo),
CopyRepo(CopyRepo),
DeleteRepo(DeleteRepo),
UpdateRepo(UpdateRepo),
RenameRepo(RenameRepo),
RefreshRepoCache(RefreshRepoCache),
CreateRepoWebhook(CreateRepoWebhook),
DeleteRepoWebhook(DeleteRepoWebhook),
@@ -118,18 +129,28 @@ pub enum WriteRequest {
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),
@@ -178,7 +199,7 @@ pub fn router() -> Router {
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<WriteRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
) -> serror::Result<axum::response::Response> {
let req_id = Uuid::new_v4();
let res = tokio::spawn(task(req_id, request, user))
@@ -189,7 +210,7 @@ async fn handler(
warn!("/write request {req_id} spawn error: {e:#}");
}
Ok((TypedHeader(ContentType::json()), res??))
res?
}
#[instrument(
@@ -204,28 +225,19 @@ async fn task(
req_id: Uuid,
request: WriteRequest,
user: User,
) -> anyhow::Result<String> {
) -> serror::Result<axum::response::Response> {
info!("/write request | user: {}", user.username);
let timer = Instant::now();
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
let res = request.resolve(&WriteArgs { user }).await;
if let Err(e) = &res {
warn!("/write request {req_id} error: {e:#}");
warn!("/write request {req_id} error: {:#}", e.error);
}
let elapsed = timer.elapsed();
debug!("/write request {req_id} | resolve time: {elapsed:?}");
res
res.map(|res| res.0)
}

View File

@@ -2,16 +2,9 @@ use std::str::FromStr;
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::{
UpdatePermissionOnResourceType,
UpdatePermissionOnResourceTypeResponse, UpdatePermissionOnTarget,
UpdatePermissionOnTargetResponse, UpdateUserAdmin,
UpdateUserAdminResponse, UpdateUserBasePermissions,
UpdateUserBasePermissionsResponse,
},
api::write::*,
entities::{
permission::{UserTarget, UserTargetVariant},
user::User,
ResourceTarget, ResourceTargetVariant,
},
};
@@ -24,37 +17,40 @@ use mungos::{
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_user,
state::{db_client, State},
};
use crate::{helpers::query::get_user, state::db_client};
impl Resolve<UpdateUserAdmin, User> for State {
use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateUserAdmin {
#[instrument(name = "UpdateUserAdmin", skip(super_admin))]
async fn resolve(
&self,
UpdateUserAdmin { user_id, admin }: UpdateUserAdmin,
super_admin: User,
) -> anyhow::Result<UpdateUserAdminResponse> {
self,
WriteArgs { user: super_admin }: &WriteArgs,
) -> serror::Result<UpdateUserAdminResponse> {
if !super_admin.super_admin {
return Err(anyhow!("Only super admins can call this method."));
return Err(
anyhow!("Only super admins can call this method.").into(),
);
}
let user = find_one_by_id(&db_client().users, &user_id)
let user = find_one_by_id(&db_client().users, &self.user_id)
.await
.context("failed to query mongo for user")?
.context("did not find user with given id")?;
if !user.enabled {
return Err(anyhow!("User is disabled. Enable user first."));
return Err(
anyhow!("User is disabled. Enable user first.").into(),
);
}
if user.super_admin {
return Err(anyhow!("Cannot update other super admins"));
return Err(anyhow!("Cannot update other super admins").into());
}
update_one_by_id(
&db_client().users,
&user_id,
doc! { "$set": { "admin": admin } },
&self.user_id,
doc! { "$set": { "admin": self.admin } },
None,
)
.await?;
@@ -63,20 +59,21 @@ impl Resolve<UpdateUserAdmin, User> for State {
}
}
impl Resolve<UpdateUserBasePermissions, User> for State {
#[instrument(name = "UpdateUserBasePermissions", skip(self, admin))]
impl Resolve<WriteArgs> for UpdateUserBasePermissions {
#[instrument(name = "UpdateUserBasePermissions", skip(admin))]
async fn resolve(
&self,
UpdateUserBasePermissions {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdateUserBasePermissionsResponse> {
let UpdateUserBasePermissions {
user_id,
enabled,
create_servers,
create_builds,
}: UpdateUserBasePermissions,
admin: User,
) -> anyhow::Result<UpdateUserBasePermissionsResponse> {
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only"));
return Err(anyhow!("this method is admin only").into());
}
let user = find_one_by_id(&db_client().users, &user_id)
@@ -84,14 +81,17 @@ impl Resolve<UpdateUserBasePermissions, User> for State {
.context("failed to query mongo for user")?
.context("did not find user with given id")?;
if user.super_admin {
return Err(anyhow!(
"Cannot use this method to update super admins permissions"
));
return Err(
anyhow!(
"Cannot use this method to update super admins permissions"
)
.into(),
);
}
if user.admin && !admin.super_admin {
return Err(anyhow!(
"Only super admins can use this method to update other admins permissions"
));
).into());
}
let mut update_doc = Document::new();
if let Some(enabled) = enabled {
@@ -116,34 +116,35 @@ impl Resolve<UpdateUserBasePermissions, User> for State {
}
}
impl Resolve<UpdatePermissionOnResourceType, User> for State {
#[instrument(
name = "UpdatePermissionOnResourceType",
skip(self, admin)
)]
impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
#[instrument(name = "UpdatePermissionOnResourceType", skip(admin))]
async fn resolve(
&self,
UpdatePermissionOnResourceType {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {
let UpdatePermissionOnResourceType {
user_target,
resource_type,
permission,
}: UpdatePermissionOnResourceType,
admin: User,
) -> anyhow::Result<UpdatePermissionOnResourceTypeResponse> {
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only"));
return Err(anyhow!("this method is admin only").into());
}
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if user.admin {
return Err(anyhow!(
return Err(
anyhow!(
"cannot use this method to update other admins permissions"
));
)
.into(),
);
}
if !user.enabled {
return Err(anyhow!("user not enabled"));
return Err(anyhow!("user not enabled").into());
}
}
@@ -181,31 +182,35 @@ impl Resolve<UpdatePermissionOnResourceType, User> for State {
}
}
impl Resolve<UpdatePermissionOnTarget, User> for State {
#[instrument(name = "UpdatePermissionOnTarget", skip(self, admin))]
impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
#[instrument(name = "UpdatePermissionOnTarget", skip(admin))]
async fn resolve(
&self,
UpdatePermissionOnTarget {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnTargetResponse> {
let UpdatePermissionOnTarget {
user_target,
resource_target,
permission,
}: UpdatePermissionOnTarget,
admin: User,
) -> anyhow::Result<UpdatePermissionOnTargetResponse> {
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only"));
return Err(anyhow!("this method is admin only").into());
}
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if user.admin {
return Err(anyhow!(
return Err(
anyhow!(
"cannot use this method to update other admins permissions"
));
)
.into(),
);
}
if !user.enabled {
return Err(anyhow!("user not enabled"));
return Err(anyhow!("user not enabled").into());
}
}
@@ -247,7 +252,7 @@ impl Resolve<UpdatePermissionOnTarget, User> for State {
/// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_user_target_with_validation(
user_target: &UserTarget,
) -> anyhow::Result<(UserTargetVariant, String)> {
) -> serror::Result<(UserTargetVariant, String)> {
match user_target {
UserTarget::User(ident) => {
let filter = match ObjectId::from_str(ident) {
@@ -283,7 +288,7 @@ async fn extract_user_target_with_validation(
/// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_resource_target_with_validation(
resource_target: &ResourceTarget,
) -> anyhow::Result<(ResourceTargetVariant, String)> {
) -> serror::Result<(ResourceTargetVariant, String)> {
match resource_target {
ResourceTarget::System(_) => {
let res = resource_target.extract_variant_id();
@@ -387,6 +392,20 @@ async fn extract_resource_target_with_validation(
.id;
Ok((ResourceTargetVariant::Procedure, id))
}
ResourceTarget::Action(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.actions
.find_one(filter)
.await
.context("failed to query db for actions")?
.context("no matching action found")?
.id;
Ok((ResourceTargetVariant::Action, id))
}
ResourceTarget::ServerTemplate(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },

View File

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

@@ -3,7 +3,6 @@ use komodo_client::{
api::write::*,
entities::{
provider::{DockerRegistryAccount, GitProviderAccount},
user::User,
Operation, ResourceTarget,
},
};
@@ -15,29 +14,31 @@ use resolver_api::Resolve;
use crate::{
helpers::update::{add_update, make_update},
state::{db_client, State},
state::db_client,
};
impl Resolve<CreateGitProviderAccount, User> for State {
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateGitProviderAccount {
async fn resolve(
&self,
CreateGitProviderAccount { account }: CreateGitProviderAccount,
user: User,
) -> anyhow::Result<CreateGitProviderAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateGitProviderAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can create git provider accounts"
));
return Err(
anyhow!("only admins can create git provider accounts")
.into(),
);
}
let mut account: GitProviderAccount = account.into();
let mut account: GitProviderAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string."));
return Err(anyhow!("domain cannot be empty string.").into());
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string."));
return Err(anyhow!("username cannot be empty string.").into());
}
let mut update = make_update(
@@ -77,36 +78,38 @@ impl Resolve<CreateGitProviderAccount, User> for State {
}
}
impl Resolve<UpdateGitProviderAccount, User> for State {
impl Resolve<WriteArgs> for UpdateGitProviderAccount {
async fn resolve(
&self,
UpdateGitProviderAccount { id, mut account }: UpdateGitProviderAccount,
user: User,
) -> anyhow::Result<UpdateGitProviderAccountResponse> {
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateGitProviderAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can update git provider accounts"
));
return Err(
anyhow!("only admins can update git provider accounts")
.into(),
);
}
if let Some(domain) = &account.domain {
if let Some(domain) = &self.account.domain {
if domain.is_empty() {
return Err(anyhow!(
"cannot update git provider with empty domain"
));
return Err(
anyhow!("cannot update git provider with empty domain")
.into(),
);
}
}
if let Some(username) = &account.username {
if let Some(username) = &self.account.username {
if username.is_empty() {
return Err(anyhow!(
"cannot update git provider with empty username"
));
return Err(
anyhow!("cannot update git provider with empty username")
.into(),
);
}
}
// Ensure update does not change id
account.id = None;
self.account.id = None;
let mut update = make_update(
ResourceTarget::system(),
@@ -114,25 +117,24 @@ impl Resolve<UpdateGitProviderAccount, User> for State {
&user,
);
let account = to_document(&account).context(
let account = to_document(&self.account).context(
"failed to serialize partial git provider account to bson",
)?;
let db = db_client();
update_one_by_id(
&db.git_accounts,
&id,
&self.id,
doc! { "$set": account },
None,
)
.await
.context("failed to update git provider account on db")?;
let Some(account) =
find_one_by_id(&db.git_accounts, &id)
.await
.context("failed to query db for git accounts")?
let Some(account) = find_one_by_id(&db.git_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
update.push_simple_log(
@@ -156,16 +158,16 @@ impl Resolve<UpdateGitProviderAccount, User> for State {
}
}
impl Resolve<DeleteGitProviderAccount, User> for State {
impl Resolve<WriteArgs> for DeleteGitProviderAccount {
async fn resolve(
&self,
DeleteGitProviderAccount { id }: DeleteGitProviderAccount,
user: User,
) -> anyhow::Result<DeleteGitProviderAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteGitProviderAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can delete git provider accounts"
));
return Err(
anyhow!("only admins can delete git provider accounts")
.into(),
);
}
let mut update = make_update(
@@ -175,14 +177,13 @@ impl Resolve<DeleteGitProviderAccount, User> for State {
);
let db = db_client();
let Some(account) =
find_one_by_id(&db.git_accounts, &id)
.await
.context("failed to query db for git accounts")?
let Some(account) = find_one_by_id(&db.git_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
delete_one_by_id(&db.git_accounts, &id, None)
delete_one_by_id(&db.git_accounts, &self.id, None)
.await
.context("failed to delete git account on db")?;
@@ -207,26 +208,28 @@ impl Resolve<DeleteGitProviderAccount, User> for State {
}
}
impl Resolve<CreateDockerRegistryAccount, User> for State {
impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
async fn resolve(
&self,
CreateDockerRegistryAccount { account }: CreateDockerRegistryAccount,
user: User,
) -> anyhow::Result<CreateDockerRegistryAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateDockerRegistryAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can create docker registry account accounts"
));
return Err(
anyhow!(
"only admins can create docker registry account accounts"
)
.into(),
);
}
let mut account: DockerRegistryAccount = account.into();
let mut account: DockerRegistryAccount = self.account.into();
if account.domain.is_empty() {
return Err(anyhow!("domain cannot be empty string."));
return Err(anyhow!("domain cannot be empty string.").into());
}
if account.username.is_empty() {
return Err(anyhow!("username cannot be empty string."));
return Err(anyhow!("username cannot be empty string.").into());
}
let mut update = make_update(
@@ -268,35 +271,41 @@ impl Resolve<CreateDockerRegistryAccount, User> for State {
}
}
impl Resolve<UpdateDockerRegistryAccount, User> for State {
impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
async fn resolve(
&self,
UpdateDockerRegistryAccount { id, mut account }: UpdateDockerRegistryAccount,
user: User,
) -> anyhow::Result<UpdateDockerRegistryAccountResponse> {
mut self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateDockerRegistryAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can update docker registry accounts"
));
return Err(
anyhow!("only admins can update docker registry accounts")
.into(),
);
}
if let Some(domain) = &account.domain {
if let Some(domain) = &self.account.domain {
if domain.is_empty() {
return Err(anyhow!(
"cannot update docker registry account with empty domain"
));
return Err(
anyhow!(
"cannot update docker registry account with empty domain"
)
.into(),
);
}
}
if let Some(username) = &account.username {
if let Some(username) = &self.account.username {
if username.is_empty() {
return Err(anyhow!(
return Err(
anyhow!(
"cannot update docker registry account with empty username"
));
)
.into(),
);
}
}
account.id = None;
self.account.id = None;
let mut update = make_update(
ResourceTarget::system(),
@@ -304,14 +313,14 @@ impl Resolve<UpdateDockerRegistryAccount, User> for State {
&user,
);
let account = to_document(&account).context(
let account = to_document(&self.account).context(
"failed to serialize partial docker registry account account to bson",
)?;
let db = db_client();
update_one_by_id(
&db.registry_accounts,
&id,
&self.id,
doc! { "$set": account },
None,
)
@@ -320,11 +329,12 @@ impl Resolve<UpdateDockerRegistryAccount, User> for State {
"failed to update docker registry account account on db",
)?;
let Some(account) = find_one_by_id(&db.registry_accounts, &id)
.await
.context("failed to query db for registry accounts")?
let Some(account) =
find_one_by_id(&db.registry_accounts, &self.id)
.await
.context("failed to query db for registry accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
update.push_simple_log(
@@ -348,16 +358,16 @@ impl Resolve<UpdateDockerRegistryAccount, User> for State {
}
}
impl Resolve<DeleteDockerRegistryAccount, User> for State {
impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
async fn resolve(
&self,
DeleteDockerRegistryAccount { id }: DeleteDockerRegistryAccount,
user: User,
) -> anyhow::Result<DeleteDockerRegistryAccountResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteDockerRegistryAccountResponse> {
if !user.admin {
return Err(anyhow!(
"only admins can delete docker registry accounts"
));
return Err(
anyhow!("only admins can delete docker registry accounts")
.into(),
);
}
let mut update = make_update(
@@ -367,13 +377,14 @@ impl Resolve<DeleteDockerRegistryAccount, User> for State {
);
let db = db_client();
let Some(account) = find_one_by_id(&db.registry_accounts, &id)
.await
.context("failed to query db for git accounts")?
let Some(account) =
find_one_by_id(&db.registry_accounts, &self.id)
.await
.context("failed to query db for git accounts")?
else {
return Err(anyhow!("no account found with given id"));
return Err(anyhow!("no account found with given id").into());
};
delete_one_by_id(&db.registry_accounts, &id, None)
delete_one_by_id(&db.registry_accounts, &self.id, None)
.await
.context("failed to delete registry account on db")?;

View File

@@ -1,95 +1,176 @@
use anyhow::{anyhow, Context};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
repo::{PartialRepoConfig, Repo, RepoInfo},
user::User,
CloneArgs, NoData,
server::Server,
to_komodo_name,
update::{Log, Update},
CloneArgs, NoData, Operation,
},
};
use mongo_indexed::doc;
use mungos::mongodb::bson::to_document;
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::git_token,
helpers::{
git_token, periphery_client,
update::{add_update, make_update},
},
resource,
state::{db_client, github_client, State},
state::{action_states, db_client, github_client},
};
impl Resolve<CreateRepo, User> for State {
#[instrument(name = "CreateRepo", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateRepo {
#[instrument(name = "CreateRepo", skip(user))]
async fn resolve(
&self,
CreateRepo { name, config }: CreateRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::create::<Repo>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
Ok(resource::create::<Repo>(&self.name, self.config, user).await?)
}
}
impl Resolve<CopyRepo, User> for State {
#[instrument(name = "CopyRepo", skip(self, user))]
impl Resolve<WriteArgs> for CopyRepo {
#[instrument(name = "CopyRepo", skip(user))]
async fn resolve(
&self,
CopyRepo { name, id }: CopyRepo,
user: User,
) -> anyhow::Result<Repo> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
let Repo { config, .. } =
resource::get_check_permissions::<Repo>(
&id,
&self.id,
&user,
PermissionLevel::Write,
)
.await?;
resource::create::<Repo>(&name, config.into(), &user).await
Ok(
resource::create::<Repo>(&self.name, config.into(), &user)
.await?,
)
}
}
impl Resolve<DeleteRepo, User> for State {
#[instrument(name = "DeleteRepo", skip(self, user))]
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,
DeleteRepo { id }: DeleteRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::delete::<Repo>(&id, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
Ok(resource::update::<Repo>(&self.id, self.config, user).await?)
}
}
impl Resolve<UpdateRepo, User> for State {
#[instrument(name = "UpdateRepo", skip(self, user))]
impl Resolve<WriteArgs> for RenameRepo {
#[instrument(name = "RenameRepo", skip(user))]
async fn resolve(
&self,
UpdateRepo { id, config }: UpdateRepo,
user: User,
) -> anyhow::Result<Repo> {
resource::update::<Repo>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&self.id,
&user,
PermissionLevel::Write,
)
.await?;
if repo.config.server_id.is_empty()
|| !repo.config.path.is_empty()
{
return Ok(
resource::rename::<Repo>(&repo.id, &self.name, &user).await?,
);
}
// get the action state for the repo (or insert default).
let action_state =
action_states().repo.get_or_insert_default(&repo.id).await;
// Will check to ensure repo not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&self.name);
let mut update = make_update(&repo, Operation::RenameRepo, &user);
update_one_by_id(
&db_client().repos,
&repo.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("Failed to update Repo name on db")?;
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let log = match periphery_client(&server)?
.request(api::git::RenameRepo {
curr_name: to_komodo_name(&repo.name),
new_name: name.clone(),
})
.await
.context("Failed to rename Repo directory on Server")
{
Ok(log) => log,
Err(e) => Log::error(
"Rename Repo directory failure",
format_serror(&e.into()),
),
};
update.logs.push(log);
update.push_simple_log(
"Rename Repo",
format!("Renamed Repo from {} to {}", repo.name, name),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<RefreshRepoCache, User> for State {
impl Resolve<WriteArgs> for RefreshRepoCache {
#[instrument(
name = "RefreshRepoCache",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
RefreshRepoCache { repo }: RefreshRepoCache,
user: User,
) -> anyhow::Result<NoData> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// repo should be able to do this.
let repo = resource::get_check_permissions::<Repo>(
&repo,
&self.repo,
&user,
PermissionLevel::Execute,
)
@@ -161,39 +242,42 @@ impl Resolve<RefreshRepoCache, User> for State {
}
}
impl Resolve<CreateRepoWebhook, User> for State {
#[instrument(name = "CreateRepoWebhook", skip(self, user))]
impl Resolve<WriteArgs> for CreateRepoWebhook {
#[instrument(name = "CreateRepoWebhook", skip(args))]
async fn resolve(
&self,
CreateRepoWebhook { repo, action }: CreateRepoWebhook,
user: User,
) -> anyhow::Result<CreateRepoWebhookResponse> {
self,
args: &WriteArgs,
) -> serror::Result<CreateRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
&self.repo,
&args.user,
PermissionLevel::Write,
)
.await?;
if repo.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
@@ -226,7 +310,7 @@ impl Resolve<CreateRepoWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}
@@ -264,64 +348,65 @@ impl Resolve<CreateRepoWebhook, User> for State {
.context("failed to create webhook")?;
if !repo.config.webhook_enabled {
self
.resolve(
UpdateRepo {
id: repo.id,
config: PartialRepoConfig {
webhook_enabled: Some(true),
..Default::default()
},
},
user,
)
.await
.context("failed to update repo to enable webhook")?;
UpdateRepo {
id: repo.id,
config: PartialRepoConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update repo to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<DeleteRepoWebhook, User> for State {
#[instrument(name = "DeleteRepoWebhook", skip(self, user))]
impl Resolve<WriteArgs> for DeleteRepoWebhook {
#[instrument(name = "DeleteRepoWebhook", skip(user))]
async fn resolve(
&self,
DeleteRepoWebhook { repo, action }: DeleteRepoWebhook,
user: User,
) -> anyhow::Result<DeleteRepoWebhookResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteRepoWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let repo = resource::get_check_permissions::<Repo>(
&repo,
&user,
&self.repo,
user,
PermissionLevel::Write,
)
.await?;
if repo.config.git_provider != "github.com" {
return Err(anyhow!(
"Can only manage github.com repo webhooks"
));
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if repo.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = repo.config.repo.split('/');
let owner = split.next().context("Repo repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo_name =
@@ -347,7 +432,7 @@ impl Resolve<DeleteRepoWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
RepoWebhookAction::Clone => {
format!("{host}/listener/github/repo/{}/clone", repo.id)
}

View File

@@ -1,17 +1,13 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
komodo_timestamp,
permission::PermissionLevel,
server::Server,
update::{Update, UpdateStatus},
user::User,
Operation,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use periphery_client::api;
use resolver_api::Resolve;
@@ -21,81 +17,59 @@ use crate::{
update::{add_update, make_update, update_update},
},
resource,
state::{db_client, State},
};
impl Resolve<CreateServer, User> for State {
#[instrument(name = "CreateServer", skip(self, user))]
async fn resolve(
&self,
CreateServer { name, config }: CreateServer,
user: User,
) -> anyhow::Result<Server> {
resource::create::<Server>(&name, config, &user).await
}
}
use super::WriteArgs;
impl Resolve<DeleteServer, User> for State {
#[instrument(name = "DeleteServer", skip(self, user))]
impl Resolve<WriteArgs> for CreateServer {
#[instrument(name = "CreateServer", skip(user))]
async fn resolve(
&self,
DeleteServer { id }: DeleteServer,
user: User,
) -> anyhow::Result<Server> {
resource::delete::<Server>(&id, &user).await
}
}
impl Resolve<UpdateServer, User> for State {
#[instrument(name = "UpdateServer", skip(self, user))]
async fn resolve(
&self,
UpdateServer { id, config }: UpdateServer,
user: User,
) -> anyhow::Result<Server> {
resource::update::<Server>(&id, config, &user).await
}
}
impl Resolve<RenameServer, User> for State {
#[instrument(name = "RenameServer", skip(self, user))]
async fn resolve(
&self,
RenameServer { id, name }: RenameServer,
user: User,
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&id,
&user,
PermissionLevel::Write,
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
Ok(
resource::create::<Server>(&self.name, self.config, user)
.await?,
)
.await?;
let mut update =
make_update(&server, Operation::RenameServer, &user);
update_one_by_id(&db_client().servers, &id, mungos::update::Update::Set(doc! { "name": &name, "updated_at": komodo_timestamp() }), None)
.await
.context("failed to update server on db. this name may already be taken.")?;
update.push_simple_log(
"rename server",
format!("renamed server {id} from {} to {name}", server.name),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<CreateNetwork, User> for State {
#[instrument(name = "CreateNetwork", skip(self, user))]
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,
CreateNetwork { server, name }: CreateNetwork,
user: User,
) -> anyhow::Result<Update> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Server> {
Ok(resource::update::<Server>(&self.id, self.config, user).await?)
}
}
impl Resolve<WriteArgs> for RenameServer {
#[instrument(name = "RenameServer", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Server>(&self.id, &self.name, user).await?)
}
}
impl Resolve<WriteArgs> for CreateNetwork {
#[instrument(name = "CreateNetwork", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&server,
&user,
&self.server,
user,
PermissionLevel::Write,
)
.await?;
@@ -108,7 +82,10 @@ impl Resolve<CreateNetwork, User> for State {
update.id = add_update(update.clone()).await?;
match periphery
.request(api::network::CreateNetwork { name, driver: None })
.request(api::network::CreateNetwork {
name: self.name,
driver: None,
})
.await
{
Ok(log) => update.logs.push(log),

View File

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

View File

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

View File

@@ -4,19 +4,15 @@ use komodo_client::{
api::write::*,
entities::{
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo},
update::Update,
user::{stack_user, User},
user::stack_user,
FileContents, NoData, Operation,
},
};
use mungos::{
by_id::update_one_by_id,
mongodb::bson::{doc, to_document},
};
use mungos::mongodb::bson::{doc, to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
@@ -27,6 +23,7 @@ use periphery_client::api::compose::{
use resolver_api::Resolve;
use crate::{
api::execute::pull_stack_inner,
config::core_config,
helpers::{
git_token, periphery_client,
@@ -36,113 +33,84 @@ use crate::{
resource,
stack::{
get_stack_and_server,
remote::{get_remote_compose_contents, RemoteComposeContents},
remote::{get_repo_compose_contents, RemoteComposeContents},
services::extract_services_into_res,
},
state::{db_client, github_client, State},
state::{db_client, github_client},
};
impl Resolve<CreateStack, User> for State {
#[instrument(name = "CreateStack", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateStack {
#[instrument(name = "CreateStack", skip(user))]
async fn resolve(
&self,
CreateStack { name, config }: CreateStack,
user: User,
) -> anyhow::Result<Stack> {
resource::create::<Stack>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
Ok(
resource::create::<Stack>(&self.name, self.config, user)
.await?,
)
}
}
impl Resolve<CopyStack, User> for State {
#[instrument(name = "CopyStack", skip(self, user))]
impl Resolve<WriteArgs> for CopyStack {
#[instrument(name = "CopyStack", skip(user))]
async fn resolve(
&self,
CopyStack { name, id }: CopyStack,
user: User,
) -> anyhow::Result<Stack> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
let Stack { config, .. } =
resource::get_check_permissions::<Stack>(
&id,
&user,
&self.id,
user,
PermissionLevel::Write,
)
.await?;
resource::create::<Stack>(&name, config.into(), &user).await
}
}
impl Resolve<DeleteStack, User> for State {
#[instrument(name = "DeleteStack", skip(self, user))]
async fn resolve(
&self,
DeleteStack { id }: DeleteStack,
user: User,
) -> anyhow::Result<Stack> {
resource::delete::<Stack>(&id, &user).await
}
}
impl Resolve<UpdateStack, User> for State {
#[instrument(name = "UpdateStack", skip(self, user))]
async fn resolve(
&self,
UpdateStack { id, config }: UpdateStack,
user: User,
) -> anyhow::Result<Stack> {
resource::update::<Stack>(&id, config, &user).await
}
}
impl Resolve<RenameStack, User> for State {
#[instrument(name = "RenameStack", skip(self, user))]
async fn resolve(
&self,
RenameStack { id, name }: RenameStack,
user: User,
) -> anyhow::Result<Update> {
let stack = resource::get_check_permissions::<Stack>(
&id,
&user,
PermissionLevel::Write,
Ok(
resource::create::<Stack>(&self.name, config.into(), user)
.await?,
)
.await?;
let mut update =
make_update(&stack, Operation::RenameStack, &user);
update_one_by_id(
&db_client().stacks,
&stack.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("failed to update stack name on db")?;
update.push_simple_log(
"rename stack",
format!("renamed stack from {} to {}", stack.name, name),
);
update.finalize();
add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<WriteStackFileContents, User> for State {
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,
WriteStackFileContents {
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 {
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let WriteStackFileContents {
stack,
file_path,
contents,
}: WriteStackFileContents,
user: User,
) -> anyhow::Result<Update> {
} = self;
let (mut stack, server) = get_stack_and_server(
&stack,
&user,
@@ -154,7 +122,7 @@ impl Resolve<WriteStackFileContents, User> for State {
if !stack.config.files_on_host && stack.config.repo.is_empty() {
return Err(anyhow!(
"Stack is not configured to use Files on Host or Git Repo, can't write file contents"
));
).into());
}
let mut update =
@@ -205,7 +173,7 @@ impl Resolve<WriteStackFileContents, User> for State {
match periphery_client(&server)?
.request(WriteCommitComposeContents {
stack,
username: Some(user.username),
username: Some(user.username.clone()),
file_path,
contents,
git_token,
@@ -225,12 +193,12 @@ impl Resolve<WriteStackFileContents, User> for State {
};
}
if let Err(e) = State
.resolve(
RefreshStackCache { stack: stack_id },
stack_user().to_owned(),
)
if let Err(e) = (RefreshStackCache { stack: stack_id })
.resolve(&WriteArgs {
user: stack_user().to_owned(),
})
.await
.map_err(|e| e.error)
.context(
"Failed to refresh stack cache after writing file contents",
)
@@ -248,22 +216,21 @@ impl Resolve<WriteStackFileContents, User> for State {
}
}
impl Resolve<RefreshStackCache, User> for State {
impl Resolve<WriteArgs> for RefreshStackCache {
#[instrument(
name = "RefreshStackCache",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
RefreshStackCache { stack }: RefreshStackCache,
user: User,
) -> anyhow::Result<NoData> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// stack should be able to do this.
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Execute,
)
.await?;
@@ -291,54 +258,56 @@ impl Resolve<RefreshStackCache, User> for State {
// =============
// FILES ON HOST
// =============
if stack.config.server_id.is_empty() {
(vec![], None, None, None, None)
let (server, state) = if stack.config.server_id.is_empty() {
(None, ServerState::Disabled)
} else {
let (server, status) =
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
if status != ServerState::Ok {
(vec![], None, None, None, None)
} else {
let GetComposeContentsOnHostResponse { contents, errors } =
match periphery_client(&server)?
.request(GetComposeContentsOnHost {
file_paths: stack.file_paths().to_vec(),
name: stack.name.clone(),
run_directory: stack.config.run_directory.clone(),
})
.await
.context(
"failed to get compose file contents from host",
) {
Ok(res) => res,
Err(e) => GetComposeContentsOnHostResponse {
contents: Default::default(),
errors: vec![FileContents {
path: stack.config.run_directory.clone(),
contents: format_serror(&e.into()),
}],
},
};
(Some(server), state)
};
if state != ServerState::Ok {
(vec![], None, None, None, None)
} else if let Some(server) = server {
let GetComposeContentsOnHostResponse { contents, errors } =
match periphery_client(&server)?
.request(GetComposeContentsOnHost {
file_paths: stack.file_paths().to_vec(),
name: stack.name.clone(),
run_directory: stack.config.run_directory.clone(),
})
.await
.context("failed to get compose file contents from host")
{
Ok(res) => res,
Err(e) => GetComposeContentsOnHostResponse {
contents: Default::default(),
errors: vec![FileContents {
path: stack.config.run_directory.clone(),
contents: format_serror(&e.into()),
}],
},
};
let project_name = stack.project_name(true);
let project_name = stack.project_name(true);
let mut services = Vec::new();
let mut services = Vec::new();
for contents in &contents {
if let Err(e) = extract_services_into_res(
&project_name,
&contents.contents,
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name
);
}
for contents in &contents {
if let Err(e) = extract_services_into_res(
&project_name,
&contents.contents,
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name
);
}
(services, Some(contents), Some(errors), None, None)
}
(services, Some(contents), Some(errors), None, None)
} else {
(vec![], None, None, None, None)
}
} else if !repo_empty {
// ================
@@ -350,9 +319,8 @@ impl Resolve<RefreshStackCache, User> for State {
hash: latest_hash,
message: latest_message,
..
} =
get_remote_compose_contents(&stack, Some(&mut missing_files))
.await?;
} = get_repo_compose_contents(&stack, Some(&mut missing_files))
.await?;
let project_name = stack.project_name(true);
@@ -390,21 +358,21 @@ impl Resolve<RefreshStackCache, User> for State {
&mut services,
) {
warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
"Failed to extract Stack services for {}, things may not work correctly. | {e:#}",
stack.name
);
services.extend(stack.info.latest_services);
services.extend(stack.info.latest_services.clone());
};
(services, None, None, None, None)
};
let info = StackInfo {
missing_files,
deployed_services: stack.info.deployed_services,
deployed_project_name: stack.info.deployed_project_name,
deployed_contents: stack.info.deployed_contents,
deployed_hash: stack.info.deployed_hash,
deployed_message: stack.info.deployed_message,
deployed_services: stack.info.deployed_services.clone(),
deployed_project_name: stack.info.deployed_project_name.clone(),
deployed_contents: stack.info.deployed_contents.clone(),
deployed_hash: stack.info.deployed_hash.clone(),
deployed_message: stack.info.deployed_message.clone(),
latest_services,
remote_contents,
remote_errors,
@@ -424,43 +392,65 @@ impl Resolve<RefreshStackCache, User> for State {
.await
.context("failed to update stack info on db")?;
if (stack.config.poll_for_updates || stack.config.auto_update)
&& !stack.config.server_id.is_empty()
{
let (server, state) =
get_server_with_state(&stack.config.server_id).await?;
if state == ServerState::Ok {
let name = stack.name.clone();
if let Err(e) =
pull_stack_inner(stack, None, &server, None).await
{
warn!(
"Failed to pull latest images for Stack {name} | {e:#}",
);
}
}
}
Ok(NoData {})
}
}
impl Resolve<CreateStackWebhook, User> for State {
#[instrument(name = "CreateStackWebhook", skip(self, user))]
impl Resolve<WriteArgs> for CreateStackWebhook {
#[instrument(name = "CreateStackWebhook", skip(args))]
async fn resolve(
&self,
CreateStackWebhook { stack, action }: CreateStackWebhook,
user: User,
) -> anyhow::Result<CreateStackWebhookResponse> {
self,
args: &WriteArgs,
) -> serror::Result<CreateStackWebhookResponse> {
let WriteArgs { user } = args;
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Write,
)
.await?;
if stack.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = stack.config.repo.split('/');
let owner = split.next().context("Stack repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -493,7 +483,7 @@ impl Resolve<CreateStackWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id)
}
@@ -528,64 +518,65 @@ impl Resolve<CreateStackWebhook, User> for State {
.context("failed to create webhook")?;
if !stack.config.webhook_enabled {
self
.resolve(
UpdateStack {
id: stack.id,
config: PartialStackConfig {
webhook_enabled: Some(true),
..Default::default()
},
},
user,
)
.await
.context("failed to update stack to enable webhook")?;
UpdateStack {
id: stack.id,
config: PartialStackConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update stack to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<DeleteStackWebhook, User> for State {
#[instrument(name = "DeleteStackWebhook", skip(self, user))]
impl Resolve<WriteArgs> for DeleteStackWebhook {
#[instrument(name = "DeleteStackWebhook", skip(user))]
async fn resolve(
&self,
DeleteStackWebhook { stack, action }: DeleteStackWebhook,
user: User,
) -> anyhow::Result<DeleteStackWebhookResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteStackWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let stack = resource::get_check_permissions::<Stack>(
&stack,
&user,
&self.stack,
user,
PermissionLevel::Write,
)
.await?;
if stack.config.git_provider != "github.com" {
return Err(anyhow!(
"Can only manage github.com repo webhooks"
));
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if stack.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = stack.config.repo.split('/');
let owner = split.next().context("Stack repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -611,7 +602,7 @@ impl Resolve<DeleteStackWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id)
}

View File

@@ -6,6 +6,7 @@ use komodo_client::{
api::{read::ExportAllResourcesToToml, write::*},
entities::{
self,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
alerter::Alerter,
all_logs_success,
@@ -25,7 +26,7 @@ use komodo_client::{
},
to_komodo_name,
update::{Log, Update},
user::{sync_user, User},
user::sync_user,
CloneArgs, NoData, Operation, ResourceTarget,
},
};
@@ -41,93 +42,123 @@ use tokio::fs;
use crate::{
alert::send_alerts,
api::read::ReadArgs,
config::core_config,
helpers::{
query::get_id_to_tags,
update::{add_update, make_update, update_update},
},
resource::{self, refresh_resource_sync_state_cache},
state::{db_client, github_client, State},
state::{db_client, github_client},
sync::{
deploy::SyncDeployParams, remote::RemoteResources,
view::push_updates_for_view, AllResourcesById,
},
};
impl Resolve<CreateResourceSync, User> for State {
#[instrument(name = "CreateResourceSync", skip(self, user))]
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateResourceSync {
#[instrument(name = "CreateResourceSync", skip(user))]
async fn resolve(
&self,
CreateResourceSync { name, config }: CreateResourceSync,
user: User,
) -> anyhow::Result<ResourceSync> {
resource::create::<ResourceSync>(&name, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
Ok(
resource::create::<ResourceSync>(
&self.name,
self.config,
&user,
)
.await?,
)
}
}
impl Resolve<CopyResourceSync, User> for State {
#[instrument(name = "CopyResourceSync", skip(self, user))]
impl Resolve<WriteArgs> for CopyResourceSync {
#[instrument(name = "CopyResourceSync", skip(user))]
async fn resolve(
&self,
CopyResourceSync { name, id }: CopyResourceSync,
user: User,
) -> anyhow::Result<ResourceSync> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
let ResourceSync { config, .. } =
resource::get_check_permissions::<ResourceSync>(
&id,
&self.id,
&user,
PermissionLevel::Write,
)
.await?;
resource::create::<ResourceSync>(&name, config.into(), &user)
.await
Ok(
resource::create::<ResourceSync>(
&self.name,
config.into(),
user,
)
.await?,
)
}
}
impl Resolve<DeleteResourceSync, User> for State {
#[instrument(name = "DeleteResourceSync", skip(self, user))]
impl Resolve<WriteArgs> for DeleteResourceSync {
#[instrument(name = "DeleteResourceSync", skip(args))]
async fn resolve(
&self,
DeleteResourceSync { id }: DeleteResourceSync,
user: User,
) -> anyhow::Result<ResourceSync> {
resource::delete::<ResourceSync>(&id, &user).await
self,
args: &WriteArgs,
) -> serror::Result<ResourceSync> {
Ok(resource::delete::<ResourceSync>(&self.id, args).await?)
}
}
impl Resolve<UpdateResourceSync, User> for State {
#[instrument(name = "UpdateResourceSync", skip(self, user))]
impl Resolve<WriteArgs> for UpdateResourceSync {
#[instrument(name = "UpdateResourceSync", skip(user))]
async fn resolve(
&self,
UpdateResourceSync { id, config }: UpdateResourceSync,
user: User,
) -> anyhow::Result<ResourceSync> {
resource::update::<ResourceSync>(&id, config, &user).await
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
Ok(
resource::update::<ResourceSync>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<WriteSyncFileContents, User> for State {
impl Resolve<WriteArgs> for RenameResourceSync {
#[instrument(name = "RenameResourceSync", skip(user))]
async fn resolve(
&self,
WriteSyncFileContents {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(
resource::rename::<ResourceSync>(&self.id, &self.name, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for WriteSyncFileContents {
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let WriteSyncFileContents {
sync,
resource_path,
file_path,
contents,
}: WriteSyncFileContents,
user: User,
) -> anyhow::Result<Update> {
} = self;
let WriteArgs { user } = args;
let sync = resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
user,
PermissionLevel::Write,
)
.await?;
if !sync.config.files_on_host && sync.config.repo.is_empty() {
return Err(anyhow!(
return Err(
anyhow!(
"This method is only for files on host, or repo based syncs."
));
)
.into(),
);
}
let mut update =
@@ -175,12 +206,14 @@ impl Resolve<WriteSyncFileContents, User> for State {
}
if sync.config.files_on_host {
if let Err(e) = State
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })
.resolve(args)
.await
{
update
.push_error_log("Refresh failed", format_serror(&e.into()));
update.push_error_log(
"Refresh failed",
format_serror(&e.error.into()),
);
}
update.finalize();
@@ -198,12 +231,14 @@ impl Resolve<WriteSyncFileContents, User> for State {
update.logs.extend(commit_res.logs);
if let Err(e) = State
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })
.resolve(args)
.await
{
update
.push_error_log("Refresh failed", format_serror(&e.into()));
update.push_error_log(
"Refresh failed",
format_serror(&e.error.into()),
);
}
update.finalize();
@@ -213,52 +248,61 @@ impl Resolve<WriteSyncFileContents, User> for State {
}
}
impl Resolve<CommitSync, User> for State {
#[instrument(name = "CommitSync", skip(self, user))]
async fn resolve(
&self,
CommitSync { sync }: CommitSync,
user: User,
) -> anyhow::Result<ResourceSync> {
impl Resolve<WriteArgs> for CommitSync {
#[instrument(name = "CommitSync", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let WriteArgs { user } = args;
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, &user, PermissionLevel::Write)
>(&self.sync, &user, PermissionLevel::Write)
.await?;
let file_contents_empty = sync.config.file_contents_empty();
let fresh_sync = !sync.config.files_on_host
&& sync.config.file_contents.is_empty()
&& sync.config.repo.is_empty();
&& sync.config.repo.is_empty()
&& file_contents_empty;
if !sync.config.managed && !fresh_sync {
return Err(anyhow!(
"Cannot commit to sync. Enabled 'managed' mode."
));
return Err(
anyhow!("Cannot commit to sync. Enabled 'managed' mode.")
.into(),
);
}
let resource_path = sync
.config
.resource_path
.first()
.context("Sync does not have resource path configured.")?
.parse::<PathBuf>()
.context("Invalid resource path")?;
// Get this here so it can fail before update created.
let resource_path =
if sync.config.files_on_host || !sync.config.repo.is_empty() {
let resource_path = sync
.config
.resource_path
.first()
.context("Sync does not have resource path configured.")?
.parse::<PathBuf>()
.context("Invalid resource path")?;
if resource_path
.extension()
.context("Resource path missing '.toml' extension")?
!= "toml"
{
return Err(anyhow!("Resource path missing '.toml' extension"));
if resource_path
.extension()
.context("Resource path missing '.toml' extension")?
!= "toml"
{
return Err(
anyhow!("Resource path missing '.toml' extension").into(),
);
}
Some(resource_path)
} else {
None
};
let res = ExportAllResourcesToToml {
tags: sync.config.match_tags.clone(),
}
let res = State
.resolve(
ExportAllResourcesToToml {
tags: sync.config.match_tags.clone(),
},
sync_user().to_owned(),
)
.await?;
.resolve(&ReadArgs {
user: sync_user().to_owned(),
})
.await?;
let mut update = make_update(&sync, Operation::CommitSync, &user);
update.id = add_update(update.clone()).await?;
@@ -266,6 +310,10 @@ impl Resolve<CommitSync, User> for State {
update.logs.push(Log::simple("Resources", res.toml.clone()));
if sync.config.files_on_host {
let Some(resource_path) = resource_path else {
// Resource path checked above for files_on_host mode.
unreachable!()
};
let file_path = core_config()
.sync_directory
.join(to_komodo_name(&sync.name))
@@ -284,8 +332,8 @@ impl Resolve<CommitSync, User> for State {
format_serror(&e.into()),
);
update.finalize();
add_update(update).await?;
return resource::get::<ResourceSync>(&sync.name).await;
add_update(update.clone()).await?;
return Ok(update);
} else {
update.push_simple_log(
"Write contents",
@@ -293,6 +341,10 @@ impl Resolve<CommitSync, User> for State {
);
}
} else if !sync.config.repo.is_empty() {
let Some(resource_path) = resource_path else {
// Resource path checked above for repo mode.
unreachable!()
};
// GIT REPO
let args: CloneArgs = (&sync).into();
let root = args.unique_path(&core_config().repo_directory)?;
@@ -311,8 +363,8 @@ impl Resolve<CommitSync, User> for State {
format_serror(&e.into()),
);
update.finalize();
add_update(update).await?;
return resource::get::<ResourceSync>(&sync.name).await;
add_update(update.clone()).await?;
return Ok(update);
}
}
// ===========
@@ -331,22 +383,18 @@ impl Resolve<CommitSync, User> for State {
format_serror(&e.into()),
);
update.finalize();
add_update(update).await?;
return resource::get::<ResourceSync>(&sync.name).await;
add_update(update.clone()).await?;
return Ok(update);
}
let res = match State
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })
.resolve(args)
.await
{
Ok(sync) => Ok(sync),
Err(e) => {
update.push_error_log(
"Refresh sync pending",
format_serror(&(&e).into()),
);
Err(e)
}
update.push_error_log(
"Refresh sync pending",
format_serror(&e.error.into()),
);
};
update.finalize();
@@ -365,28 +413,27 @@ impl Resolve<CommitSync, User> for State {
.await;
refresh_resource_sync_state_cache().await;
}
update_update(update).await?;
update_update(update.clone()).await?;
res
Ok(update)
}
}
impl Resolve<RefreshResourceSyncPending, User> for State {
impl Resolve<WriteArgs> for RefreshResourceSyncPending {
#[instrument(
name = "RefreshResourceSyncPending",
level = "debug",
skip(self, user)
skip(user)
)]
async fn resolve(
&self,
RefreshResourceSyncPending { sync }: RefreshResourceSyncPending,
user: User,
) -> anyhow::Result<ResourceSync> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// sync should be able to do this.
let mut sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, &user, PermissionLevel::Execute)
>(&self.sync, user, PermissionLevel::Execute)
.await?;
if !sync.config.managed
@@ -416,9 +463,12 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
sync.info.pending_message = message;
if !sync.info.remote_errors.is_empty() {
return Err(anyhow!(
"Remote resources have errors. Cannot compute diffs."
));
return Err(
anyhow!(
"Remote resources have errors. Cannot compute diffs."
)
.into(),
);
}
let resources = resources?;
@@ -520,6 +570,17 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
&mut diffs,
)
.await?;
push_updates_for_view::<Action>(
resources.actions,
delete,
&all_resources,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Builder>(
resources.builders,
delete,
@@ -569,8 +630,7 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
let variable_updates = if sync.config.match_tags.is_empty() {
crate::sync::variables::get_updates_for_view(
&resources.variables,
// Delete doesn't work with variables when match tags are set
sync.config.match_tags.is_empty() && delete,
delete,
)
.await?
} else {
@@ -580,8 +640,7 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
let user_group_updates = if sync.config.match_tags.is_empty() {
crate::sync::user_groups::get_updates_for_view(
resources.user_groups,
// Delete doesn't work with user groups when match tags are set
sync.config.match_tags.is_empty() && delete,
delete,
&all_resources,
)
.await?
@@ -708,43 +767,47 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
}
});
crate::resource::get::<ResourceSync>(&sync.id).await
Ok(crate::resource::get::<ResourceSync>(&sync.id).await?)
}
}
impl Resolve<CreateSyncWebhook, User> for State {
#[instrument(name = "CreateSyncWebhook", skip(self, user))]
impl Resolve<WriteArgs> for CreateSyncWebhook {
#[instrument(name = "CreateSyncWebhook", skip(args))]
async fn resolve(
&self,
CreateSyncWebhook { sync, action }: CreateSyncWebhook,
user: User,
) -> anyhow::Result<CreateSyncWebhookResponse> {
self,
args: &WriteArgs,
) -> serror::Result<CreateSyncWebhookResponse> {
let WriteArgs { user } = args;
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let sync = resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
&self.sync,
user,
PermissionLevel::Write,
)
.await?;
if sync.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = sync.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -777,7 +840,7 @@ impl Resolve<CreateSyncWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
SyncWebhookAction::Refresh => {
format!("{host}/listener/github/sync/{}/refresh", sync.id)
}
@@ -812,64 +875,65 @@ impl Resolve<CreateSyncWebhook, User> for State {
.context("failed to create webhook")?;
if !sync.config.webhook_enabled {
self
.resolve(
UpdateResourceSync {
id: sync.id,
config: PartialResourceSyncConfig {
webhook_enabled: Some(true),
..Default::default()
},
},
user,
)
.await
.context("failed to update sync to enable webhook")?;
UpdateResourceSync {
id: sync.id,
config: PartialResourceSyncConfig {
webhook_enabled: Some(true),
..Default::default()
},
}
.resolve(args)
.await
.map_err(|e| e.error)
.context("failed to update sync to enable webhook")?;
}
Ok(NoData {})
}
}
impl Resolve<DeleteSyncWebhook, User> for State {
#[instrument(name = "DeleteSyncWebhook", skip(self, user))]
impl Resolve<WriteArgs> for DeleteSyncWebhook {
#[instrument(name = "DeleteSyncWebhook", skip(user))]
async fn resolve(
&self,
DeleteSyncWebhook { sync, action }: DeleteSyncWebhook,
user: User,
) -> anyhow::Result<DeleteSyncWebhookResponse> {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<DeleteSyncWebhookResponse> {
let Some(github) = github_client() else {
return Err(anyhow!(
"github_webhook_app is not configured in core config toml"
));
return Err(
anyhow!(
"github_webhook_app is not configured in core config toml"
)
.into(),
);
};
let sync = resource::get_check_permissions::<ResourceSync>(
&sync,
&user,
&self.sync,
user,
PermissionLevel::Write,
)
.await?;
if sync.config.git_provider != "github.com" {
return Err(anyhow!(
"Can only manage github.com repo webhooks"
));
return Err(
anyhow!("Can only manage github.com repo webhooks").into(),
);
}
if sync.config.repo.is_empty() {
return Err(anyhow!(
"No repo configured, can't create webhook"
));
return Err(
anyhow!("No repo configured, can't create webhook").into(),
);
}
let mut split = sync.config.repo.split('/');
let owner = split.next().context("Sync repo has no owner")?;
let Some(github) = github.get(owner) else {
return Err(anyhow!(
"Cannot manage repo webhooks under owner {owner}"
));
return Err(
anyhow!("Cannot manage repo webhooks under owner {owner}")
.into(),
);
};
let repo =
@@ -895,7 +959,7 @@ impl Resolve<DeleteSyncWebhook, User> for State {
} else {
webhook_base_url
};
let url = match action {
let url = match self.action {
SyncWebhookAction::Refresh => {
format!("{host}/listener/github/sync/{}/refresh", sync.id)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,13 +92,19 @@ async fn login(
);
let config = core_config();
let redirect = if !config.oidc_redirect.is_empty() {
Redirect::to(
auth_url
.as_str()
.replace(&config.oidc_provider, &config.oidc_redirect)
.as_str(),
)
let redirect = if !config.oidc_redirect_host.is_empty() {
let auth_url = auth_url.as_str();
let (protocol, rest) = auth_url
.split_once("://")
.context("Invalid URL: Missing protocol (eg 'https://')")?;
let host = rest
.split_once(['/', '?'])
.map(|(host, _)| host)
.unwrap_or(rest);
Redirect::to(&auth_url.replace(
&format!("{protocol}://{host}"),
&config.oidc_redirect_host,
))
} else {
Redirect::to(auth_url.as_str())
};
@@ -136,7 +142,7 @@ async fn callback(
if komodo_timestamp() > valid_until {
return Err(anyhow!(
"CSRF token invalid (Timed out). The token must be "
"CSRF token invalid (Timed out). The token must be used within 2 minutes."
));
}

View File

@@ -212,21 +212,37 @@ async fn terminate_ec2_instance_inner(
Ok(res)
}
/// Automatically retries 5 times, waiting 2 sec in between
#[instrument(level = "debug")]
async fn get_ec2_instance_status(
client: &Client,
instance_id: &str,
) -> anyhow::Result<Option<InstanceStatus>> {
let status = client
.describe_instance_status()
.instance_ids(instance_id)
.send()
let mut try_count = 1;
loop {
match async {
anyhow::Ok(
client
.describe_instance_status()
.instance_ids(instance_id)
.send()
.await
.context("failed to describe instance status from aws")?
.instance_statuses()
.first()
.cloned(),
)
}
.await
.context("failed to get instance status from aws")?
.instance_statuses()
.first()
.cloned();
Ok(status)
{
Ok(res) => return Ok(res),
Err(e) if try_count > 4 => return Err(e),
Err(_) => {
tokio::time::sleep(Duration::from_secs(2)).await;
try_count += 1;
}
}
}
}
#[instrument(level = "debug")]
@@ -248,28 +264,43 @@ async fn get_ec2_instance_state_name(
Ok(Some(state))
}
/// Automatically retries 5 times, waiting 2 sec in between
#[instrument(level = "debug")]
async fn get_ec2_instance_public_ip(
client: &Client,
instance_id: &str,
) -> anyhow::Result<String> {
let ip = client
.describe_instances()
.instance_ids(instance_id)
.send()
let mut try_count = 1;
loop {
match async {
anyhow::Ok(
client
.describe_instances()
.instance_ids(instance_id)
.send()
.await
.context("failed to describe instances from aws")?
.reservations()
.first()
.context("instance reservations is empty")?
.instances()
.first()
.context("instances is empty")?
.public_ip_address()
.context("instance has no public ip")?
.to_string(),
)
}
.await
.context("failed to get instance status from aws")?
.reservations()
.first()
.context("instance reservations is empty")?
.instances()
.first()
.context("instances is empty")?
.public_ip_address()
.context("instance has no public ip")?
.to_string();
Ok(ip)
{
Ok(res) => return Ok(res),
Err(e) if try_count > 4 => return Err(e),
Err(_) => {
tokio::time::sleep(Duration::from_secs(2)).await;
try_count += 1;
}
}
}
}
fn handle_unknown_instance_type(
@@ -1056,7 +1087,90 @@ fn handle_unknown_instance_type(
| InstanceType::Z1d6xlarge
| InstanceType::Z1dLarge
| InstanceType::Z1dMetal
| InstanceType::Z1dXlarge => Ok(instance_type),
| InstanceType::Z1dXlarge
| InstanceType::C7gdMetal
| InstanceType::C7gnMetal
| InstanceType::C7iFlex2xlarge
| InstanceType::C7iFlex4xlarge
| InstanceType::C7iFlex8xlarge
| InstanceType::C7iFlexLarge
| InstanceType::C7iFlexXlarge
| InstanceType::C8g12xlarge
| InstanceType::C8g16xlarge
| InstanceType::C8g24xlarge
| InstanceType::C8g2xlarge
| InstanceType::C8g48xlarge
| InstanceType::C8g4xlarge
| InstanceType::C8g8xlarge
| InstanceType::C8gLarge
| InstanceType::C8gMedium
| InstanceType::C8gMetal24xl
| InstanceType::C8gMetal48xl
| InstanceType::C8gXlarge
| InstanceType::G612xlarge
| InstanceType::G616xlarge
| InstanceType::G624xlarge
| InstanceType::G62xlarge
| InstanceType::G648xlarge
| InstanceType::G64xlarge
| InstanceType::G68xlarge
| InstanceType::G6Xlarge
| InstanceType::G6e12xlarge
| InstanceType::G6e16xlarge
| InstanceType::G6e24xlarge
| InstanceType::G6e2xlarge
| InstanceType::G6e48xlarge
| InstanceType::G6e4xlarge
| InstanceType::G6e8xlarge
| InstanceType::G6eXlarge
| InstanceType::Gr64xlarge
| InstanceType::Gr68xlarge
| InstanceType::M7gdMetal
| InstanceType::M8g12xlarge
| InstanceType::M8g16xlarge
| InstanceType::M8g24xlarge
| InstanceType::M8g2xlarge
| InstanceType::M8g48xlarge
| InstanceType::M8g4xlarge
| InstanceType::M8g8xlarge
| InstanceType::M8gLarge
| InstanceType::M8gMedium
| InstanceType::M8gMetal24xl
| InstanceType::M8gMetal48xl
| InstanceType::M8gXlarge
| InstanceType::Mac2M1ultraMetal
| InstanceType::R7gdMetal
| InstanceType::R7izMetal16xl
| InstanceType::R7izMetal32xl
| InstanceType::R8g12xlarge
| InstanceType::R8g16xlarge
| InstanceType::R8g24xlarge
| InstanceType::R8g2xlarge
| InstanceType::R8g48xlarge
| InstanceType::R8g4xlarge
| InstanceType::R8g8xlarge
| InstanceType::R8gLarge
| InstanceType::R8gMedium
| InstanceType::R8gMetal24xl
| InstanceType::R8gMetal48xl
| InstanceType::R8gXlarge
| InstanceType::U7i12tb224xlarge
| InstanceType::U7ib12tb224xlarge
| InstanceType::U7in16tb224xlarge
| InstanceType::U7in24tb224xlarge
| InstanceType::U7in32tb224xlarge
| InstanceType::X8g12xlarge
| InstanceType::X8g16xlarge
| InstanceType::X8g24xlarge
| InstanceType::X8g2xlarge
| InstanceType::X8g48xlarge
| InstanceType::X8g4xlarge
| InstanceType::X8g8xlarge
| InstanceType::X8gLarge
| InstanceType::X8gMedium
| InstanceType::X8gMetal24xl
| InstanceType::X8gMetal48xl
| InstanceType::X8gXlarge => Ok(instance_type),
other => Err(anyhow!("unknown InstanceType: {other:?}")),
}
}

View File

@@ -78,7 +78,7 @@ pub fn core_config() -> &'static CoreConfig {
},
oidc_enabled: env.komodo_oidc_enabled.unwrap_or(config.oidc_enabled),
oidc_provider: env.komodo_oidc_provider.unwrap_or(config.oidc_provider),
oidc_redirect: env.komodo_oidc_redirect.unwrap_or(config.oidc_redirect),
oidc_redirect_host: env.komodo_oidc_redirect_host.unwrap_or(config.oidc_redirect_host),
oidc_client_id: maybe_read_item_from_file(env.komodo_oidc_client_id_file,env
.komodo_oidc_client_id)
.unwrap_or(config.oidc_client_id),
@@ -150,6 +150,9 @@ pub fn core_config() -> &'static CoreConfig {
repo_directory: env
.komodo_repo_directory
.unwrap_or(config.repo_directory),
action_directory: env
.komodo_action_directory
.unwrap_or(config.action_directory),
resource_poll_interval: env
.komodo_resource_poll_interval
.unwrap_or(config.resource_poll_interval),

View File

@@ -1,4 +1,5 @@
use komodo_client::entities::{
action::Action,
alert::Alert,
alerter::Alerter,
api_key::ApiKey,
@@ -47,6 +48,7 @@ pub struct DbClient {
pub builders: Collection<Builder>,
pub repos: Collection<Repo>,
pub procedures: Collection<Procedure>,
pub actions: Collection<Action>,
pub alerters: Collection<Alerter>,
pub server_templates: Collection<ServerTemplate>,
pub resource_syncs: Collection<ResourceSync>,
@@ -115,6 +117,7 @@ impl DbClient {
repos: resource_collection(&db, "Repo").await?,
alerters: resource_collection(&db, "Alerter").await?,
procedures: resource_collection(&db, "Procedure").await?,
actions: resource_collection(&db, "Action").await?,
server_templates: resource_collection(&db, "ServerTemplate")
.await?,
resource_syncs: resource_collection(&db, "ResourceSync")

View File

@@ -4,7 +4,8 @@ use anyhow::anyhow;
use komodo_client::{
busy::Busy,
entities::{
build::BuildActionState, deployment::DeploymentActionState,
action::ActionActionState, build::BuildActionState,
deployment::DeploymentActionState,
procedure::ProcedureActionState, repo::RepoActionState,
server::ServerActionState, stack::StackActionState,
sync::ResourceSyncActionState,
@@ -22,6 +23,7 @@ pub struct ActionStates {
pub repo: Cache<String, Arc<ActionState<RepoActionState>>>,
pub procedure:
Cache<String, Arc<ActionState<ProcedureActionState>>>,
pub action: Cache<String, Arc<ActionState<ActionActionState>>>,
pub resource_sync:
Cache<String, Arc<ActionState<ResourceSyncActionState>>>,
pub stack: Cache<String, Arc<ActionState<StackActionState>>>,

View File

@@ -31,7 +31,7 @@ use crate::{
use super::periphery_client;
const BUILDER_POLL_RATE_SECS: u64 = 2;
const BUILDER_POLL_MAX_TRIES: usize = 30;
const BUILDER_POLL_MAX_TRIES: usize = 60;
#[instrument(skip_all, fields(builder_id = builder.id, update_id = update.id))]
pub async fn get_builder_periphery(
@@ -42,9 +42,35 @@ pub async fn get_builder_periphery(
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
match builder.config {
BuilderConfig::Url(config) => {
if config.address.is_empty() {
return Err(anyhow!(
"Builder has not yet configured an address"
));
}
let periphery = PeripheryClient::new(
config.address,
if config.passkey.is_empty() {
core_config().passkey.clone()
} else {
config.passkey
},
Duration::from_secs(3),
);
periphery
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
return Err(anyhow!("builder has not configured a server"));
return Err(anyhow!("Builder has not configured a server"));
}
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server)?;
@@ -96,8 +122,11 @@ async fn get_aws_builder(
let protocol = if config.use_https { "https" } else { "http" };
let periphery_address =
format!("{protocol}://{ip}:{}", config.port);
let periphery =
PeripheryClient::new(&periphery_address, &core_config().passkey);
let periphery = PeripheryClient::new(
&periphery_address,
&core_config().passkey,
Duration::from_secs(3),
);
let start_connect_ts = komodo_timestamp();
let mut res = Ok(GetVersionResponse {

View File

@@ -25,9 +25,8 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng};
use resolver_api::Resolve;
use crate::{
config::core_config,
resource,
state::{db_client, State},
api::write::WriteArgs, config::core_config, resource,
state::db_client,
};
pub mod action_state;
@@ -54,10 +53,6 @@ pub fn empty_or_only_spaces(word: &str) -> bool {
true
}
pub fn random_duration(min_ms: u64, max_ms: u64) -> Duration {
Duration::from_millis(thread_rng().gen_range(min_ms..max_ms))
}
pub fn random_string(length: usize) -> String {
thread_rng()
.sample_iter(&Alphanumeric)
@@ -149,6 +144,7 @@ pub fn periphery_client(
let client = PeripheryClient::new(
&server.config.address,
&core_config().passkey,
Duration::from_secs(server.config.timeout_seconds as u64),
);
Ok(client)
@@ -308,46 +304,44 @@ pub async fn ensure_first_server_and_builder() {
let server = if let Some(server) = server {
server
} else {
match State
.resolve(
CreateServer {
name: format!("server-{}", random_string(5)),
config: PartialServerConfig {
address: Some(first_server.to_string()),
enabled: Some(true),
..Default::default()
},
},
system_user().to_owned(),
)
.await
match (CreateServer {
name: format!("server-{}", random_string(5)),
config: PartialServerConfig {
address: Some(first_server.to_string()),
enabled: Some(true),
..Default::default()
},
})
.resolve(&WriteArgs {
user: system_user().to_owned(),
})
.await
{
Ok(server) => server,
Err(e) => {
error!("Failed to initialize 'first_server'. Failed to CreateServer. {e:?}");
error!("Failed to initialize 'first_server'. Failed to CreateServer. {:#}", e.error);
return;
}
}
};
let Ok(None) = db.builders
.find_one(Document::new()).await
.inspect_err(|e| error!("Failed to initialize 'first_builder'. Failed to query db. {e:?}")) else {
.inspect_err(|e| error!("Failed to initialize 'first_builder' | Failed to query db | {e:?}")) else {
return;
};
if let Err(e) = State
.resolve(
CreateBuilder {
name: String::from("local"),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id),
},
),
if let Err(e) = (CreateBuilder {
name: String::from("local"),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id),
},
system_user().to_owned(),
)
.await
),
})
.resolve(&WriteArgs {
user: system_user().to_owned(),
})
.await
{
error!("Failed to initialize 'first_builder'. Failed to CreateBuilder. {e:?}");
error!("Failed to initialize 'first_builder' | Failed to CreateBuilder | {:#}", e.error);
}
}

View File

@@ -4,9 +4,14 @@ use anyhow::{anyhow, Context};
use formatting::{bold, colored, format_serror, muted, Color};
use futures::future::join_all;
use komodo_client::{
api::execute::Execution,
api::execute::*,
entities::{
action::Action,
build::Build,
deployment::Deployment,
procedure::Procedure,
repo::Repo,
stack::Stack,
update::{Log, Update},
user::procedure_user,
},
@@ -16,8 +21,12 @@ use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
api::execute::ExecuteRequest,
state::{db_client, State},
api::{
execute::{ExecuteArgs, ExecuteRequest},
write::WriteArgs,
},
resource::{list_full_for_user_using_pattern, KomodoResource},
state::db_client,
};
use super::update::{init_execution_update, update_update};
@@ -79,11 +88,94 @@ pub async fn execute_procedure(
#[allow(dependency_on_unit_never_type_fallback)]
#[instrument(skip(update))]
async fn execute_stage(
executions: Vec<Execution>,
_executions: Vec<Execution>,
parent_id: &str,
parent_name: &str,
update: &Mutex<Update>,
) -> anyhow::Result<()> {
let mut executions = Vec::with_capacity(_executions.capacity());
for execution in _executions {
match execution {
Execution::BatchRunAction(exec) => {
extend_batch_exection::<BatchRunAction>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchRunProcedure(exec) => {
extend_batch_exection::<BatchRunProcedure>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchRunBuild(exec) => {
extend_batch_exection::<BatchRunBuild>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchCloneRepo(exec) => {
extend_batch_exection::<BatchCloneRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchPullRepo(exec) => {
extend_batch_exection::<BatchPullRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchBuildRepo(exec) => {
extend_batch_exection::<BatchBuildRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeploy(exec) => {
extend_batch_exection::<BatchDeploy>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyDeployment(exec) => {
extend_batch_exection::<BatchDestroyDeployment>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStack(exec) => {
extend_batch_exection::<BatchDeployStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStackIfChanged(exec) => {
extend_batch_exection::<BatchDeployStackIfChanged>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyStack(exec) => {
extend_batch_exection::<BatchDestroyStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
execution => executions.push(execution),
}
}
let futures = executions.into_iter().map(|execution| async move {
let now = Instant::now();
add_line_to_update(
@@ -138,14 +230,44 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RunProcedure"),
&update_id,
)
.await?
}
Execution::BatchRunProcedure(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunProcedure not implemented correctly"
));
}
Execution::RunAction(req) => {
let req = ExecuteRequest::RunAction(req);
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunAction(req) = req else {
unreachable!()
};
let update_id = update.id.clone();
handle_resolve_result(
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RunAction"),
&update_id,
)
.await?
}
Execution::BatchRunAction(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunAction not implemented correctly"
));
}
Execution::RunBuild(req) => {
let req = ExecuteRequest::RunBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -154,14 +276,21 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RunBuild"),
&update_id,
)
.await?
}
Execution::BatchRunBuild(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunBuild not implemented correctly"
));
}
Execution::CancelBuild(req) => {
let req = ExecuteRequest::CancelBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -170,9 +299,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at CancelBuild"),
&update_id,
)
@@ -186,14 +316,38 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at Deploy"),
&update_id,
)
.await?
}
Execution::BatchDeploy(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeploy not implemented correctly"
));
}
Execution::PullDeployment(req) => {
let req = ExecuteRequest::PullDeployment(req);
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::PullDeployment(req) = req else {
unreachable!()
};
let update_id = update.id.clone();
handle_resolve_result(
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PullDeployment"),
&update_id,
)
.await?
}
Execution::StartDeployment(req) => {
let req = ExecuteRequest::StartDeployment(req);
let update = init_execution_update(&req, &user).await?;
@@ -202,9 +356,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StartDeployment"),
&update_id,
)
@@ -218,9 +373,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RestartDeployment"),
&update_id,
)
@@ -234,9 +390,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PauseDeployment"),
&update_id,
)
@@ -250,9 +407,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at UnpauseDeployment"),
&update_id,
)
@@ -266,9 +424,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StopDeployment"),
&update_id,
)
@@ -282,14 +441,21 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RemoveDeployment"),
&update_id,
)
.await?
}
Execution::BatchDestroyDeployment(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDestroyDeployment not implemented correctly"
));
}
Execution::CloneRepo(req) => {
let req = ExecuteRequest::CloneRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -298,14 +464,21 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at CloneRepo"),
&update_id,
)
.await?
}
Execution::BatchCloneRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchCloneRepo not implemented correctly"
));
}
Execution::PullRepo(req) => {
let req = ExecuteRequest::PullRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -314,14 +487,21 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PullRepo"),
&update_id,
)
.await?
}
Execution::BatchPullRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchPullRepo not implemented correctly"
));
}
Execution::BuildRepo(req) => {
let req = ExecuteRequest::BuildRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -330,14 +510,21 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at BuildRepo"),
&update_id,
)
.await?
}
Execution::BatchBuildRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchBuildRepo not implemented correctly"
));
}
Execution::CancelRepoBuild(req) => {
let req = ExecuteRequest::CancelRepoBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -346,9 +533,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at CancelRepoBuild"),
&update_id,
)
@@ -362,9 +550,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StartContainer"),
&update_id,
)
@@ -378,9 +567,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RestartContainer"),
&update_id,
)
@@ -394,9 +584,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PauseContainer"),
&update_id,
)
@@ -410,9 +601,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at UnpauseContainer"),
&update_id,
)
@@ -426,9 +618,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StopContainer"),
&update_id,
)
@@ -442,9 +635,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RemoveContainer"),
&update_id,
)
@@ -458,9 +652,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StartAllContainers"),
&update_id,
)
@@ -474,9 +669,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RestartAllContainers"),
&update_id,
)
@@ -490,9 +686,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PauseAllContainers"),
&update_id,
)
@@ -506,9 +703,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at UnpauseAllContainers"),
&update_id,
)
@@ -522,9 +720,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StopAllContainers"),
&update_id,
)
@@ -538,9 +737,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PruneContainers"),
&update_id,
)
@@ -554,9 +754,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at DeleteNetwork"),
&update_id,
)
@@ -570,9 +771,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PruneNetworks"),
&update_id,
)
@@ -586,9 +788,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at DeleteImage"),
&update_id,
)
@@ -602,9 +805,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PruneImages"),
&update_id,
)
@@ -618,9 +822,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at DeleteVolume"),
&update_id,
)
@@ -634,9 +839,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PruneVolumes"),
&update_id,
)
@@ -650,9 +856,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PruneDockerBuilders"),
&update_id,
)
@@ -666,9 +873,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PruneBuildx"),
&update_id,
)
@@ -682,9 +890,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PruneSystem"),
&update_id,
)
@@ -698,14 +907,21 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RunSync"),
&update_id,
)
.await?
}
// Exception: This is a write operation.
Execution::CommitSync(req) => req
.resolve(&WriteArgs { user })
.await
.map_err(|e| e.error)
.context("Failed at CommitSync")?,
Execution::DeployStack(req) => {
let req = ExecuteRequest::DeployStack(req);
let update = init_execution_update(&req, &user).await?;
@@ -714,14 +930,21 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at DeployStack"),
&update_id,
)
.await?
}
Execution::BatchDeployStack(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeployStack not implemented correctly"
));
}
Execution::DeployStackIfChanged(req) => {
let req = ExecuteRequest::DeployStackIfChanged(req);
let update = init_execution_update(&req, &user).await?;
@@ -730,14 +953,38 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at DeployStackIfChanged"),
&update_id,
)
.await?
}
Execution::BatchDeployStackIfChanged(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeployStackIfChanged not implemented correctly"
));
}
Execution::PullStack(req) => {
let req = ExecuteRequest::PullStack(req);
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::PullStack(req) = req else {
unreachable!()
};
let update_id = update.id.clone();
handle_resolve_result(
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PullStack"),
&update_id,
)
.await?
}
Execution::StartStack(req) => {
let req = ExecuteRequest::StartStack(req);
let update = init_execution_update(&req, &user).await?;
@@ -746,9 +993,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StartStack"),
&update_id,
)
@@ -762,9 +1010,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at RestartStack"),
&update_id,
)
@@ -778,9 +1027,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at PauseStack"),
&update_id,
)
@@ -794,9 +1044,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at UnpauseStack"),
&update_id,
)
@@ -810,9 +1061,10 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at StopStack"),
&update_id,
)
@@ -826,14 +1078,38 @@ async fn execute_execution(
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at DestroyStack"),
&update_id,
)
.await?
}
Execution::BatchDestroyStack(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDestroyStack not implemented correctly"
));
}
Execution::TestAlerter(req) => {
let req = ExecuteRequest::TestAlerter(req);
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::TestAlerter(req) = req else {
unreachable!()
};
let update_id = update.id.clone();
handle_resolve_result(
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)
.context("Failed at TestAlerter"),
&update_id,
)
.await?
}
Execution::Sleep(req) => {
let duration = Duration::from_millis(req.duration_ms as u64);
tokio::time::sleep(duration).await;
@@ -891,3 +1167,122 @@ async fn add_line_to_update(update: &Mutex<Update>, line: &str) {
error!("Failed to update an update during procedure | {e:#}");
};
}
async fn extend_batch_exection<E: ExtendBatch>(
pattern: &str,
executions: &mut Vec<Execution>,
) -> anyhow::Result<()> {
let more = list_full_for_user_using_pattern::<E::Resource>(
pattern,
Default::default(),
procedure_user(),
&[],
)
.await?
.into_iter()
.map(|resource| E::single_execution(resource.name));
executions.extend(more);
Ok(())
}
trait ExtendBatch {
type Resource: KomodoResource;
fn single_execution(name: String) -> Execution;
}
impl ExtendBatch for BatchRunProcedure {
type Resource = Procedure;
fn single_execution(procedure: String) -> Execution {
Execution::RunProcedure(RunProcedure { procedure })
}
}
impl ExtendBatch for BatchRunAction {
type Resource = Action;
fn single_execution(action: String) -> Execution {
Execution::RunAction(RunAction { action })
}
}
impl ExtendBatch for BatchRunBuild {
type Resource = Build;
fn single_execution(build: String) -> Execution {
Execution::RunBuild(RunBuild { build })
}
}
impl ExtendBatch for BatchCloneRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::CloneRepo(CloneRepo { repo })
}
}
impl ExtendBatch for BatchPullRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::PullRepo(PullRepo { repo })
}
}
impl ExtendBatch for BatchBuildRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::BuildRepo(BuildRepo { repo })
}
}
impl ExtendBatch for BatchDeploy {
type Resource = Deployment;
fn single_execution(deployment: String) -> Execution {
Execution::Deploy(Deploy {
deployment,
stop_signal: None,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDestroyDeployment {
type Resource = Deployment;
fn single_execution(deployment: String) -> Execution {
Execution::DestroyDeployment(DestroyDeployment {
deployment,
signal: None,
time: None,
})
}
}
impl ExtendBatch for BatchDeployStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DeployStack(DeployStack {
stack,
service: None,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDeployStackIfChanged {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DeployStackIfChanged(DeployStackIfChanged {
stack,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDestroyStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DestroyStack(DestroyStack {
stack,
service: None,
remove_orphans: false,
stop_time: None,
})
}
}

View File

@@ -2,6 +2,7 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context};
use komodo_client::entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
@@ -102,7 +103,7 @@ pub fn get_stack_state_from_containers(
})
.collect::<Vec<_>>();
let containers = containers.iter().filter(|container| {
services.iter().any(|StackServiceNames { service_name, container_name }| {
services.iter().any(|StackServiceNames { service_name, container_name, .. }| {
match compose_container_match_regex(container_name)
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
{
@@ -117,7 +118,7 @@ pub fn get_stack_state_from_containers(
if containers.is_empty() {
return StackState::Down;
}
if services.len() != containers.len() {
if services.len() > containers.len() {
return StackState::Unhealthy;
}
let running = containers.iter().all(|container| {
@@ -201,6 +202,14 @@ pub async fn get_tag_check_owner(
Err(anyhow!("user must be tag owner or admin"))
}
pub async fn get_all_tags(
filter: impl Into<Option<Document>>,
) -> anyhow::Result<Vec<Tag>> {
find_collect(&db_client().tags, filter, None)
.await
.context("failed to query db for tags")
}
pub async fn get_id_to_tags(
filter: impl Into<Option<Document>>,
) -> anyhow::Result<HashMap<String, Tag>> {
@@ -283,6 +292,9 @@ pub async fn get_user_permission_on_target(
ResourceTarget::Procedure(id) => {
get_user_permission_on_resource::<Procedure>(user, id).await
}
ResourceTarget::Action(id) => {
get_user_permission_on_resource::<Action>(user, id).await
}
ResourceTarget::ServerTemplate(id) => {
get_user_permission_on_resource::<ServerTemplate>(user, id)
.await

View File

@@ -1,5 +1,7 @@
use anyhow::Context;
use komodo_client::entities::{
action::Action,
alerter::Alerter,
build::Build,
deployment::Deployment,
komodo_timestamp,
@@ -260,6 +262,15 @@ pub async fn init_execution_update(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::BatchDeploy(_data) => {
return Ok(Default::default())
}
ExecuteRequest::PullDeployment(data) => (
Operation::PullDeployment,
ResourceTarget::Deployment(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::StartDeployment(data) => (
Operation::StartDeployment,
ResourceTarget::Deployment(
@@ -296,6 +307,9 @@ pub async fn init_execution_update(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::BatchDestroyDeployment(_data) => {
return Ok(Default::default())
}
// Build
ExecuteRequest::RunBuild(data) => (
@@ -304,6 +318,9 @@ pub async fn init_execution_update(
resource::get::<Build>(&data.build).await?.id,
),
),
ExecuteRequest::BatchRunBuild(_data) => {
return Ok(Default::default())
}
ExecuteRequest::CancelBuild(data) => (
Operation::CancelBuild,
ResourceTarget::Build(
@@ -318,18 +335,27 @@ pub async fn init_execution_update(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchCloneRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::PullRepo(data) => (
Operation::PullRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchPullRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::BuildRepo(data) => (
Operation::BuildRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchBuildRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::CancelRepoBuild(data) => (
Operation::CancelRepoBuild,
ResourceTarget::Repo(
@@ -344,6 +370,20 @@ pub async fn init_execution_update(
resource::get::<Procedure>(&data.procedure).await?.id,
),
),
ExecuteRequest::BatchRunProcedure(_) => {
return Ok(Default::default())
}
// Action
ExecuteRequest::RunAction(data) => (
Operation::RunAction,
ResourceTarget::Action(
resource::get::<Action>(&data.action).await?.id,
),
),
ExecuteRequest::BatchRunAction(_) => {
return Ok(Default::default())
}
// Server template
ExecuteRequest::LaunchServer(data) => (
@@ -365,17 +405,27 @@ pub async fn init_execution_update(
// Stack
ExecuteRequest::DeployStack(data) => (
Operation::DeployStack,
if data.service.is_some() {
Operation::DeployStackService
} else {
Operation::DeployStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStack(_data) => {
return Ok(Default::default())
}
ExecuteRequest::DeployStackIfChanged(data) => (
Operation::DeployStack,
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStackIfChanged(_data) => {
return Ok(Default::default())
}
ExecuteRequest::StartStack(data) => (
if data.service.is_some() {
Operation::StartStackService
@@ -386,6 +436,16 @@ pub async fn init_execution_update(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::PullStack(data) => (
if data.service.is_some() {
Operation::PullStackService
} else {
Operation::PullStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::RestartStack(data) => (
if data.service.is_some() {
Operation::RestartStackService
@@ -427,18 +487,36 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::DestroyStack(data) => (
Operation::DestroyStack,
if data.service.is_some() {
Operation::DestroyStackService
} else {
Operation::DestroyStack
},
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDestroyStack(_data) => {
return Ok(Default::default())
}
// Alerter
ExecuteRequest::TestAlerter(data) => (
Operation::TestAlerter,
ResourceTarget::Alerter(
resource::get::<Alerter>(&data.alerter).await?.id,
),
),
};
let mut update = make_update(target, operation, user);
update.in_progress();
// Hold off on even adding update for DeployStackIfChanged
if !matches!(&request, ExecuteRequest::DeployStackIfChanged(_)) {
// Don't actually send it here, let the handlers send it after they can set action state.
update.id = add_update_without_send(&update).await?;
}
Ok(update)
}

View File

@@ -1,56 +0,0 @@
use std::sync::OnceLock;
use anyhow::anyhow;
use axum::http::HeaderMap;
use komodo_client::{
api::execute::RunBuild,
entities::{build::Build, user::git_webhook_user},
};
use resolver_api::Resolve;
use crate::{
api::execute::ExecuteRequest,
helpers::update::init_execution_update, resource, state::State,
};
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
fn build_locks() -> &'static ListenerLockCache {
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_build_webhook(
build_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = build_locks().get_or_insert_default(&build_id).await;
let _lock = lock.lock().await;
let build = resource::get::<Build>(&build_id).await?;
verify_gh_signature(headers, &body, &build.config.webhook_secret)
.await?;
if !build.config.webhook_enabled {
return Err(anyhow!("build does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != build.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunBuild(RunBuild { build: build_id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunBuild(req) = req else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
Ok(())
}

View File

@@ -1,263 +0,0 @@
use std::sync::Arc;
use anyhow::{anyhow, Context};
use axum::{extract::Path, http::HeaderMap, routing::post, Router};
use hex::ToHex;
use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha2::Sha256;
use tokio::sync::Mutex;
use tracing::Instrument;
use crate::{
config::core_config,
helpers::{cache::Cache, random_duration},
};
mod build;
mod procedure;
mod repo;
mod stack;
mod sync;
type HmacSha256 = Hmac<Sha256>;
#[derive(Deserialize)]
struct Id {
id: String,
}
#[derive(Deserialize)]
struct IdBranch {
id: String,
branch: Option<String>,
}
pub fn router() -> Router {
Router::new()
.route(
"/build/:id",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("build_webhook", id);
async {
let res = build::handle_build_webhook(id.clone(), headers, body).await;
if let Err(e) = res {
warn!("failed to run build webook for build {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
),
)
.route(
"/repo/:id/clone",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("repo_clone_webhook", id);
async {
let res = repo::handle_repo_clone_webhook(id.clone(), headers, body).await;
if let Err(e) = res {
warn!("failed to run repo clone webook for repo {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/repo/:id/pull",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("repo_pull_webhook", id);
async {
let res = repo::handle_repo_pull_webhook(id.clone(), headers, body).await;
if let Err(e) = res {
warn!("failed to run repo pull webook for repo {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/repo/:id/build",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("repo_build_webhook", id);
async {
let res = repo::handle_repo_build_webhook(id.clone(), headers, body).await;
if let Err(e) = res {
warn!("failed to run repo build webook for repo {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/stack/:id/refresh",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("stack_clone_webhook", id);
async {
let res = stack::handle_stack_refresh_webhook(id.clone(), headers, body).await;
if let Err(e) = res {
warn!("failed to run stack clone webook for stack {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/stack/:id/deploy",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("stack_pull_webhook", id);
async {
let res = stack::handle_stack_deploy_webhook(id.clone(), headers, body).await;
if let Err(e) = res {
warn!("failed to run stack pull webook for stack {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/procedure/:id/:branch",
post(
|Path(IdBranch { id, branch }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("procedure_webhook", id, branch);
async {
let res = procedure::handle_procedure_webhook(
id.clone(),
branch.unwrap_or_else(|| String::from("main")),
headers,
body
).await;
if let Err(e) = res {
warn!("failed to run procedure webook for procedure {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/sync/:id/refresh",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("sync_refresh_webhook", id);
async {
let res = sync::handle_sync_refresh_webhook(
id.clone(),
headers,
body
).await;
if let Err(e) = res {
warn!("failed to run sync webook for sync {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
.route(
"/sync/:id/sync",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let span = info_span!("sync_execute_webhook", id);
async {
let res = sync::handle_sync_execute_webhook(
id.clone(),
headers,
body
).await;
if let Err(e) = res {
warn!("failed to run sync webook for sync {id} | {e:#}");
}
}
.instrument(span)
.await
});
},
)
)
}
#[instrument(skip_all)]
async fn verify_gh_signature(
headers: HeaderMap,
body: &str,
custom_secret: &str,
) -> anyhow::Result<()> {
// wait random amount of time
tokio::time::sleep(random_duration(0, 500)).await;
let signature = headers.get("x-hub-signature-256");
if signature.is_none() {
return Err(anyhow!("no signature in headers"));
}
let signature = signature.unwrap().to_str();
if signature.is_err() {
return Err(anyhow!("failed to unwrap signature"));
}
let signature = signature.unwrap().replace("sha256=", "");
let secret_bytes = if custom_secret.is_empty() {
core_config().webhook_secret.as_bytes()
} else {
custom_secret.as_bytes()
};
let mut mac = HmacSha256::new_from_slice(secret_bytes)
.expect("github webhook | failed to create hmac sha256");
mac.update(body.as_bytes());
let expected = mac.finalize().into_bytes().encode_hex::<String>();
if signature == expected {
Ok(())
} else {
Err(anyhow!("signature does not equal expected"))
}
}
#[derive(Deserialize)]
struct GithubWebhookBody {
#[serde(rename = "ref")]
branch: String,
}
fn extract_branch(body: &str) -> anyhow::Result<String> {
let branch = serde_json::from_str::<GithubWebhookBody>(body)
.context("failed to parse github request body")?
.branch
.replace("refs/heads/", "");
Ok(branch)
}
type ListenerLockCache = Cache<String, Arc<Mutex<()>>>;

View File

@@ -1,64 +0,0 @@
use std::sync::OnceLock;
use anyhow::anyhow;
use axum::http::HeaderMap;
use komodo_client::{
api::execute::RunProcedure,
entities::{procedure::Procedure, user::git_webhook_user},
};
use resolver_api::Resolve;
use crate::{
api::execute::ExecuteRequest,
helpers::update::init_execution_update, resource, state::State,
};
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
fn procedure_locks() -> &'static ListenerLockCache {
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_procedure_webhook(
procedure_id: String,
target_branch: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock =
procedure_locks().get_or_insert_default(&procedure_id).await;
let _lock = lock.lock().await;
let procedure = resource::get::<Procedure>(&procedure_id).await?;
verify_gh_signature(
headers,
&body,
&procedure.config.webhook_secret,
)
.await?;
if !procedure.config.webhook_enabled {
return Err(anyhow!("procedure does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != target_branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunProcedure(RunProcedure {
procedure: procedure_id,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunProcedure(req) = req else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
Ok(())
}

View File

@@ -1,135 +0,0 @@
use std::sync::OnceLock;
use anyhow::anyhow;
use axum::http::HeaderMap;
use komodo_client::{
api::execute::{BuildRepo, CloneRepo, PullRepo},
entities::{repo::Repo, user::git_webhook_user},
};
use resolver_api::Resolve;
use crate::{
helpers::update::init_execution_update, resource, state::State,
};
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
fn repo_locks() -> &'static ListenerLockCache {
static REPO_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
REPO_LOCKS.get_or_init(Default::default)
}
pub async fn handle_repo_clone_webhook(
repo_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != repo.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {
repo: repo_id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::CloneRepo(req) = req
else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
Ok(())
}
pub async fn handle_repo_pull_webhook(
repo_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != repo.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
let req = crate::api::execute::ExecuteRequest::PullRepo(PullRepo {
repo: repo_id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::PullRepo(req) = req else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
Ok(())
}
pub async fn handle_repo_build_webhook(
repo_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != repo.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
repo: repo_id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req
else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
Ok(())
}

View File

@@ -1,108 +0,0 @@
use std::sync::OnceLock;
use anyhow::anyhow;
use axum::http::HeaderMap;
use komodo_client::{
api::{
execute::{DeployStack, DeployStackIfChanged},
write::RefreshStackCache,
},
entities::{stack::Stack, user::git_webhook_user},
};
use resolver_api::Resolve;
use crate::{
api::execute::ExecuteRequest,
helpers::update::init_execution_update, resource, state::State,
};
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
fn stack_locks() -> &'static ListenerLockCache {
static STACK_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
STACK_LOCKS.get_or_init(Default::default)
}
pub async fn handle_stack_refresh_webhook(
stack_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through, from "action state busy".
let lock = stack_locks().get_or_insert_default(&stack_id).await;
let _lock = lock.lock().await;
let stack = resource::get::<Stack>(&stack_id).await?;
verify_gh_signature(headers, &body, &stack.config.webhook_secret)
.await?;
if !stack.config.webhook_enabled {
return Err(anyhow!("stack does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != stack.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
State
.resolve(RefreshStackCache { stack: stack.id }, user)
.await?;
Ok(())
}
pub async fn handle_stack_deploy_webhook(
stack_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = stack_locks().get_or_insert_default(&stack_id).await;
let _lock = lock.lock().await;
let stack = resource::get::<Stack>(&stack_id).await?;
verify_gh_signature(headers, &body, &stack.config.webhook_secret)
.await?;
if !stack.config.webhook_enabled {
return Err(anyhow!("stack does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != stack.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: stack_id,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::DeployStack(req) = req else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
} else {
let req =
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack: stack_id,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::DeployStackIfChanged(req) = req else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
}
Ok(())
}

View File

@@ -1,92 +0,0 @@
use std::sync::OnceLock;
use anyhow::anyhow;
use axum::http::HeaderMap;
use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{sync::ResourceSync, user::git_webhook_user},
};
use resolver_api::Resolve;
use crate::{
api::execute::ExecuteRequest,
helpers::update::init_execution_update, resource, state::State,
};
use super::{extract_branch, verify_gh_signature, ListenerLockCache};
fn sync_locks() -> &'static ListenerLockCache {
static SYNC_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
SYNC_LOCKS.get_or_init(Default::default)
}
pub async fn handle_sync_refresh_webhook(
sync_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let _lock = lock.lock().await;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
verify_gh_signature(headers, &body, &sync.config.webhook_secret)
.await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != sync.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
State
.resolve(RefreshResourceSyncPending { sync: sync_id }, user)
.await?;
Ok(())
}
pub async fn handle_sync_execute_webhook(
sync_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let _lock = lock.lock().await;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
verify_gh_signature(headers, &body, &sync.config.webhook_secret)
.await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
let request_branch = extract_branch(&body)?;
if request_branch != sync.config.branch {
return Err(anyhow!("request branch does not match expected"));
}
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunSync(RunSync {
sync: sync_id,
resource_type: None,
resources: None,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunSync(req) = req else {
unreachable!()
};
State.resolve(req, (user, update)).await?;
Ok(())
}

View File

@@ -0,0 +1,71 @@
use anyhow::{anyhow, Context};
use axum::http::HeaderMap;
use hex::ToHex;
use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha2::Sha256;
use crate::{
config::core_config,
listener::{VerifyBranch, VerifySecret},
};
type HmacSha256 = Hmac<Sha256>;
/// Listener implementation for Github type API, including Gitea
pub struct Github;
impl VerifySecret for Github {
#[instrument("VerifyGithubSecret", skip_all)]
fn verify_secret(
headers: HeaderMap,
body: &str,
custom_secret: &str,
) -> anyhow::Result<()> {
let signature = headers
.get("x-hub-signature-256")
.context("No github signature in headers")?;
let signature = signature
.to_str()
.context("Failed to get signature as string")?;
let signature =
signature.strip_prefix("sha256=").unwrap_or(signature);
let secret_bytes = if custom_secret.is_empty() {
core_config().webhook_secret.as_bytes()
} else {
custom_secret.as_bytes()
};
let mut mac = HmacSha256::new_from_slice(secret_bytes)
.context("Failed to create hmac sha256 from secret")?;
mac.update(body.as_bytes());
let expected = mac.finalize().into_bytes().encode_hex::<String>();
if signature == expected {
Ok(())
} else {
Err(anyhow!("Signature does not equal expected"))
}
}
}
#[derive(Deserialize)]
struct GithubWebhookBody {
#[serde(rename = "ref")]
branch: String,
}
impl VerifyBranch for Github {
fn verify_branch(
body: &str,
expected_branch: &str,
) -> anyhow::Result<()> {
let branch = serde_json::from_str::<GithubWebhookBody>(body)
.context("Failed to parse github request body")?
.branch
.replace("refs/heads/", "");
if branch == expected_branch {
Ok(())
} else {
Err(anyhow!("request branch does not match expected"))
}
}
}

View File

@@ -0,0 +1,58 @@
use anyhow::{anyhow, Context};
use serde::Deserialize;
use crate::{
config::core_config,
listener::{VerifyBranch, VerifySecret},
};
/// Listener implementation for Gitlab type API
pub struct Gitlab;
impl VerifySecret for Gitlab {
#[instrument("VerifyGitlabSecret", skip_all)]
fn verify_secret(
headers: axum::http::HeaderMap,
_body: &str,
custom_secret: &str,
) -> anyhow::Result<()> {
let token = headers
.get("x-gitlab-token")
.context("No gitlab token in headers")?;
let token =
token.to_str().context("Failed to get token as string")?;
let secret = if custom_secret.is_empty() {
core_config().webhook_secret.as_str()
} else {
custom_secret
};
if token == secret {
Ok(())
} else {
Err(anyhow!("Webhook secret does not match expected."))
}
}
}
#[derive(Deserialize)]
struct GitlabWebhookBody {
#[serde(rename = "ref")]
branch: String,
}
impl VerifyBranch for Gitlab {
fn verify_branch(
body: &str,
expected_branch: &str,
) -> anyhow::Result<()> {
let branch = serde_json::from_str::<GitlabWebhookBody>(body)
.context("Failed to parse gitlab request body")?
.branch
.replace("refs/heads/", "");
if branch == expected_branch {
Ok(())
} else {
Err(anyhow!("request branch does not match expected"))
}
}
}

View File

@@ -0,0 +1,2 @@
pub mod github;
pub mod gitlab;

View File

@@ -1,7 +1,52 @@
use axum::Router;
use std::sync::Arc;
mod github;
use axum::{http::HeaderMap, Router};
use komodo_client::entities::resource::Resource;
use tokio::sync::Mutex;
use crate::{helpers::cache::Cache, resource::KomodoResource};
mod integrations;
mod resources;
mod router;
use integrations::*;
pub fn router() -> Router {
Router::new().nest("/github", github::router())
Router::new()
.nest("/github", router::router::<github::Github>())
.nest("/gitlab", router::router::<gitlab::Gitlab>())
}
type ListenerLockCache = Cache<String, Arc<Mutex<()>>>;
/// Implemented for all resources which can recieve webhook.
trait CustomSecret: KomodoResource {
fn custom_secret(
resource: &Resource<Self::Config, Self::Info>,
) -> &str;
}
/// Implemented on the integration struct, eg [integrations::github::Github]
trait VerifySecret {
fn verify_secret(
headers: HeaderMap,
body: &str,
custom_secret: &str,
) -> anyhow::Result<()>;
}
/// Implemented on the integration struct, eg [integrations::github::Github]
trait VerifyBranch {
/// Returns Err if the branch extracted from request
/// body does not match the expected branch.
fn verify_branch(
body: &str,
expected_branch: &str,
) -> anyhow::Result<()>;
}
/// For Procedures and Actions, incoming webhook
/// can be triggered by any branch by using `__ANY__`
/// as the branch in the webhook URL.
const ANY_BRANCH: &str = "__ANY__";

View File

@@ -0,0 +1,505 @@
use std::sync::OnceLock;
use anyhow::anyhow;
use komodo_client::{
api::{
execute::*,
write::{RefreshResourceSyncPending, RefreshStackCache},
},
entities::{
action::Action, build::Build, procedure::Procedure, repo::Repo,
stack::Stack, sync::ResourceSync, user::git_webhook_user,
},
};
use resolver_api::Resolve;
use serde::Deserialize;
use crate::{
api::{
execute::{ExecuteArgs, ExecuteRequest},
write::WriteArgs,
},
helpers::update::init_execution_update,
};
use super::{ListenerLockCache, ANY_BRANCH};
// =======
// BUILD
// =======
impl super::CustomSecret for Build {
fn custom_secret(resource: &Self) -> &str {
&resource.config.webhook_secret
}
}
fn build_locks() -> &'static ListenerLockCache {
static BUILD_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_build_webhook<B: super::VerifyBranch>(
build: Build,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = build_locks().get_or_insert_default(&build.id).await;
let _lock = lock.lock().await;
if !build.config.webhook_enabled {
return Err(anyhow!("build does not have webhook enabled"));
}
B::verify_branch(&body, &build.config.branch)?;
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunBuild(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(())
}
// ======
// REPO
// ======
impl super::CustomSecret for Repo {
fn custom_secret(resource: &Self) -> &str {
&resource.config.webhook_secret
}
}
fn repo_locks() -> &'static ListenerLockCache {
static REPO_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
REPO_LOCKS.get_or_init(Default::default)
}
pub trait RepoExecution {
async fn resolve(repo: Repo) -> anyhow::Result<()>;
}
impl RepoExecution for CloneRepo {
async fn resolve(repo: Repo) -> anyhow::Result<()> {
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::CloneRepo(req) = req
else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(())
}
}
impl RepoExecution for PullRepo {
async fn resolve(repo: Repo) -> anyhow::Result<()> {
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::PullRepo(PullRepo {
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::PullRepo(req) = req
else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(())
}
}
impl RepoExecution for BuildRepo {
async fn resolve(repo: Repo) -> anyhow::Result<()> {
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req
else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(())
}
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RepoWebhookOption {
Clone,
Pull,
Build,
}
pub async fn handle_repo_webhook<B: super::VerifyBranch>(
option: RepoWebhookOption,
repo: Repo,
body: String,
) -> anyhow::Result<()> {
match option {
RepoWebhookOption::Clone => {
handle_repo_webhook_inner::<B, CloneRepo>(repo, body).await
}
RepoWebhookOption::Pull => {
handle_repo_webhook_inner::<B, PullRepo>(repo, body).await
}
RepoWebhookOption::Build => {
handle_repo_webhook_inner::<B, BuildRepo>(repo, body).await
}
}
}
async fn handle_repo_webhook_inner<
B: super::VerifyBranch,
E: RepoExecution,
>(
repo: Repo,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
B::verify_branch(&body, &repo.config.branch)?;
E::resolve(repo).await
}
// =======
// STACK
// =======
impl super::CustomSecret for Stack {
fn custom_secret(resource: &Self) -> &str {
&resource.config.webhook_secret
}
}
fn stack_locks() -> &'static ListenerLockCache {
static STACK_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
STACK_LOCKS.get_or_init(Default::default)
}
pub trait StackExecution {
async fn resolve(stack: Stack) -> serror::Result<()>;
}
impl StackExecution for RefreshStackCache {
async fn resolve(stack: Stack) -> serror::Result<()> {
RefreshStackCache { stack: stack.id }
.resolve(&WriteArgs {
user: git_webhook_user().to_owned(),
})
.await?;
Ok(())
}
}
impl StackExecution for DeployStack {
async fn resolve(stack: Stack) -> serror::Result<()> {
let user = git_webhook_user().to_owned();
if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: stack.id,
service: None,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::DeployStack(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
} else {
let req =
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack: stack.id,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::DeployStackIfChanged(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
}
Ok(())
}
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StackWebhookOption {
Refresh,
Deploy,
}
pub async fn handle_stack_webhook<B: super::VerifyBranch>(
option: StackWebhookOption,
stack: Stack,
body: String,
) -> anyhow::Result<()> {
match option {
StackWebhookOption::Refresh => {
handle_stack_webhook_inner::<B, RefreshStackCache>(stack, body)
.await
}
StackWebhookOption::Deploy => {
handle_stack_webhook_inner::<B, DeployStack>(stack, body).await
}
}
}
pub async fn handle_stack_webhook_inner<
B: super::VerifyBranch,
E: StackExecution,
>(
stack: Stack,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through, from "action state busy".
let lock = stack_locks().get_or_insert_default(&stack.id).await;
let _lock = lock.lock().await;
if !stack.config.webhook_enabled {
return Err(anyhow!("stack does not have webhook enabled"));
}
B::verify_branch(&body, &stack.config.branch)?;
E::resolve(stack).await.map_err(|e| e.error)
}
// ======
// SYNC
// ======
impl super::CustomSecret for ResourceSync {
fn custom_secret(resource: &Self) -> &str {
&resource.config.webhook_secret
}
}
fn sync_locks() -> &'static ListenerLockCache {
static SYNC_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
SYNC_LOCKS.get_or_init(Default::default)
}
pub trait SyncExecution {
async fn resolve(sync: ResourceSync) -> anyhow::Result<()>;
}
impl SyncExecution for RefreshResourceSyncPending {
async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {
RefreshResourceSyncPending { sync: sync.id }
.resolve(&WriteArgs {
user: git_webhook_user().to_owned(),
})
.await
.map_err(|e| e.error)?;
Ok(())
}
}
impl SyncExecution for RunSync {
async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunSync(RunSync {
sync: sync.id,
resource_type: None,
resources: None,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunSync(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(())
}
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SyncWebhookOption {
Refresh,
Sync,
}
pub async fn handle_sync_webhook<B: super::VerifyBranch>(
option: SyncWebhookOption,
sync: ResourceSync,
body: String,
) -> anyhow::Result<()> {
match option {
SyncWebhookOption::Refresh => {
handle_sync_webhook_inner::<B, RefreshResourceSyncPending>(
sync, body,
)
.await
}
SyncWebhookOption::Sync => {
handle_sync_webhook_inner::<B, RunSync>(sync, body).await
}
}
}
async fn handle_sync_webhook_inner<
B: super::VerifyBranch,
E: SyncExecution,
>(
sync: ResourceSync,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync.id).await;
let _lock = lock.lock().await;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
B::verify_branch(&body, &sync.config.branch)?;
E::resolve(sync).await
}
// ===========
// PROCEDURE
// ===========
impl super::CustomSecret for Procedure {
fn custom_secret(resource: &Self) -> &str {
&resource.config.webhook_secret
}
}
fn procedure_locks() -> &'static ListenerLockCache {
static PROCEDURE_LOCKS: OnceLock<ListenerLockCache> =
OnceLock::new();
PROCEDURE_LOCKS.get_or_init(Default::default)
}
pub async fn handle_procedure_webhook<B: super::VerifyBranch>(
procedure: Procedure,
target_branch: &str,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock =
procedure_locks().get_or_insert_default(&procedure.id).await;
let _lock = lock.lock().await;
if !procedure.config.webhook_enabled {
return Err(anyhow!("procedure does not have webhook enabled"));
}
if target_branch != ANY_BRANCH {
B::verify_branch(&body, target_branch)?;
}
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunProcedure(RunProcedure {
procedure: procedure.id,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunProcedure(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(())
}
// ========
// ACTION
// ========
impl super::CustomSecret for Action {
fn custom_secret(resource: &Self) -> &str {
&resource.config.webhook_secret
}
}
fn action_locks() -> &'static ListenerLockCache {
static ACTION_LOCKS: OnceLock<ListenerLockCache> = OnceLock::new();
ACTION_LOCKS.get_or_init(Default::default)
}
pub async fn handle_action_webhook<B: super::VerifyBranch>(
action: Action,
target_branch: &str,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = action_locks().get_or_insert_default(&action.id).await;
let _lock = lock.lock().await;
if !action.config.webhook_enabled {
return Err(anyhow!("action does not have webhook enabled"));
}
if target_branch != ANY_BRANCH {
B::verify_branch(&body, target_branch)?;
}
let user = git_webhook_user().to_owned();
let req =
ExecuteRequest::RunAction(RunAction { action: action.id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunAction(req) = req else {
unreachable!()
};
req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(())
}

View File

@@ -0,0 +1,220 @@
use axum::{extract::Path, http::HeaderMap, routing::post, Router};
use komodo_client::entities::{
action::Action, build::Build, procedure::Procedure, repo::Repo,
resource::Resource, stack::Stack, sync::ResourceSync,
};
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCode;
use tracing::Instrument;
use crate::resource::KomodoResource;
use super::{
resources::{
handle_action_webhook, handle_build_webhook,
handle_procedure_webhook, handle_repo_webhook,
handle_stack_webhook, handle_sync_webhook, RepoWebhookOption,
StackWebhookOption, SyncWebhookOption,
},
CustomSecret, VerifyBranch, VerifySecret,
};
#[derive(Deserialize)]
struct Id {
id: String,
}
#[derive(Deserialize)]
struct IdAndOption<T> {
id: String,
option: T,
}
#[derive(Deserialize)]
struct IdAndBranch {
id: String,
#[serde(default = "default_branch")]
branch: String,
}
fn default_branch() -> String {
String::from("main")
}
pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
Router::new()
.route(
"/build/{id}",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let build =
auth_webhook::<P, Build>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("BuildWebhook", id);
async {
let res = handle_build_webhook::<P>(
build, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for build {id} | {e:#}"
);
}
}
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
.route(
"/repo/{id}/{option}",
post(
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let repo =
auth_webhook::<P, Repo>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("RepoWebhook", id);
async {
let res = handle_repo_webhook::<P>(
option, repo, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for repo {id} | {e:#}"
);
}
}
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
.route(
"/stack/{id}/{option}",
post(
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let stack =
auth_webhook::<P, Stack>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("StackWebhook", id);
async {
let res = handle_stack_webhook::<P>(
option, stack, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for stack {id} | {e:#}"
);
}
}
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
.route(
"/sync/{id}/{option}",
post(
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let sync =
auth_webhook::<P, ResourceSync>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("ResourceSyncWebhook", id);
async {
let res = handle_sync_webhook::<P>(
option, sync, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for resource sync {id} | {e:#}"
);
}
}
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
.route(
"/procedure/{id}/{branch}",
post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
let procedure =
auth_webhook::<P, Procedure>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("ProcedureWebhook", id);
async {
let res = handle_procedure_webhook::<P>(
procedure, &branch, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for procedure {id} | target branch: {branch} | {e:#}"
);
}
}
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
.route(
"/action/{id}/{branch}",
post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
let action =
auth_webhook::<P, Action>(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("ActionWebhook", id);
async {
let res = handle_action_webhook::<P>(
action, &branch, body,
)
.await;
if let Err(e) = res {
warn!(
"Failed at running webhook for action {id} | target branch: {branch} | {e:#}"
);
}
}
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
}
async fn auth_webhook<P, R>(
id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Resource<R::Config, R::Info>>
where
P: VerifySecret,
R: KomodoResource + CustomSecret,
{
let resource = crate::resource::get::<R>(id)
.await
.status_code(StatusCode::BAD_REQUEST)?;
P::verify_secret(headers, body, R::custom_secret(&resource))
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(resource)
}

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