* resolver v3

add new ec2 instance types

clean up testing config

document the libraries a bit

clean up main

update sysinfo and otel

update client resolver 3.0

resolver v3 prog

clean up gitignore

implement periphery resolver v3

clean up

core read api v3

more prog

execute api

missing apis

compiling

1.16.13

work on more granular traits

prog on crud

* fmt

* format

* resource2 not really a benefit

* axum to 0.8

* bump aws deps

* just make it 1.17.0

* clean up cors

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

* add entities / message for test alerter

* test alert implementation

* rust 1.84.0

* axum update :param to {param} syntax

* fix last axum updates

* Add test alerter button

* higher quality / colored icons

* komodo-logo

* simplify network stats

* rename Test Alerter button

* escape incoming sync backslashes (BREAKING)

* clean up rust client websocket subscription

* finish oidc comment

* show update available stack table

* update available deployment table

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

* Update repo path handling in commit fetching

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

* feat: use optional name and path in GetLatestCommit

* review: don't use optional for name

* review: use helper

* review: remove redundant to_string()

* 1.17.0-dev

* feature: add post_deploy command (#288)

* feature: add post_deploy command

* review: do not run post_deploy if deploy failed

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

* feature: interpolate secrets in custom alerter

* fix rust warning

* review: sanitize errors

* review: sanitize error message

* Remove .git from remote_url (#299)

Remove .git from remote_url

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

* mbecker20 -> moghtech

* remove example from cargo toml workspace

* dev-1

* fix login screen logo

* more legible favicon

* fix new compose images

* docs new organization

* typescript subscribe_to_update_websocket

* add donate button docsite

* add config save button in desktop sidebar navigator

* add save button to config bottom

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

* feature: allow docker image text to overflow in table

* review: use break-words

* wip: revert line break in css file

* feature: update devcontainer node release

* improve First Login docs

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

* requery alerts more often

* improve update indicator style and also put on home screen

* Add all services stack log

* 1.17.0-dev-2

* fix api name chnage

* choose which stack services to include in logs

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

* feature: improve tables quick actions on mobile

* review: fix gap4

* review: use flex-wrap

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

* Fix unclear ComposePull log re #244

* use komodo_client.subscribe_to_update_websocket, and click indicator to reconnect

* dev-3

* ServerTemplate description

* improve WriteComposeContentsToHost instrument fields

* give server stat charts labels

* filters wrap

* show provider usernames from config file

* Stack: Fix git repo new compose file initialization

* init sync file new repo

* set branch on git init folder

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

* Improve resource sync Execute / Pending view selector

* standardize running commands with interpolation / output sanitizations

* fix all clippy lints

* fix rand

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

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

* ResourceSync state resolution refinement

* make sure parent directories exist whenever writing files

* don't prune images if server not enabled

* update most deps

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

* dev-4

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

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

* auto update all service deploy option

* dev-5 fix the stack service executions

* clean up service_args

* rust 1.85

* store sync edits on localstorage

* stack edits on localstorage and show last deployed config

* add yarn install to runfile

* Fix actions when core on https

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

* rust 2024 and fmt

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

* update .devcontainer / dev docs for updated runfile

* use png in topbar logo, svg quality sometimes bad

* OIDC: Support PKCE auth (secret optional)

* update docs on OIDC and client secret

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

* add KOMODO_LOCK_LOGIN_CREDENTIALS_FOR in config doc

* update deps

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

* use jsonwebtoken

* improve variable value table overflow

* colored tags

* fix sync summary count ok

* default new tag colors to grey

* soften tag opacity a bit

* Update config.tsx (#358)

* isolate stacks / deployments with pending updates

* update some deps

* use Tooltip component instead of HoverCard for mobile compatibility

* batch Build builds

* link to typescript client in the intro

* add link to main docs from client docs

* doc tweaks

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

* remove unnecessary explicit network

* periphery.compose.yaml

* clean up periphery compose

* add link to config

* update periphery container compose config

* rust 1.85.1

* update sync docs

* 1.17.0

---------

Co-authored-by: unsync <1211591+unsync@users.noreply.github.com>
Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>
Co-authored-by: komodo <komodo@komo.do>
Co-authored-by: wlatic <jamesoh@gmail.com>
This commit is contained in:
Maxwell Becker
2025-03-23 16:47:06 -07:00
committed by GitHub
parent 9c841e5bdc
commit db1cf786ac
403 changed files with 24493 additions and 11138 deletions

View File

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

View File

@@ -10,7 +10,7 @@
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "18.18.0" "version": "20.12.2"
}, },
"ghcr.io/devcontainers-community/features/deno:1": { "ghcr.io/devcontainers-community/features/deno:1": {

View File

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

8
.gitignore vendored
View File

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

1226
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,23 +8,20 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "1.16.12" version = "1.17.0"
edition = "2021" edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"] authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/mbecker20/komodo" repository = "https://github.com/moghtech/komodo"
homepage = "https://komo.do" homepage = "https://komo.do"
[patch.crates-io]
# komodo_client = { path = "client/core/rs" }
[workspace.dependencies] [workspace.dependencies]
# LOCAL # LOCAL
# komodo_client = "1.15.6"
komodo_client = { path = "client/core/rs" } komodo_client = { path = "client/core/rs" }
periphery_client = { path = "client/periphery/rs" } periphery_client = { path = "client/periphery/rs" }
environment_file = { path = "lib/environment_file" } environment_file = { path = "lib/environment_file" }
formatting = { path = "lib/formatting" } formatting = { path = "lib/formatting" }
response = { path = "lib/response" }
command = { path = "lib/command" } command = { path = "lib/command" }
logger = { path = "lib/logger" } logger = { path = "lib/logger" }
cache = { path = "lib/cache" } cache = { path = "lib/cache" }
@@ -32,7 +29,7 @@ git = { path = "lib/git" }
# MOGH # MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] } run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.4.7", default-features = false } serror = { version = "0.5.0", default-features = false }
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] } slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
derive_default_builder = "0.1.8" derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0" derive_empty_traits = "0.1.0"
@@ -41,79 +38,81 @@ async_timing_util = "1.0.0"
partial_derive2 = "0.4.3" partial_derive2 = "0.4.3"
derive_variants = "1.0.0" derive_variants = "1.0.0"
mongo_indexed = "2.0.1" mongo_indexed = "2.0.1"
resolver_api = "1.1.1" resolver_api = "3.0.0"
toml_pretty = "1.1.2" toml_pretty = "1.1.2"
mungos = "1.1.0" mungos = "3.2.0"
svi = "1.0.1" svi = "1.0.1"
# ASYNC # ASYNC
reqwest = { version = "0.12.9", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12.15", default-features = false, features = ["json", "rustls-tls-native-roots"] }
tokio = { version = "1.41.1", features = ["full"] } tokio = { version = "1.44.1", features = ["full"] }
tokio-util = "0.7.12" tokio-util = "0.7.14"
futures = "0.3.31" futures = "0.3.31"
futures-util = "0.3.31" futures-util = "0.3.31"
arc-swap = "1.7.1"
# SERVER # SERVER
axum-extra = { version = "0.9.6", features = ["typed-header"] } axum-extra = { version = "0.10.0", features = ["typed-header"] }
tower-http = { version = "0.6.2", features = ["fs", "cors"] } tower-http = { version = "0.6.2", features = ["fs", "cors"] }
axum-server = { version = "0.7.1", features = ["tls-rustls"] } axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.7.9", features = ["ws", "json"] } axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
tokio-tungstenite = "0.24.0" tokio-tungstenite = "0.26.2"
# SER/DE # SER/DE
ordered_hash_map = { version = "0.4.0", features = ["serde"] } ordered_hash_map = { version = "0.4.0", features = ["serde"] }
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
serde_json = "1.0.133" serde_json = "1.0.140"
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
toml = "0.8.19" toml = "0.8.20"
# ERROR # ERROR
anyhow = "1.0.93" anyhow = "1.0.97"
thiserror = "2.0.3" thiserror = "2.0.12"
# LOGGING # LOGGING
opentelemetry-otlp = { version = "0.27.0", features = ["tls-roots", "reqwest-rustls"] } opentelemetry-otlp = { version = "0.29.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.27.0", features = ["rt-tokio"] } opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.18", features = ["json"] } tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.27.0" opentelemetry-semantic-conventions = "0.29.0"
tracing-opentelemetry = "0.28.0" tracing-opentelemetry = "0.30.0"
opentelemetry = "0.27.0" opentelemetry = "0.29.0"
tracing = "0.1.40" tracing = "0.1.41"
# CONFIG # CONFIG
clap = { version = "4.5.21", features = ["derive"] } clap = { version = "4.5.32", features = ["derive"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
envy = "0.4.2" envy = "0.4.2"
# CRYPTO / AUTH # CRYPTO / AUTH
uuid = { version = "1.11.0", features = ["v4", "fast-rng", "serde"] } uuid = { version = "1.16.0", features = ["v4", "fast-rng", "serde"] }
openidconnect = "3.5.0" jsonwebtoken = { version = "9.3.1", default-features = false }
openidconnect = "4.0.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
nom_pem = "4.0.0" nom_pem = "4.0.0"
bcrypt = "0.16.0" bcrypt = "0.17.0"
base64 = "0.22.1" base64 = "0.22.1"
rustls = "0.23.18" rustls = "0.23.25"
hmac = "0.12.1" hmac = "0.12.1"
sha2 = "0.10.8" sha2 = "0.10.8"
rand = "0.8.5" rand = "0.9.0"
jwt = "0.16.0"
hex = "0.4.3" hex = "0.4.3"
# SYSTEM # SYSTEM
bollard = "0.18.1" bollard = "0.18.1"
sysinfo = "0.32.0" sysinfo = "0.33.1"
# CLOUD # CLOUD
aws-config = "1.5.10" aws-config = "1.6.0"
aws-sdk-ec2 = "1.91.0" aws-sdk-ec2 = "1.118.1"
aws-credential-types = "1.2.2"
# MISC # MISC
derive_builder = "0.20.2" derive_builder = "0.20.2"
typeshare = "1.0.4" typeshare = "1.0.4"
octorust = "0.7.0" octorust = "0.10.0"
dashmap = "6.1.0" dashmap = "6.1.0"
wildcard = "0.3.0" wildcard = "0.3.0"
colored = "2.1.0" colored = "3.0.0"
regex = "1.11.1" regex = "1.11.1"
bson = "2.13.0" bson = "2.14.0"

View File

@@ -1,7 +1,7 @@
## Builds the Komodo Core and Periphery binaries ## Builds the Komodo Core and Periphery binaries
## for a specific architecture. ## for a specific architecture.
FROM rust:1.82.0-bullseye AS builder FROM rust:1.85.1-bullseye AS builder
WORKDIR /builder WORKDIR /builder
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
@@ -22,6 +22,6 @@ FROM scratch
COPY --from=builder /builder/target/release/core /core COPY --from=builder /builder/target/release/core /core
COPY --from=builder /builder/target/release/periphery /periphery COPY --from=builder /builder/target/release/periphery /periphery
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Periphery" LABEL org.opencontainers.image.description="Komodo Periphery"
LABEL org.opencontainers.image.licenses=GPL-3.0 LABEL org.opencontainers.image.licenses=GPL-3.0

View File

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

View File

@@ -206,6 +206,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::BatchDestroyStack(data) => { Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed()) println!("{}: {data:?}", "Data".dimmed())
} }
Execution::TestAlerter(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => { Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed()) println!("{}: {data:?}", "Data".dimmed())
} }
@@ -454,6 +457,10 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
.execute(request) .execute(request)
.await .await
.map(ExecutionResult::Batch), .map(ExecutionResult::Batch),
Execution::TestAlerter(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Single),
Execution::Sleep(request) => { Execution::Sleep(request) => {
let duration = let duration =
Duration::from_millis(request.duration_ms as u64); Duration::from_millis(request.duration_ms as u64);

View File

@@ -19,6 +19,7 @@ komodo_client = { workspace = true, features = ["mongo"] }
periphery_client.workspace = true periphery_client.workspace = true
environment_file.workspace = true environment_file.workspace = true
formatting.workspace = true formatting.workspace = true
response.workspace = true
command.workspace = true command.workspace = true
logger.workspace = true logger.workspace = true
cache.workspace = true cache.workspace = true
@@ -36,9 +37,10 @@ mungos.workspace = true
slack.workspace = true slack.workspace = true
svi.workspace = true svi.workspace = true
# external # external
axum-server.workspace = true aws-credential-types.workspace = true
ordered_hash_map.workspace = true ordered_hash_map.workspace = true
openidconnect.workspace = true openidconnect.workspace = true
axum-server.workspace = true
urlencoding.workspace = true urlencoding.workspace = true
aws-sdk-ec2.workspace = true aws-sdk-ec2.workspace = true
aws-config.workspace = true aws-config.workspace = true
@@ -50,6 +52,7 @@ serde_yaml.workspace = true
typeshare.workspace = true typeshare.workspace = true
octorust.workspace = true octorust.workspace = true
wildcard.workspace = true wildcard.workspace = true
arc-swap.workspace = true
dashmap.workspace = true dashmap.workspace = true
tracing.workspace = true tracing.workspace = true
reqwest.workspace = true reqwest.workspace = true
@@ -70,5 +73,5 @@ envy.workspace = true
rand.workspace = true rand.workspace = true
hmac.workspace = true hmac.workspace = true
sha2.workspace = true sha2.workspace = true
jwt.workspace = true jsonwebtoken.workspace = true
hex.workspace = true hex.workspace = true

View File

@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture. ## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core # Build Core
FROM rust:1.82.0-bullseye AS core-builder FROM rust:1.85.1-bullseye AS core-builder
WORKDIR /builder WORKDIR /builder
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
@@ -48,7 +48,7 @@ RUN mkdir /action-cache && \
EXPOSE 9120 EXPOSE 9120
# Label for Ghcr # Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core" LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0 LABEL org.opencontainers.image.licenses=GPL-3.0

View File

@@ -2,8 +2,8 @@
## Sets up the necessary runtime container dependencies for Komodo Core. ## 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. ## Since theres no heavy build here, QEMU multi-arch builds are fine for this image.
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
ARG FRONTEND_IMAGE=ghcr.io/mbecker20/komodo-frontend:latest ARG FRONTEND_IMAGE=ghcr.io/moghtech/komodo-frontend:latest
ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64 ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64
ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64 ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64
@@ -43,8 +43,8 @@ RUN mkdir /action-cache && \
EXPOSE 9120 EXPOSE 9120
# Label for Ghcr # Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core" LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0 LABEL org.opencontainers.image.licenses=GPL-3.0
ENTRYPOINT [ "core" ] CMD [ "core" ]

View File

@@ -1,7 +1,7 @@
## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile). ## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile).
## Sets up the necessary runtime container dependencies for Komodo Core. ## Sets up the necessary runtime container dependencies for Komodo Core.
ARG BINARIES_IMAGE=ghcr.io/mbecker20/komodo-binaries:latest ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
# This is required to work with COPY --from # This is required to work with COPY --from
FROM ${BINARIES_IMAGE} AS binaries FROM ${BINARIES_IMAGE} AS binaries
@@ -37,8 +37,8 @@ RUN mkdir /action-cache && \
EXPOSE 9120 EXPOSE 9120
# Label for Ghcr # Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Core" LABEL org.opencontainers.image.description="Komodo Core"
LABEL org.opencontainers.image.licenses=GPL-3.0 LABEL org.opencontainers.image.licenses=GPL-3.0
ENTRYPOINT [ "core" ] CMD [ "core" ]

View File

@@ -11,6 +11,12 @@ pub async fn send_alert(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let level = fmt_level(alert.level); let level = fmt_level(alert.level);
let content = match &alert.data { 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 { AlertData::ServerUnreachable {
id, id,
name, name,
@@ -88,7 +94,9 @@ pub async fn send_alert(
} => { } => {
let link = resource_link(ResourceTargetVariant::Deployment, id); let link = resource_link(ResourceTargetVariant::Deployment, id);
let to = fmt_docker_container_state(to); 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 { AlertData::DeploymentImageUpdateAvailable {
id, id,
@@ -98,7 +106,9 @@ pub async fn send_alert(
image, image,
} => { } => {
let link = resource_link(ResourceTargetVariant::Deployment, id); let link = resource_link(ResourceTargetVariant::Deployment, id);
format!("⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}") format!(
"⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}"
)
} }
AlertData::DeploymentAutoUpdated { AlertData::DeploymentAutoUpdated {
id, id,
@@ -108,7 +118,9 @@ pub async fn send_alert(
image, image,
} => { } => {
let link = resource_link(ResourceTargetVariant::Deployment, id); let link = resource_link(ResourceTargetVariant::Deployment, id);
format!("⬆ Deployment **{name}** was updated automatically ⏫\nserver: **{server_name}**\nimage: **{image}**\n{link}") format!(
"⬆ Deployment **{name}** was updated automatically ⏫\nserver: **{server_name}**\nimage: **{image}**\n{link}"
)
} }
AlertData::StackStateChange { AlertData::StackStateChange {
id, id,
@@ -120,7 +132,9 @@ pub async fn send_alert(
} => { } => {
let link = resource_link(ResourceTargetVariant::Stack, id); let link = resource_link(ResourceTargetVariant::Stack, id);
let to = fmt_stack_state(to); 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 { AlertData::StackImageUpdateAvailable {
id, id,
@@ -131,7 +145,9 @@ pub async fn send_alert(
image, image,
} => { } => {
let link = resource_link(ResourceTargetVariant::Stack, id); let link = resource_link(ResourceTargetVariant::Stack, id);
format!("⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}") format!(
"⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}"
)
} }
AlertData::StackAutoUpdated { AlertData::StackAutoUpdated {
id, id,
@@ -144,13 +160,17 @@ pub async fn send_alert(
let images_label = let images_label =
if images.len() > 1 { "images" } else { "image" }; if images.len() > 1 { "images" } else { "image" };
let images = images.join(", "); let images = images.join(", ");
format!("⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}") format!(
"⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}"
)
} }
AlertData::AwsBuilderTerminationFailed { AlertData::AwsBuilderTerminationFailed {
instance_id, instance_id,
message, 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 } => { AlertData::ResourceSyncPendingUpdates { id, name } => {
let link = let link =
@@ -161,7 +181,9 @@ pub async fn send_alert(
} }
AlertData::BuildFailed { id, name, version } => { AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id); 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 } => { AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id); let link = resource_link(ResourceTargetVariant::Repo, id);

View File

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

View File

@@ -7,6 +7,22 @@ pub async fn send_alert(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let level = fmt_level(alert.level); let level = fmt_level(alert.level);
let (text, blocks): (_, Option<_>) = match &alert.data { 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 { AlertData::ServerUnreachable {
id, id,
name, name,
@@ -57,7 +73,9 @@ pub async fn send_alert(
let region = fmt_region(region); let region = fmt_region(region);
match alert.level { match alert.level {
SeverityLevel::Ok => { SeverityLevel::Ok => {
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%*"); let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*"
);
let blocks = vec![ let blocks = vec![
Block::header(level), Block::header(level),
Block::section(format!( Block::section(format!(
@@ -71,7 +89,9 @@ pub async fn send_alert(
(text, blocks.into()) (text, blocks.into())
} }
_ => { _ => {
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈"); let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈"
);
let blocks = vec![ let blocks = vec![
Block::header(level), Block::header(level),
Block::section(format!( Block::section(format!(
@@ -97,7 +117,9 @@ pub async fn send_alert(
let percentage = 100.0 * used_gb / total_gb; let percentage = 100.0 * used_gb / total_gb;
match alert.level { match alert.level {
SeverityLevel::Ok => { SeverityLevel::Ok => {
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"); let text = format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
);
let blocks = vec![ let blocks = vec![
Block::header(level), Block::header(level),
Block::section(format!( Block::section(format!(
@@ -114,7 +136,9 @@ pub async fn send_alert(
(text, blocks.into()) (text, blocks.into())
} }
_ => { _ => {
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"); let text = format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
);
let blocks = vec![ let blocks = vec![
Block::header(level), Block::header(level),
Block::section(format!( Block::section(format!(
@@ -144,7 +168,9 @@ pub async fn send_alert(
let percentage = 100.0 * used_gb / total_gb; let percentage = 100.0 * used_gb / total_gb;
match alert.level { match alert.level {
SeverityLevel::Ok => { SeverityLevel::Ok => {
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"); let text = format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
);
let blocks = vec![ let blocks = vec![
Block::header(level), Block::header(level),
Block::section(format!( Block::section(format!(
@@ -153,12 +179,17 @@ pub async fn send_alert(
Block::section(format!( Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*" "mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)), )),
Block::section(resource_link(ResourceTargetVariant::Server, id)), Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
]; ];
(text, blocks.into()) (text, blocks.into())
} }
_ => { _ => {
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"); let text = format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
);
let blocks = vec![ let blocks = vec![
Block::header(level), Block::header(level),
Block::section(format!( Block::section(format!(
@@ -167,7 +198,10 @@ pub async fn send_alert(
Block::section(format!( Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*" "mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)), )),
Block::section(resource_link(ResourceTargetVariant::Server, id)), Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
]; ];
(text, blocks.into()) (text, blocks.into())
} }

View File

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

View File

@@ -13,11 +13,8 @@ use komodo_client::{
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey}, user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
}, },
entities::{ entities::{
action::Action, action::Action, config::core::CoreConfig,
config::core::CoreConfig, permission::PermissionLevel, update::Update, user::action_user,
permission::PermissionLevel,
update::Update,
user::{action_user, User},
}, },
}; };
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document}; use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
@@ -25,7 +22,7 @@ use resolver_api::Resolve;
use tokio::fs; use tokio::fs;
use crate::{ use crate::{
api::execute::ExecuteRequest, api::{execute::ExecuteRequest, user::UserArgs},
config::core_config, config::core_config,
helpers::{ helpers::{
interpolate::{ interpolate::{
@@ -37,9 +34,11 @@ use crate::{
update::update_update, update::update_update,
}, },
resource::{self, refresh_action_state_cache}, resource::{self, refresh_action_state_cache},
state::{action_states, db_client, State}, state::{action_states, db_client},
}; };
use super::ExecuteArgs;
impl super::BatchExecute for BatchRunAction { impl super::BatchExecute for BatchRunAction {
type Resource = Action; type Resource = Action;
fn single_request(action: String) -> ExecuteRequest { fn single_request(action: String) -> ExecuteRequest {
@@ -47,27 +46,28 @@ impl super::BatchExecute for BatchRunAction {
} }
} }
impl Resolve<BatchRunAction, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchRunAction {
#[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchRunAction { pattern }: BatchRunAction, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchRunAction>(&pattern, &user).await super::batch_execute::<BatchRunAction>(&self.pattern, user)
.await?,
)
} }
} }
impl Resolve<RunAction, (User, Update)> for State { impl Resolve<ExecuteArgs> for RunAction {
#[instrument(name = "RunAction", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "RunAction", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
RunAction { action }: RunAction, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let mut action = resource::get_check_permissions::<Action>( let mut action = resource::get_check_permissions::<Action>(
&action, &self.action,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
@@ -83,17 +83,18 @@ impl Resolve<RunAction, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.running = true)?; action_state.update(|state| state.running = true)?;
let mut update = update.clone();
update_update(update.clone()).await?; update_update(update.clone()).await?;
let CreateApiKeyResponse { key, secret } = State let CreateApiKeyResponse { key, secret } = CreateApiKey {
.resolve( name: update.id.clone(),
CreateApiKey { expires: 0,
name: update.id.clone(), }
expires: 0, .resolve(&UserArgs {
}, user: action_user().to_owned(),
action_user().to_owned(), })
) .await?;
.await?;
let contents = &mut action.config.file_contents; let contents = &mut action.config.file_contents;
@@ -110,19 +111,31 @@ impl Resolve<RunAction, (User, Update)> for State {
let path = core_config().action_directory.join(&file); let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent).await; fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize Action file parent directory {parent:?}"))?;
} }
fs::write(&path, contents).await.with_context(|| { fs::write(&path, contents).await.with_context(|| {
format!("Failed to write action file to {path:?}") format!("Failed to write action file to {path:?}")
})?; })?;
let CoreConfig { ssl_enabled, .. } = core_config();
let https_cert_flag = if *ssl_enabled {
" --unsafely-ignore-certificate-errors=localhost"
} else {
""
};
let mut res = run_komodo_command( 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 // Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action", "Execute Action",
None, None,
format!("deno run --allow-all {}", path.display()), format!(
false, "deno run --allow-all{https_cert_flag} {}",
path.display()
),
) )
.await; .await;
@@ -133,12 +146,15 @@ impl Resolve<RunAction, (User, Update)> for State {
cleanup_run(file + ".js", &path).await; cleanup_run(file + ".js", &path).await;
if let Err(e) = State if let Err(e) = (DeleteApiKey { key })
.resolve(DeleteApiKey { key }, action_user().to_owned()) .resolve(&UserArgs {
user: action_user().to_owned(),
})
.await .await
{ {
warn!( warn!(
"Failed to delete API key after action execution | {e:#}" "Failed to delete API key after action execution | {:#}",
e.error
); );
}; };
@@ -171,7 +187,7 @@ async fn interpolate(
update: &mut Update, update: &mut Update,
key: String, key: String,
secret: String, secret: String,
) -> anyhow::Result<HashSet<(String, String)>> { ) -> serror::Result<HashSet<(String, String)>> {
let mut vars_and_secrets = get_variables_and_secrets().await?; let mut vars_and_secrets = get_variables_and_secrets().await?;
vars_and_secrets vars_and_secrets
@@ -301,8 +317,8 @@ fn delete_file(
if name == file { if name == file {
if let Err(e) = fs::remove_file(entry.path()).await { if let Err(e) = fs::remove_file(entry.path()).await {
warn!( warn!(
"Failed to clean up generated file after action execution | {e:#}" "Failed to clean up generated file after action execution | {e:#}"
); );
}; };
return true; return true;
} }

View File

@@ -0,0 +1,73 @@
use formatting::format_serror;
use komodo_client::{
api::execute::TestAlerter,
entities::{
alert::{Alert, AlertData, SeverityLevel},
alerter::Alerter,
komodo_timestamp,
permission::PermissionLevel,
},
};
use resolver_api::Resolve;
use crate::{
alert::send_alert_to_alerter, helpers::update::update_update,
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

@@ -1,6 +1,6 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration}; use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use formatting::format_serror; use formatting::format_serror;
use futures::future::join_all; use futures::future::join_all;
use komodo_client::{ use komodo_client::{
@@ -17,7 +17,7 @@ use komodo_client::{
komodo_timestamp, komodo_timestamp,
permission::PermissionLevel, permission::PermissionLevel,
update::{Log, Update}, update::{Log, Update},
user::{auto_redeploy_user, User}, user::auto_redeploy_user,
}, },
}; };
use mungos::{ use mungos::{
@@ -49,10 +49,10 @@ use crate::{
update::{init_execution_update, update_update}, update::{init_execution_update, update_update},
}, },
resource::{self, refresh_build_state_cache}, resource::{self, refresh_build_state_cache},
state::{action_states, db_client, State}, state::{action_states, db_client},
}; };
use super::ExecuteRequest; use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchRunBuild { impl super::BatchExecute for BatchRunBuild {
type Resource = Build; type Resource = Build;
@@ -61,34 +61,35 @@ impl super::BatchExecute for BatchRunBuild {
} }
} }
impl Resolve<BatchRunBuild, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchRunBuild {
#[instrument(name = "BatchRunBuild", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchRunBuild", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchRunBuild { pattern }: BatchRunBuild, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchRunBuild>(&pattern, &user).await super::batch_execute::<BatchRunBuild>(&self.pattern, user)
.await?,
)
} }
} }
impl Resolve<RunBuild, (User, Update)> for State { impl Resolve<ExecuteArgs> for RunBuild {
#[instrument(name = "RunBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "RunBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
RunBuild { build }: RunBuild, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let mut build = resource::get_check_permissions::<Build>( let mut build = resource::get_check_permissions::<Build>(
&build, &self.build,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
let mut vars_and_secrets = get_variables_and_secrets().await?; let mut vars_and_secrets = get_variables_and_secrets().await?;
if build.config.builder_id.is_empty() { 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). // get the action state for the build (or insert default).
@@ -103,6 +104,9 @@ impl Resolve<RunBuild, (User, Update)> for State {
if build.config.auto_increment_version { if build.config.auto_increment_version {
build.config.version.increment(); build.config.version.increment();
} }
let mut update = update.clone();
update.version = build.config.version; update.version = build.config.version;
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -408,7 +412,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
}); });
} }
Ok(update) Ok(update.clone())
} }
} }
@@ -418,7 +422,7 @@ async fn handle_early_return(
build_id: String, build_id: String,
build_name: String, build_name: String,
is_cancel: bool, is_cancel: bool,
) -> anyhow::Result<Update> { ) -> serror::Result<Update> {
update.finalize(); update.finalize();
// Need to manually update the update before cache refresh, // Need to manually update the update before cache refresh,
// and before broadcast with add_update. // and before broadcast with add_update.
@@ -456,7 +460,7 @@ async fn handle_early_return(
send_alerts(&[alert]).await send_alerts(&[alert]).await
}); });
} }
Ok(update) Ok(update.clone())
} }
pub async fn validate_cancel_build( pub async fn validate_cancel_build(
@@ -505,16 +509,15 @@ pub async fn validate_cancel_build(
Ok(()) Ok(())
} }
impl Resolve<CancelBuild, (User, Update)> for State { impl Resolve<ExecuteArgs> for CancelBuild {
#[instrument(name = "CancelBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "CancelBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
CancelBuild { build }: CancelBuild, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let build = resource::get_check_permissions::<Build>( let build = resource::get_check_permissions::<Build>(
&build, &self.build,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
@@ -527,9 +530,11 @@ impl Resolve<CancelBuild, (User, Update)> for State {
.and_then(|s| s.get().ok().map(|s| s.building)) .and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default() .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( update.push_simple_log(
"cancel triggered", "cancel triggered",
"the build cancel has been triggered", "the build cancel has been triggered",
@@ -555,7 +560,9 @@ impl Resolve<CancelBuild, (User, Update)> for State {
) )
.await .await
{ {
warn!("failed to set CancelBuild Update status Complete after timeout | {e:#}") warn!(
"failed to set CancelBuild Update status Complete after timeout | {e:#}"
)
} }
}); });
@@ -593,16 +600,13 @@ async fn handle_post_build_redeploy(build_id: &str) {
let user = auto_redeploy_user().to_owned(); let user = auto_redeploy_user().to_owned();
let res = async { let res = async {
let update = init_execution_update(&req, &user).await?; let update = init_execution_update(&req, &user).await?;
State Deploy {
.resolve( deployment: deployment.id.clone(),
Deploy { stop_signal: None,
deployment: deployment.id.clone(), stop_time: None,
stop_signal: None, }
stop_time: None, .resolve(&ExecuteArgs { user, update })
}, .await
(user, update),
)
.await
} }
.await; .await;
Some((deployment.id.clone(), res)) Some((deployment.id.clone(), res))
@@ -616,7 +620,10 @@ async fn handle_post_build_redeploy(build_id: &str) {
continue; continue;
}; };
if let Err(e) = res { if let Err(e) = res {
warn!("failed post build redeploy for deployment {id}: {e:#}"); warn!(
"failed post build redeploy for deployment {id}: {:#}",
e.error
);
} }
} }
} }
@@ -636,14 +643,17 @@ async fn validate_account_extract_registry_token(
}, },
.. ..
}: &Build, }: &Build,
) -> anyhow::Result<Option<String>> { ) -> serror::Result<Option<String>> {
if domain.is_empty() { if domain.is_empty() {
return Ok(None); return Ok(None);
} }
if account.is_empty() { if account.is_empty() {
return Err(anyhow!( return Err(
"Must attach account to use registry provider {domain}" anyhow!(
)); "Must attach account to use registry provider {domain}"
)
.into(),
);
} }
let registry_token = registry_token(domain, account).await.with_context( let registry_token = registry_token(domain, account).await.with_context(

View File

@@ -1,21 +1,21 @@
use std::{collections::HashSet, sync::OnceLock}; use std::{collections::HashSet, sync::OnceLock};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use cache::TimeoutCache; use cache::TimeoutCache;
use formatting::format_serror; use formatting::format_serror;
use komodo_client::{ use komodo_client::{
api::execute::*, api::execute::*,
entities::{ entities::{
Version,
build::{Build, ImageRegistryConfig}, build::{Build, ImageRegistryConfig},
deployment::{ deployment::{
extract_registry_domain, Deployment, DeploymentImage, Deployment, DeploymentImage, extract_registry_domain,
}, },
get_image_name, komodo_timestamp, optional_string, get_image_name, komodo_timestamp, optional_string,
permission::PermissionLevel, permission::PermissionLevel,
server::Server, server::Server,
update::{Log, Update}, update::{Log, Update},
user::User, user::User,
Version,
}, },
}; };
use periphery_client::api; use periphery_client::api;
@@ -35,10 +35,10 @@ use crate::{
}, },
monitor::update_cache_for_server, monitor::update_cache_for_server,
resource, resource,
state::{action_states, State}, state::action_states,
}; };
use super::ExecuteRequest; use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchDeploy { impl super::BatchExecute for BatchDeploy {
type Resource = Deployment; type Resource = Deployment;
@@ -51,14 +51,16 @@ impl super::BatchExecute for BatchDeploy {
} }
} }
impl Resolve<BatchDeploy, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchDeploy {
#[instrument(name = "BatchDeploy", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchDeploy", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchDeploy { pattern }: BatchDeploy, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchDeploy>(&pattern, &user).await super::batch_execute::<BatchDeploy>(&self.pattern, user)
.await?,
)
} }
} }
@@ -87,19 +89,14 @@ async fn setup_deployment_execution(
Ok((deployment, server)) Ok((deployment, server))
} }
impl Resolve<Deploy, (User, Update)> for State { impl Resolve<ExecuteArgs> for Deploy {
#[instrument(name = "Deploy", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "Deploy", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
Deploy { ExecuteArgs { user, update }: &ExecuteArgs,
deployment, ) -> serror::Result<Update> {
stop_signal,
stop_time,
}: Deploy,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let (mut deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -112,6 +109,8 @@ impl Resolve<Deploy, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.deploying = true)?; action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -237,8 +236,8 @@ impl Resolve<Deploy, (User, Update)> for State {
match periphery_client(&server)? match periphery_client(&server)?
.request(api::container::Deploy { .request(api::container::Deploy {
deployment, deployment,
stop_signal, stop_signal: self.stop_signal,
stop_time, stop_time: self.stop_time,
registry_token, registry_token,
replacers: secret_replacers.into_iter().collect(), replacers: secret_replacers.into_iter().collect(),
}) })
@@ -378,14 +377,14 @@ pub async fn pull_deployment_inner(
res res
} }
impl Resolve<PullDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for PullDeployment {
#[instrument(name = "PullDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
PullDeployment { deployment }: PullDeployment, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let (deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -398,6 +397,7 @@ impl Resolve<PullDeployment, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.pulling = true)?; action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -411,15 +411,14 @@ impl Resolve<PullDeployment, (User, Update)> for State {
} }
} }
impl Resolve<StartDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for StartDeployment {
#[instrument(name = "StartDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "StartDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
StartDeployment { deployment }: StartDeployment, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let (deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -432,6 +431,8 @@ impl Resolve<StartDeployment, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.starting = true)?; action_state.update(|state| state.starting = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -457,15 +458,14 @@ impl Resolve<StartDeployment, (User, Update)> for State {
} }
} }
impl Resolve<RestartDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for RestartDeployment {
#[instrument(name = "RestartDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "RestartDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
RestartDeployment { deployment }: RestartDeployment, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let (deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -478,6 +478,8 @@ impl Resolve<RestartDeployment, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.restarting = true)?; action_state.update(|state| state.restarting = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -505,15 +507,14 @@ impl Resolve<RestartDeployment, (User, Update)> for State {
} }
} }
impl Resolve<PauseDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for PauseDeployment {
#[instrument(name = "PauseDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "PauseDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
PauseDeployment { deployment }: PauseDeployment, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let (deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -526,6 +527,8 @@ impl Resolve<PauseDeployment, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.pausing = true)?; action_state.update(|state| state.pausing = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -551,15 +554,14 @@ impl Resolve<PauseDeployment, (User, Update)> for State {
} }
} }
impl Resolve<UnpauseDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for UnpauseDeployment {
#[instrument(name = "UnpauseDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "UnpauseDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
UnpauseDeployment { deployment }: UnpauseDeployment, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let (deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -572,6 +574,8 @@ impl Resolve<UnpauseDeployment, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.unpausing = true)?; action_state.update(|state| state.unpausing = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -599,19 +603,14 @@ impl Resolve<UnpauseDeployment, (User, Update)> for State {
} }
} }
impl Resolve<StopDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for StopDeployment {
#[instrument(name = "StopDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "StopDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
StopDeployment { ExecuteArgs { user, update }: &ExecuteArgs,
deployment, ) -> serror::Result<Update> {
signal,
time,
}: StopDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let (deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -624,16 +623,20 @@ impl Resolve<StopDeployment, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.stopping = true)?; action_state.update(|state| state.stopping = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
let log = match periphery_client(&server)? let log = match periphery_client(&server)?
.request(api::container::StopContainer { .request(api::container::StopContainer {
name: deployment.name, name: deployment.name,
signal: signal signal: self
.signal
.unwrap_or(deployment.config.termination_signal) .unwrap_or(deployment.config.termination_signal)
.into(), .into(),
time: time time: self
.time
.unwrap_or(deployment.config.termination_timeout) .unwrap_or(deployment.config.termination_timeout)
.into(), .into(),
}) })
@@ -666,31 +669,30 @@ impl super::BatchExecute for BatchDestroyDeployment {
} }
} }
impl Resolve<BatchDestroyDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
#[instrument(name = "BatchDestroyDeployment", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchDestroyDeployment", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchDestroyDeployment { pattern }: BatchDestroyDeployment, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchDestroyDeployment>(&pattern, &user) super::batch_execute::<BatchDestroyDeployment>(
.await &self.pattern,
user,
)
.await?,
)
} }
} }
impl Resolve<DestroyDeployment, (User, Update)> for State { impl Resolve<ExecuteArgs> for DestroyDeployment {
#[instrument(name = "DestroyDeployment", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "DestroyDeployment", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
DestroyDeployment { ExecuteArgs { user, update }: &ExecuteArgs,
deployment, ) -> serror::Result<Update> {
signal,
time,
}: DestroyDeployment,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let (deployment, server) = 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). // get the action state for the deployment (or insert default).
let action_state = action_states() let action_state = action_states()
@@ -703,16 +705,20 @@ impl Resolve<DestroyDeployment, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.destroying = true)?; action_state.update(|state| state.destroying = true)?;
let mut update = update.clone();
// Send update after setting action state, this way frontend gets correct state. // Send update after setting action state, this way frontend gets correct state.
update_update(update.clone()).await?; update_update(update.clone()).await?;
let log = match periphery_client(&server)? let log = match periphery_client(&server)?
.request(api::container::RemoveContainer { .request(api::container::RemoveContainer {
name: deployment.name, name: deployment.name,
signal: signal signal: self
.signal
.unwrap_or(deployment.config.termination_signal) .unwrap_or(deployment.config.termination_signal)
.into(), .into(),
time: time time: self
.time
.unwrap_or(deployment.config.termination_timeout) .unwrap_or(deployment.config.termination_timeout)
.into(), .into(),
}) })

View File

@@ -1,21 +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::{Extension, Router, middleware, routing::post};
use axum_extra::{headers::ContentType, TypedHeader}; use axum_extra::{TypedHeader, headers::ContentType};
use derive_variants::{EnumVariants, ExtractVariant}; use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror; use formatting::format_serror;
use futures::future::join_all; use futures::future::join_all;
use komodo_client::{ use komodo_client::{
api::execute::*, api::execute::*,
entities::{ entities::{
Operation,
update::{Log, Update}, update::{Log, Update},
user::User, user::User,
Operation,
}, },
}; };
use mungos::by_id::find_one_by_id; use mungos::by_id::find_one_by_id;
use resolver_api::{derive::Resolver, Resolver}; use resolver_api::Resolve;
use response::JsonString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serror::Json; use serror::Json;
use typeshare::typeshare; use typeshare::typeshare;
@@ -24,11 +25,12 @@ use uuid::Uuid;
use crate::{ use crate::{
auth::auth_request, auth::auth_request,
helpers::update::{init_execution_update, update_update}, helpers::update::{init_execution_update, update_update},
resource::{list_full_for_user_using_pattern, KomodoResource}, resource::{KomodoResource, list_full_for_user_using_pattern},
state::{db_client, State}, state::db_client,
}; };
mod action; mod action;
mod alerter;
mod build; mod build;
mod deployment; mod deployment;
mod procedure; mod procedure;
@@ -42,13 +44,19 @@ pub use {
deployment::pull_deployment_inner, stack::pull_stack_inner, deployment::pull_deployment_inner, stack::pull_stack_inner,
}; };
pub struct ExecuteArgs {
pub user: User,
pub update: Update,
}
#[typeshare] #[typeshare]
#[derive( #[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants, Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)] )]
#[variant_derive(Debug)] #[variant_derive(Debug)]
#[resolver_target(State)] #[args(ExecuteArgs)]
#[resolver_args((User, Update))] #[response(JsonString)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")] #[serde(tag = "type", content = "params")]
pub enum ExecuteRequest { pub enum ExecuteRequest {
// ==== SERVER ==== // ==== SERVER ====
@@ -125,6 +133,9 @@ pub enum ExecuteRequest {
// ==== SERVER TEMPLATE ==== // ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer), LaunchServer(LaunchServer),
// ==== ALERTER ====
TestAlerter(TestAlerter),
// ==== SYNC ==== // ==== SYNC ====
RunSync(RunSync), RunSync(RunSync),
} }
@@ -153,64 +164,73 @@ pub enum ExecutionResult {
Batch(String), Batch(String),
} }
pub async fn inner_handler( pub fn inner_handler(
request: ExecuteRequest, request: ExecuteRequest,
user: User, user: User,
) -> anyhow::Result<ExecutionResult> { ) -> Pin<
let req_id = Uuid::new_v4(); Box<
dyn std::future::Future<Output = anyhow::Result<ExecutionResult>>
+ Send,
>,
> {
Box::pin(async move {
let req_id = Uuid::new_v4();
// need to validate no cancel is active before any update is created. // need to validate no cancel is active before any update is created.
build::validate_cancel_build(&request).await?; build::validate_cancel_build(&request).await?;
let update = init_execution_update(&request, &user).await?; let update = init_execution_update(&request, &user).await?;
// This will be the case for the Batch exections, // This will be the case for the Batch exections,
// they don't have their own updates. // they don't have their own updates.
// The batch calls also call "inner_handler" themselves, // The batch calls also call "inner_handler" themselves,
// and in their case will spawn tasks, so that isn't necessary // and in their case will spawn tasks, so that isn't necessary
// here either. // here either.
if update.operation == Operation::None { if update.operation == Operation::None {
return Ok(ExecutionResult::Batch( return Ok(ExecutionResult::Batch(
task(req_id, request, user, update).await?, task(req_id, request, user, update).await?,
)); ));
}
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
tokio::spawn({
let update_id = update.id.clone();
async move {
let log = match handle.await {
Ok(Err(e)) => {
warn!("/execute request {req_id} task error: {e:#}",);
Log::error("task error", format_serror(&e.into()))
}
Err(e) => {
warn!("/execute request {req_id} spawn error: {e:?}",);
Log::error("spawn error", format!("{e:#?}"))
}
_ => return,
};
let res = async {
let mut update =
find_one_by_id(&db_client().updates, &update_id)
.await
.context("failed to query to db")?
.context("no update exists with given id")?;
update.logs.push(log);
update.finalize();
update_update(update).await
}
.await;
if let Err(e) = res {
warn!("failed to update update with task error log | {e:#}");
}
} }
});
Ok(ExecutionResult::Single(update)) 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( #[instrument(
@@ -231,15 +251,14 @@ async fn task(
info!("/execute request {req_id} | user: {}", user.username); info!("/execute request {req_id} | user: {}", user.username);
let timer = Instant::now(); let timer = Instant::now();
let res = State let res = match request.resolve(&ExecuteArgs { user, update }).await
.resolve_request(request, (user, update)) {
.await Err(e) => Err(e.error),
.map_err(|e| match e { Ok(JsonString::Err(e)) => Err(
resolver_api::Error::Serialization(e) => { anyhow::Error::from(e).context("failed to serialize response"),
anyhow!("{e:?}").context("response serialization error") ),
} Ok(JsonString::Ok(res)) => Ok(res),
resolver_api::Error::Inner(e) => e, };
});
if let Err(e) = &res { if let Err(e) = &res {
warn!("/execute request {req_id} error: {e:#}"); warn!("/execute request {req_id} error: {e:#}");

View File

@@ -1,6 +1,6 @@
use std::pin::Pin; use std::pin::Pin;
use formatting::{bold, colored, format_serror, muted, Color}; use formatting::{Color, bold, colored, format_serror, muted};
use komodo_client::{ use komodo_client::{
api::execute::{ api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure, BatchExecutionResponse, BatchRunProcedure, RunProcedure,
@@ -17,10 +17,10 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
helpers::{procedure::execute_procedure, update::update_update}, helpers::{procedure::execute_procedure, update::update_update},
resource::{self, refresh_procedure_state_cache}, resource::{self, refresh_procedure_state_cache},
state::{action_states, db_client, State}, state::{action_states, db_client},
}; };
use super::ExecuteRequest; use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchRunProcedure { impl super::BatchExecute for BatchRunProcedure {
type Resource = Procedure; type Resource = Procedure;
@@ -29,25 +29,29 @@ impl super::BatchExecute for BatchRunProcedure {
} }
} }
impl Resolve<BatchRunProcedure, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchRunProcedure {
#[instrument(name = "BatchRunProcedure", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchRunProcedure", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchRunProcedure { pattern }: BatchRunProcedure, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchRunProcedure>(&pattern, &user).await super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
.await?,
)
} }
} }
impl Resolve<RunProcedure, (User, Update)> for State { impl Resolve<ExecuteArgs> for RunProcedure {
#[instrument(name = "RunProcedure", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "RunProcedure", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
RunProcedure { procedure }: RunProcedure, ExecuteArgs { user, update }: &ExecuteArgs,
(user, update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> { Ok(
resolve_inner(procedure, user, update).await resolve_inner(self.procedure, user.clone(), update.clone())
.await?,
)
} }
} }

View File

@@ -1,6 +1,6 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration}; use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use formatting::format_serror; use formatting::format_serror;
use komodo_client::{ use komodo_client::{
api::{execute::*, write::RefreshRepoCache}, api::{execute::*, write::RefreshRepoCache},
@@ -12,7 +12,6 @@ use komodo_client::{
repo::Repo, repo::Repo,
server::Server, server::Server,
update::{Log, Update}, update::{Log, Update},
user::User,
}, },
}; };
use mungos::{ use mungos::{
@@ -28,6 +27,7 @@ use tokio_util::sync::CancellationToken;
use crate::{ use crate::{
alert::send_alerts, alert::send_alerts,
api::write::WriteArgs,
helpers::{ helpers::{
builder::{cleanup_builder_instance, get_builder_periphery}, builder::{cleanup_builder_instance, get_builder_periphery},
channel::repo_cancel_channel, channel::repo_cancel_channel,
@@ -42,10 +42,10 @@ use crate::{
update::update_update, update::update_update,
}, },
resource::{self, refresh_repo_state_cache}, resource::{self, refresh_repo_state_cache},
state::{action_states, db_client, State}, state::{action_states, db_client},
}; };
use super::ExecuteRequest; use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchCloneRepo { impl super::BatchExecute for BatchCloneRepo {
type Resource = Repo; type Resource = Repo;
@@ -54,27 +54,28 @@ impl super::BatchExecute for BatchCloneRepo {
} }
} }
impl Resolve<BatchCloneRepo, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchCloneRepo {
#[instrument(name = "BatchCloneRepo", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchCloneRepo", skip( user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchCloneRepo { pattern }: BatchCloneRepo, ExecuteArgs { user, update }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchCloneRepo>(&pattern, &user).await super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
.await?,
)
} }
} }
impl Resolve<CloneRepo, (User, Update)> for State { impl Resolve<ExecuteArgs> for CloneRepo {
#[instrument(name = "CloneRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "CloneRepo", skip( user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
CloneRepo { repo }: CloneRepo, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>( let mut repo = resource::get_check_permissions::<Repo>(
&repo, &self.repo,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
@@ -88,10 +89,11 @@ impl Resolve<CloneRepo, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.cloning = true)?; action_state.update(|state| state.cloning = true)?;
let mut update = update.clone();
update_update(update.clone()).await?; update_update(update.clone()).await?;
if repo.config.server_id.is_empty() { 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( let git_token = git_token(
@@ -141,9 +143,10 @@ impl Resolve<CloneRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await; update_last_pulled_time(&repo.name).await;
} }
if let Err(e) = State if let Err(e) = (RefreshRepoCache { repo: repo.id })
.resolve(RefreshRepoCache { repo: repo.id }, user) .resolve(&WriteArgs { user: user.clone() })
.await .await
.map_err(|e| e.error)
.context("Failed to refresh repo cache") .context("Failed to refresh repo cache")
{ {
update.push_error_log( update.push_error_log(
@@ -163,27 +166,28 @@ impl super::BatchExecute for BatchPullRepo {
} }
} }
impl Resolve<BatchPullRepo, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchPullRepo {
#[instrument(name = "BatchPullRepo", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchPullRepo", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchPullRepo { pattern }: BatchPullRepo, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchPullRepo>(&pattern, &user).await super::batch_execute::<BatchPullRepo>(&self.pattern, user)
.await?,
)
} }
} }
impl Resolve<PullRepo, (User, Update)> for State { impl Resolve<ExecuteArgs> for PullRepo {
#[instrument(name = "PullRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "PullRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
PullRepo { repo }: PullRepo, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>( let mut repo = resource::get_check_permissions::<Repo>(
&repo, &self.repo,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
@@ -197,10 +201,12 @@ impl Resolve<PullRepo, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.pulling = true)?; action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
update_update(update.clone()).await?; update_update(update.clone()).await?;
if repo.config.server_id.is_empty() { 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( let git_token = git_token(
@@ -254,9 +260,10 @@ impl Resolve<PullRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await; update_last_pulled_time(&repo.name).await;
} }
if let Err(e) = State if let Err(e) = (RefreshRepoCache { repo: repo.id })
.resolve(RefreshRepoCache { repo: repo.id }, user) .resolve(&WriteArgs { user: user.clone() })
.await .await
.map_err(|e| e.error)
.context("Failed to refresh repo cache") .context("Failed to refresh repo cache")
{ {
update.push_error_log( update.push_error_log(
@@ -272,7 +279,7 @@ impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(skip_all, fields(update_id = update.id))] #[instrument(skip_all, fields(update_id = update.id))]
async fn handle_server_update_return( async fn handle_server_update_return(
update: Update, update: Update,
) -> anyhow::Result<Update> { ) -> serror::Result<Update> {
// Need to manually update the update before cache refresh, // Need to manually update the update before cache refresh,
// and before broadcast with add_update. // and before broadcast with add_update.
// The Err case of to_document should be unreachable, // The Err case of to_document should be unreachable,
@@ -314,33 +321,34 @@ impl super::BatchExecute for BatchBuildRepo {
} }
} }
impl Resolve<BatchBuildRepo, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchBuildRepo {
#[instrument(name = "BatchBuildRepo", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchBuildRepo", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchBuildRepo { pattern }: BatchBuildRepo, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchBuildRepo>(&pattern, &user).await super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
.await?,
)
} }
} }
impl Resolve<BuildRepo, (User, Update)> for State { impl Resolve<ExecuteArgs> for BuildRepo {
#[instrument(name = "BuildRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "BuildRepo", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
BuildRepo { repo }: BuildRepo, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>( let mut repo = resource::get_check_permissions::<Repo>(
&repo, &self.repo,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
if repo.config.builder_id.is_empty() { 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). // get the action state for the repo (or insert default).
@@ -352,6 +360,7 @@ impl Resolve<BuildRepo, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.building = true)?; action_state.update(|state| state.building = true)?;
let mut update = update.clone();
update_update(update.clone()).await?; update_update(update.clone()).await?;
let git_token = git_token( let git_token = git_token(
@@ -547,7 +556,7 @@ async fn handle_builder_early_return(
repo_id: String, repo_id: String,
repo_name: String, repo_name: String,
is_cancel: bool, is_cancel: bool,
) -> anyhow::Result<Update> { ) -> serror::Result<Update> {
update.finalize(); update.finalize();
// Need to manually update the update before cache refresh, // Need to manually update the update before cache refresh,
// and before broadcast with add_update. // and before broadcast with add_update.
@@ -635,16 +644,15 @@ pub async fn validate_cancel_repo_build(
Ok(()) Ok(())
} }
impl Resolve<CancelRepoBuild, (User, Update)> for State { impl Resolve<ExecuteArgs> for CancelRepoBuild {
#[instrument(name = "CancelRepoBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "CancelRepoBuild", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
CancelRepoBuild { repo }: CancelRepoBuild, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let repo = resource::get_check_permissions::<Repo>( let repo = resource::get_check_permissions::<Repo>(
&repo, &self.repo,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
@@ -657,9 +665,11 @@ impl Resolve<CancelRepoBuild, (User, Update)> for State {
.and_then(|s| s.get().ok().map(|s| s.building)) .and_then(|s| s.get().ok().map(|s| s.building))
.unwrap_or_default() .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( update.push_simple_log(
"cancel triggered", "cancel triggered",
"the repo build cancel has been triggered", "the repo build cancel has been triggered",
@@ -685,7 +695,9 @@ impl Resolve<CancelRepoBuild, (User, Update)> for State {
) )
.await .await
{ {
warn!("failed to set CancelRepoBuild Update status Complete after timeout | {e:#}") warn!(
"failed to set CancelRepoBuild Update status Complete after timeout | {e:#}"
)
} }
}); });

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ use komodo_client::{
server::Server, server::Server,
stack::{Stack, StackInfo}, stack::{Stack, StackInfo},
update::{Log, Update}, update::{Log, Update},
user::User,
}, },
}; };
use mungos::mongodb::bson::{doc, to_document}; use mungos::mongodb::bson::{doc, to_document};
@@ -17,6 +16,7 @@ use periphery_client::api::compose::*;
use resolver_api::Resolve; use resolver_api::Resolve;
use crate::{ use crate::{
api::write::WriteArgs,
helpers::{ helpers::{
interpolate::{ interpolate::{
add_interp_update_log, add_interp_update_log,
@@ -31,47 +31,44 @@ use crate::{
monitor::update_cache_for_server, monitor::update_cache_for_server,
resource, resource,
stack::{execute::execute_compose, get_stack_and_server}, stack::{execute::execute_compose, get_stack_and_server},
state::{action_states, db_client, State}, state::{action_states, db_client},
}; };
use super::ExecuteRequest; use super::{ExecuteArgs, ExecuteRequest};
impl super::BatchExecute for BatchDeployStack { impl super::BatchExecute for BatchDeployStack {
type Resource = Stack; type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest { fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack { ExecuteRequest::DeployStack(DeployStack {
stack, stack,
service: None, services: Vec::new(),
stop_time: None, stop_time: None,
}) })
} }
} }
impl Resolve<BatchDeployStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchDeployStack {
#[instrument(name = "BatchDeployStack", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchDeployStack", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchDeployStack { pattern }: BatchDeployStack, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchDeployStack>(&pattern, &user).await super::batch_execute::<BatchDeployStack>(&self.pattern, user)
.await?,
)
} }
} }
impl Resolve<DeployStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for DeployStack {
#[instrument(name = "DeployStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "DeployStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
DeployStack { ExecuteArgs { user, update }: &ExecuteArgs,
stack, ) -> serror::Result<Update> {
service,
stop_time,
}: DeployStack,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let (mut stack, server) = get_stack_and_server( let (mut stack, server) = get_stack_and_server(
&stack, &self.stack,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
true, true,
) )
@@ -86,12 +83,17 @@ impl Resolve<DeployStack, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.deploying = true)?; action_state.update(|state| state.deploying = true)?;
let mut update = update.clone();
update_update(update.clone()).await?; update_update(update.clone()).await?;
if let Some(service) = &service { if !self.services.is_empty() {
update.logs.push(Log::simple( update.logs.push(Log::simple(
&format!("Service: {service}"), "Service/s",
format!("Execution requested for Stack service {service}"), format!(
"Execution requested for Stack service/s {}",
self.services.join(", ")
),
)) ))
} }
@@ -153,6 +155,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
&mut secret_replacers, &mut secret_replacers,
)?; )?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut stack.config.post_deploy,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log( add_interp_update_log(
&mut update, &mut update,
&global_replacers, &global_replacers,
@@ -171,12 +180,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
file_contents, file_contents,
missing_files, missing_files,
remote_errors, remote_errors,
compose_config,
commit_hash, commit_hash,
commit_message, commit_message,
} = periphery_client(&server)? } = periphery_client(&server)?
.request(ComposeUp { .request(ComposeUp {
stack: stack.clone(), stack: stack.clone(),
service, services: self.services,
git_token, git_token,
registry_token, registry_token,
replacers: secret_replacers.into_iter().collect(), replacers: secret_replacers.into_iter().collect(),
@@ -200,12 +210,14 @@ impl Resolve<DeployStack, (User, Update)> for State {
let ( let (
deployed_services, deployed_services,
deployed_contents, deployed_contents,
deployed_config,
deployed_hash, deployed_hash,
deployed_message, deployed_message,
) = if deployed { ) = if deployed {
( (
Some(latest_services.clone()), Some(latest_services.clone()),
Some(file_contents.clone()), Some(file_contents.clone()),
compose_config,
commit_hash.clone(), commit_hash.clone(),
commit_message.clone(), commit_message.clone(),
) )
@@ -213,6 +225,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
( (
stack.info.deployed_services, stack.info.deployed_services,
stack.info.deployed_contents, stack.info.deployed_contents,
stack.info.deployed_config,
stack.info.deployed_hash, stack.info.deployed_hash,
stack.info.deployed_message, stack.info.deployed_message,
) )
@@ -223,6 +236,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
deployed_project_name: project_name.into(), deployed_project_name: project_name.into(),
deployed_services, deployed_services,
deployed_contents, deployed_contents,
deployed_config,
deployed_hash, deployed_hash,
deployed_message, deployed_message,
latest_services, latest_services,
@@ -284,39 +298,39 @@ impl super::BatchExecute for BatchDeployStackIfChanged {
} }
} }
impl Resolve<BatchDeployStackIfChanged, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
#[instrument(name = "BatchDeployStackIfChanged", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchDeployStackIfChanged", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchDeployStackIfChanged { pattern }: BatchDeployStackIfChanged, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { Ok(
super::batch_execute::<BatchDeployStackIfChanged>(&pattern, &user) super::batch_execute::<BatchDeployStackIfChanged>(
.await &self.pattern,
user,
)
.await?,
)
} }
} }
impl Resolve<DeployStackIfChanged, (User, Update)> for State { impl Resolve<ExecuteArgs> for DeployStackIfChanged {
#[instrument(name = "DeployStackIfChanged", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "DeployStackIfChanged", skip(user, update), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
DeployStackIfChanged { stack, stop_time }: DeployStackIfChanged, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let stack = resource::get_check_permissions::<Stack>( let stack = resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
State RefreshStackCache {
.resolve( stack: stack.id.clone(),
RefreshStackCache { }
stack: stack.id.clone(), .resolve(&WriteArgs { user: user.clone() })
}, .await?;
user.clone(),
)
.await?;
let stack = resource::get::<Stack>(&stack.id).await?; let stack = resource::get::<Stack>(&stack.id).await?;
let changed = match ( let changed = match (
&stack.info.deployed_contents, &stack.info.deployed_contents,
@@ -343,6 +357,8 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
_ => false, _ => false,
}; };
let mut update = update.clone();
if !changed { if !changed {
update.push_simple_log( update.push_simple_log(
"Diff compose files", "Diff compose files",
@@ -356,30 +372,35 @@ impl Resolve<DeployStackIfChanged, (User, Update)> for State {
// This is usually done in crate::helpers::update::init_execution_update. // This is usually done in crate::helpers::update::init_execution_update.
update.id = add_update_without_send(&update).await?; update.id = add_update_without_send(&update).await?;
State DeployStack {
.resolve( stack: stack.name,
DeployStack { services: Vec::new(),
stack: stack.name, stop_time: self.stop_time,
service: None, }
stop_time, .resolve(&ExecuteArgs {
}, user: user.clone(),
(user, update), update,
) })
.await .await
} }
} }
pub async fn pull_stack_inner( pub async fn pull_stack_inner(
mut stack: Stack, mut stack: Stack,
service: Option<String>, services: Vec<String>,
server: &Server, server: &Server,
update: Option<&mut Update>, mut update: Option<&mut Update>,
) -> anyhow::Result<ComposePullResponse> { ) -> anyhow::Result<ComposePullResponse> {
if let (Some(service), Some(update)) = (&service, update) { if let Some(update) = update.as_mut() {
update.logs.push(Log::simple( if !services.is_empty() {
&format!("Service: {service}"), update.logs.push(Log::simple(
format!("Execution requested for Stack service {service}"), "Service/s",
)) format!(
"Execution requested for Stack service/s {}",
services.join(", ")
),
))
}
} }
let git_token = crate::helpers::git_token( let git_token = crate::helpers::git_token(
@@ -397,10 +418,40 @@ pub async fn pull_stack_inner(
|| format!("Failed to get registry token in call to db. Stopping run. | {} | {}", stack.config.registry_provider, stack.config.registry_account), || format!("Failed to get registry token in call to db. Stopping run. | {} | {}", stack.config.registry_provider, stack.config.registry_account),
)?; )?;
// interpolate variables / secrets
if !stack.config.skip_secret_interp {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.file_contents,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.environment,
&mut global_replacers,
&mut secret_replacers,
)?;
if let Some(update) = update {
add_interp_update_log(
update,
&global_replacers,
&secret_replacers,
);
}
};
let res = periphery_client(server)? let res = periphery_client(server)?
.request(ComposePull { .request(ComposePull {
stack, stack,
service, services,
git_token, git_token,
registry_token, registry_token,
}) })
@@ -412,16 +463,15 @@ pub async fn pull_stack_inner(
Ok(res) Ok(res)
} }
impl Resolve<PullStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for PullStack {
#[instrument(name = "PullStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "PullStack", skip(user, update), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
PullStack { stack, service }: PullStack, ExecuteArgs { user, update }: &ExecuteArgs,
(user, mut update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let (stack, server) = get_stack_and_server( let (stack, server) = get_stack_and_server(
&stack, &self.stack,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
true, true,
) )
@@ -436,11 +486,16 @@ impl Resolve<PullStack, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.pulling = true)?; action_state.update(|state| state.pulling = true)?;
let mut update = update.clone();
update_update(update.clone()).await?; update_update(update.clone()).await?;
let res = let res = pull_stack_inner(
pull_stack_inner(stack, service, &server, Some(&mut update)) stack,
.await?; self.services,
&server,
Some(&mut update),
)
.await?;
update.logs.extend(res.logs); update.logs.extend(res.logs);
update.finalize(); update.finalize();
@@ -450,104 +505,100 @@ impl Resolve<PullStack, (User, Update)> for State {
} }
} }
impl Resolve<StartStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for StartStack {
#[instrument(name = "StartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "StartStack", skip(user, update), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
StartStack { stack, service }: StartStack, ExecuteArgs { user, update }: &ExecuteArgs,
(user, update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
execute_compose::<StartStack>( execute_compose::<StartStack>(
&stack, &self.stack,
service, self.services,
&user, user,
|state| state.starting = true, |state| state.starting = true,
update, update.clone(),
(), (),
) )
.await .await
.map_err(Into::into)
} }
} }
impl Resolve<RestartStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for RestartStack {
#[instrument(name = "RestartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "RestartStack", skip(user, update), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
RestartStack { stack, service }: RestartStack, ExecuteArgs { user, update }: &ExecuteArgs,
(user, update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
execute_compose::<RestartStack>( execute_compose::<RestartStack>(
&stack, &self.stack,
service, self.services,
&user, user,
|state| { |state| {
state.restarting = true; state.restarting = true;
}, },
update, update.clone(),
(), (),
) )
.await .await
.map_err(Into::into)
} }
} }
impl Resolve<PauseStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for PauseStack {
#[instrument(name = "PauseStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "PauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
PauseStack { stack, service }: PauseStack, ExecuteArgs { user, update }: &ExecuteArgs,
(user, update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
execute_compose::<PauseStack>( execute_compose::<PauseStack>(
&stack, &self.stack,
service, self.services,
&user, user,
|state| state.pausing = true, |state| state.pausing = true,
update, update.clone(),
(), (),
) )
.await .await
.map_err(Into::into)
} }
} }
impl Resolve<UnpauseStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for UnpauseStack {
#[instrument(name = "UnpauseStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "UnpauseStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
UnpauseStack { stack, service }: UnpauseStack, ExecuteArgs { user, update }: &ExecuteArgs,
(user, update): (User, Update), ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
execute_compose::<UnpauseStack>( execute_compose::<UnpauseStack>(
&stack, &self.stack,
service, self.services,
&user, user,
|state| state.unpausing = true, |state| state.unpausing = true,
update, update.clone(),
(), (),
) )
.await .await
.map_err(Into::into)
} }
} }
impl Resolve<StopStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for StopStack {
#[instrument(name = "StopStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "StopStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
StopStack { ExecuteArgs { user, update }: &ExecuteArgs,
stack, ) -> serror::Result<Update> {
stop_time,
service,
}: StopStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
execute_compose::<StopStack>( execute_compose::<StopStack>(
&stack, &self.stack,
service, self.services,
&user, user,
|state| state.stopping = true, |state| state.stopping = true,
update, update.clone(),
stop_time, self.stop_time,
) )
.await .await
.map_err(Into::into)
} }
} }
@@ -556,44 +607,40 @@ impl super::BatchExecute for BatchDestroyStack {
fn single_request(stack: String) -> ExecuteRequest { fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack { ExecuteRequest::DestroyStack(DestroyStack {
stack, stack,
service: None, services: Vec::new(),
remove_orphans: false, remove_orphans: false,
stop_time: None, stop_time: None,
}) })
} }
} }
impl Resolve<BatchDestroyStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for BatchDestroyStack {
#[instrument(name = "BatchDestroyStack", skip(self, user), fields(user_id = user.id))] #[instrument(name = "BatchDestroyStack", skip(user), fields(user_id = user.id))]
async fn resolve( async fn resolve(
&self, self,
BatchDestroyStack { pattern }: BatchDestroyStack, ExecuteArgs { user, .. }: &ExecuteArgs,
(user, _): (User, Update), ) -> serror::Result<BatchExecutionResponse> {
) -> anyhow::Result<BatchExecutionResponse> { super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
super::batch_execute::<BatchDestroyStack>(&pattern, &user).await .await
.map_err(Into::into)
} }
} }
impl Resolve<DestroyStack, (User, Update)> for State { impl Resolve<ExecuteArgs> for DestroyStack {
#[instrument(name = "DestroyStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] #[instrument(name = "DestroyStack", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
DestroyStack { ExecuteArgs { user, update }: &ExecuteArgs,
stack, ) -> serror::Result<Update> {
service,
remove_orphans,
stop_time,
}: DestroyStack,
(user, update): (User, Update),
) -> anyhow::Result<Update> {
execute_compose::<DestroyStack>( execute_compose::<DestroyStack>(
&stack, &self.stack,
service, self.services,
&user, user,
|state| state.destroying = true, |state| state.destroying = true,
update, update.clone(),
(stop_time, remove_orphans), (self.stop_time, self.remove_orphans),
) )
.await .await
.map_err(Into::into)
} }
} }

View File

@@ -1,11 +1,11 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use formatting::{colored, format_serror, Color}; use formatting::{Color, colored, format_serror};
use komodo_client::{ use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending}, api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{ entities::{
self, self, ResourceTargetVariant,
action::Action, action::Action,
alerter::Alerter, alerter::Alerter,
build::Build, build::Build,
@@ -20,45 +20,44 @@ use komodo_client::{
stack::Stack, stack::Stack,
sync::ResourceSync, sync::ResourceSync,
update::{Log, Update}, update::{Log, Update},
user::{sync_user, User}, user::sync_user,
ResourceTargetVariant,
}, },
}; };
use mongo_indexed::doc; use mongo_indexed::doc;
use mungos::{ use mungos::{by_id::update_one_by_id, mongodb::bson::oid::ObjectId};
by_id::update_one_by_id,
mongodb::bson::{oid::ObjectId, to_document},
};
use resolver_api::Resolve; use resolver_api::Resolve;
use crate::{ use crate::{
api::write::WriteArgs,
helpers::{query::get_id_to_tags, update::update_update}, helpers::{query::get_id_to_tags, update::update_update},
resource::{self, refresh_resource_sync_state_cache}, resource,
state::{action_states, db_client, State}, state::{action_states, db_client},
sync::{ sync::{
deploy::{
build_deploy_cache, deploy_from_cache, SyncDeployParams,
},
execute::{get_updates_for_execution, ExecuteResourceSync},
remote::RemoteResources,
AllResourcesById, ResourceSyncTrait, AllResourcesById, ResourceSyncTrait,
deploy::{
SyncDeployParams, build_deploy_cache, deploy_from_cache,
},
execute::{ExecuteResourceSync, get_updates_for_execution},
remote::RemoteResources,
}, },
}; };
impl Resolve<RunSync, (User, Update)> for State { use super::ExecuteArgs;
#[instrument(name = "RunSync", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
impl Resolve<ExecuteArgs> for RunSync {
#[instrument(name = "RunSync", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve( async fn resolve(
&self, self,
RunSync { ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let RunSync {
sync, sync,
resource_type: match_resource_type, resource_type: match_resource_type,
resources: match_resources, resources: match_resources,
}: RunSync, } = self;
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let sync = resource::get_check_permissions::< let sync = resource::get_check_permissions::<
entities::sync::ResourceSync, entities::sync::ResourceSync,
>(&sync, &user, PermissionLevel::Execute) >(&sync, user, PermissionLevel::Execute)
.await?; .await?;
// get the action state for the sync (or insert default). // get the action state for the sync (or insert default).
@@ -72,6 +71,8 @@ impl Resolve<RunSync, (User, Update)> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.syncing = true)?; action_state.update(|state| state.syncing = true)?;
let mut update = update.clone();
// Send update here for FE to recheck action state // Send update here for FE to recheck action state
update_update(update.clone()).await?; update_update(update.clone()).await?;
@@ -90,7 +91,9 @@ impl Resolve<RunSync, (User, Update)> for State {
update_update(update.clone()).await?; update_update(update.clone()).await?;
if !file_errors.is_empty() { 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?; let resources = resources?;
@@ -203,7 +206,7 @@ impl Resolve<RunSync, (User, Update)> for State {
let delete = sync.config.managed || sync.config.delete; let delete = sync.config.managed || sync.config.delete;
let (servers_to_create, servers_to_update, servers_to_delete) = let server_deltas = if sync.config.include_resources {
get_updates_for_execution::<Server>( get_updates_for_execution::<Server>(
resources.servers, resources.servers,
delete, delete,
@@ -213,22 +216,11 @@ impl Resolve<RunSync, (User, Update)> for State {
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let ( } else {
deployments_to_create, Default::default()
deployments_to_update, };
deployments_to_delete, let stack_deltas = if sync.config.include_resources {
) = get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (stacks_to_create, stacks_to_update, stacks_to_delete) =
get_updates_for_execution::<Stack>( get_updates_for_execution::<Stack>(
resources.stacks, resources.stacks,
delete, delete,
@@ -238,8 +230,25 @@ impl Resolve<RunSync, (User, Update)> for State {
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let (builds_to_create, builds_to_update, builds_to_delete) = } else {
Default::default()
};
let deployment_deltas = if sync.config.include_resources {
get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let build_deltas = if sync.config.include_resources {
get_updates_for_execution::<Build>( get_updates_for_execution::<Build>(
resources.builds, resources.builds,
delete, delete,
@@ -249,8 +258,11 @@ impl Resolve<RunSync, (User, Update)> for State {
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let (repos_to_create, repos_to_update, repos_to_delete) = } else {
Default::default()
};
let repo_deltas = if sync.config.include_resources {
get_updates_for_execution::<Repo>( get_updates_for_execution::<Repo>(
resources.repos, resources.repos,
delete, delete,
@@ -260,22 +272,25 @@ impl Resolve<RunSync, (User, Update)> for State {
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let ( } else {
procedures_to_create, Default::default()
procedures_to_update, };
procedures_to_delete, let procedure_deltas = if sync.config.include_resources {
) = get_updates_for_execution::<Procedure>( get_updates_for_execution::<Procedure>(
resources.procedures, resources.procedures,
delete, delete,
&all_resources, &all_resources,
match_resource_type, match_resource_type,
match_resources.as_deref(), match_resources.as_deref(),
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let (actions_to_create, actions_to_update, actions_to_delete) = } else {
Default::default()
};
let action_deltas = if sync.config.include_resources {
get_updates_for_execution::<Action>( get_updates_for_execution::<Action>(
resources.actions, resources.actions,
delete, delete,
@@ -285,8 +300,11 @@ impl Resolve<RunSync, (User, Update)> for State {
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let (builders_to_create, builders_to_update, builders_to_delete) = } else {
Default::default()
};
let builder_deltas = if sync.config.include_resources {
get_updates_for_execution::<Builder>( get_updates_for_execution::<Builder>(
resources.builders, resources.builders,
delete, delete,
@@ -296,8 +314,11 @@ impl Resolve<RunSync, (User, Update)> for State {
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let (alerters_to_create, alerters_to_update, alerters_to_delete) = } else {
Default::default()
};
let alerter_deltas = if sync.config.include_resources {
get_updates_for_execution::<Alerter>( get_updates_for_execution::<Alerter>(
resources.alerters, resources.alerters,
delete, delete,
@@ -307,35 +328,38 @@ impl Resolve<RunSync, (User, Update)> for State {
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let ( } else {
server_templates_to_create, Default::default()
server_templates_to_update, };
server_templates_to_delete, let server_template_deltas = if sync.config.include_resources {
) = get_updates_for_execution::<ServerTemplate>( get_updates_for_execution::<ServerTemplate>(
resources.server_templates, resources.server_templates,
delete, delete,
&all_resources, &all_resources,
match_resource_type, match_resource_type,
match_resources.as_deref(), match_resources.as_deref(),
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
let ( } else {
resource_syncs_to_create, Default::default()
resource_syncs_to_update, };
resource_syncs_to_delete, let resource_sync_deltas = if sync.config.include_resources {
) = get_updates_for_execution::<entities::sync::ResourceSync>( get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs, resources.resource_syncs,
delete, delete,
&all_resources, &all_resources,
match_resource_type, match_resource_type,
match_resources.as_deref(), match_resources.as_deref(),
&id_to_tags, &id_to_tags,
&sync.config.match_tags, &sync.config.match_tags,
) )
.await?; .await?
} else {
Default::default()
};
let ( let (
variables_to_create, variables_to_create,
@@ -343,12 +367,11 @@ impl Resolve<RunSync, (User, Update)> for State {
variables_to_delete, variables_to_delete,
) = if match_resource_type.is_none() ) = if match_resource_type.is_none()
&& match_resources.is_none() && match_resources.is_none()
&& sync.config.match_tags.is_empty() && sync.config.include_variables
{ {
crate::sync::variables::get_updates_for_execution( crate::sync::variables::get_updates_for_execution(
resources.variables, resources.variables,
// Delete doesn't work with variables when match tags are set delete,
sync.config.match_tags.is_empty() && delete,
) )
.await? .await?
} else { } else {
@@ -360,12 +383,11 @@ impl Resolve<RunSync, (User, Update)> for State {
user_groups_to_delete, user_groups_to_delete,
) = if match_resource_type.is_none() ) = if match_resource_type.is_none()
&& match_resources.is_none() && match_resources.is_none()
&& sync.config.match_tags.is_empty() && sync.config.include_user_groups
{ {
crate::sync::user_groups::get_updates_for_execution( crate::sync::user_groups::get_updates_for_execution(
resources.user_groups, resources.user_groups,
// Delete doesn't work with user groups when match tags are set delete,
sync.config.match_tags.is_empty() && delete,
&all_resources, &all_resources,
) )
.await? .await?
@@ -374,39 +396,17 @@ impl Resolve<RunSync, (User, Update)> for State {
}; };
if deploy_cache.is_empty() if deploy_cache.is_empty()
&& resource_syncs_to_create.is_empty() && resource_sync_deltas.no_changes()
&& resource_syncs_to_update.is_empty() && server_template_deltas.no_changes()
&& resource_syncs_to_delete.is_empty() && server_deltas.no_changes()
&& server_templates_to_create.is_empty() && deployment_deltas.no_changes()
&& server_templates_to_update.is_empty() && stack_deltas.no_changes()
&& server_templates_to_delete.is_empty() && build_deltas.no_changes()
&& servers_to_create.is_empty() && builder_deltas.no_changes()
&& servers_to_update.is_empty() && alerter_deltas.no_changes()
&& servers_to_delete.is_empty() && repo_deltas.no_changes()
&& deployments_to_create.is_empty() && procedure_deltas.no_changes()
&& deployments_to_update.is_empty() && action_deltas.no_changes()
&& deployments_to_delete.is_empty()
&& stacks_to_create.is_empty()
&& stacks_to_update.is_empty()
&& stacks_to_delete.is_empty()
&& builds_to_create.is_empty()
&& builds_to_update.is_empty()
&& builds_to_delete.is_empty()
&& builders_to_create.is_empty()
&& builders_to_update.is_empty()
&& builders_to_delete.is_empty()
&& alerters_to_create.is_empty()
&& alerters_to_update.is_empty()
&& alerters_to_delete.is_empty()
&& repos_to_create.is_empty()
&& repos_to_update.is_empty()
&& repos_to_delete.is_empty()
&& procedures_to_create.is_empty()
&& procedures_to_update.is_empty()
&& procedures_to_delete.is_empty()
&& actions_to_create.is_empty()
&& actions_to_update.is_empty()
&& actions_to_delete.is_empty()
&& user_groups_to_create.is_empty() && user_groups_to_create.is_empty()
&& user_groups_to_update.is_empty() && user_groups_to_update.is_empty()
&& user_groups_to_delete.is_empty() && user_groups_to_delete.is_empty()
@@ -449,111 +449,57 @@ impl Resolve<RunSync, (User, Update)> for State {
); );
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
ResourceSync::execute_sync_updates( ResourceSync::execute_sync_updates(resource_sync_deltas).await,
resource_syncs_to_create,
resource_syncs_to_update,
resource_syncs_to_delete,
)
.await,
); );
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
ServerTemplate::execute_sync_updates( ServerTemplate::execute_sync_updates(server_template_deltas)
server_templates_to_create, .await,
server_templates_to_update,
server_templates_to_delete,
)
.await,
); );
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Server::execute_sync_updates( Server::execute_sync_updates(server_deltas).await,
servers_to_create,
servers_to_update,
servers_to_delete,
)
.await,
); );
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Alerter::execute_sync_updates( Alerter::execute_sync_updates(alerter_deltas).await,
alerters_to_create,
alerters_to_update,
alerters_to_delete,
)
.await,
); );
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Action::execute_sync_updates( Action::execute_sync_updates(action_deltas).await,
actions_to_create,
actions_to_update,
actions_to_delete,
)
.await,
); );
// Dependent on server // Dependent on server
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Builder::execute_sync_updates( Builder::execute_sync_updates(builder_deltas).await,
builders_to_create,
builders_to_update,
builders_to_delete,
)
.await,
); );
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Repo::execute_sync_updates( Repo::execute_sync_updates(repo_deltas).await,
repos_to_create,
repos_to_update,
repos_to_delete,
)
.await,
); );
// Dependant on builder // Dependant on builder
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Build::execute_sync_updates( Build::execute_sync_updates(build_deltas).await,
builds_to_create,
builds_to_update,
builds_to_delete,
)
.await,
); );
// Dependant on server / build // Dependant on server / build
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Deployment::execute_sync_updates( Deployment::execute_sync_updates(deployment_deltas).await,
deployments_to_create,
deployments_to_update,
deployments_to_delete,
)
.await,
); );
// stack only depends on server, but maybe will depend on build later. // stack only depends on server, but maybe will depend on build later.
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Stack::execute_sync_updates( Stack::execute_sync_updates(stack_deltas).await,
stacks_to_create,
stacks_to_update,
stacks_to_delete,
)
.await,
); );
// Dependant on everything // Dependant on everything
maybe_extend( maybe_extend(
&mut update.logs, &mut update.logs,
Procedure::execute_sync_updates( Procedure::execute_sync_updates(procedure_deltas).await,
procedures_to_create,
procedures_to_update,
procedures_to_delete,
)
.await,
); );
// Execute the deploy cache // Execute the deploy cache
@@ -581,39 +527,27 @@ impl Resolve<RunSync, (User, Update)> for State {
) )
} }
if let Err(e) = State if let Err(e) = (RefreshResourceSyncPending { sync: sync.id })
.resolve( .resolve(&WriteArgs {
RefreshResourceSyncPending { sync: sync.id }, user: sync_user().to_owned(),
sync_user().to_owned(), })
)
.await .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( update.push_error_log(
"refresh sync", "refresh sync",
format_serror( format_serror(
&e.context("failed to refresh sync pending after run") &e.error
.context("failed to refresh sync pending after run")
.into(), .into(),
), ),
); );
} }
update.finalize(); update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
// but will fail to update cache in that case.
if let Ok(update_doc) = to_document(&update) {
let _ = update_one_by_id(
&db.updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_resource_sync_state_cache().await;
}
update_update(update.clone()).await?; update_update(update.clone()).await?;
Ok(update) Ok(update)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
use std::{cmp, collections::HashSet}; use std::{cmp, collections::HashSet};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use komodo_client::{ use komodo_client::{
api::read::*, api::read::*,
entities::{ entities::{
@@ -12,7 +12,6 @@ use komodo_client::{
permission::PermissionLevel, permission::PermissionLevel,
server::Server, server::Server,
update::Log, update::Log,
user::User,
}, },
}; };
use periphery_client::api; use periphery_client::api;
@@ -21,67 +20,81 @@ use resolver_api::Resolve;
use crate::{ use crate::{
helpers::{periphery_client, query::get_all_tags}, helpers::{periphery_client, query::get_all_tags},
resource, 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( async fn resolve(
&self, self,
GetDeployment { deployment }: GetDeployment, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Deployment> {
) -> anyhow::Result<Deployment> { Ok(
resource::get_check_permissions::<Deployment>( resource::get_check_permissions::<Deployment>(
&deployment, &self.deployment,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
)
.await?,
) )
.await
} }
} }
impl Resolve<ListDeployments, User> for State { impl Resolve<ReadArgs> for ListDeployments {
async fn resolve( async fn resolve(
&self, self,
ListDeployments { query }: ListDeployments, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Vec<DeploymentListItem>> {
) -> anyhow::Result<Vec<DeploymentListItem>> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_for_user::<Deployment>(query, &user, &all_tags) let only_update_available = self.query.specific.update_available;
.await let deployments = resource::list_for_user::<Deployment>(
self.query, user, &all_tags,
)
.await?;
let deployments = if only_update_available {
deployments
.into_iter()
.filter(|deployment| deployment.info.update_available)
.collect()
} else {
deployments
};
Ok(deployments)
} }
} }
impl Resolve<ListFullDeployments, User> for State { impl Resolve<ReadArgs> for ListFullDeployments {
async fn resolve( async fn resolve(
&self, self,
ListFullDeployments { query }: ListFullDeployments, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListFullDeploymentsResponse> {
) -> anyhow::Result<ListFullDeploymentsResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_full_for_user::<Deployment>( Ok(
query, &user, &all_tags, resource::list_full_for_user::<Deployment>(
self.query, user, &all_tags,
)
.await?,
) )
.await
} }
} }
impl Resolve<GetDeploymentContainer, User> for State { impl Resolve<ReadArgs> for GetDeploymentContainer {
async fn resolve( async fn resolve(
&self, self,
GetDeploymentContainer { deployment }: GetDeploymentContainer, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetDeploymentContainerResponse> {
) -> anyhow::Result<GetDeploymentContainerResponse> {
let deployment = resource::get_check_permissions::<Deployment>( let deployment = resource::get_check_permissions::<Deployment>(
&deployment, &self.deployment,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -99,23 +112,23 @@ impl Resolve<GetDeploymentContainer, User> for State {
const MAX_LOG_LENGTH: u64 = 5000; const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<GetDeploymentLog, User> for State { impl Resolve<ReadArgs> for GetDeploymentLog {
async fn resolve( async fn resolve(
&self, self,
GetDeploymentLog { ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let GetDeploymentLog {
deployment, deployment,
tail, tail,
timestamps, timestamps,
}: GetDeploymentLog, } = self;
user: User,
) -> anyhow::Result<Log> {
let Deployment { let Deployment {
name, name,
config: DeploymentConfig { server_id, .. }, config: DeploymentConfig { server_id, .. },
.. ..
} = resource::get_check_permissions::<Deployment>( } = resource::get_check_permissions::<Deployment>(
&deployment, &deployment,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -123,36 +136,37 @@ impl Resolve<GetDeploymentLog, User> for State {
return Ok(Log::default()); return Ok(Log::default());
} }
let server = resource::get::<Server>(&server_id).await?; let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)? let res = periphery_client(&server)?
.request(api::container::GetContainerLog { .request(api::container::GetContainerLog {
name, name,
tail: cmp::min(tail, MAX_LOG_LENGTH), tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps, timestamps,
}) })
.await .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( async fn resolve(
&self, self,
SearchDeploymentLog { ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let SearchDeploymentLog {
deployment, deployment,
terms, terms,
combinator, combinator,
invert, invert,
timestamps, timestamps,
}: SearchDeploymentLog, } = self;
user: User,
) -> anyhow::Result<Log> {
let Deployment { let Deployment {
name, name,
config: DeploymentConfig { server_id, .. }, config: DeploymentConfig { server_id, .. },
.. ..
} = resource::get_check_permissions::<Deployment>( } = resource::get_check_permissions::<Deployment>(
&deployment, &deployment,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -160,7 +174,7 @@ impl Resolve<SearchDeploymentLog, User> for State {
return Ok(Log::default()); return Ok(Log::default());
} }
let server = resource::get::<Server>(&server_id).await?; let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)? let res = periphery_client(&server)?
.request(api::container::GetContainerLogSearch { .request(api::container::GetContainerLogSearch {
name, name,
terms, terms,
@@ -169,46 +183,48 @@ impl Resolve<SearchDeploymentLog, User> for State {
timestamps, timestamps,
}) })
.await .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( async fn resolve(
&self, self,
GetDeploymentStats { deployment }: GetDeploymentStats, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ContainerStats> {
) -> anyhow::Result<ContainerStats> {
let Deployment { let Deployment {
name, name,
config: DeploymentConfig { server_id, .. }, config: DeploymentConfig { server_id, .. },
.. ..
} = resource::get_check_permissions::<Deployment>( } = resource::get_check_permissions::<Deployment>(
&deployment, &self.deployment,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
if server_id.is_empty() { 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?; let server = resource::get::<Server>(&server_id).await?;
periphery_client(&server)? let res = periphery_client(&server)?
.request(api::container::GetContainerStats { name }) .request(api::container::GetContainerStats { name })
.await .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( async fn resolve(
&self, self,
GetDeploymentActionState { deployment }: GetDeploymentActionState, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<DeploymentActionState> {
) -> anyhow::Result<DeploymentActionState> {
let deployment = resource::get_check_permissions::<Deployment>( let deployment = resource::get_check_permissions::<Deployment>(
&deployment, &self.deployment,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -222,15 +238,14 @@ impl Resolve<GetDeploymentActionState, User> for State {
} }
} }
impl Resolve<GetDeploymentsSummary, User> for State { impl Resolve<ReadArgs> for GetDeploymentsSummary {
async fn resolve( async fn resolve(
&self, self,
GetDeploymentsSummary {}: GetDeploymentsSummary, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetDeploymentsSummaryResponse> {
) -> anyhow::Result<GetDeploymentsSummaryResponse> {
let deployments = resource::list_full_for_user::<Deployment>( let deployments = resource::list_full_for_user::<Deployment>(
Default::default(), Default::default(),
&user, user,
&[], &[],
) )
.await .await
@@ -263,19 +278,18 @@ impl Resolve<GetDeploymentsSummary, User> for State {
} }
} }
impl Resolve<ListCommonDeploymentExtraArgs, User> for State { impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
async fn resolve( async fn resolve(
&self, self,
ListCommonDeploymentExtraArgs { query }: ListCommonDeploymentExtraArgs, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListCommonDeploymentExtraArgsResponse> {
) -> anyhow::Result<ListCommonDeploymentExtraArgsResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
let deployments = resource::list_full_for_user::<Deployment>( let deployments = resource::list_full_for_user::<Deployment>(
query, &user, &all_tags, self.query, user, &all_tags,
) )
.await .await
.context("failed to get resources matching query")?; .context("failed to get resources matching query")?;

View File

@@ -1,11 +1,11 @@
use std::{collections::HashSet, sync::OnceLock, time::Instant}; use std::{collections::HashSet, sync::OnceLock, time::Instant};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use axum::{middleware, routing::post, Extension, Router}; use axum::{Extension, Router, middleware, routing::post};
use axum_extra::{headers::ContentType, TypedHeader};
use komodo_client::{ use komodo_client::{
api::read::*, api::read::*,
entities::{ entities::{
ResourceTarget,
build::Build, build::Build,
builder::{Builder, BuilderConfig}, builder::{Builder, BuilderConfig},
config::{DockerRegistry, GitProvider}, config::{DockerRegistry, GitProvider},
@@ -13,12 +13,10 @@ use komodo_client::{
server::Server, server::Server,
sync::ResourceSync, sync::ResourceSync,
user::User, user::User,
ResourceTarget,
}, },
}; };
use resolver_api::{ use resolver_api::Resolve;
derive::Resolver, Resolve, ResolveToString, Resolver, use response::Response;
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serror::Json; use serror::Json;
use typeshare::typeshare; use typeshare::typeshare;
@@ -26,7 +24,7 @@ use uuid::Uuid;
use crate::{ use crate::{
auth::auth_request, config::core_config, helpers::periphery_client, auth::auth_request, config::core_config, helpers::periphery_client,
resource, state::State, resource,
}; };
mod action; mod action;
@@ -39,7 +37,6 @@ mod permission;
mod procedure; mod procedure;
mod provider; mod provider;
mod repo; mod repo;
mod search;
mod server; mod server;
mod server_template; mod server_template;
mod stack; mod stack;
@@ -51,15 +48,18 @@ mod user;
mod user_group; mod user_group;
mod variable; mod variable;
pub struct ReadArgs {
pub user: User,
}
#[typeshare] #[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] #[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
#[resolver_target(State)] #[args(ReadArgs)]
#[resolver_args(User)] #[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")] #[serde(tag = "type", content = "params")]
enum ReadRequest { enum ReadRequest {
#[to_string_resolver]
GetVersion(GetVersion), GetVersion(GetVersion),
#[to_string_resolver]
GetCoreInfo(GetCoreInfo), GetCoreInfo(GetCoreInfo),
ListSecrets(ListSecrets), ListSecrets(ListSecrets),
ListGitProvidersFromConfig(ListGitProvidersFromConfig), ListGitProvidersFromConfig(ListGitProvidersFromConfig),
@@ -79,9 +79,6 @@ enum ReadRequest {
GetUserGroup(GetUserGroup), GetUserGroup(GetUserGroup),
ListUserGroups(ListUserGroups), ListUserGroups(ListUserGroups),
// ==== SEARCH ====
FindResources(FindResources),
// ==== PROCEDURE ==== // ==== PROCEDURE ====
GetProceduresSummary(GetProceduresSummary), GetProceduresSummary(GetProceduresSummary),
GetProcedure(GetProcedure), GetProcedure(GetProcedure),
@@ -120,15 +117,10 @@ enum ReadRequest {
ListDockerImageHistory(ListDockerImageHistory), ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume), InspectDockerVolume(InspectDockerVolume),
ListAllDockerContainers(ListAllDockerContainers), ListAllDockerContainers(ListAllDockerContainers),
#[to_string_resolver]
ListDockerContainers(ListDockerContainers), ListDockerContainers(ListDockerContainers),
#[to_string_resolver]
ListDockerNetworks(ListDockerNetworks), ListDockerNetworks(ListDockerNetworks),
#[to_string_resolver]
ListDockerImages(ListDockerImages), ListDockerImages(ListDockerImages),
#[to_string_resolver]
ListDockerVolumes(ListDockerVolumes), ListDockerVolumes(ListDockerVolumes),
#[to_string_resolver]
ListComposeProjects(ListComposeProjects), ListComposeProjects(ListComposeProjects),
// ==== DEPLOYMENT ==== // ==== DEPLOYMENT ====
@@ -175,8 +167,8 @@ enum ReadRequest {
GetStack(GetStack), GetStack(GetStack),
GetStackActionState(GetStackActionState), GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled), GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackServiceLog(GetStackServiceLog), GetStackLog(GetStackLog),
SearchStackServiceLog(SearchStackServiceLog), SearchStackLog(SearchStackLog),
ListStacks(ListStacks), ListStacks(ListStacks),
ListFullStacks(ListFullStacks), ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices), ListStackServices(ListStackServices),
@@ -212,11 +204,8 @@ enum ReadRequest {
GetAlert(GetAlert), GetAlert(GetAlert),
// ==== SERVER STATS ==== // ==== SERVER STATS ====
#[to_string_resolver]
GetSystemInformation(GetSystemInformation), GetSystemInformation(GetSystemInformation),
#[to_string_resolver]
GetSystemStats(GetSystemStats), GetSystemStats(GetSystemStats),
#[to_string_resolver]
ListSystemProcesses(ListSystemProcesses), ListSystemProcesses(ListSystemProcesses),
// ==== VARIABLE ==== // ==== VARIABLE ====
@@ -240,54 +229,35 @@ pub fn router() -> Router {
async fn handler( async fn handler(
Extension(user): Extension<User>, Extension(user): Extension<User>,
Json(request): Json<ReadRequest>, Json(request): Json<ReadRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> { ) -> serror::Result<axum::response::Response> {
let timer = Instant::now(); let timer = Instant::now();
let req_id = Uuid::new_v4(); let req_id = Uuid::new_v4();
debug!("/read request | user: {}", user.username); debug!("/read request | user: {}", user.username);
let res = let res = request.resolve(&ReadArgs { user }).await;
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,
});
if let Err(e) = &res { if let Err(e) = &res {
debug!("/read request {req_id} error: {e:#}"); debug!("/read request {req_id} error: {:#}", e.error);
} }
let elapsed = timer.elapsed(); let elapsed = timer.elapsed();
debug!("/read request {req_id} | resolve time: {elapsed:?}"); debug!("/read request {req_id} | resolve time: {elapsed:?}");
Ok((TypedHeader(ContentType::json()), res?)) res.map(|res| res.0)
} }
fn version() -> &'static String { impl Resolve<ReadArgs> for GetVersion {
static VERSION: OnceLock<String> = OnceLock::new(); async fn resolve(
VERSION.get_or_init(|| { self,
serde_json::to_string(&GetVersionResponse { _: &ReadArgs,
) -> serror::Result<GetVersionResponse> {
Ok(GetVersionResponse {
version: env!("CARGO_PKG_VERSION").to_string(), 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 { fn core_info() -> &'static GetCoreInfoResponse {
static CORE_INFO: OnceLock<String> = OnceLock::new(); static CORE_INFO: OnceLock<GetCoreInfoResponse> = OnceLock::new();
CORE_INFO.get_or_init(|| { CORE_INFO.get_or_init(|| {
let config = core_config(); let config = core_config();
let info = GetCoreInfoResponse { GetCoreInfoResponse {
title: config.title.clone(), title: config.title.clone(),
monitoring_interval: config.monitoring_interval, monitoring_interval: config.monitoring_interval,
webhook_base_url: if config.webhook_base_url.is_empty() { webhook_base_url: if config.webhook_base_url.is_empty() {
@@ -305,36 +275,31 @@ fn core_info() -> &'static String {
.iter() .iter()
.map(|i| i.namespace.to_string()) .map(|i| i.namespace.to_string())
.collect(), .collect(),
}; }
serde_json::to_string(&info)
.context("failed to serialize GetCoreInfoResponse")
.unwrap()
}) })
} }
impl ResolveToString<GetCoreInfo, User> for State { impl Resolve<ReadArgs> for GetCoreInfo {
async fn resolve_to_string( async fn resolve(
&self, self,
GetCoreInfo {}: GetCoreInfo, _: &ReadArgs,
_: User, ) -> serror::Result<GetCoreInfoResponse> {
) -> anyhow::Result<String> { Ok(core_info().clone())
Ok(core_info().to_string())
} }
} }
impl Resolve<ListSecrets, User> for State { impl Resolve<ReadArgs> for ListSecrets {
async fn resolve( async fn resolve(
&self, self,
ListSecrets { target }: ListSecrets, _: &ReadArgs,
_: User, ) -> serror::Result<ListSecretsResponse> {
) -> anyhow::Result<ListSecretsResponse> {
let mut secrets = core_config() let mut secrets = core_config()
.secrets .secrets
.keys() .keys()
.cloned() .cloned()
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if let Some(target) = target { if let Some(target) = self.target {
let server_id = match target { let server_id = match target {
ResourceTarget::Server(id) => Some(id), ResourceTarget::Server(id) => Some(id),
ResourceTarget::Builder(id) => { ResourceTarget::Builder(id) => {
@@ -348,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 { if let Some(id) = server_id {
@@ -373,15 +340,14 @@ impl Resolve<ListSecrets, User> for State {
} }
} }
impl Resolve<ListGitProvidersFromConfig, User> for State { impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
async fn resolve( async fn resolve(
&self, self,
ListGitProvidersFromConfig { target }: ListGitProvidersFromConfig, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListGitProvidersFromConfigResponse> {
) -> anyhow::Result<ListGitProvidersFromConfigResponse> {
let mut providers = core_config().git_providers.clone(); let mut providers = core_config().git_providers.clone();
if let Some(target) = target { if let Some(target) = self.target {
match target { match target {
ResourceTarget::Server(id) => { ResourceTarget::Server(id) => {
merge_git_providers_for_server(&mut providers, &id).await?; merge_git_providers_for_server(&mut providers, &id).await?;
@@ -405,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(),
);
} }
} }
} }
@@ -413,17 +381,17 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
let (builds, repos, syncs) = tokio::try_join!( let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>( resource::list_full_for_user::<Build>(
Default::default(), Default::default(),
&user, user,
&[] &[]
), ),
resource::list_full_for_user::<Repo>( resource::list_full_for_user::<Repo>(
Default::default(), Default::default(),
&user, user,
&[] &[]
), ),
resource::list_full_for_user::<ResourceSync>( resource::list_full_for_user::<ResourceSync>(
Default::default(), Default::default(),
&user, user,
&[] &[]
), ),
)?; )?;
@@ -471,15 +439,14 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
} }
} }
impl Resolve<ListDockerRegistriesFromConfig, User> for State { impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
async fn resolve( async fn resolve(
&self, self,
ListDockerRegistriesFromConfig { target }: ListDockerRegistriesFromConfig, _: &ReadArgs,
_: User, ) -> serror::Result<ListDockerRegistriesFromConfigResponse> {
) -> anyhow::Result<ListDockerRegistriesFromConfigResponse> {
let mut registries = core_config().docker_registries.clone(); let mut registries = core_config().docker_registries.clone();
if let Some(target) = target { if let Some(target) = self.target {
match target { match target {
ResourceTarget::Server(id) => { ResourceTarget::Server(id) => {
merge_docker_registries_for_server(&mut registries, &id) merge_docker_registries_for_server(&mut registries, &id)
@@ -504,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(),
);
} }
} }
} }
@@ -518,7 +487,7 @@ impl Resolve<ListDockerRegistriesFromConfig, User> for State {
async fn merge_git_providers_for_server( async fn merge_git_providers_for_server(
providers: &mut Vec<GitProvider>, providers: &mut Vec<GitProvider>,
server_id: &str, server_id: &str,
) -> anyhow::Result<()> { ) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?; let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)? let more = periphery_client(&server)?
.request(periphery_client::api::ListGitProviders {}) .request(periphery_client::api::ListGitProviders {})
@@ -556,7 +525,7 @@ fn merge_git_providers(
async fn merge_docker_registries_for_server( async fn merge_docker_registries_for_server(
registries: &mut Vec<DockerRegistry>, registries: &mut Vec<DockerRegistry>,
server_id: &str, server_id: &str,
) -> anyhow::Result<()> { ) -> serror::Result<()> {
let server = resource::get::<Server>(server_id).await?; let server = resource::get::<Server>(server_id).await?;
let more = periphery_client(&server)? let more = periphery_client(&server)?
.request(periphery_client::api::ListDockerRegistries {}) .request(periphery_client::api::ListDockerRegistries {})

View File

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

View File

@@ -4,7 +4,6 @@ use komodo_client::{
entities::{ entities::{
permission::PermissionLevel, permission::PermissionLevel,
procedure::{Procedure, ProcedureState}, procedure::{Procedure, ProcedureState},
user::User,
}, },
}; };
use resolver_api::Resolve; use resolver_api::Resolve;
@@ -12,65 +11,73 @@ use resolver_api::Resolve;
use crate::{ use crate::{
helpers::query::get_all_tags, helpers::query::get_all_tags,
resource, 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( async fn resolve(
&self, self,
GetProcedure { procedure }: GetProcedure, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetProcedureResponse> {
) -> anyhow::Result<GetProcedureResponse> { Ok(
resource::get_check_permissions::<Procedure>( resource::get_check_permissions::<Procedure>(
&procedure, &self.procedure,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
)
.await?,
) )
.await
} }
} }
impl Resolve<ListProcedures, User> for State { impl Resolve<ReadArgs> for ListProcedures {
async fn resolve( async fn resolve(
&self, self,
ListProcedures { query }: ListProcedures, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListProceduresResponse> {
) -> anyhow::Result<ListProceduresResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_for_user::<Procedure>(query, &user, &all_tags) Ok(
.await 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( async fn resolve(
&self, self,
ListFullProcedures { query }: ListFullProcedures, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListFullProceduresResponse> {
) -> anyhow::Result<ListFullProceduresResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_full_for_user::<Procedure>(query, &user, &all_tags) Ok(
.await 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( async fn resolve(
&self, self,
GetProceduresSummary {}: GetProceduresSummary, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetProceduresSummaryResponse> {
) -> anyhow::Result<GetProceduresSummaryResponse> {
let procedures = resource::list_full_for_user::<Procedure>( let procedures = resource::list_full_for_user::<Procedure>(
Default::default(), Default::default(),
&user, user,
&[], &[],
) )
.await .await
@@ -108,15 +115,14 @@ impl Resolve<GetProceduresSummary, User> for State {
} }
} }
impl Resolve<GetProcedureActionState, User> for State { impl Resolve<ReadArgs> for GetProcedureActionState {
async fn resolve( async fn resolve(
&self, self,
GetProcedureActionState { procedure }: GetProcedureActionState, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetProcedureActionStateResponse> {
) -> anyhow::Result<GetProcedureActionStateResponse> {
let procedure = resource::get_check_permissions::<Procedure>( let procedure = resource::get_check_permissions::<Procedure>(
&procedure, &self.procedure,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;

View File

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

View File

@@ -5,7 +5,6 @@ use komodo_client::{
config::core::CoreConfig, config::core::CoreConfig,
permission::PermissionLevel, permission::PermissionLevel,
repo::{Repo, RepoActionState, RepoListItem, RepoState}, repo::{Repo, RepoActionState, RepoListItem, RepoState},
user::User,
}, },
}; };
use resolver_api::Resolve; use resolver_api::Resolve;
@@ -14,64 +13,71 @@ use crate::{
config::core_config, config::core_config,
helpers::query::get_all_tags, helpers::query::get_all_tags,
resource, 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( async fn resolve(
&self, self,
GetRepo { repo }: GetRepo, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Repo> {
) -> anyhow::Result<Repo> { Ok(
resource::get_check_permissions::<Repo>( resource::get_check_permissions::<Repo>(
&repo, &self.repo,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
)
.await?,
) )
.await
} }
} }
impl Resolve<ListRepos, User> for State { impl Resolve<ReadArgs> for ListRepos {
async fn resolve( async fn resolve(
&self, self,
ListRepos { query }: ListRepos, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Vec<RepoListItem>> {
) -> anyhow::Result<Vec<RepoListItem>> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_for_user::<Repo>(query, &user, &all_tags).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( async fn resolve(
&self, self,
ListFullRepos { query }: ListFullRepos, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListFullReposResponse> {
) -> anyhow::Result<ListFullReposResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_full_for_user::<Repo>(query, &user, &all_tags) Ok(
.await 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( async fn resolve(
&self, self,
GetRepoActionState { repo }: GetRepoActionState, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<RepoActionState> {
) -> anyhow::Result<RepoActionState> {
let repo = resource::get_check_permissions::<Repo>( let repo = resource::get_check_permissions::<Repo>(
&repo, &self.repo,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -85,15 +91,14 @@ impl Resolve<GetRepoActionState, User> for State {
} }
} }
impl Resolve<GetReposSummary, User> for State { impl Resolve<ReadArgs> for GetReposSummary {
async fn resolve( async fn resolve(
&self, self,
GetReposSummary {}: GetReposSummary, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetReposSummaryResponse> {
) -> anyhow::Result<GetReposSummaryResponse> {
let repos = resource::list_full_for_user::<Repo>( let repos = resource::list_full_for_user::<Repo>(
Default::default(), Default::default(),
&user, user,
&[], &[],
) )
.await .await
@@ -141,12 +146,11 @@ impl Resolve<GetReposSummary, User> for State {
} }
} }
impl Resolve<GetRepoWebhooksEnabled, User> for State { impl Resolve<ReadArgs> for GetRepoWebhooksEnabled {
async fn resolve( async fn resolve(
&self, self,
GetRepoWebhooksEnabled { repo }: GetRepoWebhooksEnabled, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetRepoWebhooksEnabledResponse> {
) -> anyhow::Result<GetRepoWebhooksEnabledResponse> {
let Some(github) = github_client() else { let Some(github) = github_client() else {
return Ok(GetRepoWebhooksEnabledResponse { return Ok(GetRepoWebhooksEnabledResponse {
managed: false, managed: false,
@@ -157,8 +161,8 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
}; };
let repo = resource::get_check_permissions::<Repo>( let repo = resource::get_check_permissions::<Repo>(
&repo, &self.repo,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .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

@@ -4,13 +4,14 @@ use std::{
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
}; };
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use async_timing_util::{ use async_timing_util::{
get_timelength_in_ms, unix_timestamp_ms, FIFTEEN_SECONDS_MS, FIFTEEN_SECONDS_MS, get_timelength_in_ms, unix_timestamp_ms,
}; };
use komodo_client::{ use komodo_client::{
api::read::*, api::read::*,
entities::{ entities::{
ResourceTarget,
deployment::Deployment, deployment::Deployment,
docker::{ docker::{
container::{Container, ContainerListItem}, container::{Container, ContainerListItem},
@@ -23,9 +24,8 @@ use komodo_client::{
Server, ServerActionState, ServerListItem, ServerState, Server, ServerActionState, ServerListItem, ServerState,
}, },
stack::{Stack, StackServiceNames}, stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log, update::Log,
user::User,
ResourceTarget,
}, },
}; };
use mungos::{ use mungos::{
@@ -39,25 +39,26 @@ use periphery_client::api::{
network::InspectNetwork, network::InspectNetwork,
volume::InspectVolume, volume::InspectVolume,
}; };
use resolver_api::{Resolve, ResolveToString}; use resolver_api::Resolve;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
helpers::{periphery_client, query::get_all_tags}, helpers::{periphery_client, query::get_all_tags},
resource, resource,
stack::compose_container_match_regex, 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( async fn resolve(
&self, self,
GetServersSummary {}: GetServersSummary, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetServersSummaryResponse> {
) -> anyhow::Result<GetServersSummaryResponse> {
let servers = resource::list_for_user::<Server>( let servers = resource::list_for_user::<Server>(
Default::default(), Default::default(),
&user, user,
&[], &[],
) )
.await?; .await?;
@@ -80,15 +81,14 @@ impl Resolve<GetServersSummary, User> for State {
} }
} }
impl Resolve<GetPeripheryVersion, User> for State { impl Resolve<ReadArgs> for GetPeripheryVersion {
async fn resolve( async fn resolve(
&self, self,
req: GetPeripheryVersion, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetPeripheryVersionResponse> {
) -> anyhow::Result<GetPeripheryVersionResponse> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&req.server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -101,61 +101,66 @@ impl Resolve<GetPeripheryVersion, User> for State {
} }
} }
impl Resolve<GetServer, User> for State { impl Resolve<ReadArgs> for GetServer {
async fn resolve( async fn resolve(
&self, self,
req: GetServer, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Server> {
) -> anyhow::Result<Server> { Ok(
resource::get_check_permissions::<Server>( resource::get_check_permissions::<Server>(
&req.server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
)
.await?,
) )
.await
} }
} }
impl Resolve<ListServers, User> for State { impl Resolve<ReadArgs> for ListServers {
async fn resolve( async fn resolve(
&self, self,
ListServers { query }: ListServers, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Vec<ServerListItem>> {
) -> anyhow::Result<Vec<ServerListItem>> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_for_user::<Server>(query, &user, &all_tags).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( async fn resolve(
&self, self,
ListFullServers { query }: ListFullServers, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListFullServersResponse> {
) -> anyhow::Result<ListFullServersResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_full_for_user::<Server>(query, &user, &all_tags) Ok(
.await 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( async fn resolve(
&self, self,
GetServerState { server }: GetServerState, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetServerStateResponse> {
) -> anyhow::Result<GetServerStateResponse> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -170,15 +175,14 @@ impl Resolve<GetServerState, User> for State {
} }
} }
impl Resolve<GetServerActionState, User> for State { impl Resolve<ReadArgs> for GetServerActionState {
async fn resolve( async fn resolve(
&self, self,
GetServerActionState { server }: GetServerActionState, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ServerActionState> {
) -> anyhow::Result<ServerActionState> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -194,22 +198,22 @@ impl Resolve<GetServerActionState, User> for State {
// This protects the peripheries from spam requests // This protects the peripheries from spam requests
const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS; 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 { fn system_info_cache() -> &'static SystemInfoCache {
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> = static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
OnceLock::new(); OnceLock::new();
SYSTEM_INFO_CACHE.get_or_init(Default::default) SYSTEM_INFO_CACHE.get_or_init(Default::default)
} }
impl ResolveToString<GetSystemInformation, User> for State { impl Resolve<ReadArgs> for GetSystemInformation {
async fn resolve_to_string( async fn resolve(
&self, self,
GetSystemInformation { server }: GetSystemInformation, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<SystemInformation> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -223,28 +227,26 @@ impl ResolveToString<GetSystemInformation, User> for State {
let stats = periphery_client(&server)? let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemInformation {}) .request(periphery::stats::GetSystemInformation {})
.await?; .await?;
let res = serde_json::to_string(&stats)?;
lock.insert( lock.insert(
server.id, server.id,
(res.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY) (stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
.into(), .into(),
); );
res stats
} }
}; };
Ok(res) Ok(res)
} }
} }
impl ResolveToString<GetSystemStats, User> for State { impl Resolve<ReadArgs> for GetSystemStats {
async fn resolve_to_string( async fn resolve(
&self, self,
GetSystemStats { server }: GetSystemStats, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetSystemStatsResponse> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -256,28 +258,27 @@ impl ResolveToString<GetSystemStats, User> for State {
.stats .stats
.as_ref() .as_ref()
.context("server stats not available")?; .context("server stats not available")?;
let stats = serde_json::to_string(&stats)?; Ok(stats.clone())
Ok(stats)
} }
} }
// This protects the peripheries from spam requests // This protects the peripheries from spam requests
const PROCESSES_EXPIRY: u128 = FIFTEEN_SECONDS_MS; 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 { fn processes_cache() -> &'static ProcessesCache {
static PROCESSES_CACHE: OnceLock<ProcessesCache> = OnceLock::new(); static PROCESSES_CACHE: OnceLock<ProcessesCache> = OnceLock::new();
PROCESSES_CACHE.get_or_init(Default::default) PROCESSES_CACHE.get_or_init(Default::default)
} }
impl ResolveToString<ListSystemProcesses, User> for State { impl Resolve<ReadArgs> for ListSystemProcesses {
async fn resolve_to_string( async fn resolve(
&self, self,
ListSystemProcesses { server }: ListSystemProcesses, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListSystemProcessesResponse> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -290,13 +291,12 @@ impl ResolveToString<ListSystemProcesses, User> for State {
let stats = periphery_client(&server)? let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemProcesses {}) .request(periphery::stats::GetSystemProcesses {})
.await?; .await?;
let res = serde_json::to_string(&stats)?;
lock.insert( lock.insert(
server.id, server.id,
(res.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY) (stats.clone(), unix_timestamp_ms() + PROCESSES_EXPIRY)
.into(), .into(),
); );
res stats
} }
}; };
Ok(res) Ok(res)
@@ -305,19 +305,19 @@ impl ResolveToString<ListSystemProcesses, User> for State {
const STATS_PER_PAGE: i64 = 200; const STATS_PER_PAGE: i64 = 200;
impl Resolve<GetHistoricalServerStats, User> for State { impl Resolve<ReadArgs> for GetHistoricalServerStats {
async fn resolve( async fn resolve(
&self, self,
GetHistoricalServerStats { ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetHistoricalServerStatsResponse> {
let GetHistoricalServerStats {
server, server,
granularity, granularity,
page, page,
}: GetHistoricalServerStats, } = self;
user: User,
) -> anyhow::Result<GetHistoricalServerStatsResponse> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -358,15 +358,14 @@ impl Resolve<GetHistoricalServerStats, User> for State {
} }
} }
impl ResolveToString<ListDockerContainers, User> for State { impl Resolve<ReadArgs> for ListDockerContainers {
async fn resolve_to_string( async fn resolve(
&self, self,
ListDockerContainers { server }: ListDockerContainers, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListDockerContainersResponse> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -374,31 +373,29 @@ impl ResolveToString<ListDockerContainers, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if let Some(containers) = &cache.containers { if let Some(containers) = &cache.containers {
serde_json::to_string(containers) Ok(containers.clone())
.context("failed to serialize response")
} else { } else {
Ok(String::from("[]")) Ok(Vec::new())
} }
} }
} }
impl Resolve<ListAllDockerContainers, User> for State { impl Resolve<ReadArgs> for ListAllDockerContainers {
async fn resolve( async fn resolve(
&self, self,
ListAllDockerContainers { servers }: ListAllDockerContainers, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListAllDockerContainersResponse> {
) -> anyhow::Result<Vec<ContainerListItem>> {
let servers = resource::list_for_user::<Server>( let servers = resource::list_for_user::<Server>(
Default::default(), Default::default(),
&user, user,
&[], &[],
) )
.await? .await?
.into_iter() .into_iter()
.filter(|server| { .filter(|server| {
servers.is_empty() self.servers.is_empty()
|| servers.contains(&server.id) || self.servers.contains(&server.id)
|| servers.contains(&server.name) || self.servers.contains(&server.name)
}); });
let mut containers = Vec::<ContainerListItem>::new(); let mut containers = Vec::<ContainerListItem>::new();
@@ -416,15 +413,14 @@ impl Resolve<ListAllDockerContainers, User> for State {
} }
} }
impl Resolve<InspectDockerContainer, User> for State { impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve( async fn resolve(
&self, self,
InspectDockerContainer { server, container }: InspectDockerContainer, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Container> {
) -> anyhow::Result<Container> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -432,67 +428,74 @@ impl Resolve<InspectDockerContainer, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if cache.state != ServerState::Ok { if cache.state != ServerState::Ok {
return Err(anyhow!( return Err(
"Cannot inspect container: server is {:?}", anyhow!(
cache.state "Cannot inspect container: server is {:?}",
)); cache.state
)
.into(),
);
} }
periphery_client(&server)? let res = periphery_client(&server)?
.request(InspectContainer { name: container }) .request(InspectContainer {
.await name: self.container,
})
.await?;
Ok(res)
} }
} }
const MAX_LOG_LENGTH: u64 = 5000; const MAX_LOG_LENGTH: u64 = 5000;
impl Resolve<GetContainerLog, User> for State { impl Resolve<ReadArgs> for GetContainerLog {
async fn resolve( async fn resolve(
&self, self,
GetContainerLog { ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let GetContainerLog {
server, server,
container, container,
tail, tail,
timestamps, timestamps,
}: GetContainerLog, } = self;
user: User,
) -> anyhow::Result<Log> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
periphery_client(&server)? let res = periphery_client(&server)?
.request(periphery::container::GetContainerLog { .request(periphery::container::GetContainerLog {
name: container, name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH), tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps, timestamps,
}) })
.await .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( async fn resolve(
&self, self,
SearchContainerLog { ReadArgs { user }: &ReadArgs,
) -> serror::Result<Log> {
let SearchContainerLog {
server, server,
container, container,
terms, terms,
combinator, combinator,
invert, invert,
timestamps, timestamps,
}: SearchContainerLog, } = self;
user: User,
) -> anyhow::Result<Log> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
periphery_client(&server)? let res = periphery_client(&server)?
.request(periphery::container::GetContainerLogSearch { .request(periphery::container::GetContainerLogSearch {
name: container, name: container,
terms, terms,
@@ -501,25 +504,25 @@ impl Resolve<SearchContainerLog, User> for State {
timestamps, timestamps,
}) })
.await .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( async fn resolve(
&self, self,
GetResourceMatchingContainer { server, container }: GetResourceMatchingContainer, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetResourceMatchingContainerResponse> {
) -> anyhow::Result<GetResourceMatchingContainerResponse> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
// first check deployments // first check deployments
if let Ok(deployment) = if let Ok(deployment) =
resource::get::<Deployment>(&container).await resource::get::<Deployment>(&self.container).await
{ {
return Ok(GetResourceMatchingContainerResponse { return Ok(GetResourceMatchingContainerResponse {
resource: ResourceTarget::Deployment(deployment.id).into(), resource: ResourceTarget::Deployment(deployment.id).into(),
@@ -530,7 +533,7 @@ impl Resolve<GetResourceMatchingContainer, User> for State {
let stacks = let stacks =
resource::list_full_for_user_using_document::<Stack>( resource::list_full_for_user_using_document::<Stack>(
doc! { "config.server_id": &server.id }, doc! { "config.server_id": &server.id },
&user, user,
) )
.await?; .await?;
@@ -553,7 +556,7 @@ impl Resolve<GetResourceMatchingContainer, User> for State {
warn!("{e:#}"); warn!("{e:#}");
continue; continue;
} }
}.is_match(&container); }.is_match(&self.container);
if is_match { if is_match {
return Ok(GetResourceMatchingContainerResponse { return Ok(GetResourceMatchingContainerResponse {
@@ -567,15 +570,14 @@ impl Resolve<GetResourceMatchingContainer, User> for State {
} }
} }
impl ResolveToString<ListDockerNetworks, User> for State { impl Resolve<ReadArgs> for ListDockerNetworks {
async fn resolve_to_string( async fn resolve(
&self, self,
ListDockerNetworks { server }: ListDockerNetworks, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListDockerNetworksResponse> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -583,23 +585,21 @@ impl ResolveToString<ListDockerNetworks, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if let Some(networks) = &cache.networks { if let Some(networks) = &cache.networks {
serde_json::to_string(networks) Ok(networks.clone())
.context("failed to serialize response")
} else { } else {
Ok(String::from("[]")) Ok(Vec::new())
} }
} }
} }
impl Resolve<InspectDockerNetwork, User> for State { impl Resolve<ReadArgs> for InspectDockerNetwork {
async fn resolve( async fn resolve(
&self, self,
InspectDockerNetwork { server, network }: InspectDockerNetwork, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Network> {
) -> anyhow::Result<Network> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -607,26 +607,29 @@ impl Resolve<InspectDockerNetwork, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if cache.state != ServerState::Ok { if cache.state != ServerState::Ok {
return Err(anyhow!( return Err(
"Cannot inspect network: server is {:?}", anyhow!(
cache.state "Cannot inspect network: server is {:?}",
)); cache.state
)
.into(),
);
} }
periphery_client(&server)? let res = periphery_client(&server)?
.request(InspectNetwork { name: network }) .request(InspectNetwork { name: self.network })
.await .await?;
Ok(res)
} }
} }
impl ResolveToString<ListDockerImages, User> for State { impl Resolve<ReadArgs> for ListDockerImages {
async fn resolve_to_string( async fn resolve(
&self, self,
ListDockerImages { server }: ListDockerImages, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListDockerImagesResponse> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -634,23 +637,21 @@ impl ResolveToString<ListDockerImages, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if let Some(images) = &cache.images { if let Some(images) = &cache.images {
serde_json::to_string(images) Ok(images.clone())
.context("failed to serialize response")
} else { } else {
Ok(String::from("[]")) Ok(Vec::new())
} }
} }
} }
impl Resolve<InspectDockerImage, User> for State { impl Resolve<ReadArgs> for InspectDockerImage {
async fn resolve( async fn resolve(
&self, self,
InspectDockerImage { server, image }: InspectDockerImage, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Image> {
) -> anyhow::Result<Image> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -658,26 +659,26 @@ impl Resolve<InspectDockerImage, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if cache.state != ServerState::Ok { if cache.state != ServerState::Ok {
return Err(anyhow!( return Err(
"Cannot inspect image: server is {:?}", anyhow!("Cannot inspect image: server is {:?}", cache.state)
cache.state .into(),
)); );
} }
periphery_client(&server)? let res = periphery_client(&server)?
.request(InspectImage { name: image }) .request(InspectImage { name: self.image })
.await .await?;
Ok(res)
} }
} }
impl Resolve<ListDockerImageHistory, User> for State { impl Resolve<ReadArgs> for ListDockerImageHistory {
async fn resolve( async fn resolve(
&self, self,
ListDockerImageHistory { server, image }: ListDockerImageHistory, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Vec<ImageHistoryResponseItem>> {
) -> anyhow::Result<Vec<ImageHistoryResponseItem>> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -685,26 +686,29 @@ impl Resolve<ListDockerImageHistory, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if cache.state != ServerState::Ok { if cache.state != ServerState::Ok {
return Err(anyhow!( return Err(
"Cannot get image history: server is {:?}", anyhow!(
cache.state "Cannot get image history: server is {:?}",
)); cache.state
)
.into(),
);
} }
periphery_client(&server)? let res = periphery_client(&server)?
.request(ImageHistory { name: image }) .request(ImageHistory { name: self.image })
.await .await?;
Ok(res)
} }
} }
impl ResolveToString<ListDockerVolumes, User> for State { impl Resolve<ReadArgs> for ListDockerVolumes {
async fn resolve_to_string( async fn resolve(
&self, self,
ListDockerVolumes { server }: ListDockerVolumes, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListDockerVolumesResponse> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -712,23 +716,21 @@ impl ResolveToString<ListDockerVolumes, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if let Some(volumes) = &cache.volumes { if let Some(volumes) = &cache.volumes {
serde_json::to_string(volumes) Ok(volumes.clone())
.context("failed to serialize response")
} else { } else {
Ok(String::from("[]")) Ok(Vec::new())
} }
} }
} }
impl Resolve<InspectDockerVolume, User> for State { impl Resolve<ReadArgs> for InspectDockerVolume {
async fn resolve( async fn resolve(
&self, self,
InspectDockerVolume { server, volume }: InspectDockerVolume, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Volume> {
) -> anyhow::Result<Volume> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -736,26 +738,26 @@ impl Resolve<InspectDockerVolume, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if cache.state != ServerState::Ok { if cache.state != ServerState::Ok {
return Err(anyhow!( return Err(
"Cannot inspect volume: server is {:?}", anyhow!("Cannot inspect volume: server is {:?}", cache.state)
cache.state .into(),
)); );
} }
periphery_client(&server)? let res = periphery_client(&server)?
.request(InspectVolume { name: volume }) .request(InspectVolume { name: self.volume })
.await .await?;
Ok(res)
} }
} }
impl ResolveToString<ListComposeProjects, User> for State { impl Resolve<ReadArgs> for ListComposeProjects {
async fn resolve_to_string( async fn resolve(
&self, self,
ListComposeProjects { server }: ListComposeProjects, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListComposeProjectsResponse> {
) -> anyhow::Result<String> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -763,10 +765,9 @@ impl ResolveToString<ListComposeProjects, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if let Some(projects) = &cache.projects { if let Some(projects) = &cache.projects {
serde_json::to_string(projects) Ok(projects.clone())
.context("failed to serialize response")
} else { } else {
Ok(String::from("[]")) Ok(Vec::new())
} }
} }
} }

View File

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

View File

@@ -7,11 +7,10 @@ use komodo_client::{
config::core::CoreConfig, config::core::CoreConfig,
permission::PermissionLevel, permission::PermissionLevel,
stack::{Stack, StackActionState, StackListItem, StackState}, stack::{Stack, StackActionState, StackListItem, StackState},
user::User,
}, },
}; };
use periphery_client::api::compose::{ use periphery_client::api::compose::{
GetComposeServiceLog, GetComposeServiceLogSearch, GetComposeLog, GetComposeLogSearch,
}; };
use resolver_api::Resolve; use resolver_api::Resolve;
@@ -20,33 +19,35 @@ use crate::{
helpers::{periphery_client, query::get_all_tags}, helpers::{periphery_client, query::get_all_tags},
resource, resource,
stack::get_stack_and_server, 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( async fn resolve(
&self, self,
GetStack { stack }: GetStack, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Stack> {
) -> anyhow::Result<Stack> { Ok(
resource::get_check_permissions::<Stack>( resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
)
.await?,
) )
.await
} }
} }
impl Resolve<ListStackServices, User> for State { impl Resolve<ReadArgs> for ListStackServices {
async fn resolve( async fn resolve(
&self, self,
ListStackServices { stack }: ListStackServices, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListStackServicesResponse> {
) -> anyhow::Result<ListStackServicesResponse> {
let stack = resource::get_check_permissions::<Stack>( let stack = resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -63,85 +64,79 @@ impl Resolve<ListStackServices, User> for State {
} }
} }
impl Resolve<GetStackServiceLog, User> for State { impl Resolve<ReadArgs> for GetStackLog {
async fn resolve( async fn resolve(
&self, self,
GetStackServiceLog { ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackLogResponse> {
let GetStackLog {
stack, stack,
service, services,
tail, tail,
timestamps, timestamps,
}: GetStackServiceLog, } = self;
user: User, let (stack, server) =
) -> anyhow::Result<GetStackServiceLogResponse> { get_stack_and_server(&stack, user, PermissionLevel::Read, true)
let (stack, server) = get_stack_and_server( .await?;
&stack, let res = periphery_client(&server)?
&user, .request(GetComposeLog {
PermissionLevel::Read,
true,
)
.await?;
periphery_client(&server)?
.request(GetComposeServiceLog {
project: stack.project_name(false), project: stack.project_name(false),
service, services,
tail, tail,
timestamps, timestamps,
}) })
.await .await
.context("failed to get stack service log from periphery") .context("Failed to get stack log from periphery")?;
Ok(res)
} }
} }
impl Resolve<SearchStackServiceLog, User> for State { impl Resolve<ReadArgs> for SearchStackLog {
async fn resolve( async fn resolve(
&self, self,
SearchStackServiceLog { ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchStackLogResponse> {
let SearchStackLog {
stack, stack,
service, services,
terms, terms,
combinator, combinator,
invert, invert,
timestamps, timestamps,
}: SearchStackServiceLog, } = self;
user: User, let (stack, server) =
) -> anyhow::Result<SearchStackServiceLogResponse> { get_stack_and_server(&stack, user, PermissionLevel::Read, true)
let (stack, server) = get_stack_and_server( .await?;
&stack, let res = periphery_client(&server)?
&user, .request(GetComposeLogSearch {
PermissionLevel::Read,
true,
)
.await?;
periphery_client(&server)?
.request(GetComposeServiceLogSearch {
project: stack.project_name(false), project: stack.project_name(false),
service, services,
terms, terms,
combinator, combinator,
invert, invert,
timestamps, timestamps,
}) })
.await .await
.context("failed to get stack service log from periphery") .context("Failed to search stack log from periphery")?;
Ok(res)
} }
} }
impl Resolve<ListCommonStackExtraArgs, User> for State { impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
async fn resolve( async fn resolve(
&self, self,
ListCommonStackExtraArgs { query }: ListCommonStackExtraArgs, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListCommonStackExtraArgsResponse> {
) -> anyhow::Result<ListCommonStackExtraArgsResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
let stacks = let stacks = resource::list_full_for_user::<Stack>(
resource::list_full_for_user::<Stack>(query, &user, &all_tags) self.query, user, &all_tags,
.await )
.context("failed to get resources matching query")?; .await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness // first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new(); let mut res = HashSet::<String>::new();
@@ -158,21 +153,21 @@ impl Resolve<ListCommonStackExtraArgs, User> for State {
} }
} }
impl Resolve<ListCommonStackBuildExtraArgs, User> for State { impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
async fn resolve( async fn resolve(
&self, self,
ListCommonStackBuildExtraArgs { query }: ListCommonStackBuildExtraArgs, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListCommonStackBuildExtraArgsResponse> {
) -> anyhow::Result<ListCommonStackBuildExtraArgsResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
let stacks = let stacks = resource::list_full_for_user::<Stack>(
resource::list_full_for_user::<Stack>(query, &user, &all_tags) self.query, user, &all_tags,
.await )
.context("failed to get resources matching query")?; .await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness // first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new(); let mut res = HashSet::<String>::new();
@@ -189,46 +184,65 @@ impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
} }
} }
impl Resolve<ListStacks, User> for State { impl Resolve<ReadArgs> for ListStacks {
async fn resolve( async fn resolve(
&self, self,
ListStacks { query }: ListStacks, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Vec<StackListItem>> {
) -> anyhow::Result<Vec<StackListItem>> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_for_user::<Stack>(query, &user, &all_tags).await let only_update_available = self.query.specific.update_available;
let stacks =
resource::list_for_user::<Stack>(self.query, user, &all_tags)
.await?;
let stacks = if only_update_available {
stacks
.into_iter()
.filter(|stack| {
stack
.info
.services
.iter()
.any(|service| service.update_available)
})
.collect()
} else {
stacks
};
Ok(stacks)
} }
} }
impl Resolve<ListFullStacks, User> for State { impl Resolve<ReadArgs> for ListFullStacks {
async fn resolve( async fn resolve(
&self, self,
ListFullStacks { query }: ListFullStacks, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListFullStacksResponse> {
) -> anyhow::Result<ListFullStacksResponse> { let all_tags = if self.query.tags.is_empty() {
let all_tags = if query.tags.is_empty() {
vec![] vec![]
} else { } else {
get_all_tags(None).await? get_all_tags(None).await?
}; };
resource::list_full_for_user::<Stack>(query, &user, &all_tags) Ok(
.await 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( async fn resolve(
&self, self,
GetStackActionState { stack }: GetStackActionState, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<StackActionState> {
) -> anyhow::Result<StackActionState> {
let stack = resource::get_check_permissions::<Stack>( let stack = resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -242,15 +256,14 @@ impl Resolve<GetStackActionState, User> for State {
} }
} }
impl Resolve<GetStacksSummary, User> for State { impl Resolve<ReadArgs> for GetStacksSummary {
async fn resolve( async fn resolve(
&self, self,
GetStacksSummary {}: GetStacksSummary, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetStacksSummaryResponse> {
) -> anyhow::Result<GetStacksSummaryResponse> {
let stacks = resource::list_full_for_user::<Stack>( let stacks = resource::list_full_for_user::<Stack>(
Default::default(), Default::default(),
&user, user,
&[], &[],
) )
.await .await
@@ -276,12 +289,11 @@ impl Resolve<GetStacksSummary, User> for State {
} }
} }
impl Resolve<GetStackWebhooksEnabled, User> for State { impl Resolve<ReadArgs> for GetStackWebhooksEnabled {
async fn resolve( async fn resolve(
&self, self,
GetStackWebhooksEnabled { stack }: GetStackWebhooksEnabled, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<GetStackWebhooksEnabledResponse> {
) -> anyhow::Result<GetStackWebhooksEnabledResponse> {
let Some(github) = github_client() else { let Some(github) = github_client() else {
return Ok(GetStackWebhooksEnabledResponse { return Ok(GetStackWebhooksEnabledResponse {
managed: false, managed: false,
@@ -291,8 +303,8 @@ impl Resolve<GetStackWebhooksEnabled, User> for State {
}; };
let stack = resource::get_check_permissions::<Stack>( let stack = resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;

View File

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

View File

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

View File

@@ -6,12 +6,12 @@ use komodo_client::{
ListUserGroups, ListUserGroups,
}, },
entities::{ entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder, ResourceTarget, action::Action, alerter::Alerter, build::Build,
deployment::Deployment, permission::PermissionLevel, builder::Builder, deployment::Deployment,
procedure::Procedure, repo::Repo, resource::ResourceQuery, permission::PermissionLevel, procedure::Procedure, repo::Repo,
server::Server, server_template::ServerTemplate, stack::Stack, resource::ResourceQuery, server::Server,
server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, user::User, sync::ResourceSync, toml::ResourcesToml, user::User,
ResourceTarget,
}, },
}; };
use mungos::find::find_collect; use mungos::find::find_collect;
@@ -22,183 +22,196 @@ use crate::{
get_all_tags, get_id_to_tags, get_user_user_group_ids, get_all_tags, get_id_to_tags, get_user_user_group_ids,
}, },
resource, resource,
state::{db_client, State}, state::db_client,
sync::{ sync::{
toml::{convert_resource, ToToml, TOML_PRETTY_OPTIONS},
user_groups::convert_user_groups,
AllResourcesById, AllResourcesById,
toml::{TOML_PRETTY_OPTIONS, ToToml, convert_resource},
user_groups::convert_user_groups,
}, },
}; };
impl Resolve<ExportAllResourcesToToml, User> for State { use super::ReadArgs;
async fn resolve(
&self,
ExportAllResourcesToToml { tags }: ExportAllResourcesToToml,
user: User,
) -> anyhow::Result<ExportAllResourcesToTomlResponse> {
let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if tags.is_empty() { async fn get_all_targets(
vec![] tags: &[String],
} else { user: &User,
get_all_tags(None).await? ) -> anyhow::Result<Vec<ResourceTarget>> {
}; let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if tags.is_empty() {
targets.extend( vec![]
resource::list_for_user::<Alerter>( } else {
ResourceQuery::builder().tags(tags.clone()).build(), get_all_tags(None).await?
&user, };
&all_tags, targets.extend(
) resource::list_for_user::<Alerter>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Alerter(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Builder>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Alerter(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Builder>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Builder(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Server>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Builder(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Server>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Server(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Deployment>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Server(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Stack>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Deployment(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Stack>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Stack(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Deployment>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Stack(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Build>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Deployment(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Build>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Build(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Repo>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Build(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Repo>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Repo(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Procedure>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Repo(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Procedure>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Procedure(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<Action>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Procedure(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<Action>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::Action(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_for_user::<ServerTemplate>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::Action(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_for_user::<ServerTemplate>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
.map(|resource| ResourceTarget::ServerTemplate(resource.id)), &all_tags,
); )
targets.extend( .await?
resource::list_full_for_user::<ResourceSync>( .into_iter()
ResourceQuery::builder().tags(tags.clone()).build(), .map(|resource| ResourceTarget::ServerTemplate(resource.id)),
&user, );
&all_tags, targets.extend(
) resource::list_full_for_user::<ResourceSync>(
.await? ResourceQuery::builder().tags(tags).build(),
.into_iter() user,
// These will already be filtered by [ExportResourcesToToml] &all_tags,
.map(|resource| ResourceTarget::ResourceSync(resource.id)), )
); .await?
.into_iter()
let user_groups = if user.admin && tags.is_empty() { // These will already be filtered by [ExportResourcesToToml]
find_collect(&db_client().user_groups, None, None) .map(|resource| ResourceTarget::ResourceSync(resource.id)),
.await );
.context("failed to query db for user groups")? Ok(targets)
.into_iter()
.map(|user_group| user_group.id)
.collect()
} else {
get_user_user_group_ids(&user.id).await?
};
self
.resolve(
ExportResourcesToToml {
targets,
user_groups,
include_variables: tags.is_empty(),
},
user,
)
.await
}
} }
impl Resolve<ExportResourcesToToml, User> for State { impl Resolve<ReadArgs> for ExportAllResourcesToToml {
async fn resolve( async fn resolve(
&self, self,
args: &ReadArgs,
) -> serror::Result<ExportAllResourcesToTomlResponse> {
let targets = if self.include_resources {
get_all_targets(&self.tags, &args.user).await?
} else {
Vec::new()
};
let user_groups = if self.include_user_groups {
if args.user.admin {
find_collect(&db_client().user_groups, None, None)
.await
.context("failed to query db for user groups")?
.into_iter()
.map(|user_group| user_group.id)
.collect()
} else {
get_user_user_group_ids(&args.user.id).await?
}
} else {
Vec::new()
};
ExportResourcesToToml { ExportResourcesToToml {
targets,
user_groups,
include_variables: self.include_variables,
}
.resolve(args)
.await
}
}
impl Resolve<ReadArgs> for ExportResourcesToToml {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<ExportResourcesToTomlResponse> {
let ExportResourcesToToml {
targets, targets,
user_groups, user_groups,
include_variables, include_variables,
}: ExportResourcesToToml, } = self;
user: User,
) -> anyhow::Result<ExportResourcesToTomlResponse> {
let mut res = ResourcesToml::default(); let mut res = ResourcesToml::default();
let all = AllResourcesById::load().await?; let all = AllResourcesById::load().await?;
let id_to_tags = get_id_to_tags(None).await?; let id_to_tags = get_id_to_tags(None).await?;
let ReadArgs { user } = args;
for target in targets { for target in targets {
match target { match target {
ResourceTarget::Alerter(id) => { ResourceTarget::Alerter(id) => {
let alerter = resource::get_check_permissions::<Alerter>( let alerter = resource::get_check_permissions::<Alerter>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -212,7 +225,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
ResourceTarget::ResourceSync(id) => { ResourceTarget::ResourceSync(id) => {
let sync = resource::get_check_permissions::<ResourceSync>( let sync = resource::get_check_permissions::<ResourceSync>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -231,9 +244,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
ResourceTarget::ServerTemplate(id) => { ResourceTarget::ServerTemplate(id) => {
let template = resource::get_check_permissions::< let template = resource::get_check_permissions::<
ServerTemplate, ServerTemplate,
>( >(&id, user, PermissionLevel::Read)
&id, &user, PermissionLevel::Read
)
.await?; .await?;
res.server_templates.push( res.server_templates.push(
convert_resource::<ServerTemplate>( convert_resource::<ServerTemplate>(
@@ -247,7 +258,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
ResourceTarget::Server(id) => { ResourceTarget::Server(id) => {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -262,7 +273,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
let mut builder = let mut builder =
resource::get_check_permissions::<Builder>( resource::get_check_permissions::<Builder>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -277,7 +288,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
ResourceTarget::Build(id) => { ResourceTarget::Build(id) => {
let mut build = resource::get_check_permissions::<Build>( let mut build = resource::get_check_permissions::<Build>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -293,7 +304,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
let mut deployment = resource::get_check_permissions::< let mut deployment = resource::get_check_permissions::<
Deployment, Deployment,
>( >(
&id, &user, PermissionLevel::Read &id, user, PermissionLevel::Read
) )
.await?; .await?;
Deployment::replace_ids(&mut deployment, &all); Deployment::replace_ids(&mut deployment, &all);
@@ -307,7 +318,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
ResourceTarget::Repo(id) => { ResourceTarget::Repo(id) => {
let mut repo = resource::get_check_permissions::<Repo>( let mut repo = resource::get_check_permissions::<Repo>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -322,7 +333,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
ResourceTarget::Stack(id) => { ResourceTarget::Stack(id) => {
let mut stack = resource::get_check_permissions::<Stack>( let mut stack = resource::get_check_permissions::<Stack>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -338,7 +349,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
let mut procedure = resource::get_check_permissions::< let mut procedure = resource::get_check_permissions::<
Procedure, Procedure,
>( >(
&id, &user, PermissionLevel::Read &id, user, PermissionLevel::Read
) )
.await?; .await?;
Procedure::replace_ids(&mut procedure, &all); Procedure::replace_ids(&mut procedure, &all);
@@ -352,7 +363,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
ResourceTarget::Action(id) => { ResourceTarget::Action(id) => {
let mut action = resource::get_check_permissions::<Action>( let mut action = resource::get_check_permissions::<Action>(
&id, &id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -368,7 +379,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
}; };
} }
add_user_groups(user_groups, &mut res, &all, &user) add_user_groups(user_groups, &mut res, &all, args)
.await .await
.context("failed to add user groups")?; .context("failed to add user groups")?;
@@ -398,11 +409,12 @@ async fn add_user_groups(
user_groups: Vec<String>, user_groups: Vec<String>,
res: &mut ResourcesToml, res: &mut ResourcesToml,
all: &AllResourcesById, all: &AllResourcesById,
user: &User, args: &ReadArgs,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let user_groups = State let user_groups = ListUserGroups {}
.resolve(ListUserGroups {}, user.clone()) .resolve(args)
.await? .await
.map_err(|e| e.error)?
.into_iter() .into_iter()
.filter(|ug| { .filter(|ug| {
user_groups.contains(&ug.name) || user_groups.contains(&ug.id) user_groups.contains(&ug.name) || user_groups.contains(&ug.id)

View File

@@ -1,9 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use komodo_client::{ use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse}, api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{ entities::{
ResourceTarget,
action::Action, action::Action,
alerter::Alerter, alerter::Alerter,
build::Build, build::Build,
@@ -18,7 +19,6 @@ use komodo_client::{
sync::ResourceSync, sync::ResourceSync,
update::{Update, UpdateListItem}, update::{Update, UpdateListItem},
user::User, user::User,
ResourceTarget,
}, },
}; };
use mungos::{ use mungos::{
@@ -28,25 +28,22 @@ use mungos::{
}; };
use resolver_api::Resolve; use resolver_api::Resolve;
use crate::{ use crate::{config::core_config, resource, state::db_client};
config::core_config,
resource, use super::ReadArgs;
state::{db_client, State},
};
const UPDATES_PER_PAGE: i64 = 100; const UPDATES_PER_PAGE: i64 = 100;
impl Resolve<ListUpdates, User> for State { impl Resolve<ReadArgs> for ListUpdates {
async fn resolve( async fn resolve(
&self, self,
ListUpdates { query, page }: ListUpdates, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<ListUpdatesResponse> {
) -> anyhow::Result<ListUpdatesResponse> {
let query = if user.admin || core_config().transparent_mode { let query = if user.admin || core_config().transparent_mode {
query self.query
} else { } else {
let server_query = let server_query =
resource::get_resource_ids_for_user::<Server>(&user) resource::get_resource_ids_for_user::<Server>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -56,7 +53,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Server" }); .unwrap_or_else(|| doc! { "target.type": "Server" });
let deployment_query = let deployment_query =
resource::get_resource_ids_for_user::<Deployment>(&user) resource::get_resource_ids_for_user::<Deployment>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -66,7 +63,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Deployment" }); .unwrap_or_else(|| doc! { "target.type": "Deployment" });
let stack_query = let stack_query =
resource::get_resource_ids_for_user::<Stack>(&user) resource::get_resource_ids_for_user::<Stack>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -76,7 +73,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Stack" }); .unwrap_or_else(|| doc! { "target.type": "Stack" });
let build_query = let build_query =
resource::get_resource_ids_for_user::<Build>(&user) resource::get_resource_ids_for_user::<Build>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -86,7 +83,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Build" }); .unwrap_or_else(|| doc! { "target.type": "Build" });
let repo_query = let repo_query =
resource::get_resource_ids_for_user::<Repo>(&user) resource::get_resource_ids_for_user::<Repo>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -96,7 +93,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Repo" }); .unwrap_or_else(|| doc! { "target.type": "Repo" });
let procedure_query = let procedure_query =
resource::get_resource_ids_for_user::<Procedure>(&user) resource::get_resource_ids_for_user::<Procedure>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -106,7 +103,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Procedure" }); .unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query = let action_query =
resource::get_resource_ids_for_user::<Action>(&user) resource::get_resource_ids_for_user::<Action>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -116,7 +113,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Action" }); .unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query = let builder_query =
resource::get_resource_ids_for_user::<Builder>(&user) resource::get_resource_ids_for_user::<Builder>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -126,7 +123,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Builder" }); .unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query = let alerter_query =
resource::get_resource_ids_for_user::<Alerter>(&user) resource::get_resource_ids_for_user::<Alerter>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -136,7 +133,7 @@ impl Resolve<ListUpdates, User> for State {
.unwrap_or_else(|| doc! { "target.type": "Alerter" }); .unwrap_or_else(|| doc! { "target.type": "Alerter" });
let server_template_query = let server_template_query =
resource::get_resource_ids_for_user::<ServerTemplate>(&user) resource::get_resource_ids_for_user::<ServerTemplate>(user)
.await? .await?
.map(|ids| { .map(|ids| {
doc! { doc! {
@@ -147,7 +144,7 @@ impl Resolve<ListUpdates, User> for State {
let resource_sync_query = let resource_sync_query =
resource::get_resource_ids_for_user::<ResourceSync>( resource::get_resource_ids_for_user::<ResourceSync>(
&user, user,
) )
.await? .await?
.map(|ids| { .map(|ids| {
@@ -157,7 +154,7 @@ impl Resolve<ListUpdates, User> for State {
}) })
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" }); .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! { query.extend(doc! {
"$or": [ "$or": [
server_query, server_query,
@@ -188,7 +185,7 @@ impl Resolve<ListUpdates, User> for State {
query, query,
FindOptions::builder() FindOptions::builder()
.sort(doc! { "start_ts": -1 }) .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) .limit(UPDATES_PER_PAGE)
.build(), .build(),
) )
@@ -220,7 +217,7 @@ impl Resolve<ListUpdates, User> for State {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let next_page = if updates.len() == UPDATES_PER_PAGE as usize { let next_page = if updates.len() == UPDATES_PER_PAGE as usize {
Some(page + 1) Some(self.page + 1)
} else { } else {
None None
}; };
@@ -229,13 +226,12 @@ impl Resolve<ListUpdates, User> for State {
} }
} }
impl Resolve<GetUpdate, User> for State { impl Resolve<ReadArgs> for GetUpdate {
async fn resolve( async fn resolve(
&self, self,
GetUpdate { id }: GetUpdate, ReadArgs { user }: &ReadArgs,
user: User, ) -> serror::Result<Update> {
) -> anyhow::Result<Update> { let update = find_one_by_id(&db_client().updates, &self.id)
let update = find_one_by_id(&db_client().updates, &id)
.await .await
.context("failed to query to db")? .context("failed to query to db")?
.context("no update exists with given id")?; .context("no update exists with given id")?;
@@ -244,14 +240,14 @@ impl Resolve<GetUpdate, User> for State {
} }
match &update.target { match &update.target {
ResourceTarget::System(_) => { ResourceTarget::System(_) => {
return Err(anyhow!( return Err(
"user must be admin to view system updates" anyhow!("user must be admin to view system updates").into(),
)) );
} }
ResourceTarget::Server(id) => { ResourceTarget::Server(id) => {
resource::get_check_permissions::<Server>( resource::get_check_permissions::<Server>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -259,7 +255,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Deployment(id) => { ResourceTarget::Deployment(id) => {
resource::get_check_permissions::<Deployment>( resource::get_check_permissions::<Deployment>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -267,7 +263,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Build(id) => { ResourceTarget::Build(id) => {
resource::get_check_permissions::<Build>( resource::get_check_permissions::<Build>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -275,7 +271,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Repo(id) => { ResourceTarget::Repo(id) => {
resource::get_check_permissions::<Repo>( resource::get_check_permissions::<Repo>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -283,7 +279,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Builder(id) => { ResourceTarget::Builder(id) => {
resource::get_check_permissions::<Builder>( resource::get_check_permissions::<Builder>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -291,7 +287,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Alerter(id) => { ResourceTarget::Alerter(id) => {
resource::get_check_permissions::<Alerter>( resource::get_check_permissions::<Alerter>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -299,7 +295,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Procedure(id) => { ResourceTarget::Procedure(id) => {
resource::get_check_permissions::<Procedure>( resource::get_check_permissions::<Procedure>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -307,7 +303,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Action(id) => { ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>( resource::get_check_permissions::<Action>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -315,7 +311,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::ServerTemplate(id) => { ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>( resource::get_check_permissions::<ServerTemplate>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -323,7 +319,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::ResourceSync(id) => { ResourceTarget::ResourceSync(id) => {
resource::get_check_permissions::<ResourceSync>( resource::get_check_permissions::<ResourceSync>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;
@@ -331,7 +327,7 @@ impl Resolve<GetUpdate, User> for State {
ResourceTarget::Stack(id) => { ResourceTarget::Stack(id) => {
resource::get_check_permissions::<Stack>( resource::get_check_permissions::<Stack>(
id, id,
&user, user,
PermissionLevel::Read, PermissionLevel::Read,
) )
.await?; .await?;

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

@@ -1,7 +1,8 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use komodo_client::{ use komodo_client::{
api::write::*, api::write::*,
entities::{ entities::{
Operation,
deployment::{ deployment::{
Deployment, DeploymentImage, DeploymentState, Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode, PartialDeploymentConfig, RestartMode,
@@ -12,8 +13,6 @@ use komodo_client::{
server::{Server, ServerState}, server::{Server, ServerState},
to_komodo_name, to_komodo_name,
update::Update, update::Update,
user::User,
Operation,
}, },
}; };
use mungos::{by_id::update_one_by_id, mongodb::bson::doc}; use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
@@ -27,51 +26,53 @@ use crate::{
update::{add_update, make_update}, update::{add_update, make_update},
}, },
resource, resource,
state::{action_states, db_client, server_status_cache, State}, state::{action_states, db_client, server_status_cache},
}; };
impl Resolve<CreateDeployment, User> for State { use super::WriteArgs;
#[instrument(name = "CreateDeployment", skip(self, user))]
impl Resolve<WriteArgs> for CreateDeployment {
#[instrument(name = "CreateDeployment", skip(user))]
async fn resolve( async fn resolve(
&self, self,
CreateDeployment { name, config }: CreateDeployment, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Deployment> {
) -> anyhow::Result<Deployment> { Ok(
resource::create::<Deployment>(&name, config, &user).await resource::create::<Deployment>(&self.name, self.config, user)
.await?,
)
} }
} }
impl Resolve<CopyDeployment, User> for State { impl Resolve<WriteArgs> for CopyDeployment {
#[instrument(name = "CopyDeployment", skip(self, user))] #[instrument(name = "CopyDeployment", skip(user))]
async fn resolve( async fn resolve(
&self, self,
CopyDeployment { name, id }: CopyDeployment, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Deployment> {
) -> anyhow::Result<Deployment> {
let Deployment { config, .. } = let Deployment { config, .. } =
resource::get_check_permissions::<Deployment>( resource::get_check_permissions::<Deployment>(
&id, &self.id,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
) )
.await?; .await?;
resource::create::<Deployment>(&name, config.into(), &user).await Ok(
resource::create::<Deployment>(&self.name, config.into(), user)
.await?,
)
} }
} }
impl Resolve<CreateDeploymentFromContainer, User> for State { impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
#[instrument( #[instrument(name = "CreateDeploymentFromContainer", skip(user))]
name = "CreateDeploymentFromContainer",
skip(self, user)
)]
async fn resolve( async fn resolve(
&self, self,
CreateDeploymentFromContainer { name, server }: CreateDeploymentFromContainer, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Deployment> {
) -> anyhow::Result<Deployment> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
) )
.await?; .await?;
@@ -79,13 +80,18 @@ impl Resolve<CreateDeploymentFromContainer, User> for State {
.get_or_insert_default(&server.id) .get_or_insert_default(&server.id)
.await; .await;
if cache.state != ServerState::Ok { if cache.state != ServerState::Ok {
return Err(anyhow!( return Err(
"Cannot inspect container: server is {:?}", anyhow!(
cache.state "Cannot inspect container: server is {:?}",
)); cache.state
)
.into(),
);
} }
let container = periphery_client(&server)? let container = periphery_client(&server)?
.request(InspectContainer { name: name.clone() }) .request(InspectContainer {
name: self.name.clone(),
})
.await .await
.context("Failed to inspect container")?; .context("Failed to inspect container")?;
@@ -146,42 +152,45 @@ impl Resolve<CreateDeploymentFromContainer, User> for State {
}); });
} }
resource::create::<Deployment>(&name, config, &user).await Ok(
resource::create::<Deployment>(&self.name, config, user)
.await?,
)
} }
} }
impl Resolve<DeleteDeployment, User> for State { impl Resolve<WriteArgs> for DeleteDeployment {
#[instrument(name = "DeleteDeployment", skip(self, user))] #[instrument(name = "DeleteDeployment", skip(args))]
async fn resolve( async fn resolve(
&self, self,
DeleteDeployment { id }: DeleteDeployment, args: &WriteArgs,
user: User, ) -> serror::Result<Deployment> {
) -> anyhow::Result<Deployment> { Ok(resource::delete::<Deployment>(&self.id, args).await?)
resource::delete::<Deployment>(&id, &user).await
} }
} }
impl Resolve<UpdateDeployment, User> for State { impl Resolve<WriteArgs> for UpdateDeployment {
#[instrument(name = "UpdateDeployment", skip(self, user))] #[instrument(name = "UpdateDeployment", skip(user))]
async fn resolve( async fn resolve(
&self, self,
UpdateDeployment { id, config }: UpdateDeployment, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Deployment> {
) -> anyhow::Result<Deployment> { Ok(
resource::update::<Deployment>(&id, config, &user).await resource::update::<Deployment>(&self.id, self.config, user)
.await?,
)
} }
} }
impl Resolve<RenameDeployment, User> for State { impl Resolve<WriteArgs> for RenameDeployment {
#[instrument(name = "RenameDeployment", skip(self, user))] #[instrument(name = "RenameDeployment", skip(user))]
async fn resolve( async fn resolve(
&self, self,
RenameDeployment { id, name }: RenameDeployment, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let deployment = resource::get_check_permissions::<Deployment>( let deployment = resource::get_check_permissions::<Deployment>(
&id, &self.id,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
) )
.await?; .await?;
@@ -197,18 +206,21 @@ impl Resolve<RenameDeployment, User> for State {
let _action_guard = let _action_guard =
action_state.update(|state| state.renaming = true)?; 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?; let container_state = get_deployment_state(&deployment).await?;
if container_state == DeploymentState::Unknown { if container_state == DeploymentState::Unknown {
return Err(anyhow!( return Err(
"Cannot rename Deployment when container status is unknown" anyhow!(
)); "Cannot rename Deployment when container status is unknown"
)
.into(),
);
} }
let mut update = let mut update =
make_update(&deployment, Operation::RenameDeployment, &user); make_update(&deployment, Operation::RenameDeployment, user);
update_one_by_id( update_one_by_id(
&db_client().deployments, &db_client().deployments,

View File

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

View File

@@ -1,17 +1,17 @@
use std::time::Instant; use std::time::Instant;
use anyhow::{anyhow, Context}; use anyhow::Context;
use axum::{middleware, routing::post, Extension, Router}; use axum::{Extension, Router, middleware, routing::post};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::{EnumVariants, ExtractVariant}; use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::write::*, entities::user::User}; use komodo_client::{api::write::*, entities::user::User};
use resolver_api::{derive::Resolver, Resolver}; use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serror::Json; use serror::Json;
use typeshare::typeshare; use typeshare::typeshare;
use uuid::Uuid; use uuid::Uuid;
use crate::{auth::auth_request, state::State}; use crate::auth::auth_request;
mod action; mod action;
mod alerter; mod alerter;
@@ -33,13 +33,18 @@ mod user;
mod user_group; mod user_group;
mod variable; mod variable;
pub struct WriteArgs {
pub user: User,
}
#[typeshare] #[typeshare]
#[derive( #[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants, Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
)] )]
#[variant_derive(Debug)] #[variant_derive(Debug)]
#[resolver_target(State)] #[args(WriteArgs)]
#[resolver_args(User)] #[response(Response)]
#[error(serror::Error)]
#[serde(tag = "type", content = "params")] #[serde(tag = "type", content = "params")]
pub enum WriteRequest { pub enum WriteRequest {
// ==== USER ==== // ==== USER ====
@@ -167,6 +172,7 @@ pub enum WriteRequest {
CreateTag(CreateTag), CreateTag(CreateTag),
DeleteTag(DeleteTag), DeleteTag(DeleteTag),
RenameTag(RenameTag), RenameTag(RenameTag),
UpdateTagColor(UpdateTagColor),
UpdateTagsOnResource(UpdateTagsOnResource), UpdateTagsOnResource(UpdateTagsOnResource),
// ==== VARIABLE ==== // ==== VARIABLE ====
@@ -194,7 +200,7 @@ pub fn router() -> Router {
async fn handler( async fn handler(
Extension(user): Extension<User>, Extension(user): Extension<User>,
Json(request): Json<WriteRequest>, Json(request): Json<WriteRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> { ) -> serror::Result<axum::response::Response> {
let req_id = Uuid::new_v4(); let req_id = Uuid::new_v4();
let res = tokio::spawn(task(req_id, request, user)) let res = tokio::spawn(task(req_id, request, user))
@@ -205,7 +211,7 @@ async fn handler(
warn!("/write request {req_id} spawn error: {e:#}"); warn!("/write request {req_id} spawn error: {e:#}");
} }
Ok((TypedHeader(ContentType::json()), res??)) res?
} }
#[instrument( #[instrument(
@@ -220,28 +226,19 @@ async fn task(
req_id: Uuid, req_id: Uuid,
request: WriteRequest, request: WriteRequest,
user: User, user: User,
) -> anyhow::Result<String> { ) -> serror::Result<axum::response::Response> {
info!("/write request | user: {}", user.username); info!("/write request | user: {}", user.username);
let timer = Instant::now(); let timer = Instant::now();
let res = let res = request.resolve(&WriteArgs { user }).await;
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,
});
if let Err(e) = &res { if let Err(e) = &res {
warn!("/write request {req_id} error: {e:#}"); warn!("/write request {req_id} error: {:#}", e.error);
} }
let elapsed = timer.elapsed(); let elapsed = timer.elapsed();
debug!("/write request {req_id} | resolve time: {elapsed:?}"); debug!("/write request {req_id} | resolve time: {elapsed:?}");
res res.map(|res| res.0)
} }

View File

@@ -1,60 +1,56 @@
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use komodo_client::{ use komodo_client::{
api::write::{ api::write::*,
UpdatePermissionOnResourceType,
UpdatePermissionOnResourceTypeResponse, UpdatePermissionOnTarget,
UpdatePermissionOnTargetResponse, UpdateUserAdmin,
UpdateUserAdminResponse, UpdateUserBasePermissions,
UpdateUserBasePermissionsResponse,
},
entities::{ entities::{
permission::{UserTarget, UserTargetVariant},
user::User,
ResourceTarget, ResourceTargetVariant, ResourceTarget, ResourceTargetVariant,
permission::{UserTarget, UserTargetVariant},
}, },
}; };
use mungos::{ use mungos::{
by_id::{find_one_by_id, update_one_by_id}, by_id::{find_one_by_id, update_one_by_id},
mongodb::{ mongodb::{
bson::{doc, oid::ObjectId, Document}, bson::{Document, doc, oid::ObjectId},
options::UpdateOptions, options::UpdateOptions,
}, },
}; };
use resolver_api::Resolve; use resolver_api::Resolve;
use crate::{ use crate::{helpers::query::get_user, state::db_client};
helpers::query::get_user,
state::{db_client, State},
};
impl Resolve<UpdateUserAdmin, User> for State { use super::WriteArgs;
impl Resolve<WriteArgs> for UpdateUserAdmin {
#[instrument(name = "UpdateUserAdmin", skip(super_admin))]
async fn resolve( async fn resolve(
&self, self,
UpdateUserAdmin { user_id, admin }: UpdateUserAdmin, WriteArgs { user: super_admin }: &WriteArgs,
super_admin: User, ) -> serror::Result<UpdateUserAdminResponse> {
) -> anyhow::Result<UpdateUserAdminResponse> {
if !super_admin.super_admin { 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 .await
.context("failed to query mongo for user")? .context("failed to query mongo for user")?
.context("did not find user with given id")?; .context("did not find user with given id")?;
if !user.enabled { 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 { 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( update_one_by_id(
&db_client().users, &db_client().users,
&user_id, &self.user_id,
doc! { "$set": { "admin": admin } }, doc! { "$set": { "admin": self.admin } },
None, None,
) )
.await?; .await?;
@@ -63,20 +59,21 @@ impl Resolve<UpdateUserAdmin, User> for State {
} }
} }
impl Resolve<UpdateUserBasePermissions, User> for State { impl Resolve<WriteArgs> for UpdateUserBasePermissions {
#[instrument(name = "UpdateUserBasePermissions", skip(self, admin))] #[instrument(name = "UpdateUserBasePermissions", skip(admin))]
async fn resolve( async fn resolve(
&self, self,
UpdateUserBasePermissions { WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdateUserBasePermissionsResponse> {
let UpdateUserBasePermissions {
user_id, user_id,
enabled, enabled,
create_servers, create_servers,
create_builds, create_builds,
}: UpdateUserBasePermissions, } = self;
admin: User,
) -> anyhow::Result<UpdateUserBasePermissionsResponse> {
if !admin.admin { 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) 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("failed to query mongo for user")?
.context("did not find user with given id")?; .context("did not find user with given id")?;
if user.super_admin { if user.super_admin {
return Err(anyhow!( return Err(
"Cannot use this method to update super admins permissions" anyhow!(
)); "Cannot use this method to update super admins permissions"
)
.into(),
);
} }
if user.admin && !admin.super_admin { if user.admin && !admin.super_admin {
return Err(anyhow!( return Err(anyhow!(
"Only super admins can use this method to update other admins permissions" "Only super admins can use this method to update other admins permissions"
)); ).into());
} }
let mut update_doc = Document::new(); let mut update_doc = Document::new();
if let Some(enabled) = enabled { if let Some(enabled) = enabled {
@@ -116,34 +116,35 @@ impl Resolve<UpdateUserBasePermissions, User> for State {
} }
} }
impl Resolve<UpdatePermissionOnResourceType, User> for State { impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
#[instrument( #[instrument(name = "UpdatePermissionOnResourceType", skip(admin))]
name = "UpdatePermissionOnResourceType",
skip(self, admin)
)]
async fn resolve( async fn resolve(
&self, self,
UpdatePermissionOnResourceType { WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {
let UpdatePermissionOnResourceType {
user_target, user_target,
resource_type, resource_type,
permission, permission,
}: UpdatePermissionOnResourceType, } = self;
admin: User,
) -> anyhow::Result<UpdatePermissionOnResourceTypeResponse> {
if !admin.admin { 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 // Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target { if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?; let user = get_user(user_id).await?;
if user.admin { if user.admin {
return Err(anyhow!( return Err(
anyhow!(
"cannot use this method to update other admins permissions" "cannot use this method to update other admins permissions"
)); )
.into(),
);
} }
if !user.enabled { 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 { impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
#[instrument(name = "UpdatePermissionOnTarget", skip(self, admin))] #[instrument(name = "UpdatePermissionOnTarget", skip(admin))]
async fn resolve( async fn resolve(
&self, self,
UpdatePermissionOnTarget { WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnTargetResponse> {
let UpdatePermissionOnTarget {
user_target, user_target,
resource_target, resource_target,
permission, permission,
}: UpdatePermissionOnTarget, } = self;
admin: User,
) -> anyhow::Result<UpdatePermissionOnTargetResponse> {
if !admin.admin { 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 // Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target { if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?; let user = get_user(user_id).await?;
if user.admin { if user.admin {
return Err(anyhow!( return Err(
anyhow!(
"cannot use this method to update other admins permissions" "cannot use this method to update other admins permissions"
)); )
.into(),
);
} }
if !user.enabled { 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. /// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_user_target_with_validation( async fn extract_user_target_with_validation(
user_target: &UserTarget, user_target: &UserTarget,
) -> anyhow::Result<(UserTargetVariant, String)> { ) -> serror::Result<(UserTargetVariant, String)> {
match user_target { match user_target {
UserTarget::User(ident) => { UserTarget::User(ident) => {
let filter = match ObjectId::from_str(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. /// checks if inner id is actually a `name`, and replaces it with id if so.
async fn extract_resource_target_with_validation( async fn extract_resource_target_with_validation(
resource_target: &ResourceTarget, resource_target: &ResourceTarget,
) -> anyhow::Result<(ResourceTargetVariant, String)> { ) -> serror::Result<(ResourceTargetVariant, String)> {
match resource_target { match resource_target {
ResourceTarget::System(_) => { ResourceTarget::System(_) => {
let res = resource_target.extract_variant_id(); let res = resource_target.extract_variant_id();

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,10 @@ use formatting::format_serror;
use komodo_client::{ use komodo_client::{
api::write::*, api::write::*,
entities::{ entities::{
Operation,
permission::PermissionLevel, permission::PermissionLevel,
server::Server, server::Server,
update::{Update, UpdateStatus}, update::{Update, UpdateStatus},
user::User,
Operation,
}, },
}; };
use periphery_client::api; use periphery_client::api;
@@ -18,63 +17,59 @@ use crate::{
update::{add_update, make_update, update_update}, update::{add_update, make_update, update_update},
}, },
resource, resource,
state::State,
}; };
impl Resolve<CreateServer, User> for State { use super::WriteArgs;
#[instrument(name = "CreateServer", skip(self, user))]
impl Resolve<WriteArgs> for CreateServer {
#[instrument(name = "CreateServer", skip(user))]
async fn resolve( async fn resolve(
&self, self,
CreateServer { name, config }: CreateServer, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Server> {
) -> anyhow::Result<Server> { Ok(
resource::create::<Server>(&name, config, &user).await resource::create::<Server>(&self.name, self.config, user)
.await?,
)
} }
} }
impl Resolve<DeleteServer, User> for State { impl Resolve<WriteArgs> for DeleteServer {
#[instrument(name = "DeleteServer", skip(self, user))] #[instrument(name = "DeleteServer", skip(args))]
async fn resolve( async fn resolve(self, args: &WriteArgs) -> serror::Result<Server> {
&self, Ok(resource::delete::<Server>(&self.id, args).await?)
DeleteServer { id }: DeleteServer,
user: User,
) -> anyhow::Result<Server> {
resource::delete::<Server>(&id, &user).await
} }
} }
impl Resolve<UpdateServer, User> for State { impl Resolve<WriteArgs> for UpdateServer {
#[instrument(name = "UpdateServer", skip(self, user))] #[instrument(name = "UpdateServer", skip(user))]
async fn resolve( async fn resolve(
&self, self,
UpdateServer { id, config }: UpdateServer, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Server> {
) -> anyhow::Result<Server> { Ok(resource::update::<Server>(&self.id, self.config, user).await?)
resource::update::<Server>(&id, config, &user).await
} }
} }
impl Resolve<RenameServer, User> for State { impl Resolve<WriteArgs> for RenameServer {
#[instrument(name = "RenameServer", skip(self, user))] #[instrument(name = "RenameServer", skip(user))]
async fn resolve( async fn resolve(
&self, self,
RenameServer { id, name }: RenameServer, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Update> {
) -> anyhow::Result<Update> { Ok(resource::rename::<Server>(&self.id, &self.name, user).await?)
resource::rename::<Server>(&id, &name, &user).await
} }
} }
impl Resolve<CreateNetwork, User> for State { impl Resolve<WriteArgs> for CreateNetwork {
#[instrument(name = "CreateNetwork", skip(self, user))] #[instrument(name = "CreateNetwork", skip(user))]
async fn resolve( async fn resolve(
&self, self,
CreateNetwork { server, name }: CreateNetwork, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Update> {
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>( let server = resource::get_check_permissions::<Server>(
&server, &self.server,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
) )
.await?; .await?;
@@ -82,12 +77,15 @@ impl Resolve<CreateNetwork, User> for State {
let periphery = periphery_client(&server)?; let periphery = periphery_client(&server)?;
let mut update = let mut update =
make_update(&server, Operation::CreateNetwork, &user); make_update(&server, Operation::CreateNetwork, user);
update.status = UpdateStatus::InProgress; update.status = UpdateStatus::InProgress;
update.id = add_update(update.clone()).await?; update.id = add_update(update.clone()).await?;
match periphery match periphery
.request(api::network::CreateNetwork { name, driver: None }) .request(api::network::CreateNetwork {
name: self.name,
driver: None,
})
.await .await
{ {
Ok(log) => update.logs.push(log), Ok(log) => update.logs.push(log),

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use formatting::format_serror; use formatting::format_serror;
use komodo_client::{ use komodo_client::{
api::write::*, api::write::*,
entities::{ entities::{
FileContents, NoData, Operation,
config::core::CoreConfig, config::core::CoreConfig,
permission::PermissionLevel, permission::PermissionLevel,
server::ServerState, server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo}, stack::{PartialStackConfig, Stack, StackInfo},
update::Update, update::Update,
user::{stack_user, User}, user::stack_user,
FileContents, NoData, Operation,
}, },
}; };
use mungos::mongodb::bson::{doc, to_document}; use mungos::mongodb::bson::{doc, to_document};
@@ -33,87 +33,88 @@ use crate::{
resource, resource,
stack::{ stack::{
get_stack_and_server, get_stack_and_server,
remote::{get_repo_compose_contents, RemoteComposeContents}, remote::{RemoteComposeContents, get_repo_compose_contents},
services::extract_services_into_res, services::extract_services_into_res,
}, },
state::{db_client, github_client, State}, state::{db_client, github_client},
}; };
impl Resolve<CreateStack, User> for State { use super::WriteArgs;
#[instrument(name = "CreateStack", skip(self, user))]
impl Resolve<WriteArgs> for CreateStack {
#[instrument(name = "CreateStack", skip(user))]
async fn resolve( async fn resolve(
&self, self,
CreateStack { name, config }: CreateStack, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Stack> {
) -> anyhow::Result<Stack> { Ok(
resource::create::<Stack>(&name, config, &user).await resource::create::<Stack>(&self.name, self.config, user)
.await?,
)
} }
} }
impl Resolve<CopyStack, User> for State { impl Resolve<WriteArgs> for CopyStack {
#[instrument(name = "CopyStack", skip(self, user))] #[instrument(name = "CopyStack", skip(user))]
async fn resolve( async fn resolve(
&self, self,
CopyStack { name, id }: CopyStack, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Stack> {
) -> anyhow::Result<Stack> {
let Stack { config, .. } = let Stack { config, .. } =
resource::get_check_permissions::<Stack>( resource::get_check_permissions::<Stack>(
&id, &self.id,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
) )
.await?; .await?;
resource::create::<Stack>(&name, config.into(), &user).await Ok(
resource::create::<Stack>(&self.name, config.into(), user)
.await?,
)
} }
} }
impl Resolve<DeleteStack, User> for State { impl Resolve<WriteArgs> for DeleteStack {
#[instrument(name = "DeleteStack", skip(self, user))] #[instrument(name = "DeleteStack", skip(args))]
async fn resolve( async fn resolve(self, args: &WriteArgs) -> serror::Result<Stack> {
&self, Ok(resource::delete::<Stack>(&self.id, args).await?)
DeleteStack { id }: DeleteStack,
user: User,
) -> anyhow::Result<Stack> {
resource::delete::<Stack>(&id, &user).await
} }
} }
impl Resolve<UpdateStack, User> for State { impl Resolve<WriteArgs> for UpdateStack {
#[instrument(name = "UpdateStack", skip(self, user))] #[instrument(name = "UpdateStack", skip(user))]
async fn resolve( async fn resolve(
&self, self,
UpdateStack { id, config }: UpdateStack, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Stack> {
) -> anyhow::Result<Stack> { Ok(resource::update::<Stack>(&self.id, self.config, user).await?)
resource::update::<Stack>(&id, config, &user).await
} }
} }
impl Resolve<RenameStack, User> for State { impl Resolve<WriteArgs> for RenameStack {
#[instrument(name = "RenameStack", skip(self, user))] #[instrument(name = "RenameStack", skip(user))]
async fn resolve( async fn resolve(
&self, self,
RenameStack { id, name }: RenameStack, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<Update> {
) -> anyhow::Result<Update> { Ok(resource::rename::<Stack>(&self.id, &self.name, user).await?)
resource::rename::<Stack>(&id, &name, &user).await
} }
} }
impl Resolve<WriteStackFileContents, User> for State { impl Resolve<WriteArgs> for WriteStackFileContents {
#[instrument(name = "WriteStackFileContents", skip(user))]
async fn resolve( async fn resolve(
&self, self,
WriteStackFileContents { WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let WriteStackFileContents {
stack, stack,
file_path, file_path,
contents, contents,
}: WriteStackFileContents, } = self;
user: User,
) -> anyhow::Result<Update> {
let (mut stack, server) = get_stack_and_server( let (mut stack, server) = get_stack_and_server(
&stack, &stack,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
true, true,
) )
@@ -122,11 +123,11 @@ impl Resolve<WriteStackFileContents, User> for State {
if !stack.config.files_on_host && stack.config.repo.is_empty() { if !stack.config.files_on_host && stack.config.repo.is_empty() {
return Err(anyhow!( return Err(anyhow!(
"Stack is not configured to use Files on Host or Git Repo, can't write file contents" "Stack is not configured to use Files on Host or Git Repo, can't write file contents"
)); ).into());
} }
let mut update = let mut update =
make_update(&stack, Operation::WriteStackContents, &user); make_update(&stack, Operation::WriteStackContents, user);
update.push_simple_log("File contents to write", &contents); update.push_simple_log("File contents to write", &contents);
@@ -148,7 +149,7 @@ impl Resolve<WriteStackFileContents, User> for State {
} }
Err(e) => { Err(e) => {
update.push_error_log( update.push_error_log(
"Write file contents", "Write File Contents",
format_serror(&e.into()), format_serror(&e.into()),
); );
} }
@@ -173,7 +174,7 @@ impl Resolve<WriteStackFileContents, User> for State {
match periphery_client(&server)? match periphery_client(&server)?
.request(WriteCommitComposeContents { .request(WriteCommitComposeContents {
stack, stack,
username: Some(user.username), username: Some(user.username.clone()),
file_path, file_path,
contents, contents,
git_token, git_token,
@@ -186,19 +187,19 @@ impl Resolve<WriteStackFileContents, User> for State {
} }
Err(e) => { Err(e) => {
update.push_error_log( update.push_error_log(
"Write file contents", "Write File Contents",
format_serror(&e.into()), format_serror(&e.into()),
); );
} }
}; };
} }
if let Err(e) = State if let Err(e) = (RefreshStackCache { stack: stack_id })
.resolve( .resolve(&WriteArgs {
RefreshStackCache { stack: stack_id }, user: stack_user().to_owned(),
stack_user().to_owned(), })
)
.await .await
.map_err(|e| e.error)
.context( .context(
"Failed to refresh stack cache after writing file contents", "Failed to refresh stack cache after writing file contents",
) )
@@ -216,22 +217,21 @@ impl Resolve<WriteStackFileContents, User> for State {
} }
} }
impl Resolve<RefreshStackCache, User> for State { impl Resolve<WriteArgs> for RefreshStackCache {
#[instrument( #[instrument(
name = "RefreshStackCache", name = "RefreshStackCache",
level = "debug", level = "debug",
skip(self, user) skip(user)
)] )]
async fn resolve( async fn resolve(
&self, self,
RefreshStackCache { stack }: RefreshStackCache, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<NoData> {
) -> anyhow::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the // 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. // stack should be able to do this.
let stack = resource::get_check_permissions::<Stack>( let stack = resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Execute, PermissionLevel::Execute,
) )
.await?; .await?;
@@ -300,9 +300,9 @@ impl Resolve<RefreshStackCache, User> for State {
&mut services, &mut services,
) { ) {
warn!( warn!(
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}", "failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
stack.name stack.name
); );
} }
} }
@@ -372,6 +372,7 @@ impl Resolve<RefreshStackCache, User> for State {
deployed_services: stack.info.deployed_services.clone(), deployed_services: stack.info.deployed_services.clone(),
deployed_project_name: stack.info.deployed_project_name.clone(), deployed_project_name: stack.info.deployed_project_name.clone(),
deployed_contents: stack.info.deployed_contents.clone(), deployed_contents: stack.info.deployed_contents.clone(),
deployed_config: stack.info.deployed_config.clone(),
deployed_hash: stack.info.deployed_hash.clone(), deployed_hash: stack.info.deployed_hash.clone(),
deployed_message: stack.info.deployed_message.clone(), deployed_message: stack.info.deployed_message.clone(),
latest_services, latest_services,
@@ -401,7 +402,7 @@ impl Resolve<RefreshStackCache, User> for State {
if state == ServerState::Ok { if state == ServerState::Ok {
let name = stack.name.clone(); let name = stack.name.clone();
if let Err(e) = if let Err(e) =
pull_stack_inner(stack, None, &server, None).await pull_stack_inner(stack, Vec::new(), &server, None).await
{ {
warn!( warn!(
"Failed to pull latest images for Stack {name} | {e:#}", "Failed to pull latest images for Stack {name} | {e:#}",
@@ -414,39 +415,44 @@ impl Resolve<RefreshStackCache, User> for State {
} }
} }
impl Resolve<CreateStackWebhook, User> for State { impl Resolve<WriteArgs> for CreateStackWebhook {
#[instrument(name = "CreateStackWebhook", skip(self, user))] #[instrument(name = "CreateStackWebhook", skip(args))]
async fn resolve( async fn resolve(
&self, self,
CreateStackWebhook { stack, action }: CreateStackWebhook, args: &WriteArgs,
user: User, ) -> serror::Result<CreateStackWebhookResponse> {
) -> anyhow::Result<CreateStackWebhookResponse> { let WriteArgs { user } = args;
let Some(github) = github_client() else { let Some(github) = github_client() else {
return Err(anyhow!( return Err(
"github_webhook_app is not configured in core config toml" anyhow!(
)); "github_webhook_app is not configured in core config toml"
)
.into(),
);
}; };
let stack = resource::get_check_permissions::<Stack>( let stack = resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
) )
.await?; .await?;
if stack.config.repo.is_empty() { if stack.config.repo.is_empty() {
return Err(anyhow!( return Err(
"No repo configured, can't create webhook" anyhow!("No repo configured, can't create webhook").into(),
)); );
} }
let mut split = stack.config.repo.split('/'); let mut split = stack.config.repo.split('/');
let owner = split.next().context("Stack repo has no owner")?; let owner = split.next().context("Stack repo has no owner")?;
let Some(github) = github.get(owner) else { let Some(github) = github.get(owner) else {
return Err(anyhow!( return Err(
"Cannot manage repo webhooks under owner {owner}" anyhow!("Cannot manage repo webhooks under owner {owner}")
)); .into(),
);
}; };
let repo = let repo =
@@ -479,7 +485,7 @@ impl Resolve<CreateStackWebhook, User> for State {
} else { } else {
webhook_base_url webhook_base_url
}; };
let url = match action { let url = match self.action {
StackWebhookAction::Refresh => { StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id) format!("{host}/listener/github/stack/{}/refresh", stack.id)
} }
@@ -514,64 +520,65 @@ impl Resolve<CreateStackWebhook, User> for State {
.context("failed to create webhook")?; .context("failed to create webhook")?;
if !stack.config.webhook_enabled { if !stack.config.webhook_enabled {
self UpdateStack {
.resolve( id: stack.id,
UpdateStack { config: PartialStackConfig {
id: stack.id, webhook_enabled: Some(true),
config: PartialStackConfig { ..Default::default()
webhook_enabled: Some(true), },
..Default::default() }
}, .resolve(args)
}, .await
user, .map_err(|e| e.error)
) .context("failed to update stack to enable webhook")?;
.await
.context("failed to update stack to enable webhook")?;
} }
Ok(NoData {}) Ok(NoData {})
} }
} }
impl Resolve<DeleteStackWebhook, User> for State { impl Resolve<WriteArgs> for DeleteStackWebhook {
#[instrument(name = "DeleteStackWebhook", skip(self, user))] #[instrument(name = "DeleteStackWebhook", skip(user))]
async fn resolve( async fn resolve(
&self, self,
DeleteStackWebhook { stack, action }: DeleteStackWebhook, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<DeleteStackWebhookResponse> {
) -> anyhow::Result<DeleteStackWebhookResponse> {
let Some(github) = github_client() else { let Some(github) = github_client() else {
return Err(anyhow!( return Err(
"github_webhook_app is not configured in core config toml" anyhow!(
)); "github_webhook_app is not configured in core config toml"
)
.into(),
);
}; };
let stack = resource::get_check_permissions::<Stack>( let stack = resource::get_check_permissions::<Stack>(
&stack, &self.stack,
&user, user,
PermissionLevel::Write, PermissionLevel::Write,
) )
.await?; .await?;
if stack.config.git_provider != "github.com" { if stack.config.git_provider != "github.com" {
return Err(anyhow!( return Err(
"Can only manage github.com repo webhooks" anyhow!("Can only manage github.com repo webhooks").into(),
)); );
} }
if stack.config.repo.is_empty() { if stack.config.repo.is_empty() {
return Err(anyhow!( return Err(
"No repo configured, can't create webhook" anyhow!("No repo configured, can't create webhook").into(),
)); );
} }
let mut split = stack.config.repo.split('/'); let mut split = stack.config.repo.split('/');
let owner = split.next().context("Stack repo has no owner")?; let owner = split.next().context("Stack repo has no owner")?;
let Some(github) = github.get(owner) else { let Some(github) = github.get(owner) else {
return Err(anyhow!( return Err(
"Cannot manage repo webhooks under owner {owner}" anyhow!("Cannot manage repo webhooks under owner {owner}")
)); .into(),
);
}; };
let repo = let repo =
@@ -597,7 +604,7 @@ impl Resolve<DeleteStackWebhook, User> for State {
} else { } else {
webhook_base_url webhook_base_url
}; };
let url = match action { let url = match self.action {
StackWebhookAction::Refresh => { StackWebhookAction::Refresh => {
format!("{host}/listener/github/stack/{}/refresh", stack.id) format!("{host}/listener/github/stack/{}/refresh", stack.id)
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use komodo_client::{ use komodo_client::{
api::write::{ api::write::{
AddUserToUserGroup, CreateUserGroup, DeleteUserGroup, AddUserToUserGroup, CreateUserGroup, DeleteUserGroup,
RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup, RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup,
}, },
entities::{komodo_timestamp, user::User, user_group::UserGroup}, entities::{komodo_timestamp, user_group::UserGroup},
}; };
use mungos::{ use mungos::{
by_id::{delete_one_by_id, find_one_by_id, update_one_by_id}, 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 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( async fn resolve(
&self, self,
CreateUserGroup { name }: CreateUserGroup, WriteArgs { user: admin }: &WriteArgs,
admin: User, ) -> serror::Result<UserGroup> {
) -> anyhow::Result<UserGroup> {
if !admin.admin { if !admin.admin {
return Err(anyhow!("This call is admin-only")); return Err(anyhow!("This call is admin-only").into());
} }
let user_group = UserGroup { let user_group = UserGroup {
id: Default::default(), id: Default::default(),
users: Default::default(), users: Default::default(),
all: Default::default(), all: Default::default(),
updated_at: komodo_timestamp(), updated_at: komodo_timestamp(),
name, name: self.name,
}; };
let db = db_client(); let db = db_client();
let id = db let id = db
@@ -43,63 +44,63 @@ impl Resolve<CreateUserGroup, User> for State {
.as_object_id() .as_object_id()
.context("inserted id is not ObjectId")? .context("inserted id is not ObjectId")?
.to_string(); .to_string();
find_one_by_id(&db.user_groups, &id) let res = find_one_by_id(&db.user_groups, &id)
.await .await
.context("failed to query db for user groups")? .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( async fn resolve(
&self, self,
RenameUserGroup { id, name }: RenameUserGroup, WriteArgs { user: admin }: &WriteArgs,
admin: User, ) -> serror::Result<UserGroup> {
) -> anyhow::Result<UserGroup> {
if !admin.admin { 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 db = db_client();
update_one_by_id( update_one_by_id(
&db.user_groups, &db.user_groups,
&id, &self.id,
doc! { "$set": { "name": name } }, doc! { "$set": { "name": self.name } },
None, None,
) )
.await .await
.context("failed to rename UserGroup on db")?; .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 .await
.context("failed to query db for UserGroups")? .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( async fn resolve(
&self, self,
DeleteUserGroup { id }: DeleteUserGroup, WriteArgs { user: admin }: &WriteArgs,
admin: User, ) -> serror::Result<UserGroup> {
) -> anyhow::Result<UserGroup> {
if !admin.admin { 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 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 .await
.context("failed to query db for UserGroups")? .context("failed to query db for UserGroups")?
.context("no UserGroup found with given id")?; .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 .await
.context("failed to delete UserGroup from db")?; .context("failed to delete UserGroup from db")?;
db.permissions db.permissions
.delete_many(doc! { .delete_many(doc! {
"user_target.type": "UserGroup", "user_target.type": "UserGroup",
"user_target.id": id, "user_target.id": self.id,
}) })
.await .await
.context("failed to clean up UserGroups permissions. User Group has been deleted")?; .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( async fn resolve(
&self, self,
AddUserToUserGroup { user_group, user }: AddUserToUserGroup, WriteArgs { user: admin }: &WriteArgs,
admin: User, ) -> serror::Result<UserGroup> {
) -> anyhow::Result<UserGroup> {
if !admin.admin { 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 db = db_client();
let filter = match ObjectId::from_str(&user) { let filter = match ObjectId::from_str(&self.user) {
Ok(id) => doc! { "_id": id }, Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": &user }, Err(_) => doc! { "username": &self.user },
}; };
let user = db let user = db
.users .users
@@ -131,9 +131,9 @@ impl Resolve<AddUserToUserGroup, User> for State {
.context("failed to query mongo for users")? .context("failed to query mongo for users")?
.context("no matching user found")?; .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 }, Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &user_group }, Err(_) => doc! { "name": &self.user_group },
}; };
db.user_groups db.user_groups
.update_one( .update_one(
@@ -142,32 +142,30 @@ impl Resolve<AddUserToUserGroup, User> for State {
) )
.await .await
.context("failed to add user to group on db")?; .context("failed to add user to group on db")?;
db.user_groups let res = db
.user_groups
.find_one(filter) .find_one(filter)
.await .await
.context("failed to query db for UserGroups")? .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( async fn resolve(
&self, self,
RemoveUserFromUserGroup { WriteArgs { user: admin }: &WriteArgs,
user_group, ) -> serror::Result<UserGroup> {
user,
}: RemoveUserFromUserGroup,
admin: User,
) -> anyhow::Result<UserGroup> {
if !admin.admin { 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 db = db_client();
let filter = match ObjectId::from_str(&user) { let filter = match ObjectId::from_str(&self.user) {
Ok(id) => doc! { "_id": id }, Ok(id) => doc! { "_id": id },
Err(_) => doc! { "username": &user }, Err(_) => doc! { "username": &self.user },
}; };
let user = db let user = db
.users .users
@@ -176,9 +174,9 @@ impl Resolve<RemoveUserFromUserGroup, User> for State {
.context("failed to query mongo for users")? .context("failed to query mongo for users")?
.context("no matching user found")?; .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 }, Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &user_group }, Err(_) => doc! { "name": &self.user_group },
}; };
db.user_groups db.user_groups
.update_one( .update_one(
@@ -187,22 +185,23 @@ impl Resolve<RemoveUserFromUserGroup, User> for State {
) )
.await .await
.context("failed to add user to group on db")?; .context("failed to add user to group on db")?;
db.user_groups let res = db
.user_groups
.find_one(filter) .find_one(filter)
.await .await
.context("failed to query db for UserGroups")? .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( async fn resolve(
&self, self,
SetUsersInUserGroup { user_group, users }: SetUsersInUserGroup, WriteArgs { user: admin }: &WriteArgs,
admin: User, ) -> serror::Result<UserGroup> {
) -> anyhow::Result<UserGroup> {
if !admin.admin { 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 db = db_client();
@@ -215,7 +214,8 @@ impl Resolve<SetUsersInUserGroup, User> for State {
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
// Make sure all users are user ids // Make sure all users are user ids
let users = users let users = self
.users
.into_iter() .into_iter()
.filter_map(|user| match ObjectId::from_str(&user) { .filter_map(|user| match ObjectId::from_str(&user) {
Ok(_) => Some(user), Ok(_) => Some(user),
@@ -223,18 +223,20 @@ impl Resolve<SetUsersInUserGroup, User> for State {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let filter = match ObjectId::from_str(&user_group) { let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id }, Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &user_group }, Err(_) => doc! { "name": &self.user_group },
}; };
db.user_groups db.user_groups
.update_one(filter.clone(), doc! { "$set": { "users": users } }) .update_one(filter.clone(), doc! { "$set": { "users": users } })
.await .await
.context("failed to set users on user group")?; .context("failed to set users on user group")?;
db.user_groups let res = db
.user_groups
.find_one(filter) .find_one(filter)
.await .await
.context("failed to query db for UserGroups")? .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 anyhow::{Context, anyhow};
use komodo_client::{ use komodo_client::{
api::write::{ api::write::*,
CreateVariable, CreateVariableResponse, DeleteVariable, entities::{Operation, ResourceTarget, variable::Variable},
DeleteVariableResponse, UpdateVariableDescription,
UpdateVariableDescriptionResponse, UpdateVariableIsSecret,
UpdateVariableIsSecretResponse, UpdateVariableValue,
UpdateVariableValueResponse,
},
entities::{
user::User, variable::Variable, Operation, ResourceTarget,
},
}; };
use mungos::mongodb::bson::doc; use mungos::mongodb::bson::doc;
use resolver_api::Resolve; use resolver_api::Resolve;
@@ -19,23 +11,26 @@ use crate::{
query::get_variable, query::get_variable,
update::{add_update, make_update}, update::{add_update, make_update},
}, },
state::{db_client, State}, state::db_client,
}; };
impl Resolve<CreateVariable, User> for State { use super::WriteArgs;
#[instrument(name = "CreateVariable", skip(self, user, value))]
impl Resolve<WriteArgs> for CreateVariable {
#[instrument(name = "CreateVariable", skip(user, self), fields(name = &self.name))]
async fn resolve( async fn resolve(
&self, self,
CreateVariable { WriteArgs { user }: &WriteArgs,
) -> serror::Result<CreateVariableResponse> {
let CreateVariable {
name, name,
value, value,
description, description,
is_secret, is_secret,
}: CreateVariable, } = self;
user: User,
) -> anyhow::Result<CreateVariableResponse> {
if !user.admin { if !user.admin {
return Err(anyhow!("only admins can create variables")); return Err(anyhow!("only admins can create variables").into());
} }
let variable = Variable { let variable = Variable {
@@ -54,7 +49,7 @@ impl Resolve<CreateVariable, User> for State {
let mut update = make_update( let mut update = make_update(
ResourceTarget::system(), ResourceTarget::system(),
Operation::CreateVariable, Operation::CreateVariable,
&user, user,
); );
update update
@@ -63,21 +58,22 @@ impl Resolve<CreateVariable, User> for State {
add_update(update).await?; add_update(update).await?;
get_variable(&variable.name).await Ok(get_variable(&variable.name).await?)
} }
} }
impl Resolve<UpdateVariableValue, User> for State { impl Resolve<WriteArgs> for UpdateVariableValue {
#[instrument(name = "UpdateVariableValue", skip(self, user, value))] #[instrument(name = "UpdateVariableValue", skip(user, self), fields(name = &self.name))]
async fn resolve( async fn resolve(
&self, self,
UpdateVariableValue { name, value }: UpdateVariableValue, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<UpdateVariableValueResponse> {
) -> anyhow::Result<UpdateVariableValueResponse> {
if !user.admin { 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?; let variable = get_variable(&name).await?;
if value == variable.value { if value == variable.value {
@@ -96,7 +92,7 @@ impl Resolve<UpdateVariableValue, User> for State {
let mut update = make_update( let mut update = make_update(
ResourceTarget::system(), ResourceTarget::system(),
Operation::UpdateVariableValue, Operation::UpdateVariableValue,
&user, user,
); );
let log = if variable.is_secret { let log = if variable.is_secret {
@@ -116,74 +112,71 @@ impl Resolve<UpdateVariableValue, User> for State {
add_update(update).await?; add_update(update).await?;
get_variable(&name).await Ok(get_variable(&name).await?)
} }
} }
impl Resolve<UpdateVariableDescription, User> for State { impl Resolve<WriteArgs> for UpdateVariableDescription {
#[instrument(name = "UpdateVariableDescription", skip(self, user))] #[instrument(name = "UpdateVariableDescription", skip(user))]
async fn resolve( async fn resolve(
&self, self,
UpdateVariableDescription { name, description }: UpdateVariableDescription, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<UpdateVariableDescriptionResponse> {
) -> anyhow::Result<UpdateVariableDescriptionResponse> {
if !user.admin { if !user.admin {
return Err(anyhow!("only admins can update variables")); return Err(anyhow!("only admins can update variables").into());
} }
db_client() db_client()
.variables .variables
.update_one( .update_one(
doc! { "name": &name }, doc! { "name": &self.name },
doc! { "$set": { "description": &description } }, doc! { "$set": { "description": &self.description } },
) )
.await .await
.context("failed to update variable description on db")?; .context("failed to update variable description on db")?;
get_variable(&name).await Ok(get_variable(&self.name).await?)
} }
} }
impl Resolve<UpdateVariableIsSecret, User> for State { impl Resolve<WriteArgs> for UpdateVariableIsSecret {
#[instrument(name = "UpdateVariableIsSecret", skip(self, user))] #[instrument(name = "UpdateVariableIsSecret", skip(user))]
async fn resolve( async fn resolve(
&self, self,
UpdateVariableIsSecret { name, is_secret }: UpdateVariableIsSecret, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<UpdateVariableIsSecretResponse> {
) -> anyhow::Result<UpdateVariableIsSecretResponse> {
if !user.admin { if !user.admin {
return Err(anyhow!("only admins can update variables")); return Err(anyhow!("only admins can update variables").into());
} }
db_client() db_client()
.variables .variables
.update_one( .update_one(
doc! { "name": &name }, doc! { "name": &self.name },
doc! { "$set": { "is_secret": is_secret } }, doc! { "$set": { "is_secret": self.is_secret } },
) )
.await .await
.context("failed to update variable is secret on db")?; .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( async fn resolve(
&self, self,
DeleteVariable { name }: DeleteVariable, WriteArgs { user }: &WriteArgs,
user: User, ) -> serror::Result<DeleteVariableResponse> {
) -> anyhow::Result<DeleteVariableResponse> {
if !user.admin { 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() db_client()
.variables .variables
.delete_one(doc! { "name": &name }) .delete_one(doc! { "name": &self.name })
.await .await
.context("failed to delete variable on db")?; .context("failed to delete variable on db")?;
let mut update = make_update( let mut update = make_update(
ResourceTarget::system(), ResourceTarget::system(),
Operation::DeleteVariable, Operation::DeleteVariable,
&user, user,
); );
update update

View File

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

View File

@@ -1,6 +1,6 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use axum::{ use axum::{
extract::Query, response::Redirect, routing::get, Router, Router, extract::Query, response::Redirect, routing::get,
}; };
use komodo_client::entities::{ use komodo_client::entities::{
komodo_timestamp, komodo_timestamp,
@@ -72,7 +72,7 @@ async fn callback(
.context("failed at find user query from database")?; .context("failed at find user query from database")?;
let jwt = match user { let jwt = match user {
Some(user) => jwt_client() Some(user) => jwt_client()
.generate(user.id) .encode(user.id)
.context("failed to generate jwt")?, .context("failed to generate jwt")?,
None => { None => {
let ts = komodo_timestamp(); let ts = komodo_timestamp();
@@ -109,7 +109,7 @@ async fn callback(
.context("inserted_id is not ObjectId")? .context("inserted_id is not ObjectId")?
.to_string(); .to_string();
jwt_client() jwt_client()
.generate(user_id) .encode(user_id)
.context("failed to generate jwt")? .context("failed to generate jwt")?
} }
}; };

View File

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

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use async_timing_util::unix_timestamp_ms; use async_timing_util::unix_timestamp_ms;
use axum::{ use axum::{
extract::Query, response::Redirect, routing::get, Router, Router, extract::Query, response::Redirect, routing::get,
}; };
use komodo_client::entities::user::{User, UserConfig}; use komodo_client::entities::user::{User, UserConfig};
use mongo_indexed::Document; use mongo_indexed::Document;
@@ -81,7 +81,7 @@ async fn callback(
.context("failed at find user query from mongo")?; .context("failed at find user query from mongo")?;
let jwt = match user { let jwt = match user {
Some(user) => jwt_client() Some(user) => jwt_client()
.generate(user.id) .encode(user.id)
.context("failed to generate jwt")?, .context("failed to generate jwt")?,
None => { None => {
let ts = unix_timestamp_ms() as i64; let ts = unix_timestamp_ms() as i64;
@@ -124,7 +124,7 @@ async fn callback(
.context("inserted_id is not ObjectId")? .context("inserted_id is not ObjectId")?
.to_string(); .to_string();
jwt_client() jwt_client()
.generate(user_id) .encode(user_id)
.context("failed to generate jwt")? .context("failed to generate jwt")?
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,20 @@
use std::sync::OnceLock; use std::sync::OnceLock;
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use axum::{ use axum::{
extract::Query, response::Redirect, routing::get, Router, Router, extract::Query, response::Redirect, routing::get,
}; };
use client::default_oidc_client; use client::oidc_client;
use dashmap::DashMap; use dashmap::DashMap;
use komodo_client::entities::{ use komodo_client::entities::{
komodo_timestamp, komodo_timestamp,
user::{User, UserConfig}, user::{User, UserConfig},
}; };
use mungos::mongodb::bson::{doc, Document}; use mungos::mongodb::bson::{Document, doc};
use openidconnect::{ use openidconnect::{
core::CoreAuthenticationFlow, AccessTokenHash, AuthorizationCode, AccessTokenHash, AuthorizationCode, CsrfToken, Nonce,
CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeChallenge, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, Scope,
PkceCodeVerifier, Scope, TokenResponse, TokenResponse, core::CoreAuthenticationFlow,
}; };
use reqwest::StatusCode; use reqwest::StatusCode;
use serde::Deserialize; use serde::Deserialize;
@@ -29,16 +29,28 @@ use super::RedirectQuery;
pub mod client; pub mod client;
fn reqwest_client() -> &'static reqwest::Client {
static REQWEST: OnceLock<reqwest::Client> = OnceLock::new();
REQWEST.get_or_init(|| {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("Invalid OIDC reqwest client")
})
}
/// CSRF tokens can only be used once from the callback, /// CSRF tokens can only be used once from the callback,
/// and must be used within this timeframe /// and must be used within this timeframe
const CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in. const CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in.
type RedirectUrl = Option<String>; type RedirectUrl = Option<String>;
type CsrfMap = /// Maps the csrf secrets to other information added in the "login" method (before auth provider redirect).
/// This information is retrieved in the "callback" method (after auth provider redirect).
type VerifierMap =
DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>; DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>;
fn csrf_verifier_tokens() -> &'static CsrfMap { fn verifier_tokens() -> &'static VerifierMap {
static CSRF: OnceLock<CsrfMap> = OnceLock::new(); static VERIFIERS: OnceLock<VerifierMap> = OnceLock::new();
CSRF.get_or_init(Default::default) VERIFIERS.get_or_init(Default::default)
} }
pub fn router() -> Router { pub fn router() -> Router {
@@ -61,10 +73,10 @@ pub fn router() -> Router {
async fn login( async fn login(
Query(RedirectQuery { redirect }): Query<RedirectQuery>, Query(RedirectQuery { redirect }): Query<RedirectQuery>,
) -> anyhow::Result<Redirect> { ) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client = let client =
default_oidc_client().context("OIDC Client not configured")?; client.as_ref().context("OIDC Client not configured")?;
// Generate a PKCE challenge.
let (pkce_challenge, pkce_verifier) = let (pkce_challenge, pkce_verifier) =
PkceCodeChallenge::new_random_sha256(); PkceCodeChallenge::new_random_sha256();
@@ -75,13 +87,13 @@ async fn login(
CsrfToken::new_random, CsrfToken::new_random,
Nonce::new_random, Nonce::new_random,
) )
.set_pkce_challenge(pkce_challenge)
.add_scope(Scope::new("openid".to_string())) .add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("email".to_string())) .add_scope(Scope::new("email".to_string()))
.set_pkce_challenge(pkce_challenge)
.url(); .url();
// Data inserted here will be matched on callback side for csrf protection. // Data inserted here will be matched on callback side for csrf protection.
csrf_verifier_tokens().insert( verifier_tokens().insert(
csrf_token.secret().clone(), csrf_token.secret().clone(),
( (
pkce_verifier, pkce_verifier,
@@ -123,8 +135,9 @@ struct CallbackQuery {
async fn callback( async fn callback(
Query(query): Query<CallbackQuery>, Query(query): Query<CallbackQuery>,
) -> anyhow::Result<Redirect> { ) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client = let client =
default_oidc_client().context("OIDC Client not configured")?; client.as_ref().context("OIDC Client not configured")?;
if let Some(e) = query.error { if let Some(e) = query.error {
return Err(anyhow!("Provider returned error: {e}")); return Err(anyhow!("Provider returned error: {e}"));
@@ -136,21 +149,21 @@ async fn callback(
); );
let (_, (pkce_verifier, nonce, redirect, valid_until)) = let (_, (pkce_verifier, nonce, redirect, valid_until)) =
csrf_verifier_tokens() verifier_tokens()
.remove(state.secret()) .remove(state.secret())
.context("CSRF Token invalid")?; .context("CSRF token invalid")?;
if komodo_timestamp() > valid_until { if komodo_timestamp() > valid_until {
return Err(anyhow!( return Err(anyhow!(
"CSRF token invalid (Timed out). The token must be " "CSRF token invalid (Timed out). The token must be used within 2 minutes."
)); ));
} }
let token_response = client let token_response = client
.exchange_code(AuthorizationCode::new(code)) .exchange_code(AuthorizationCode::new(code))
// Set the PKCE code verifier. .context("Failed to get Oauth token at exchange code")?
.set_pkce_verifier(pkce_verifier) .set_pkce_verifier(pkce_verifier)
.request_async(openidconnect::reqwest::async_http_client) .request_async(reqwest_client())
.await .await
.context("Failed to get Oauth token")?; .context("Failed to get Oauth token")?;
@@ -173,7 +186,7 @@ async fn callback(
let claims = id_token let claims = id_token
.claims(&verifier, &nonce) .claims(&verifier, &nonce)
.context("Failed to verify token claims")?; .context("Failed to verify token claims. This issue may be temporary (60 seconds max).")?;
// Verify the access token hash to ensure that the access token hasn't been substituted for // Verify the access token hash to ensure that the access token hasn't been substituted for
// another user's. // another user's.
@@ -181,7 +194,8 @@ async fn callback(
{ {
let actual_access_token_hash = AccessTokenHash::from_token( let actual_access_token_hash = AccessTokenHash::from_token(
token_response.access_token(), token_response.access_token(),
&id_token.signing_alg()?, id_token.signing_alg()?,
id_token.signing_key(&verifier)?,
)?; )?;
if actual_access_token_hash != *expected_access_token_hash { if actual_access_token_hash != *expected_access_token_hash {
return Err(anyhow!("Invalid access token")); return Err(anyhow!("Invalid access token"));
@@ -202,7 +216,7 @@ async fn callback(
let jwt = match user { let jwt = match user {
Some(user) => jwt_client() Some(user) => jwt_client()
.generate(user.id) .encode(user.id)
.context("failed to generate jwt")?, .context("failed to generate jwt")?,
None => { None => {
let ts = komodo_timestamp(); let ts = komodo_timestamp();
@@ -258,7 +272,7 @@ async fn callback(
.context("inserted_id is not ObjectId")? .context("inserted_id is not ObjectId")?
.to_string(); .to_string();
jwt_client() jwt_client()
.generate(user_id) .encode(user_id)
.context("failed to generate jwt")? .context("failed to generate jwt")?
} }
}; };

View File

@@ -1,22 +1,22 @@
use std::{str::FromStr, time::Duration}; use std::{str::FromStr, time::Duration};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use aws_config::{BehaviorVersion, Region}; use aws_config::{BehaviorVersion, Region};
use aws_sdk_ec2::{ use aws_sdk_ec2::{
Client,
types::{ types::{
BlockDeviceMapping, EbsBlockDevice, BlockDeviceMapping, EbsBlockDevice,
InstanceNetworkInterfaceSpecification, InstanceStateChange, InstanceNetworkInterfaceSpecification, InstanceStateChange,
InstanceStateName, InstanceStatus, InstanceType, ResourceType, InstanceStateName, InstanceStatus, InstanceType, ResourceType,
Tag, TagSpecification, VolumeType, Tag, TagSpecification, VolumeType,
}, },
Client,
}; };
use base64::Engine; use base64::Engine;
use komodo_client::entities::{ use komodo_client::entities::{
ResourceTarget,
alert::{Alert, AlertData, SeverityLevel}, alert::{Alert, AlertData, SeverityLevel},
komodo_timestamp, komodo_timestamp,
server_template::aws::AwsServerTemplateConfig, server_template::aws::AwsServerTemplateConfig,
ResourceTarget,
}; };
use crate::{alert::send_alerts, config::core_config}; use crate::{alert::send_alerts, config::core_config};
@@ -29,20 +29,40 @@ pub struct Ec2Instance {
pub ip: String, pub ip: String,
} }
/// Provides credentials in the core config file to the AWS client
#[derive(Debug)]
struct CredentialsFromConfig;
impl aws_credential_types::provider::ProvideCredentials
for CredentialsFromConfig
{
fn provide_credentials<'a>(
&'a self,
) -> aws_credential_types::provider::future::ProvideCredentials<'a>
where
Self: 'a,
{
aws_credential_types::provider::future::ProvideCredentials::new(
async {
let config = core_config();
Ok(aws_credential_types::Credentials::new(
&config.aws.access_key_id,
&config.aws.secret_access_key,
None,
None,
"komodo-config",
))
},
)
}
}
#[instrument] #[instrument]
async fn create_ec2_client(region: String) -> Client { async fn create_ec2_client(region: String) -> Client {
// There may be a better way to pass these keys to client
std::env::set_var(
"AWS_ACCESS_KEY_ID",
&core_config().aws.access_key_id,
);
std::env::set_var(
"AWS_SECRET_ACCESS_KEY",
&core_config().aws.secret_access_key,
);
let region = Region::new(region); let region = Region::new(region);
let config = aws_config::defaults(BehaviorVersion::v2024_03_28()) let config = aws_config::defaults(BehaviorVersion::latest())
.region(region) .region(region)
.credentials_provider(CredentialsFromConfig)
.load() .load()
.await; .await;
Client::new(&config) Client::new(&config)
@@ -1087,7 +1107,90 @@ fn handle_unknown_instance_type(
| InstanceType::Z1d6xlarge | InstanceType::Z1d6xlarge
| InstanceType::Z1dLarge | InstanceType::Z1dLarge
| InstanceType::Z1dMetal | 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:?}")), other => Err(anyhow!("unknown InstanceType: {other:?}")),
} }
} }

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use axum::http::{HeaderName, HeaderValue}; use axum::http::{HeaderName, HeaderValue};
use reqwest::{RequestBuilder, StatusCode}; use reqwest::{RequestBuilder, StatusCode};
use serde::{de::DeserializeOwned, Serialize}; use serde::{Serialize, de::DeserializeOwned};
use super::{ use super::{
common::{ common::{

View File

@@ -3,7 +3,7 @@ use std::{
time::Duration, time::Duration,
}; };
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use futures::future::join_all; use futures::future::join_all;
use komodo_client::entities::server_template::hetzner::{ use komodo_client::entities::server_template::hetzner::{
HetznerDatacenter, HetznerServerTemplateConfig, HetznerServerType, HetznerDatacenter, HetznerServerTemplateConfig, HetznerServerType,

View File

@@ -182,6 +182,8 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.disable_user_registration), .unwrap_or(config.disable_user_registration),
disable_non_admin_create: env.komodo_disable_non_admin_create disable_non_admin_create: env.komodo_disable_non_admin_create
.unwrap_or(config.disable_non_admin_create), .unwrap_or(config.disable_non_admin_create),
lock_login_credentials_for: env.komodo_lock_login_credentials_for
.unwrap_or(config.lock_login_credentials_for),
local_auth: env.komodo_local_auth local_auth: env.komodo_local_auth
.unwrap_or(config.local_auth), .unwrap_or(config.local_auth),
logging: LogConfig { logging: LogConfig {

View File

@@ -89,7 +89,9 @@ impl DbClient {
client = client.address(address); client = client.address(address);
} }
_ => { _ => {
error!("config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?"); error!(
"config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?"
);
std::process::exit(1) std::process::exit(1)
} }
} }

View File

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

View File

@@ -1,27 +1,27 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use formatting::muted; use formatting::muted;
use komodo_client::entities::{ use komodo_client::entities::{
Version,
builder::{AwsBuilderConfig, Builder, BuilderConfig}, builder::{AwsBuilderConfig, Builder, BuilderConfig},
komodo_timestamp, komodo_timestamp,
server::Server, server::Server,
server_template::aws::AwsServerTemplateConfig, server_template::aws::AwsServerTemplateConfig,
update::{Log, Update}, update::{Log, Update},
Version,
}; };
use periphery_client::{ use periphery_client::{
api::{self, GetVersionResponse},
PeripheryClient, PeripheryClient,
api::{self, GetVersionResponse},
}; };
use crate::{ use crate::{
cloud::{ cloud::{
aws::ec2::{
launch_ec2_instance, terminate_ec2_instance_with_retry,
Ec2Instance,
},
BuildCleanupData, BuildCleanupData,
aws::ec2::{
Ec2Instance, launch_ec2_instance,
terminate_ec2_instance_with_retry,
},
}, },
config::core_config, config::core_config,
helpers::update::update_update, helpers::update::update_update,
@@ -122,8 +122,11 @@ async fn get_aws_builder(
let protocol = if config.use_https { "https" } else { "http" }; let protocol = if config.use_https { "https" } else { "http" };
let periphery_address = let periphery_address =
format!("{protocol}://{ip}:{}", config.port); format!("{protocol}://{ip}:{}", config.port);
let periphery = let periphery = PeripheryClient::new(
PeripheryClient::new(&periphery_address, &core_config().passkey, Duration::from_secs(3)); &periphery_address,
&core_config().passkey,
Duration::from_secs(3),
);
let start_connect_ts = komodo_timestamp(); let start_connect_ts = komodo_timestamp();
let mut res = Ok(GetVersionResponse { let mut res = Ok(GetVersionResponse {

View File

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

View File

@@ -1,11 +1,11 @@
use std::sync::OnceLock; use std::sync::OnceLock;
use komodo_client::entities::update::{Update, UpdateListItem}; use komodo_client::entities::update::{Update, UpdateListItem};
use tokio::sync::{broadcast, Mutex}; use tokio::sync::{Mutex, broadcast};
/// A channel sending (build_id, update_id) /// A channel sending (build_id, update_id)
pub fn build_cancel_channel( pub fn build_cancel_channel()
) -> &'static BroadcastChannel<(String, Update)> { -> &'static BroadcastChannel<(String, Update)> {
static BUILD_CANCEL_CHANNEL: OnceLock< static BUILD_CANCEL_CHANNEL: OnceLock<
BroadcastChannel<(String, Update)>, BroadcastChannel<(String, Update)>,
> = OnceLock::new(); > = OnceLock::new();
@@ -13,8 +13,8 @@ pub fn build_cancel_channel(
} }
/// A channel sending (repo_id, update_id) /// A channel sending (repo_id, update_id)
pub fn repo_cancel_channel( pub fn repo_cancel_channel()
) -> &'static BroadcastChannel<(String, Update)> { -> &'static BroadcastChannel<(String, Update)> {
static REPO_CANCEL_CHANNEL: OnceLock< static REPO_CANCEL_CHANNEL: OnceLock<
BroadcastChannel<(String, Update)>, BroadcastChannel<(String, Update)>,
> = OnceLock::new(); > = OnceLock::new();

View File

@@ -1,7 +1,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use anyhow::Context; use anyhow::Context;
use komodo_client::entities::{update::Update, SystemCommand}; use komodo_client::entities::{SystemCommand, update::Update};
use super::query::VariablesAndSecrets; use super::query::VariablesAndSecrets;

View File

@@ -1,33 +1,32 @@
use std::{str::FromStr, time::Duration}; use std::{str::FromStr, time::Duration};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use futures::future::join_all; use futures::future::join_all;
use komodo_client::{ use komodo_client::{
api::write::{CreateBuilder, CreateServer}, api::write::{CreateBuilder, CreateServer},
entities::{ entities::{
ResourceTarget,
builder::{PartialBuilderConfig, PartialServerBuilderConfig}, builder::{PartialBuilderConfig, PartialServerBuilderConfig},
komodo_timestamp, komodo_timestamp,
permission::{Permission, PermissionLevel, UserTarget}, permission::{Permission, PermissionLevel, UserTarget},
server::{PartialServerConfig, Server}, server::{PartialServerConfig, Server},
sync::ResourceSync, sync::ResourceSync,
update::Log, update::Log,
user::{system_user, User}, user::{User, system_user},
ResourceTarget,
}, },
}; };
use mongo_indexed::Document; use mongo_indexed::Document;
use mungos::{ use mungos::{
find::find_collect, find::find_collect,
mongodb::bson::{doc, oid::ObjectId, to_document, Bson}, mongodb::bson::{Bson, doc, oid::ObjectId, to_document},
}; };
use periphery_client::PeripheryClient; use periphery_client::PeripheryClient;
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::Rng;
use resolver_api::Resolve; use resolver_api::Resolve;
use crate::{ use crate::{
config::core_config, api::write::WriteArgs, config::core_config, resource,
resource, state::db_client,
state::{db_client, State},
}; };
pub mod action_state; pub mod action_state;
@@ -55,8 +54,8 @@ pub fn empty_or_only_spaces(word: &str) -> bool {
} }
pub fn random_string(length: usize) -> String { pub fn random_string(length: usize) -> String {
thread_rng() rand::rng()
.sample_iter(&Alphanumeric) .sample_iter(&rand::distr::Alphanumeric)
.take(length) .take(length)
.map(char::from) .map(char::from)
.collect() .collect()
@@ -209,7 +208,9 @@ pub async fn startup_cleanup() {
async fn startup_in_progress_update_cleanup() { async fn startup_in_progress_update_cleanup() {
let log = Log::error( let log = Log::error(
"Komodo shutdown", "Komodo shutdown",
String::from("Komodo shutdown during execution. If this is a build, the builder may not have been terminated.") String::from(
"Komodo shutdown during execution. If this is a build, the builder may not have been terminated.",
),
); );
// This static log won't fail to serialize, unwrap ok. // This static log won't fail to serialize, unwrap ok.
let log = to_document(&log).unwrap(); let log = to_document(&log).unwrap();
@@ -305,46 +306,50 @@ pub async fn ensure_first_server_and_builder() {
let server = if let Some(server) = server { let server = if let Some(server) = server {
server server
} else { } else {
match State match (CreateServer {
.resolve( name: format!("server-{}", random_string(5)),
CreateServer { config: PartialServerConfig {
name: format!("server-{}", random_string(5)), address: Some(first_server.to_string()),
config: PartialServerConfig { enabled: Some(true),
address: Some(first_server.to_string()), ..Default::default()
enabled: Some(true), },
..Default::default() })
}, .resolve(&WriteArgs {
}, user: system_user().to_owned(),
system_user().to_owned(), })
) .await
.await
{ {
Ok(server) => server, Ok(server) => server,
Err(e) => { Err(e) => {
error!("Failed to initialize 'first_server'. Failed to CreateServer. {e:?}"); error!(
"Failed to initialize 'first_server'. Failed to CreateServer. {:#}",
e.error
);
return; return;
} }
} }
}; };
let Ok(None) = db.builders let Ok(None) = db.builders
.find_one(Document::new()).await .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; return;
}; };
if let Err(e) = State if let Err(e) = (CreateBuilder {
.resolve( name: String::from("local"),
CreateBuilder { config: PartialBuilderConfig::Server(
name: String::from("local"), PartialServerBuilderConfig {
config: PartialBuilderConfig::Server( server_id: Some(server.id),
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

@@ -1,7 +1,7 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use formatting::{bold, colored, format_serror, muted, Color}; use formatting::{Color, bold, colored, format_serror, muted};
use futures::future::join_all; use futures::future::join_all;
use komodo_client::{ use komodo_client::{
api::execute::*, api::execute::*,
@@ -21,9 +21,12 @@ use resolver_api::Resolve;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
api::execute::ExecuteRequest, api::{
resource::{list_full_for_user_using_pattern, KomodoResource}, execute::{ExecuteArgs, ExecuteRequest},
state::{db_client, State}, write::WriteArgs,
},
resource::{KomodoResource, list_full_for_user_using_pattern},
state::db_client,
}; };
use super::update::{init_execution_update, update_update}; use super::update::{init_execution_update, update_update};
@@ -203,7 +206,7 @@ async fn execute_stage(
join_all(futures) join_all(futures)
.await .await
.into_iter() .into_iter()
.collect::<anyhow::Result<_>>()?; .collect::<anyhow::Result<Vec<_>>>()?;
Ok(()) Ok(())
} }
@@ -227,9 +230,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RunProcedure"), .context("Failed at RunProcedure"),
&update_id, &update_id,
) )
@@ -249,9 +253,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RunAction"), .context("Failed at RunAction"),
&update_id, &update_id,
) )
@@ -271,9 +276,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RunBuild"), .context("Failed at RunBuild"),
&update_id, &update_id,
) )
@@ -293,9 +299,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at CancelBuild"), .context("Failed at CancelBuild"),
&update_id, &update_id,
) )
@@ -309,9 +316,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at Deploy"), .context("Failed at Deploy"),
&update_id, &update_id,
) )
@@ -331,9 +339,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PullDeployment"), .context("Failed at PullDeployment"),
&update_id, &update_id,
) )
@@ -347,9 +356,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StartDeployment"), .context("Failed at StartDeployment"),
&update_id, &update_id,
) )
@@ -363,9 +373,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RestartDeployment"), .context("Failed at RestartDeployment"),
&update_id, &update_id,
) )
@@ -379,9 +390,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PauseDeployment"), .context("Failed at PauseDeployment"),
&update_id, &update_id,
) )
@@ -395,9 +407,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at UnpauseDeployment"), .context("Failed at UnpauseDeployment"),
&update_id, &update_id,
) )
@@ -411,9 +424,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StopDeployment"), .context("Failed at StopDeployment"),
&update_id, &update_id,
) )
@@ -427,9 +441,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RemoveDeployment"), .context("Failed at RemoveDeployment"),
&update_id, &update_id,
) )
@@ -449,9 +464,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at CloneRepo"), .context("Failed at CloneRepo"),
&update_id, &update_id,
) )
@@ -471,9 +487,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PullRepo"), .context("Failed at PullRepo"),
&update_id, &update_id,
) )
@@ -493,9 +510,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at BuildRepo"), .context("Failed at BuildRepo"),
&update_id, &update_id,
) )
@@ -515,9 +533,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at CancelRepoBuild"), .context("Failed at CancelRepoBuild"),
&update_id, &update_id,
) )
@@ -531,9 +550,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StartContainer"), .context("Failed at StartContainer"),
&update_id, &update_id,
) )
@@ -547,9 +567,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RestartContainer"), .context("Failed at RestartContainer"),
&update_id, &update_id,
) )
@@ -563,9 +584,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PauseContainer"), .context("Failed at PauseContainer"),
&update_id, &update_id,
) )
@@ -579,9 +601,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at UnpauseContainer"), .context("Failed at UnpauseContainer"),
&update_id, &update_id,
) )
@@ -595,9 +618,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StopContainer"), .context("Failed at StopContainer"),
&update_id, &update_id,
) )
@@ -611,9 +635,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RemoveContainer"), .context("Failed at RemoveContainer"),
&update_id, &update_id,
) )
@@ -627,9 +652,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StartAllContainers"), .context("Failed at StartAllContainers"),
&update_id, &update_id,
) )
@@ -643,9 +669,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RestartAllContainers"), .context("Failed at RestartAllContainers"),
&update_id, &update_id,
) )
@@ -659,9 +686,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PauseAllContainers"), .context("Failed at PauseAllContainers"),
&update_id, &update_id,
) )
@@ -675,9 +703,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at UnpauseAllContainers"), .context("Failed at UnpauseAllContainers"),
&update_id, &update_id,
) )
@@ -691,9 +720,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StopAllContainers"), .context("Failed at StopAllContainers"),
&update_id, &update_id,
) )
@@ -707,9 +737,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PruneContainers"), .context("Failed at PruneContainers"),
&update_id, &update_id,
) )
@@ -723,9 +754,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at DeleteNetwork"), .context("Failed at DeleteNetwork"),
&update_id, &update_id,
) )
@@ -739,9 +771,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PruneNetworks"), .context("Failed at PruneNetworks"),
&update_id, &update_id,
) )
@@ -755,9 +788,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at DeleteImage"), .context("Failed at DeleteImage"),
&update_id, &update_id,
) )
@@ -771,9 +805,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PruneImages"), .context("Failed at PruneImages"),
&update_id, &update_id,
) )
@@ -787,9 +822,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at DeleteVolume"), .context("Failed at DeleteVolume"),
&update_id, &update_id,
) )
@@ -803,9 +839,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PruneVolumes"), .context("Failed at PruneVolumes"),
&update_id, &update_id,
) )
@@ -819,9 +856,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PruneDockerBuilders"), .context("Failed at PruneDockerBuilders"),
&update_id, &update_id,
) )
@@ -835,9 +873,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PruneBuildx"), .context("Failed at PruneBuildx"),
&update_id, &update_id,
) )
@@ -851,9 +890,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PruneSystem"), .context("Failed at PruneSystem"),
&update_id, &update_id,
) )
@@ -867,18 +907,20 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RunSync"), .context("Failed at RunSync"),
&update_id, &update_id,
) )
.await? .await?
} }
// Exception: This is a write operation. // Exception: This is a write operation.
Execution::CommitSync(req) => State Execution::CommitSync(req) => req
.resolve(req, user) .resolve(&WriteArgs { user })
.await .await
.map_err(|e| e.error)
.context("Failed at CommitSync")?, .context("Failed at CommitSync")?,
Execution::DeployStack(req) => { Execution::DeployStack(req) => {
let req = ExecuteRequest::DeployStack(req); let req = ExecuteRequest::DeployStack(req);
@@ -888,9 +930,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at DeployStack"), .context("Failed at DeployStack"),
&update_id, &update_id,
) )
@@ -910,9 +953,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at DeployStackIfChanged"), .context("Failed at DeployStackIfChanged"),
&update_id, &update_id,
) )
@@ -932,9 +976,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PullStack"), .context("Failed at PullStack"),
&update_id, &update_id,
) )
@@ -948,9 +993,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StartStack"), .context("Failed at StartStack"),
&update_id, &update_id,
) )
@@ -964,9 +1010,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at RestartStack"), .context("Failed at RestartStack"),
&update_id, &update_id,
) )
@@ -980,9 +1027,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at PauseStack"), .context("Failed at PauseStack"),
&update_id, &update_id,
) )
@@ -996,9 +1044,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at UnpauseStack"), .context("Failed at UnpauseStack"),
&update_id, &update_id,
) )
@@ -1012,9 +1061,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at StopStack"), .context("Failed at StopStack"),
&update_id, &update_id,
) )
@@ -1028,9 +1078,10 @@ async fn execute_execution(
}; };
let update_id = update.id.clone(); let update_id = update.id.clone();
handle_resolve_result( handle_resolve_result(
State req
.resolve(req, (user, update)) .resolve(&ExecuteArgs { user, update })
.await .await
.map_err(|e| e.error)
.context("Failed at DestroyStack"), .context("Failed at DestroyStack"),
&update_id, &update_id,
) )
@@ -1042,6 +1093,23 @@ async fn execute_execution(
"Batch method BatchDestroyStack not implemented correctly" "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) => { Execution::Sleep(req) => {
let duration = Duration::from_millis(req.duration_ms as u64); let duration = Duration::from_millis(req.duration_ms as u64);
tokio::time::sleep(duration).await; tokio::time::sleep(duration).await;
@@ -1191,7 +1259,7 @@ impl ExtendBatch for BatchDeployStack {
fn single_execution(stack: String) -> Execution { fn single_execution(stack: String) -> Execution {
Execution::DeployStack(DeployStack { Execution::DeployStack(DeployStack {
stack, stack,
service: None, services: Vec::new(),
stop_time: None, stop_time: None,
}) })
} }
@@ -1212,7 +1280,7 @@ impl ExtendBatch for BatchDestroyStack {
fn single_execution(stack: String) -> Execution { fn single_execution(stack: String) -> Execution {
Execution::DestroyStack(DestroyStack { Execution::DestroyStack(DestroyStack {
stack, stack,
service: None, services: Vec::new(),
remove_orphans: false, remove_orphans: false,
stop_time: None, stop_time: None,
}) })

View File

@@ -1,6 +1,6 @@
use anyhow::Context; use anyhow::Context;
use async_timing_util::{ use async_timing_util::{
unix_timestamp_ms, wait_until_timelength, Timelength, ONE_DAY_MS, ONE_DAY_MS, Timelength, unix_timestamp_ms, wait_until_timelength,
}; };
use futures::future::join_all; use futures::future::join_all;
use mungos::{find::find_collect, mongodb::bson::doc}; use mungos::{find::find_collect, mongodb::bson::doc};
@@ -34,8 +34,9 @@ async fn prune_images() -> anyhow::Result<()> {
.await .await
.context("failed to get servers from db")? .context("failed to get servers from db")?
.into_iter() .into_iter()
// This could be done in the mongo query, but rather have rust type system guarantee this. .filter(|server| {
.filter(|server| server.config.auto_prune) server.config.enabled && server.config.auto_prune
})
.map(|server| async move { .map(|server| async move {
( (
async { async {

View File

@@ -1,7 +1,8 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use komodo_client::entities::{ use komodo_client::entities::{
Operation, ResourceTarget, ResourceTargetVariant,
action::Action, action::Action,
alerter::Alerter, alerter::Alerter,
build::Build, build::Build,
@@ -17,15 +18,14 @@ use komodo_client::entities::{
sync::ResourceSync, sync::ResourceSync,
tag::Tag, tag::Tag,
update::Update, update::Update,
user::{admin_service_user, User}, user::{User, admin_service_user},
user_group::UserGroup, user_group::UserGroup,
variable::Variable, variable::Variable,
Operation, ResourceTarget, ResourceTargetVariant,
}; };
use mungos::{ use mungos::{
find::find_collect, find::find_collect,
mongodb::{ mongodb::{
bson::{doc, oid::ObjectId, Document}, bson::{Document, doc, oid::ObjectId},
options::FindOneOptions, options::FindOneOptions,
}, },
}; };
@@ -359,8 +359,8 @@ pub struct VariablesAndSecrets {
pub secrets: HashMap<String, String>, pub secrets: HashMap<String, String>,
} }
pub async fn get_variables_and_secrets( pub async fn get_variables_and_secrets()
) -> anyhow::Result<VariablesAndSecrets> { -> anyhow::Result<VariablesAndSecrets> {
let variables = find_collect(&db_client().variables, None, None) let variables = find_collect(&db_client().variables, None, None)
.await .await
.context("failed to get all variables from db")?; .context("failed to get all variables from db")?;

View File

@@ -1,6 +1,8 @@
use anyhow::Context; use anyhow::Context;
use komodo_client::entities::{ use komodo_client::entities::{
Operation, ResourceTarget,
action::Action, action::Action,
alerter::Alerter,
build::Build, build::Build,
deployment::Deployment, deployment::Deployment,
komodo_timestamp, komodo_timestamp,
@@ -12,7 +14,6 @@ use komodo_client::entities::{
sync::ResourceSync, sync::ResourceSync,
update::{Update, UpdateListItem}, update::{Update, UpdateListItem},
user::User, user::User,
Operation, ResourceTarget,
}; };
use mungos::{ use mungos::{
by_id::{find_one_by_id, update_one_by_id}, by_id::{find_one_by_id, update_one_by_id},
@@ -262,7 +263,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchDeploy(_data) => { ExecuteRequest::BatchDeploy(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
ExecuteRequest::PullDeployment(data) => ( ExecuteRequest::PullDeployment(data) => (
Operation::PullDeployment, Operation::PullDeployment,
@@ -307,7 +308,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchDestroyDeployment(_data) => { ExecuteRequest::BatchDestroyDeployment(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
// Build // Build
@@ -318,7 +319,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchRunBuild(_data) => { ExecuteRequest::BatchRunBuild(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
ExecuteRequest::CancelBuild(data) => ( ExecuteRequest::CancelBuild(data) => (
Operation::CancelBuild, Operation::CancelBuild,
@@ -335,7 +336,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchCloneRepo(_data) => { ExecuteRequest::BatchCloneRepo(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
ExecuteRequest::PullRepo(data) => ( ExecuteRequest::PullRepo(data) => (
Operation::PullRepo, Operation::PullRepo,
@@ -344,7 +345,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchPullRepo(_data) => { ExecuteRequest::BatchPullRepo(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
ExecuteRequest::BuildRepo(data) => ( ExecuteRequest::BuildRepo(data) => (
Operation::BuildRepo, Operation::BuildRepo,
@@ -353,7 +354,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchBuildRepo(_data) => { ExecuteRequest::BatchBuildRepo(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
ExecuteRequest::CancelRepoBuild(data) => ( ExecuteRequest::CancelRepoBuild(data) => (
Operation::CancelRepoBuild, Operation::CancelRepoBuild,
@@ -370,7 +371,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchRunProcedure(_) => { ExecuteRequest::BatchRunProcedure(_) => {
return Ok(Default::default()) return Ok(Default::default());
} }
// Action // Action
@@ -381,7 +382,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchRunAction(_) => { ExecuteRequest::BatchRunAction(_) => {
return Ok(Default::default()) return Ok(Default::default());
} }
// Server template // Server template
@@ -404,7 +405,7 @@ pub async fn init_execution_update(
// Stack // Stack
ExecuteRequest::DeployStack(data) => ( ExecuteRequest::DeployStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::DeployStackService Operation::DeployStackService
} else { } else {
Operation::DeployStack Operation::DeployStack
@@ -414,7 +415,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchDeployStack(_data) => { ExecuteRequest::BatchDeployStack(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
ExecuteRequest::DeployStackIfChanged(data) => ( ExecuteRequest::DeployStackIfChanged(data) => (
Operation::DeployStack, Operation::DeployStack,
@@ -423,10 +424,10 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchDeployStackIfChanged(_data) => { ExecuteRequest::BatchDeployStackIfChanged(_data) => {
return Ok(Default::default()) return Ok(Default::default());
} }
ExecuteRequest::StartStack(data) => ( ExecuteRequest::StartStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::StartStackService Operation::StartStackService
} else { } else {
Operation::StartStack Operation::StartStack
@@ -436,7 +437,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::PullStack(data) => ( ExecuteRequest::PullStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::PullStackService Operation::PullStackService
} else { } else {
Operation::PullStack Operation::PullStack
@@ -446,7 +447,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::RestartStack(data) => ( ExecuteRequest::RestartStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::RestartStackService Operation::RestartStackService
} else { } else {
Operation::RestartStack Operation::RestartStack
@@ -456,7 +457,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::PauseStack(data) => ( ExecuteRequest::PauseStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::PauseStackService Operation::PauseStackService
} else { } else {
Operation::PauseStack Operation::PauseStack
@@ -466,7 +467,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::UnpauseStack(data) => ( ExecuteRequest::UnpauseStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::UnpauseStackService Operation::UnpauseStackService
} else { } else {
Operation::UnpauseStack Operation::UnpauseStack
@@ -476,7 +477,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::StopStack(data) => ( ExecuteRequest::StopStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::StopStackService Operation::StopStackService
} else { } else {
Operation::StopStack Operation::StopStack
@@ -486,7 +487,7 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::DestroyStack(data) => ( ExecuteRequest::DestroyStack(data) => (
if data.service.is_some() { if !data.services.is_empty() {
Operation::DestroyStackService Operation::DestroyStackService
} else { } else {
Operation::DestroyStack Operation::DestroyStack
@@ -496,15 +497,26 @@ pub async fn init_execution_update(
), ),
), ),
ExecuteRequest::BatchDestroyStack(_data) => { ExecuteRequest::BatchDestroyStack(_data) => {
return Ok(Default::default()) 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); let mut update = make_update(target, operation, user);
update.in_progress(); update.in_progress();
// Hold off on even adding update for DeployStackIfChanged // Hold off on even adding update for DeployStackIfChanged
if !matches!(&request, ExecuteRequest::DeployStackIfChanged(_)) { if !matches!(&request, ExecuteRequest::DeployStackIfChanged(_)) {
// Don't actually send it here, let the handlers send it after they can set action state. // 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?; update.id = add_update_without_send(&update).await?;
} }
Ok(update) Ok(update)
} }

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use axum::http::HeaderMap; use axum::http::HeaderMap;
use hex::ToHex; use hex::ToHex;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{

View File

@@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{http::HeaderMap, Router}; use axum::{Router, http::HeaderMap};
use komodo_client::entities::resource::Resource; use komodo_client::entities::resource::Resource;
use tokio::sync::Mutex; use tokio::sync::Mutex;

View File

@@ -15,11 +15,14 @@ use resolver_api::Resolve;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
api::execute::ExecuteRequest, api::{
helpers::update::init_execution_update, state::State, execute::{ExecuteArgs, ExecuteRequest},
write::WriteArgs,
},
helpers::update::init_execution_update,
}; };
use super::{ListenerLockCache, ANY_BRANCH}; use super::{ANY_BRANCH, ListenerLockCache};
// ======= // =======
// BUILD // BUILD
@@ -58,7 +61,10 @@ pub async fn handle_build_webhook<B: super::VerifyBranch>(
let ExecuteRequest::RunBuild(req) = req else { let ExecuteRequest::RunBuild(req) = req else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }
@@ -93,7 +99,10 @@ impl RepoExecution for CloneRepo {
else { else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }
} }
@@ -110,7 +119,10 @@ impl RepoExecution for PullRepo {
else { else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }
} }
@@ -127,7 +139,10 @@ impl RepoExecution for BuildRepo {
else { else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }
} }
@@ -196,33 +211,37 @@ fn stack_locks() -> &'static ListenerLockCache {
} }
pub trait StackExecution { pub trait StackExecution {
async fn resolve(stack: Stack) -> anyhow::Result<()>; async fn resolve(stack: Stack) -> serror::Result<()>;
} }
impl StackExecution for RefreshStackCache { impl StackExecution for RefreshStackCache {
async fn resolve(stack: Stack) -> anyhow::Result<()> { async fn resolve(stack: Stack) -> serror::Result<()> {
let user = git_webhook_user().to_owned(); RefreshStackCache { stack: stack.id }
State .resolve(&WriteArgs {
.resolve(RefreshStackCache { stack: stack.id }, user) user: git_webhook_user().to_owned(),
})
.await?; .await?;
Ok(()) Ok(())
} }
} }
impl StackExecution for DeployStack { impl StackExecution for DeployStack {
async fn resolve(stack: Stack) -> anyhow::Result<()> { async fn resolve(stack: Stack) -> serror::Result<()> {
let user = git_webhook_user().to_owned(); let user = git_webhook_user().to_owned();
if stack.config.webhook_force_deploy { if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack { let req = ExecuteRequest::DeployStack(DeployStack {
stack: stack.id, stack: stack.id,
service: None, services: Vec::new(),
stop_time: None, stop_time: None,
}); });
let update = init_execution_update(&req, &user).await?; let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::DeployStack(req) = req else { let ExecuteRequest::DeployStack(req) = req else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
} else { } else {
let req = let req =
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged { ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
@@ -233,7 +252,10 @@ impl StackExecution for DeployStack {
let ExecuteRequest::DeployStackIfChanged(req) = req else { let ExecuteRequest::DeployStackIfChanged(req) = req else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
} }
Ok(()) Ok(())
@@ -282,7 +304,7 @@ pub async fn handle_stack_webhook_inner<
B::verify_branch(&body, &stack.config.branch)?; B::verify_branch(&body, &stack.config.branch)?;
E::resolve(stack).await E::resolve(stack).await.map_err(|e| e.error)
} }
// ====== // ======
@@ -306,10 +328,12 @@ pub trait SyncExecution {
impl SyncExecution for RefreshResourceSyncPending { impl SyncExecution for RefreshResourceSyncPending {
async fn resolve(sync: ResourceSync) -> anyhow::Result<()> { async fn resolve(sync: ResourceSync) -> anyhow::Result<()> {
let user = git_webhook_user().to_owned(); RefreshResourceSyncPending { sync: sync.id }
State .resolve(&WriteArgs {
.resolve(RefreshResourceSyncPending { sync: sync.id }, user) user: git_webhook_user().to_owned(),
.await?; })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }
} }
@@ -326,7 +350,10 @@ impl SyncExecution for RunSync {
let ExecuteRequest::RunSync(req) = req else { let ExecuteRequest::RunSync(req) = req else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }
} }
@@ -422,7 +449,10 @@ pub async fn handle_procedure_webhook<B: super::VerifyBranch>(
let ExecuteRequest::RunProcedure(req) = req else { let ExecuteRequest::RunProcedure(req) = req else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }
@@ -467,6 +497,9 @@ pub async fn handle_action_webhook<B: super::VerifyBranch>(
let ExecuteRequest::RunAction(req) = req else { let ExecuteRequest::RunAction(req) = req else {
unreachable!() unreachable!()
}; };
State.resolve(req, (user, update)).await?; req
.resolve(&ExecuteArgs { user, update })
.await
.map_err(|e| e.error)?;
Ok(()) Ok(())
} }

View File

@@ -1,4 +1,4 @@
use axum::{extract::Path, http::HeaderMap, routing::post, Router}; use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use komodo_client::entities::{ use komodo_client::entities::{
action::Action, build::Build, procedure::Procedure, repo::Repo, action::Action, build::Build, procedure::Procedure, repo::Repo,
resource::Resource, stack::Stack, sync::ResourceSync, resource::Resource, stack::Stack, sync::ResourceSync,
@@ -11,13 +11,13 @@ use tracing::Instrument;
use crate::resource::KomodoResource; use crate::resource::KomodoResource;
use super::{ use super::{
CustomSecret, VerifyBranch, VerifySecret,
resources::{ resources::{
RepoWebhookOption, StackWebhookOption, SyncWebhookOption,
handle_action_webhook, handle_build_webhook, handle_action_webhook, handle_build_webhook,
handle_procedure_webhook, handle_repo_webhook, handle_procedure_webhook, handle_repo_webhook,
handle_stack_webhook, handle_sync_webhook, RepoWebhookOption, handle_stack_webhook, handle_sync_webhook,
StackWebhookOption, SyncWebhookOption,
}, },
CustomSecret, VerifyBranch, VerifySecret,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -45,7 +45,7 @@ fn default_branch() -> String {
pub fn router<P: VerifySecret + VerifyBranch>() -> Router { pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
Router::new() Router::new()
.route( .route(
"/build/:id", "/build/{id}",
post( post(
|Path(Id { id }), headers: HeaderMap, body: String| async move { |Path(Id { id }), headers: HeaderMap, body: String| async move {
let build = let build =
@@ -71,7 +71,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
), ),
) )
.route( .route(
"/repo/:id/:option", "/repo/{id}/{option}",
post( post(
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move { |Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let repo = let repo =
@@ -97,7 +97,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
), ),
) )
.route( .route(
"/stack/:id/:option", "/stack/{id}/{option}",
post( post(
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move { |Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let stack = let stack =
@@ -123,7 +123,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
), ),
) )
.route( .route(
"/sync/:id/:option", "/sync/{id}/{option}",
post( post(
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move { |Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
let sync = let sync =
@@ -149,7 +149,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
), ),
) )
.route( .route(
"/procedure/:id/:branch", "/procedure/{id}/{branch}",
post( post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move { |Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
let procedure = let procedure =
@@ -175,7 +175,7 @@ pub fn router<P: VerifySecret + VerifyBranch>() -> Router {
), ),
) )
.route( .route(
"/action/:id/:branch", "/action/{id}/{branch}",
post( post(
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move { |Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
let action = let action =

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