Compare commits

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

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

* fix rust warning

* review: sanitize errors

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

* review: do not run post_deploy if deploy failed
2025-02-08 18:45:32 -08:00
mbecker20 4c9479a8bc 1.17.0-dev 2025-02-08 18:45:32 -08:00
unsyncandmbecker20 19aa5fb260 feature: use the repo path instead of name in GetLatestCommit (#282)
* Update repo path handling in commit fetching

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

* feat: use optional name and path in GetLatestCommit

* review: don't use optional for name

* review: use helper

* review: remove redundant to_string()
2025-02-08 18:45:32 -08:00
mbecker20 75c0c967ac update available deployment table 2025-02-08 18:45:32 -08:00
mbecker20 8832a05ffe show update available stack table 2025-02-08 18:45:32 -08:00
mbecker20 7598978cd1 finish oidc comment 2025-02-08 18:45:32 -08:00
mbecker20 b868ad5794 clean up rust client websocket subscription 2025-02-08 18:45:32 -08:00
mbecker20 be6a0db511 escape incoming sync backslashes (BREAKING) 2025-02-08 18:45:32 -08:00
mbecker20 c360289984 rename Test Alerter button 2025-02-08 18:45:32 -08:00
mbecker20 23abdb85b0 simplify network stats 2025-02-08 18:45:32 -08:00
mbecker20 b510d6dc41 komodo-logo 2025-02-08 18:45:31 -08:00
mbecker20 f29c60a6e9 higher quality / colored icons 2025-02-08 18:45:31 -08:00
mbecker20 ee33fc98d9 Add test alerter button 2025-02-08 18:45:31 -08:00
mbecker20 729ffc9c3c fix last axum updates 2025-02-08 18:45:31 -08:00
mbecker20 395930094d axum update :param to {param} syntax 2025-02-08 18:45:31 -08:00
mbecker20 79a16c6f22 rust 1.84.0 2025-02-08 18:45:31 -08:00
mbecker20 4e4aa8c567 test alert implementation 2025-02-08 18:45:31 -08:00
mbecker20 bfbcd33c1a add entities / message for test alerter 2025-02-08 18:45:31 -08:00
mbecker20 01e2e0be11 the komodo env file should be highest priority over additional files 2025-02-08 18:45:31 -08:00
mbecker20 2e6e409b85 clean up cors 2025-02-08 18:45:31 -08:00
mbecker20 30e87b6aff just make it 1.17.0 2025-02-08 18:45:31 -08:00
mbecker20 8423a53106 bump aws deps 2025-02-08 18:45:31 -08:00
mbecker20 9e6c122313 axum to 0.8 2025-02-08 18:45:31 -08:00
mbecker20 d03d0b3b78 resource2 not really a benefit 2025-02-08 18:45:31 -08:00
mbecker20 1bf76f1b57 format 2025-02-08 18:45:31 -08:00
mbecker20 7815639aeb fmt 2025-02-08 18:45:31 -08:00
mbecker20 af9fbf9667 resolver v3
add new ec2 instance types

clean up testing config

document the libraries a bit

clean up main

update sysinfo and otel

update client resolver 3.0

resolver v3 prog

clean up gitignore

implement periphery resolver v3

clean up

core read api v3

more prog

execute api

missing apis

compiling

1.16.13

work on more granular traits

prog on crud
2025-02-08 18:45:31 -08:00
471 changed files with 18007 additions and 38842 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ services:
db:
extends:
file: ../dev.compose.yaml
file: ../test.compose.yaml
service: ferretdb
volumes:
+1 -1
View File
@@ -10,7 +10,7 @@
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20.12.2"
"version": "18.18.0"
},
"ghcr.io/devcontainers-community/features/deno:1": {
-2
View File
@@ -1,2 +0,0 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
open_collective: komodo
Generated
+956 -1296
View File
File diff suppressed because it is too large Load Diff
+38 -51
View File
@@ -8,8 +8,8 @@ members = [
]
[workspace.package]
version = "1.18.2"
edition = "2024"
version = "1.17.0-dev-1"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
repository = "https://github.com/moghtech/komodo"
@@ -30,7 +30,7 @@ git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.5.0", default-features = false }
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
merge_config_files = "0.1.5"
@@ -40,90 +40,77 @@ derive_variants = "1.0.0"
mongo_indexed = "2.0.1"
resolver_api = "3.0.0"
toml_pretty = "1.1.2"
mungos = "3.2.0"
mungos = "1.1.0"
svi = "1.0.1"
# ASYNC
reqwest = { version = "0.12.20", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
tokio = { version = "1.45.1", features = ["full"] }
tokio-util = { version = "0.7.15", features = ["io", "codec"] }
tokio-stream = { version = "0.1.17", features = ["sync"] }
pin-project-lite = "0.2.16"
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "1.43.0", features = ["full"] }
tokio-util = "0.7.13"
futures = "0.3.31"
futures-util = "0.3.31"
arc-swap = "1.7.1"
# SERVER
tokio-tungstenite = { version = "0.27.0", features = ["rustls-tls-native-roots"] }
axum-extra = { version = "0.10.1", features = ["typed-header"] }
tower-http = { version = "0.6.4", features = ["fs", "cors"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
axum = { version = "0.8.4", features = ["ws", "json", "macros"] }
axum-extra = { version = "0.10.0", features = ["typed-header"] }
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
tokio-tungstenite = "0.26.1"
# SER/DE
indexmap = { version = "2.9.0", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
strum = { version = "0.27.1", features = ["derive"] }
serde_json = "1.0.140"
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
serde = { version = "1.0.217", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
serde_json = "1.0.135"
serde_yaml = "0.9.34"
serde_qs = "0.15.0"
toml = "0.8.22"
toml = "0.8.19"
# ERROR
anyhow = "1.0.98"
thiserror = "2.0.12"
anyhow = "1.0.95"
thiserror = "2.0.11"
# LOGGING
opentelemetry-otlp = { version = "0.29.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.27.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.29.0"
tracing-opentelemetry = "0.30.0"
opentelemetry = "0.29.1"
opentelemetry-semantic-conventions = "0.27.0"
tracing-opentelemetry = "0.28.0"
opentelemetry = "0.27.1"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.38", features = ["derive"] }
clap = { version = "4.5.26", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.17.0", features = ["v4", "fast-rng", "serde"] }
jsonwebtoken = { version = "9.3.1", default-features = false }
openidconnect = "4.0.0"
uuid = { version = "1.12.0", features = ["v4", "fast-rng", "serde"] }
openidconnect = "3.5.0"
urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.17.0"
bcrypt = "0.16.0"
base64 = "0.22.1"
rustls = "0.23.27"
rustls = "0.23.21"
hmac = "0.12.1"
sha2 = "0.10.9"
rand = "0.9.1"
sha2 = "0.10.8"
rand = "0.8.5"
jwt = "0.16.0"
hex = "0.4.3"
# SYSTEM
portable-pty = "0.9.0"
bollard = "0.19.0"
sysinfo = "0.35.1"
bollard = "0.18.1"
sysinfo = "0.33.1"
# CLOUD
aws-config = "1.6.3"
aws-sdk-ec2 = "1.134.0"
aws-credential-types = "1.2.3"
## CRON
english-to-cron = "0.1.6"
chrono-tz = "0.10.3"
chrono = "0.4.41"
croner = "2.1.0"
aws-config = "1.5.13"
aws-sdk-ec2 = "1.101.0"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.4"
octorust = "0.10.0"
octorust = "0.9.0"
dashmap = "6.1.0"
wildcard = "0.3.0"
colored = "3.0.0"
regex = "1.11.1"
bytes = "1.10.1"
bson = "2.15.0"
bson = "2.13.0"
+4 -7
View File
@@ -1,7 +1,7 @@
## Builds the Komodo Core, Periphery, and Util binaries
## Builds the Komodo Core and Periphery binaries
## for a specific architecture.
FROM rust:1.87.0-bullseye AS builder
FROM rust:1.84.1-bullseye AS builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -10,21 +10,18 @@ COPY ./client/core/rs ./client/core/rs
COPY ./client/periphery ./client/periphery
COPY ./bin/core ./bin/core
COPY ./bin/periphery ./bin/periphery
COPY ./bin/util ./bin/util
# Compile bin
RUN \
cargo build -p komodo_core --release && \
cargo build -p komodo_periphery --release && \
cargo build -p komodo_util --release
cargo build -p komodo_periphery --release
# Copy just the binaries to scratch image
FROM scratch
COPY --from=builder /builder/target/release/core /core
COPY --from=builder /builder/target/release/periphery /periphery
COPY --from=builder /builder/target/release/util /util
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
LABEL org.opencontainers.image.description="Komodo Binaries"
LABEL org.opencontainers.image.description="Komodo Periphery"
LABEL org.opencontainers.image.licenses=GPL-3.0
+50 -57
View File
@@ -12,7 +12,7 @@ use crate::{
};
pub enum ExecutionResult {
Single(Box<Update>),
Single(Update),
Batch(BatchExecutionResponse),
}
@@ -185,9 +185,6 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::PullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -227,7 +224,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchRunAction(request) => komodo_client()
.execute(request)
.await
@@ -235,7 +232,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchRunProcedure(request) => komodo_client()
.execute(request)
.await
@@ -243,7 +240,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchRunBuild(request) => komodo_client()
.execute(request)
.await
@@ -251,11 +248,11 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::CancelBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::Deploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchDeploy(request) => komodo_client()
.execute(request)
.await
@@ -263,31 +260,31 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::PullDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::StartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::RestartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::UnpauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::StopDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::DestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchDestroyDeployment(request) => komodo_client()
.execute(request)
.await
@@ -295,7 +292,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::CloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchCloneRepo(request) => komodo_client()
.execute(request)
.await
@@ -303,7 +300,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::PullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchPullRepo(request) => komodo_client()
.execute(request)
.await
@@ -311,7 +308,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::BuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchBuildRepo(request) => komodo_client()
.execute(request)
.await
@@ -319,103 +316,103 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::CancelRepoBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::StartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::RestartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::UnpauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::StopContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::DestroyContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::StartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::RestartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::UnpauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::StopAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PruneContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::DeleteNetwork(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PruneNetworks(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::DeleteImage(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PruneImages(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::DeleteVolume(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PruneVolumes(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PruneDockerBuilders(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PruneBuildx(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PruneSystem(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::RunSync(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::CommitSync(request) => komodo_client()
.write(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::DeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchDeployStack(request) => komodo_client()
.execute(request)
.await
@@ -423,7 +420,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchDeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
@@ -431,35 +428,31 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::PullStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
Execution::BatchPullStack(request) => komodo_client()
.execute(request)
.await
.map(ExecutionResult::Batch),
.map(ExecutionResult::Single),
Execution::StartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::RestartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::PauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::UnpauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::StopStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::DestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::BatchDestroyStack(request) => komodo_client()
.execute(request)
.await
@@ -467,7 +460,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::TestAlerter(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u.into())),
.map(ExecutionResult::Single),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);
+3 -10
View File
@@ -37,12 +37,9 @@ mungos.workspace = true
slack.workspace = true
svi.workspace = true
# external
aws-credential-types.workspace = true
tokio-tungstenite.workspace = true
english-to-cron.workspace = true
openidconnect.workspace = true
jsonwebtoken.workspace = true
axum-server.workspace = true
ordered_hash_map.workspace = true
openidconnect.workspace = true
urlencoding.workspace = true
aws-sdk-ec2.workspace = true
aws-config.workspace = true
@@ -52,11 +49,8 @@ tower-http.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
typeshare.workspace = true
chrono-tz.workspace = true
indexmap.workspace = true
octorust.workspace = true
wildcard.workspace = true
arc-swap.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
@@ -64,8 +58,6 @@ futures.workspace = true
nom_pem.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
croner.workspace = true
chrono.workspace = true
bcrypt.workspace = true
base64.workspace = true
rustls.workspace = true
@@ -79,4 +71,5 @@ envy.workspace = true
rand.workspace = true
hmac.workspace = true
sha2.workspace = true
jwt.workspace = true
hex.workspace = true
+5 -4
View File
@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.87.0-bullseye AS core-builder
FROM rust:1.84.1-bullseye AS core-builder
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -24,9 +24,10 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Setup an application directory
WORKDIR /app
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
## Core deps installer
apt-get update
apt-get install -y git curl ca-certificates
rm -rf /var/lib/apt/lists/*
# Starship prompt
curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin
echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc
echo 'eval "$(starship init bash)"' >> /root/.bashrc
+4 -3
View File
@@ -15,9 +15,10 @@ FROM ${FRONTEND_IMAGE} AS frontend
# Final Image
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
+4 -3
View File
@@ -16,9 +16,10 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
FROM debian:bullseye-slim
COPY ./bin/core/starship.toml /config/starship.toml
COPY ./bin/core/debian-deps.sh .
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
# Install Deps
RUN apt update && \
apt install -y git ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Copy
COPY ./config/core.config.toml /config/config.toml
+9 -67
View File
@@ -94,9 +94,7 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to = fmt_docker_container_state(to);
format!(
"📦 Deployment **{name}** is now **{to}**\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
)
format!("📦 Deployment **{name}** is now **{to}**\nserver: **{server_name}**\nprevious: **{from}**\n{link}")
}
AlertData::DeploymentImageUpdateAvailable {
id,
@@ -106,9 +104,7 @@ pub async fn send_alert(
image,
} => {
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 {
id,
@@ -118,9 +114,7 @@ pub async fn send_alert(
image,
} => {
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 {
id,
@@ -132,9 +126,7 @@ pub async fn send_alert(
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to = fmt_stack_state(to);
format!(
"🥞 Stack **{name}** is now {to}\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
)
format!("🥞 Stack **{name}** is now {to}\nserver: **{server_name}**\nprevious: **{from}**\n{link}")
}
AlertData::StackImageUpdateAvailable {
id,
@@ -145,9 +137,7 @@ pub async fn send_alert(
image,
} => {
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 {
id,
@@ -160,17 +150,13 @@ pub async fn send_alert(
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
format!(
"⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}"
)
format!("⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}")
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminated AWS builder instance\ninstance id: **{instance_id}**\n{message}"
)
format!("{level} | Failed to terminated AWS builder instance\ninstance id: **{instance_id}**\n{message}")
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
@@ -181,60 +167,16 @@ pub async fn send_alert(
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!(
"{level} | Build **{name}** failed\nversion: **v{version}**\n{link}"
)
format!("{level} | Build **{name}** failed\nversion: **v{version}**\n{link}")
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for **{name}** failed\n{link}")
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure **{name}** failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action **{name}** failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | **{name}** ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
send_message(&url_interpolated, &content)
.await
.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
send_message(url, &content).await?;
}
Ok(())
}
+12 -44
View File
@@ -1,29 +1,23 @@
use std::collections::HashSet;
use ::slack::types::Block;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use derive_variants::ExtractVariant;
use futures::future::join_all;
use komodo_client::entities::{
ResourceTargetVariant,
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
alerter::*,
deployment::DeploymentState,
komodo_timestamp,
stack::StackState,
ResourceTargetVariant,
};
use mungos::{find::find_collect, mongodb::bson::doc};
use std::collections::HashSet;
use tracing::Instrument;
use crate::helpers::query::get_variables_and_secrets;
use crate::helpers::{
interpolate::interpolate_variables_secrets_into_string,
maintenance::is_in_maintenance,
};
use crate::{config::core_config, state::db_client};
use crate::helpers::interpolate::interpolate_variables_secrets_into_string;
use crate::helpers::query::get_variables_and_secrets;
mod discord;
mod ntfy;
mod pushover;
mod slack;
#[instrument(level = "debug")]
@@ -84,13 +78,6 @@ pub async fn send_alert_to_alerter(
return Ok(());
}
if is_in_maintenance(
&alerter.config.maintenance_windows,
komodo_timestamp(),
) {
return Ok(());
}
let alert_type = alert.data.extract_variant();
// In the test case, we don't want the filters inside this
@@ -141,24 +128,6 @@ pub async fn send_alert_to_alerter(
)
})
}
AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url, email }) => {
ntfy::send_alert(url, email.as_deref(), alert)
.await
.with_context(|| {
format!(
"Failed to send alert to ntfy Alerter {}",
alerter.name
)
})
}
AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {
pushover::send_alert(url, alert).await.with_context(|| {
format!(
"Failed to send alert to Pushover Alerter {}",
alerter.name
)
})
}
}
}
@@ -167,6 +136,7 @@ async fn send_custom_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
@@ -186,14 +156,9 @@ async fn send_custom_alert(
.send()
.await
.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with request: {}",
sanitized_error
))
let replacers = secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error = svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!("Error with request: {}", sanitized_error))
})
.context("failed at post request to alerter")?;
let status = res.status();
@@ -273,6 +238,9 @@ fn resource_link(
ResourceTargetVariant::Action => {
format!("/actions/{id}")
}
ResourceTargetVariant::ServerTemplate => {
format!("/server-templates/{id}")
}
ResourceTargetVariant::ResourceSync => {
format!("/resource-syncs/{id}")
}
-272
View File
@@ -1,272 +0,0 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
email: Option<&str>,
alert: &Alert,
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter {} is working\n{link}",
name,
)
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {}{} is now reachable\n{link}",
name, region
)
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {:#?}", e))
.unwrap_or_default();
format!(
"{level} | {}{} is unreachable ❌\n{link}{err}",
name, region
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
name, region,
)
}
AlertData::ServerMem {
id,
name,
region,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region,
)
}
AlertData::ServerDisk {
id,
name,
region,
path,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region, path,
)
}
AlertData::ContainerStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to_state = fmt_docker_container_state(to);
format!(
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::StackStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to_state = fmt_stack_state(to);
format!(
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!(
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
name, server_name, service, image,
)
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images_str = images.join(", ");
format!(
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
name, server_name, images_label, images_str,
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
instance_id, message,
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on {}\n{link}",
name,
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!(
"{level} | Build {} failed\nversion: v{}\n{link}",
name, version,
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for {} failed\n{link}", name,)
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure {name} failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action {name} failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, email, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
email: Option<&str>,
content: String,
) -> anyhow::Result<()> {
let mut request = http_client()
.post(url)
.header("Title", "ntfy Alert")
.body(content);
if let Some(email) = email {
request = request.header("X-Email", email);
}
let response =
request.send().await.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("ntfy alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to ntfy | {} | failed to get response text",
status
)
})?;
Err(anyhow!(
"Failed to send message to ntfy | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}
-270
View File
@@ -1,270 +0,0 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter {} is working\n{link}",
name,
)
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {}{} is now reachable\n{link}",
name, region
)
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {:#?}", e))
.unwrap_or_default();
format!(
"{level} | {}{} is unreachable ❌\n{link}{err}",
name, region
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
name, region,
)
}
AlertData::ServerMem {
id,
name,
region,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region,
)
}
AlertData::ServerDisk {
id,
name,
region,
path,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region, path,
)
}
AlertData::ContainerStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to_state = fmt_docker_container_state(to);
format!(
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::StackStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to_state = fmt_stack_state(to);
format!(
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!(
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
name, server_name, service, image,
)
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images_str = images.join(", ");
format!(
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
name, server_name, images_label, images_str,
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
instance_id, message,
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on {}\n{link}",
name,
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!(
"{level} | Build {name} failed\nversion: v{version}\n{link}",
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for {} failed\n{link}", name,)
}
AlertData::ProcedureFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Procedure, id);
format!("{level} | Procedure {name} failed\n{link}")
}
AlertData::ActionFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Action, id);
format!("{level} | Action {name} failed\n{link}")
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let link = resource_link(*resource_type, id);
format!(
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
content: String,
) -> anyhow::Result<()> {
// pushover needs all information to be encoded in the URL. At minimum they need
// the user key, the application token, and the message (url encoded).
// other optional params here: https://pushover.net/api (just add them to the
// webhook url along with the application token and the user key).
let content = [("message", content)];
let response = http_client()
.post(url)
.form(&content)
.send()
.await
.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("pushover alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to pushover | {} | failed to get response text",
status
)
})?;
Err(anyhow!(
"Failed to send message to pushover | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}
+14 -87
View File
@@ -73,9 +73,7 @@ pub async fn send_alert(
let region = fmt_region(region);
match alert.level {
SeverityLevel::Ok => {
let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%*"
);
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%*");
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -89,9 +87,7 @@ pub async fn send_alert(
(text, blocks.into())
}
_ => {
let text = format!(
"{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈"
);
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈");
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -117,9 +113,7 @@ pub async fn send_alert(
let percentage = 100.0 * used_gb / total_gb;
match alert.level {
SeverityLevel::Ok => {
let text = format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
);
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -136,9 +130,7 @@ pub async fn send_alert(
(text, blocks.into())
}
_ => {
let text = format!(
"{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾"
);
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -168,9 +160,7 @@ pub async fn send_alert(
let percentage = 100.0 * used_gb / total_gb;
match alert.level {
SeverityLevel::Ok => {
let text = format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
);
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -179,17 +169,12 @@ pub async fn send_alert(
Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
Block::section(resource_link(ResourceTargetVariant::Server, id)),
];
(text, blocks.into())
}
_ => {
let text = format!(
"{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿"
);
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
let blocks = vec![
Block::header(level),
Block::section(format!(
@@ -198,10 +183,7 @@ pub async fn send_alert(
Block::section(format!(
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
)),
Block::section(resource_link(
ResourceTargetVariant::Server,
id,
)),
Block::section(resource_link(ResourceTargetVariant::Server, id)),
];
(text, blocks.into())
}
@@ -373,7 +355,9 @@ pub async fn send_alert(
let text = format!("{level} | Build {name} has failed");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!("version: *v{version}*",)),
Block::section(format!(
"build name: *{name}*\nversion: *v{version}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Build,
id,
@@ -386,6 +370,7 @@ pub async fn send_alert(
format!("{level} | Repo build for *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!("repo name: *{name}*",)),
Block::section(resource_link(
ResourceTargetVariant::Repo,
id,
@@ -393,69 +378,11 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::ProcedureFailed { id, name } => {
let text = format!("{level} | Procedure *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Procedure,
id,
)),
];
(text, blocks.into())
}
AlertData::ActionFailed { id, name } => {
let text = format!("{level} | Action *{name}* has *failed*");
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(
ResourceTargetVariant::Action,
id,
)),
];
(text, blocks.into())
}
AlertData::ScheduleRun {
resource_type,
id,
name,
} => {
let text = format!(
"{level} | *{name}* ({resource_type}) | Scheduled run started 🕝"
);
let blocks = vec![
Block::header(text.clone()),
Block::section(resource_link(*resource_type, id)),
];
(text, blocks.into())
}
AlertData::None {} => Default::default(),
};
if !text.is_empty() {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
let slack = ::slack::Client::new(url_interpolated);
slack.send_message(text, blocks).await.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
let slack = ::slack::Client::new(url);
slack.send_message(text, blocks).await?;
}
Ok(())
}
+13 -23
View File
@@ -1,12 +1,11 @@
use std::{sync::OnceLock, time::Instant};
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
use axum::{http::HeaderMap, routing::post, Router};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::auth::*, entities::user::User};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -16,15 +15,13 @@ use crate::{
get_user_id_from_headers,
github::{self, client::github_oauth_client},
google::{self, client::google_oauth_client},
oidc::{self, client::oidc_client},
oidc,
},
config::core_config,
helpers::query::get_user,
state::jwt_client,
};
use super::Variant;
pub struct AuthArgs {
pub headers: HeaderMap,
}
@@ -48,9 +45,7 @@ pub enum AuthRequest {
}
pub fn router() -> Router {
let mut router = Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler));
let mut router = Router::new().route("/", post(handler));
if core_config().local_auth {
info!("🔑 Local Login Enabled");
@@ -74,18 +69,6 @@ pub fn router() -> Router {
router
}
async fn variant_handler(
headers: HeaderMap,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: AuthRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(headers, Json(req)).await
}
#[instrument(name = "AuthHandler", level = "debug", skip(headers))]
async fn handler(
headers: HeaderMap,
@@ -114,9 +97,16 @@ fn login_options_reponse() -> &'static GetLoginOptionsResponse {
let config = core_config();
GetLoginOptionsResponse {
local: config.local_auth,
github: github_oauth_client().is_some(),
google: google_oauth_client().is_some(),
oidc: oidc_client().load().is_some(),
github: config.github_oauth.enabled
&& !config.github_oauth.id.is_empty()
&& !config.github_oauth.secret.is_empty(),
google: config.google_oauth.enabled
&& !config.google_oauth.id.is_empty()
&& !config.google_oauth.secret.is_empty(),
oidc: config.oidc_enabled
&& !config.oidc_provider.is_empty()
&& !config.oidc_client_id.is_empty()
&& !config.oidc_client_secret.is_empty(),
registration_disabled: config.disable_user_registration,
}
})
+13 -58
View File
@@ -13,13 +13,8 @@ use komodo_client::{
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
action::Action,
alert::{Alert, AlertData, SeverityLevel},
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
update::Update,
user::action_user,
action::Action, config::core::CoreConfig,
permission::PermissionLevel, update::Update, user::action_user,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
@@ -27,7 +22,6 @@ use resolver_api::Resolve;
use tokio::fs;
use crate::{
alert::send_alerts,
api::{execute::ExecuteRequest, user::UserArgs},
config::core_config,
helpers::{
@@ -39,8 +33,7 @@ use crate::{
random_string,
update::update_update,
},
permission::get_check_permissions,
resource::refresh_action_state_cache,
resource::{self, refresh_action_state_cache},
state::{action_states, db_client},
};
@@ -72,10 +65,10 @@ impl Resolve<ExecuteArgs> for RunAction {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut action = get_check_permissions::<Action>(
let mut action = resource::get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Execute.into(),
&user,
PermissionLevel::Execute,
)
.await?;
@@ -118,37 +111,19 @@ impl Resolve<ExecuteArgs> for RunAction {
let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize Action file parent directory {parent:?}"))?;
let _ = fs::create_dir_all(parent).await;
}
fs::write(&path, contents).await.with_context(|| {
format!("Failed to write action file to {path:?}")
})?;
let CoreConfig { ssl_enabled, .. } = core_config();
let https_cert_flag = if *ssl_enabled {
" --unsafely-ignore-certificate-errors=localhost"
} else {
""
};
let reload = if action.config.reload_deno_deps {
" --reload"
} else {
""
};
let mut res = run_komodo_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!(
"deno run --allow-all{https_cert_flag}{reload} {}",
path.display()
),
format!("deno run --allow-all {}", path.display()),
false,
)
.await;
@@ -191,26 +166,6 @@ impl Resolve<ExecuteArgs> for RunAction {
update_update(update.clone()).await?;
if !update.success && action.config.failure_alert {
warn!("action unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::ActionFailed {
id: action.id,
name: action.name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
}
}
@@ -252,7 +207,7 @@ fn full_contents(contents: &str, key: &str, secret: &str) -> String {
let protocol = if *ssl_enabled { "https" } else { "http" };
let base_url = format!("{protocol}://localhost:{port}");
format!(
"import {{ KomodoClient, Types }} from '{base_url}/client/lib.js';
"import {{ KomodoClient }} from '{base_url}/client/lib.js';
import * as __YAML__ from 'jsr:@std/yaml';
import * as __TOML__ from 'jsr:@std/toml';
@@ -288,7 +243,7 @@ main()
console.error('Status:', error.status);
console.error(JSON.stringify(error.result, null, 2));
}} else {{
console.error(error);
console.error(JSON.stringify(error, null, 2));
}}
Deno.exit(1)
}});"
@@ -350,8 +305,8 @@ fn delete_file(
if name == file {
if let Err(e) = fs::remove_file(entry.path()).await {
warn!(
"Failed to clean up generated file after action execution | {e:#}"
);
"Failed to clean up generated file after action execution | {e:#}"
);
};
return true;
}
+2 -2
View File
@@ -12,7 +12,7 @@ use resolver_api::Resolve;
use crate::{
alert::send_alert_to_alerter, helpers::update::update_update,
permission::get_check_permissions,
resource::get_check_permissions,
};
use super::ExecuteArgs;
@@ -26,7 +26,7 @@ impl Resolve<ExecuteArgs> for TestAlerter {
let alerter = get_check_permissions::<Alerter>(
&self.alerter,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
+109 -117
View File
@@ -1,6 +1,6 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
@@ -16,7 +16,6 @@ use komodo_client::{
deployment::DeploymentState,
komodo_timestamp,
permission::PermissionLevel,
repo::Repo,
update::{Log, Update},
user::auto_redeploy_user,
},
@@ -36,9 +35,9 @@ use tokio_util::sync::CancellationToken;
use crate::{
alert::send_alerts,
helpers::{
build_git_token,
builder::{cleanup_builder_instance, get_builder_periphery},
channel::build_cancel_channel,
git_token,
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_extra_args,
@@ -49,7 +48,6 @@ use crate::{
registry_token,
update::{init_execution_update, update_update},
},
permission::get_check_permissions,
resource::{self, refresh_build_state_cache},
state::{action_states, db_client},
};
@@ -82,29 +80,13 @@ impl Resolve<ExecuteArgs> for RunBuild {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut build = get_check_permissions::<Build>(
let mut build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
&user,
PermissionLevel::Execute,
)
.await?;
let mut repo = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&build.config.linked_repo)
.await?
.into()
} else {
None
};
let mut vars_and_secrets = get_variables_and_secrets().await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
if build.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to RunBuild").into());
@@ -128,8 +110,23 @@ impl Resolve<ExecuteArgs> for RunBuild {
update.version = build.config.version;
update_update(update.clone()).await?;
let git_token =
build_git_token(&mut build, repo.as_mut()).await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
if !vars_and_secrets.variables.contains_key("$VERSION") {
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
}
let git_token = git_token(
&build.config.git_provider,
&build.config.git_account,
|https| build.config.git_https = https,
)
.await
.with_context(
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", build.config.git_provider, build.config.git_account),
)?;
let registry_token =
validate_account_extract_registry_token(&build).await?;
@@ -180,6 +177,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
build.name.clone(),
Some(build.config.version),
@@ -205,8 +203,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
}
};
// INTERPOLATE VARIABLES
// CLONE REPO
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into pre build command
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
@@ -217,34 +216,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.dockerfile,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
@@ -256,63 +227,88 @@ impl Resolve<ExecuteArgs> for RunBuild {
Default::default()
};
let commit_message = if !build.config.files_on_host
&& (!build.config.repo.is_empty()
|| !build.config.linked_repo.is_empty())
{
// PULL OR CLONE REPO
let res = tokio::select! {
res = periphery
.request(api::git::PullOrCloneRepo {
args: repo.as_ref().map(Into::into).unwrap_or((&build).into()),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("Failed build at clone repo | {e:#}");
update.push_error_log(
"Clone Repo",
format_serror(&e.context("Failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
Some(commit_message)
} else {
None
let res = tokio::select! {
res = periphery
.request(api::git::CloneRepo {
args: (&build).into(),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: secret_replacers.into_iter().collect(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("failed build at clone repo | {e:#}");
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
if all_logs_success(&update.logs) {
// RUN BUILD
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into build args
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
&secret_replacers,
);
secret_replacers
} else {
Default::default()
};
let res = tokio::select! {
res = periphery
.request(api::build::Build {
build: build.clone(),
repo,
registry_token,
replacers: secret_replacers.into_iter().collect(),
// Push a commit hash tagged image
@@ -325,7 +321,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
_ = cancel.cancelled() => {
info!("build cancelled during build, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
cleanup_builder_instance(cleanup_data, &mut update)
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
},
@@ -369,9 +365,8 @@ impl Resolve<ExecuteArgs> for RunBuild {
// stop the cancel listening task from going forever
cancel.cancel();
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -520,10 +515,10 @@ impl Resolve<ExecuteArgs> for CancelBuild {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let build = get_check_permissions::<Build>(
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
&user,
PermissionLevel::Execute,
)
.await?;
@@ -565,9 +560,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
)
.await
{
warn!(
"failed to set CancelBuild Update status Complete after timeout | {e:#}"
)
warn!("failed to set CancelBuild Update status Complete after timeout | {e:#}")
}
});
@@ -594,9 +587,8 @@ async fn handle_post_build_redeploy(build_id: &str) {
redeploy_deployments
.into_iter()
.map(|deployment| async move {
let state = get_deployment_state(&deployment.id)
.await
.unwrap_or_default();
let state =
get_deployment_state(&deployment).await.unwrap_or_default();
if state == DeploymentState::Running {
let req = super::ExecuteRequest::Deploy(Deploy {
deployment: deployment.id.clone(),
+7 -8
View File
@@ -1,21 +1,21 @@
use std::{collections::HashSet, sync::OnceLock};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use cache::TimeoutCache;
use formatting::format_serror;
use komodo_client::{
api::execute::*,
entities::{
Version,
build::{Build, ImageRegistryConfig},
deployment::{
Deployment, DeploymentImage, extract_registry_domain,
extract_registry_domain, Deployment, DeploymentImage,
},
get_image_name, komodo_timestamp, optional_string,
permission::PermissionLevel,
server::Server,
update::{Log, Update},
user::User,
Version,
},
};
use periphery_client::api;
@@ -34,7 +34,6 @@ use crate::{
update::update_update,
},
monitor::update_cache_for_server,
permission::get_check_permissions,
resource,
state::action_states,
};
@@ -69,10 +68,10 @@ async fn setup_deployment_execution(
deployment: &str,
user: &User,
) -> anyhow::Result<(Deployment, Server)> {
let deployment = get_check_permissions::<Deployment>(
let deployment = resource::get_check_permissions::<Deployment>(
deployment,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -562,7 +561,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
setup_deployment_execution(&self.deployment, &user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -611,7 +610,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
setup_deployment_execution(&self.deployment, &user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
+24 -44
View File
@@ -1,27 +1,23 @@
use std::{pin::Pin, time::Instant};
use anyhow::Context;
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use axum_extra::{TypedHeader, headers::ContentType};
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::*,
entities::{
Operation,
permission::PermissionLevel,
update::{Log, Update},
user::User,
Operation,
},
};
use mungos::by_id::find_one_by_id;
use resolver_api::Resolve;
use response::JsonString;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -29,7 +25,7 @@ use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::update::{init_execution_update, update_update},
resource::{KomodoResource, list_full_for_user_using_pattern},
resource::{list_full_for_user_using_pattern, KomodoResource},
state::db_client,
};
@@ -40,11 +36,10 @@ mod deployment;
mod procedure;
mod repo;
mod server;
mod server_template;
mod stack;
mod sync;
use super::Variant;
pub use {
deployment::pull_deployment_inner, stack::pull_stack_inner,
};
@@ -87,21 +82,6 @@ pub enum ExecuteRequest {
PruneBuildx(PruneBuildx),
PruneSystem(PruneSystem),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
BatchPullStack(BatchPullStack),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
@@ -114,6 +94,20 @@ pub enum ExecuteRequest {
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
PullStack(PullStack),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// ==== BUILD ====
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
@@ -136,6 +130,9 @@ pub enum ExecuteRequest {
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer),
// ==== ALERTER ====
TestAlerter(TestAlerter),
@@ -146,22 +143,9 @@ pub enum ExecuteRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let req: ExecuteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
@@ -174,11 +158,8 @@ async fn handler(
Ok((TypedHeader(ContentType::json()), res))
}
#[typeshare(serialized_as = "Update")]
type BoxUpdate = Box<Update>;
pub enum ExecutionResult {
Single(BoxUpdate),
Single(Update),
/// The batch contents will be pre serialized here
Batch(String),
}
@@ -248,7 +229,7 @@ pub fn inner_handler(
}
});
Ok(ExecutionResult::Single(update.into()))
Ok(ExecutionResult::Single(update))
})
}
@@ -302,7 +283,6 @@ async fn batch_execute<E: BatchExecute>(
pattern,
Default::default(),
user,
PermissionLevel::Execute.into(),
&[],
)
.await?;
+6 -32
View File
@@ -1,17 +1,13 @@
use std::pin::Pin;
use formatting::{Color, bold, colored, format_serror, muted};
use formatting::{bold, colored, format_serror, muted, Color};
use komodo_client::{
api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
komodo_timestamp,
permission::PermissionLevel,
procedure::Procedure,
update::Update,
user::User,
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
@@ -19,10 +15,8 @@ use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
alert::send_alerts,
helpers::{procedure::execute_procedure, update::update_update},
permission::get_check_permissions,
resource::refresh_procedure_state_cache,
resource::{self, refresh_procedure_state_cache},
state::{action_states, db_client},
};
@@ -71,10 +65,10 @@ fn resolve_inner(
>,
> {
Box::pin(async move {
let procedure = get_check_permissions::<Procedure>(
let procedure = resource::get_check_permissions::<Procedure>(
&procedure,
&user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -143,26 +137,6 @@ fn resolve_inner(
update_update(update.clone()).await?;
if !update.success && procedure.config.failure_alert {
warn!("procedure unsuccessful, alerting...");
let target = update.target.clone();
tokio::spawn(async move {
let alert = Alert {
id: Default::default(),
target,
ts: komodo_timestamp(),
resolved_ts: Some(komodo_timestamp()),
resolved: true,
level: SeverityLevel::Warning,
data: AlertData::ProcedureFailed {
id: procedure.id,
name: procedure.name,
},
};
send_alerts(&[alert]).await
});
}
Ok(update)
})
}
+22 -25
View File
@@ -1,6 +1,6 @@
use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use formatting::format_serror;
use komodo_client::{
api::{execute::*, write::RefreshRepoCache},
@@ -41,7 +41,6 @@ use crate::{
query::get_variables_and_secrets,
update::update_update,
},
permission::get_check_permissions,
resource::{self, refresh_repo_state_cache},
state::{action_states, db_client},
};
@@ -74,10 +73,10 @@ impl Resolve<ExecuteArgs> for CloneRepo {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
let mut repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -131,8 +130,8 @@ impl Resolve<ExecuteArgs> for CloneRepo {
Ok(res) => res.logs,
Err(e) => {
vec![Log::error(
"Clone Repo",
format_serror(&e.context("Failed to clone repo").into()),
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
)]
}
};
@@ -163,7 +162,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
impl super::BatchExecute for BatchPullRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::PullRepo(PullRepo { repo })
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
@@ -174,7 +173,7 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
super::batch_execute::<BatchPullRepo>(&self.pattern, &user)
.await?,
)
}
@@ -186,10 +185,10 @@ impl Resolve<ExecuteArgs> for PullRepo {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
let mut repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
&user,
PermissionLevel::Execute,
)
.await?;
@@ -341,10 +340,10 @@ impl Resolve<ExecuteArgs> for BuildRepo {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let mut repo = get_check_permissions::<Repo>(
let mut repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -439,7 +438,8 @@ impl Resolve<ExecuteArgs> for BuildRepo {
return handle_builder_early_return(
update, repo.id, repo.name, false,
)
.await;
.await
.map_err(Into::into);
}
};
@@ -463,7 +463,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(cleanup_data, &mut update)
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_builder_early_return(update, repo.id, repo.name, true).await
@@ -479,8 +479,8 @@ impl Resolve<ExecuteArgs> for BuildRepo {
}
Err(e) => {
update.push_error_log(
"Clone Repo",
format_serror(&e.context("Failed to clone repo").into()),
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
@@ -507,9 +507,8 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// stop the cancel listening task from going forever
cancel.cancel();
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
@@ -652,10 +651,10 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let repo = get_check_permissions::<Repo>(
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -697,9 +696,7 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
)
.await
{
warn!(
"failed to set CancelRepoBuild Update status Complete after timeout | {e:#}"
)
warn!("failed to set CancelRepoBuild Update status Complete after timeout | {e:#}")
}
});
+43 -43
View File
@@ -15,7 +15,7 @@ use resolver_api::Resolve;
use crate::{
helpers::{periphery_client, update::update_update},
monitor::update_cache_for_server,
permission::get_check_permissions,
resource,
state::action_states,
};
@@ -27,10 +27,10 @@ impl Resolve<ExecuteArgs> for StartContainer {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -81,10 +81,10 @@ impl Resolve<ExecuteArgs> for RestartContainer {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -137,10 +137,10 @@ impl Resolve<ExecuteArgs> for PauseContainer {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -191,10 +191,10 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -247,10 +247,10 @@ impl Resolve<ExecuteArgs> for StopContainer {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -309,10 +309,10 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
signal,
time,
} = self;
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -365,10 +365,10 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -415,10 +415,10 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -467,10 +467,10 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -517,10 +517,10 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -569,10 +569,10 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -619,10 +619,10 @@ impl Resolve<ExecuteArgs> for PruneContainers {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -675,10 +675,10 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -726,10 +726,10 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -780,10 +780,10 @@ impl Resolve<ExecuteArgs> for DeleteImage {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -828,10 +828,10 @@ impl Resolve<ExecuteArgs> for PruneImages {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -880,10 +880,10 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -931,10 +931,10 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -983,10 +983,10 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -1035,10 +1035,10 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
@@ -1087,10 +1087,10 @@ impl Resolve<ExecuteArgs> for PruneSystem {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
+156
View File
@@ -0,0 +1,156 @@
use anyhow::{anyhow, Context};
use formatting::format_serror;
use komodo_client::{
api::{execute::LaunchServer, write::CreateServer},
entities::{
permission::PermissionLevel,
server::PartialServerConfig,
server_template::{ServerTemplate, ServerTemplateConfig},
update::Update,
},
};
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
cloud::{
aws::ec2::launch_ec2_instance, hetzner::launch_hetzner_server,
},
helpers::update::update_update,
resource,
state::db_client,
};
use super::ExecuteArgs;
impl Resolve<ExecuteArgs> for LaunchServer {
#[instrument(name = "LaunchServer", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
// validate name isn't already taken by another server
if db_client()
.servers
.find_one(doc! {
"name": &self.name
})
.await
.context("failed to query db for servers")?
.is_some()
{
return Err(anyhow!("name is already taken").into());
}
let template = resource::get_check_permissions::<ServerTemplate>(
&self.server_template,
user,
PermissionLevel::Execute,
)
.await?;
let mut update = update.clone();
update.push_simple_log(
"launching server",
format!("{:#?}", template.config),
);
update_update(update.clone()).await?;
let config = match template.config {
ServerTemplateConfig::Aws(config) => {
let region = config.region.clone();
let use_https = config.use_https;
let port = config.port;
let instance =
match launch_ec2_instance(&self.name, config).await {
Ok(instance) => instance,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch aws instance\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
update.push_simple_log(
"launch server",
format!(
"successfully launched server {} on ip {}",
self.name, instance.ip
),
);
let protocol = if use_https { "https" } else { "http" };
PartialServerConfig {
address: format!("{protocol}://{}:{port}", instance.ip)
.into(),
region: region.into(),
..Default::default()
}
}
ServerTemplateConfig::Hetzner(config) => {
let datacenter = config.datacenter;
let use_https = config.use_https;
let port = config.port;
let server =
match launch_hetzner_server(&self.name, config).await {
Ok(server) => server,
Err(e) => {
update.push_error_log(
"launch server",
format!("failed to launch hetzner server\n\n{e:#?}"),
);
update.finalize();
update_update(update.clone()).await?;
return Ok(update);
}
};
update.push_simple_log(
"launch server",
format!(
"successfully launched server {} on ip {}",
self.name, server.ip
),
);
let protocol = if use_https { "https" } else { "http" };
PartialServerConfig {
address: format!("{protocol}://{}:{port}", server.ip)
.into(),
region: datacenter.as_ref().to_string().into(),
..Default::default()
}
}
};
match (CreateServer {
name: self.name,
config,
})
.resolve(&WriteArgs { user: user.clone() })
.await
{
Ok(server) => {
update.push_simple_log(
"create server",
format!("created server {} ({})", server.name, server.id),
);
update.other_data = server.id;
}
Err(e) => {
update.push_error_log(
"create server",
format_serror(
&e.error.context("failed to create server").into(),
),
);
}
};
update.finalize();
update_update(update.clone()).await?;
Ok(update)
}
}
+40 -122
View File
@@ -6,7 +6,6 @@ use komodo_client::{
api::{execute::*, write::RefreshStackCache},
entities::{
permission::PermissionLevel,
repo::Repo,
server::Server,
stack::{Stack, StackInfo},
update::{Log, Update},
@@ -27,11 +26,9 @@ use crate::{
},
periphery_client,
query::get_variables_and_secrets,
stack_git_token,
update::{add_update_without_send, update_update},
},
monitor::update_cache_for_server,
permission::get_check_permissions,
resource,
stack::{execute::execute_compose, get_stack_and_server},
state::{action_states, db_client},
@@ -44,7 +41,7 @@ impl super::BatchExecute for BatchDeployStack {
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack {
stack,
services: Vec::new(),
service: None,
stop_time: None,
})
}
@@ -72,21 +69,11 @@ impl Resolve<ExecuteArgs> for DeployStack {
let (mut stack, server) = get_stack_and_server(
&self.stack,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
true,
)
.await?;
let mut repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
// get the action state for the stack (or insert default).
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
@@ -100,18 +87,20 @@ impl Resolve<ExecuteArgs> for DeployStack {
update_update(update.clone()).await?;
if !self.services.is_empty() {
if let Some(service) = &self.service {
update.logs.push(Log::simple(
"Service/s",
format!(
"Execution requested for Stack service/s {}",
self.services.join(", ")
),
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
let git_token =
stack_git_token(&mut stack, repo.as_mut()).await?;
let git_token = crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {}", stack.config.git_provider, stack.config.git_account),
)?;
let registry_token = crate::helpers::registry_token(
&stack.config.registry_provider,
@@ -188,14 +177,12 @@ impl Resolve<ExecuteArgs> for DeployStack {
file_contents,
missing_files,
remote_errors,
compose_config,
commit_hash,
commit_message,
} = periphery_client(&server)?
.request(ComposeUp {
stack: stack.clone(),
services: self.services,
repo,
service: self.service,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
@@ -219,14 +206,12 @@ impl Resolve<ExecuteArgs> for DeployStack {
let (
deployed_services,
deployed_contents,
deployed_config,
deployed_hash,
deployed_message,
) = if deployed {
(
Some(latest_services.clone()),
Some(file_contents.clone()),
compose_config,
commit_hash.clone(),
commit_message.clone(),
)
@@ -234,7 +219,6 @@ impl Resolve<ExecuteArgs> for DeployStack {
(
stack.info.deployed_services,
stack.info.deployed_contents,
stack.info.deployed_config,
stack.info.deployed_hash,
stack.info.deployed_message,
)
@@ -245,7 +229,6 @@ impl Resolve<ExecuteArgs> for DeployStack {
deployed_project_name: project_name.into(),
deployed_services,
deployed_contents,
deployed_config,
deployed_hash,
deployed_message,
latest_services,
@@ -329,10 +312,10 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
self,
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let stack = get_check_permissions::<Stack>(
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
RefreshStackCache {
@@ -383,7 +366,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
DeployStack {
stack: stack.name,
services: Vec::new(),
service: None,
stop_time: self.stop_time,
}
.resolve(&ExecuteArgs {
@@ -394,49 +377,26 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
}
}
impl super::BatchExecute for BatchPullStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::PullStack(PullStack {
stack,
services: Vec::new(),
})
}
}
impl Resolve<ExecuteArgs> for BatchPullStack {
#[instrument(name = "BatchPullStack", skip(user), fields(user_id = user.id))]
async fn resolve(
self,
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullStack>(&self.pattern, user)
.await?,
)
}
}
pub async fn pull_stack_inner(
mut stack: Stack,
services: Vec<String>,
service: Option<String>,
server: &Server,
mut repo: Option<Repo>,
mut update: Option<&mut Update>,
update: Option<&mut Update>,
) -> anyhow::Result<ComposePullResponse> {
if let Some(update) = update.as_mut() {
if !services.is_empty() {
update.logs.push(Log::simple(
"Service/s",
format!(
"Execution requested for Stack service/s {}",
services.join(", ")
),
))
}
if let (Some(service), Some(update)) = (&service, update) {
update.logs.push(Log::simple(
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
let git_token = stack_git_token(&mut stack, repo.as_mut()).await?;
let git_token = crate::helpers::git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
).await.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {}", stack.config.git_provider, stack.config.git_account),
)?;
let registry_token = crate::helpers::registry_token(
&stack.config.registry_provider,
@@ -445,41 +405,10 @@ 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),
)?;
// interpolate variables / secrets
if !stack.config.skip_secret_interp {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.file_contents,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.environment,
&mut global_replacers,
&mut secret_replacers,
)?;
if let Some(update) = update {
add_interp_update_log(
update,
&global_replacers,
&secret_replacers,
);
}
};
let res = periphery_client(server)?
.request(ComposePull {
stack,
services,
repo,
service,
git_token,
registry_token,
})
@@ -500,21 +429,11 @@ impl Resolve<ExecuteArgs> for PullStack {
let (stack, server) = get_stack_and_server(
&self.stack,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
true,
)
.await?;
let repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
// get the action state for the stack (or insert default).
let action_state =
action_states().stack.get_or_insert_default(&stack.id).await;
@@ -529,9 +448,8 @@ impl Resolve<ExecuteArgs> for PullStack {
let res = pull_stack_inner(
stack,
self.services,
self.service,
&server,
repo,
Some(&mut update),
)
.await?;
@@ -552,7 +470,7 @@ impl Resolve<ExecuteArgs> for StartStack {
) -> serror::Result<Update> {
execute_compose::<StartStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.starting = true,
update.clone(),
@@ -571,7 +489,7 @@ impl Resolve<ExecuteArgs> for RestartStack {
) -> serror::Result<Update> {
execute_compose::<RestartStack>(
&self.stack,
self.services,
self.service,
user,
|state| {
state.restarting = true;
@@ -592,7 +510,7 @@ impl Resolve<ExecuteArgs> for PauseStack {
) -> serror::Result<Update> {
execute_compose::<PauseStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.pausing = true,
update.clone(),
@@ -611,7 +529,7 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
) -> serror::Result<Update> {
execute_compose::<UnpauseStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.unpausing = true,
update.clone(),
@@ -630,7 +548,7 @@ impl Resolve<ExecuteArgs> for StopStack {
) -> serror::Result<Update> {
execute_compose::<StopStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.stopping = true,
update.clone(),
@@ -646,7 +564,7 @@ impl super::BatchExecute for BatchDestroyStack {
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
services: Vec::new(),
service: None,
remove_orphans: false,
stop_time: None,
})
@@ -673,7 +591,7 @@ impl Resolve<ExecuteArgs> for DestroyStack {
) -> serror::Result<Update> {
execute_compose::<DestroyStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.destroying = true,
update.clone(),
+226 -129
View File
@@ -1,11 +1,11 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, anyhow};
use formatting::{Color, colored, format_serror};
use anyhow::{anyhow, Context};
use formatting::{colored, format_serror, Color};
use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{
self, ResourceTargetVariant,
self,
action::Action,
alerter::Alerter,
build::Build,
@@ -16,31 +16,33 @@ use komodo_client::{
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
update::{Log, Update},
user::sync_user,
ResourceTargetVariant,
},
};
use mongo_indexed::doc;
use mungos::{by_id::update_one_by_id, mongodb::bson::oid::ObjectId};
use mungos::{
by_id::update_one_by_id,
mongodb::bson::{oid::ObjectId, to_document},
};
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{
all_resources::AllResourcesById, query::get_id_to_tags,
update::update_update,
},
permission::get_check_permissions,
helpers::{query::get_id_to_tags, update::update_update},
resource::{self, refresh_resource_sync_state_cache},
state::{action_states, db_client},
sync::{
ResourceSyncTrait,
deploy::{
SyncDeployParams, build_deploy_cache, deploy_from_cache,
build_deploy_cache, deploy_from_cache, SyncDeployParams,
},
execute::{ExecuteResourceSync, get_updates_for_execution},
execute::{get_updates_for_execution, ExecuteResourceSync},
remote::RemoteResources,
AllResourcesById, ResourceSyncTrait,
},
};
@@ -57,23 +59,11 @@ impl Resolve<ExecuteArgs> for RunSync {
resource_type: match_resource_type,
resources: match_resources,
} = self;
let sync = get_check_permissions::<entities::sync::ResourceSync>(
&sync,
user,
PermissionLevel::Execute.into(),
)
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, &user, PermissionLevel::Execute)
.await?;
let repo = if !sync.config.files_on_host
&& !sync.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&sync.config.linked_repo)
.await?
.into()
} else {
None
};
// get the action state for the sync (or insert default).
let action_state = action_states()
.resource_sync
@@ -97,10 +87,9 @@ impl Resolve<ExecuteArgs> for RunSync {
message,
file_errors,
..
} =
crate::sync::remote::get_remote_resources(&sync, repo.as_ref())
.await
.context("failed to get remote resources")?;
} = crate::sync::remote::get_remote_resources(&sync)
.await
.context("failed to get remote resources")?;
update.logs.extend(logs);
update_update(update.clone()).await?;
@@ -157,6 +146,10 @@ impl Resolve<ExecuteArgs> for RunSync {
.servers
.get(&name_or_id)
.map(|s| s.name.clone()),
ResourceTargetVariant::ServerTemplate => all_resources
.templates
.get(&name_or_id)
.map(|t| t.name.clone()),
ResourceTargetVariant::Stack => all_resources
.stacks
.get(&name_or_id)
@@ -211,141 +204,145 @@ impl Resolve<ExecuteArgs> for RunSync {
deployment_map: &deployments_by_name,
stacks: &resources.stacks,
stack_map: &stacks_by_name,
all_resources: &all_resources,
})
.await?;
let delete = sync.config.managed || sync.config.delete;
let server_deltas = if sync.config.include_resources {
let (servers_to_create, servers_to_update, servers_to_delete) =
get_updates_for_execution::<Server>(
resources.servers,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let stack_deltas = if sync.config.include_resources {
.await?;
let (
deployments_to_create,
deployments_to_update,
deployments_to_delete,
) = get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (stacks_to_create, stacks_to_update, stacks_to_delete) =
get_updates_for_execution::<Stack>(
resources.stacks,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let deployment_deltas = if sync.config.include_resources {
get_updates_for_execution::<Deployment>(
resources.deployments,
delete,
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 {
.await?;
let (builds_to_create, builds_to_update, builds_to_delete) =
get_updates_for_execution::<Build>(
resources.builds,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let repo_deltas = if sync.config.include_resources {
.await?;
let (repos_to_create, repos_to_update, repos_to_delete) =
get_updates_for_execution::<Repo>(
resources.repos,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let procedure_deltas = if sync.config.include_resources {
get_updates_for_execution::<Procedure>(
resources.procedures,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let action_deltas = if sync.config.include_resources {
.await?;
let (
procedures_to_create,
procedures_to_update,
procedures_to_delete,
) = get_updates_for_execution::<Procedure>(
resources.procedures,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (actions_to_create, actions_to_update, actions_to_delete) =
get_updates_for_execution::<Action>(
resources.actions,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let builder_deltas = if sync.config.include_resources {
.await?;
let (builders_to_create, builders_to_update, builders_to_delete) =
get_updates_for_execution::<Builder>(
resources.builders,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let alerter_deltas = if sync.config.include_resources {
.await?;
let (alerters_to_create, alerters_to_update, alerters_to_delete) =
get_updates_for_execution::<Alerter>(
resources.alerters,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
let resource_sync_deltas = if sync.config.include_resources {
get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs,
delete,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?
} else {
Default::default()
};
.await?;
let (
server_templates_to_create,
server_templates_to_update,
server_templates_to_delete,
) = get_updates_for_execution::<ServerTemplate>(
resources.server_templates,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (
resource_syncs_to_create,
resource_syncs_to_update,
resource_syncs_to_delete,
) = get_updates_for_execution::<entities::sync::ResourceSync>(
resources.resource_syncs,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (
variables_to_create,
@@ -353,11 +350,12 @@ impl Resolve<ExecuteArgs> for RunSync {
variables_to_delete,
) = if match_resource_type.is_none()
&& match_resources.is_none()
&& sync.config.include_variables
&& sync.config.match_tags.is_empty()
{
crate::sync::variables::get_updates_for_execution(
resources.variables,
delete,
// Delete doesn't work with variables when match tags are set
sync.config.match_tags.is_empty() && delete,
)
.await?
} else {
@@ -369,11 +367,13 @@ impl Resolve<ExecuteArgs> for RunSync {
user_groups_to_delete,
) = if match_resource_type.is_none()
&& match_resources.is_none()
&& sync.config.include_user_groups
&& sync.config.match_tags.is_empty()
{
crate::sync::user_groups::get_updates_for_execution(
resources.user_groups,
delete,
// Delete doesn't work with user groups when match tags are set
sync.config.match_tags.is_empty() && delete,
&all_resources,
)
.await?
} else {
@@ -381,16 +381,39 @@ impl Resolve<ExecuteArgs> for RunSync {
};
if deploy_cache.is_empty()
&& resource_sync_deltas.no_changes()
&& server_deltas.no_changes()
&& deployment_deltas.no_changes()
&& stack_deltas.no_changes()
&& build_deltas.no_changes()
&& builder_deltas.no_changes()
&& alerter_deltas.no_changes()
&& repo_deltas.no_changes()
&& procedure_deltas.no_changes()
&& action_deltas.no_changes()
&& resource_syncs_to_create.is_empty()
&& resource_syncs_to_update.is_empty()
&& resource_syncs_to_delete.is_empty()
&& server_templates_to_create.is_empty()
&& server_templates_to_update.is_empty()
&& server_templates_to_delete.is_empty()
&& servers_to_create.is_empty()
&& servers_to_update.is_empty()
&& servers_to_delete.is_empty()
&& deployments_to_create.is_empty()
&& deployments_to_update.is_empty()
&& deployments_to_delete.is_empty()
&& stacks_to_create.is_empty()
&& stacks_to_update.is_empty()
&& stacks_to_delete.is_empty()
&& builds_to_create.is_empty()
&& builds_to_update.is_empty()
&& builds_to_delete.is_empty()
&& builders_to_create.is_empty()
&& builders_to_update.is_empty()
&& builders_to_delete.is_empty()
&& alerters_to_create.is_empty()
&& alerters_to_update.is_empty()
&& alerters_to_delete.is_empty()
&& repos_to_create.is_empty()
&& repos_to_update.is_empty()
&& repos_to_delete.is_empty()
&& procedures_to_create.is_empty()
&& procedures_to_update.is_empty()
&& procedures_to_delete.is_empty()
&& actions_to_create.is_empty()
&& actions_to_update.is_empty()
&& actions_to_delete.is_empty()
&& user_groups_to_create.is_empty()
&& user_groups_to_update.is_empty()
&& user_groups_to_delete.is_empty()
@@ -433,52 +456,111 @@ impl Resolve<ExecuteArgs> for RunSync {
);
maybe_extend(
&mut update.logs,
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
ResourceSync::execute_sync_updates(
resource_syncs_to_create,
resource_syncs_to_update,
resource_syncs_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
Server::execute_sync_updates(server_deltas).await,
ServerTemplate::execute_sync_updates(
server_templates_to_create,
server_templates_to_update,
server_templates_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
Alerter::execute_sync_updates(alerter_deltas).await,
Server::execute_sync_updates(
servers_to_create,
servers_to_update,
servers_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
Action::execute_sync_updates(action_deltas).await,
Alerter::execute_sync_updates(
alerters_to_create,
alerters_to_update,
alerters_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
Action::execute_sync_updates(
actions_to_create,
actions_to_update,
actions_to_delete,
)
.await,
);
// Dependent on server
maybe_extend(
&mut update.logs,
Builder::execute_sync_updates(builder_deltas).await,
Builder::execute_sync_updates(
builders_to_create,
builders_to_update,
builders_to_delete,
)
.await,
);
maybe_extend(
&mut update.logs,
Repo::execute_sync_updates(repo_deltas).await,
Repo::execute_sync_updates(
repos_to_create,
repos_to_update,
repos_to_delete,
)
.await,
);
// Dependant on builder
maybe_extend(
&mut update.logs,
Build::execute_sync_updates(build_deltas).await,
Build::execute_sync_updates(
builds_to_create,
builds_to_update,
builds_to_delete,
)
.await,
);
// Dependant on server / build
maybe_extend(
&mut update.logs,
Deployment::execute_sync_updates(deployment_deltas).await,
Deployment::execute_sync_updates(
deployments_to_create,
deployments_to_update,
deployments_to_delete,
)
.await,
);
// stack only depends on server, but maybe will depend on build later.
maybe_extend(
&mut update.logs,
Stack::execute_sync_updates(stack_deltas).await,
Stack::execute_sync_updates(
stacks_to_create,
stacks_to_update,
stacks_to_delete,
)
.await,
);
// Dependant on everything
maybe_extend(
&mut update.logs,
Procedure::execute_sync_updates(procedure_deltas).await,
Procedure::execute_sync_updates(
procedures_to_create,
procedures_to_update,
procedures_to_delete,
)
.await,
);
// Execute the deploy cache
@@ -527,6 +609,21 @@ impl Resolve<ExecuteArgs> for RunSync {
}
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
// The Err case of to_document should be unreachable,
// but will fail to update cache in that case.
if let Ok(update_doc) = to_document(&update) {
let _ = update_one_by_id(
&db.updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_resource_sync_state_cache().await;
}
update_update(update.clone()).await?;
Ok(update)
-6
View File
@@ -1,11 +1,5 @@
pub mod auth;
pub mod execute;
pub mod read;
pub mod terminal;
pub mod user;
pub mod write;
#[derive(serde::Deserialize)]
struct Variant {
variant: String,
}
+9 -19
View File
@@ -12,7 +12,6 @@ use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_state_cache, action_states},
};
@@ -25,10 +24,10 @@ impl Resolve<ReadArgs> for GetAction {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Action> {
Ok(
get_check_permissions::<Action>(
resource::get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -46,13 +45,8 @@ impl Resolve<ReadArgs> for ListActions {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Action>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
resource::list_for_user::<Action>(self.query, &user, &all_tags)
.await?,
)
}
}
@@ -69,10 +63,7 @@ impl Resolve<ReadArgs> for ListFullActions {
};
Ok(
resource::list_full_for_user::<Action>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -84,10 +75,10 @@ impl Resolve<ReadArgs> for GetActionActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ActionActionState> {
let action = get_check_permissions::<Action>(
let action = resource::get_check_permissions::<Action>(
&self.action,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
@@ -107,8 +98,7 @@ impl Resolve<ReadArgs> for GetActionsSummary {
) -> serror::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
user,
PermissionLevel::Read.into(),
&user,
&[],
)
.await
+1 -1
View File
@@ -16,7 +16,7 @@ use mungos::{
use resolver_api::Resolve;
use crate::{
config::core_config, permission::get_resource_ids_for_user,
config::core_config, resource::get_resource_ids_for_user,
state::db_client,
};
+7 -16
View File
@@ -11,8 +11,7 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
helpers::query::get_all_tags, resource, state::db_client,
};
use super::ReadArgs;
@@ -23,10 +22,10 @@ impl Resolve<ReadArgs> for GetAlerter {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Alerter> {
Ok(
get_check_permissions::<Alerter>(
resource::get_check_permissions::<Alerter>(
&self.alerter,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -44,13 +43,8 @@ impl Resolve<ReadArgs> for ListAlerters {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Alerter>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
resource::list_for_user::<Alerter>(self.query, user, &all_tags)
.await?,
)
}
}
@@ -67,10 +61,7 @@ impl Resolve<ReadArgs> for ListFullAlerters {
};
Ok(
resource::list_full_for_user::<Alerter>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -84,7 +75,7 @@ impl Resolve<ReadArgs> for GetAlertersSummary {
) -> serror::Result<GetAlertersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Alerter,
>(user)
>(&user)
.await?
{
Some(ids) => doc! {
+13 -26
View File
@@ -6,11 +6,11 @@ use futures::TryStreamExt;
use komodo_client::{
api::read::*,
entities::{
Operation,
build::{Build, BuildActionState, BuildListItem, BuildState},
config::core::CoreConfig,
permission::PermissionLevel,
update::UpdateStatus,
Operation,
},
};
use mungos::{
@@ -22,7 +22,6 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{
action_states, build_state_cache, db_client, github_client,
@@ -37,10 +36,10 @@ impl Resolve<ReadArgs> for GetBuild {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Build> {
Ok(
get_check_permissions::<Build>(
resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -58,13 +57,8 @@ impl Resolve<ReadArgs> for ListBuilds {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
resource::list_for_user::<Build>(self.query, user, &all_tags)
.await?,
)
}
}
@@ -81,10 +75,7 @@ impl Resolve<ReadArgs> for ListFullBuilds {
};
Ok(
resource::list_full_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, user, &all_tags,
)
.await?,
)
@@ -96,10 +87,10 @@ impl Resolve<ReadArgs> for GetBuildActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<BuildActionState> {
let build = get_check_permissions::<Build>(
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
@@ -120,7 +111,6 @@ impl Resolve<ReadArgs> for GetBuildsSummary {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -228,10 +218,10 @@ impl Resolve<ReadArgs> for ListBuildVersions {
patch,
limit,
} = self;
let build = get_check_permissions::<Build>(
let build = resource::get_check_permissions::<Build>(
&build,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
@@ -284,10 +274,7 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
get_all_tags(None).await?
};
let builds = resource::list_full_for_user::<Build>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
@@ -319,10 +306,10 @@ impl Resolve<ReadArgs> for GetBuildWebhookEnabled {
});
};
let build = get_check_permissions::<Build>(
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
+7 -16
View File
@@ -11,8 +11,7 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, permission::get_check_permissions,
resource, state::db_client,
helpers::query::get_all_tags, resource, state::db_client,
};
use super::ReadArgs;
@@ -23,10 +22,10 @@ impl Resolve<ReadArgs> for GetBuilder {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Builder> {
Ok(
get_check_permissions::<Builder>(
resource::get_check_permissions::<Builder>(
&self.builder,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -44,13 +43,8 @@ impl Resolve<ReadArgs> for ListBuilders {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Builder>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
resource::list_for_user::<Builder>(self.query, user, &all_tags)
.await?,
)
}
}
@@ -67,10 +61,7 @@ impl Resolve<ReadArgs> for ListFullBuilders {
};
Ok(
resource::list_full_for_user::<Builder>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, user, &all_tags,
)
.await?,
)
@@ -84,7 +75,7 @@ impl Resolve<ReadArgs> for GetBuildersSummary {
) -> serror::Result<GetBuildersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Builder,
>(user)
>(&user)
.await?
{
Some(ids) => doc! {
+24 -89
View File
@@ -1,6 +1,6 @@
use std::{cmp, collections::HashSet};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::read::*,
entities::{
@@ -8,22 +8,19 @@ use komodo_client::{
Deployment, DeploymentActionState, DeploymentConfig,
DeploymentListItem, DeploymentState,
},
docker::container::{Container, ContainerStats},
docker::container::ContainerStats,
permission::PermissionLevel,
server::{Server, ServerState},
server::Server,
update::Log,
},
};
use periphery_client::api::{self, container::InspectContainer};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::{periphery_client, query::get_all_tags},
permission::get_check_permissions,
resource,
state::{
action_states, deployment_status_cache, server_status_cache,
},
state::{action_states, deployment_status_cache},
};
use super::ReadArgs;
@@ -34,10 +31,10 @@ impl Resolve<ReadArgs> for GetDeployment {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Deployment> {
Ok(
get_check_permissions::<Deployment>(
resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -54,23 +51,12 @@ impl Resolve<ReadArgs> for ListDeployments {
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let deployments = resource::list_for_user::<Deployment>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
Ok(
resource::list_for_user::<Deployment>(
self.query, user, &all_tags,
)
.await?,
)
.await?;
let deployments = if only_update_available {
deployments
.into_iter()
.filter(|deployment| deployment.info.update_available)
.collect()
} else {
deployments
};
Ok(deployments)
}
}
@@ -86,10 +72,7 @@ impl Resolve<ReadArgs> for ListFullDeployments {
};
Ok(
resource::list_full_for_user::<Deployment>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, user, &all_tags,
)
.await?,
)
@@ -101,10 +84,10 @@ impl Resolve<ReadArgs> for GetDeploymentContainer {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDeploymentContainerResponse> {
let deployment = get_check_permissions::<Deployment>(
let deployment = resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let status = deployment_status_cache()
@@ -135,10 +118,10 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
} = resource::get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read.logs(),
PermissionLevel::Read,
)
.await?;
if server_id.is_empty() {
@@ -173,10 +156,10 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
} = resource::get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read.logs(),
PermissionLevel::Read,
)
.await?;
if server_id.is_empty() {
@@ -197,50 +180,6 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
}
}
impl Resolve<ReadArgs> for InspectDeploymentContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let InspectDeploymentContainer { deployment } = self;
let Deployment {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
&deployment,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if server_id.is_empty() {
return Err(
anyhow!(
"Cannot inspect deployment, not attached to any server"
)
.into(),
);
}
let server = resource::get::<Server>(&server_id).await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let res = periphery_client(&server)?
.request(InspectContainer { name })
.await?;
Ok(res)
}
}
impl Resolve<ReadArgs> for GetDeploymentStats {
async fn resolve(
self,
@@ -250,10 +189,10 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
name,
config: DeploymentConfig { server_id, .. },
..
} = get_check_permissions::<Deployment>(
} = resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
if server_id.is_empty() {
@@ -275,10 +214,10 @@ impl Resolve<ReadArgs> for GetDeploymentActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<DeploymentActionState> {
let deployment = get_check_permissions::<Deployment>(
let deployment = resource::get_check_permissions::<Deployment>(
&self.deployment,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
@@ -299,7 +238,6 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -343,10 +281,7 @@ impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
get_all_tags(None).await?
};
let deployments = resource::list_full_for_user::<Deployment>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
+34 -59
View File
@@ -1,27 +1,23 @@
use std::{collections::HashSet, sync::OnceLock, time::Instant};
use anyhow::{Context, anyhow};
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
build::Build,
builder::{Builder, BuilderConfig},
config::{DockerRegistry, GitProvider},
permission::PermissionLevel,
repo::Repo,
server::Server,
sync::ResourceSync,
user::User,
ResourceTarget,
},
};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -31,8 +27,6 @@ use crate::{
resource,
};
use super::Variant;
mod action;
mod alert;
mod alerter;
@@ -43,8 +37,8 @@ mod permission;
mod procedure;
mod provider;
mod repo;
mod schedule;
mod server;
mod server_template;
mod stack;
mod sync;
mod tag;
@@ -73,7 +67,7 @@ enum ReadRequest {
// ==== USER ====
GetUsername(GetUsername),
GetPermission(GetPermission),
GetPermissionLevel(GetPermissionLevel),
FindUser(FindUser),
ListUsers(ListUsers),
ListApiKeys(ListApiKeys),
@@ -99,8 +93,11 @@ enum ReadRequest {
ListActions(ListActions),
ListFullActions(ListFullActions),
// ==== SCHEDULE ====
ListSchedules(ListSchedules),
// ==== SERVER TEMPLATE ====
GetServerTemplate(GetServerTemplate),
GetServerTemplatesSummary(GetServerTemplatesSummary),
ListServerTemplates(ListServerTemplates),
ListFullServerTemplates(ListFullServerTemplates),
// ==== SERVER ====
GetServersSummary(GetServersSummary),
@@ -119,33 +116,12 @@ enum ReadRequest {
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
ListDockerNetworks(ListDockerNetworks),
ListDockerImages(ListDockerImages),
ListDockerVolumes(ListDockerVolumes),
ListComposeProjects(ListComposeProjects),
ListTerminals(ListTerminals),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
ListSystemProcesses(ListSystemProcesses),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackLog(GetStackLog),
SearchStackLog(SearchStackLog),
InspectStackContainer(InspectStackContainer),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== DEPLOYMENT ====
GetDeploymentsSummary(GetDeploymentsSummary),
@@ -155,7 +131,6 @@ enum ReadRequest {
GetDeploymentStats(GetDeploymentStats),
GetDeploymentLog(GetDeploymentLog),
SearchDeploymentLog(SearchDeploymentLog),
InspectDeploymentContainer(InspectDeploymentContainer),
ListDeployments(ListDeployments),
ListFullDeployments(ListFullDeployments),
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
@@ -187,6 +162,19 @@ enum ReadRequest {
ListResourceSyncs(ListResourceSyncs),
ListFullResourceSyncs(ListFullResourceSyncs),
// ==== STACK ====
GetStacksSummary(GetStacksSummary),
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackServiceLog(GetStackServiceLog),
SearchStackServiceLog(SearchStackServiceLog),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
// ==== BUILDER ====
GetBuildersSummary(GetBuildersSummary),
GetBuilder(GetBuilder),
@@ -215,6 +203,11 @@ enum ReadRequest {
ListAlerts(ListAlerts),
GetAlert(GetAlert),
// ==== SERVER STATS ====
GetSystemInformation(GetSystemInformation),
GetSystemStats(GetSystemStats),
ListSystemProcesses(ListSystemProcesses),
// ==== VARIABLE ====
GetVariable(GetVariable),
ListVariables(ListVariables),
@@ -229,22 +222,9 @@ enum ReadRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: ReadRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
#[instrument(name = "ReadHandler", level = "debug", skip(user), fields(user_id = user.id))]
async fn handler(
Extension(user): Extension<User>,
@@ -289,14 +269,12 @@ fn core_info() -> &'static GetCoreInfoResponse {
ui_write_disabled: config.ui_write_disabled,
disable_confirm_dialog: config.disable_confirm_dialog,
disable_non_admin_create: config.disable_non_admin_create,
disable_websocket_reconnect: config.disable_websocket_reconnect,
github_webhook_owners: config
.github_webhook_app
.installations
.iter()
.map(|i| i.namespace.to_string())
.collect(),
timezone: config.timezone.clone(),
}
})
}
@@ -337,7 +315,7 @@ impl Resolve<ReadArgs> for ListSecrets {
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
)
}
};
if let Some(id) = server_id {
@@ -395,7 +373,7 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
)
}
}
}
@@ -403,20 +381,17 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>(
Default::default(),
user,
PermissionLevel::Read.into(),
&user,
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
user,
PermissionLevel::Read.into(),
&user,
&[]
),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
user,
PermissionLevel::Read.into(),
&user,
&[]
),
)?;
@@ -498,7 +473,7 @@ impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
_ => {
return Err(
anyhow!("target must be `Server` or `Builder`").into(),
);
)
}
}
}
+5 -5
View File
@@ -1,7 +1,7 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::read::{
GetPermission, GetPermissionResponse, ListPermissions,
GetPermissionLevel, GetPermissionLevelResponse, ListPermissions,
ListPermissionsResponse, ListUserTargetPermissions,
ListUserTargetPermissionsResponse,
},
@@ -35,13 +35,13 @@ impl Resolve<ReadArgs> for ListPermissions {
}
}
impl Resolve<ReadArgs> for GetPermission {
impl Resolve<ReadArgs> for GetPermissionLevel {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPermissionResponse> {
) -> serror::Result<GetPermissionLevelResponse> {
if user.admin {
return Ok(PermissionLevel::Write.all());
return Ok(PermissionLevel::Write);
}
Ok(get_user_permission_on_target(user, &self.target).await?)
}
+6 -14
View File
@@ -10,7 +10,6 @@ use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, procedure_state_cache},
};
@@ -23,10 +22,10 @@ impl Resolve<ReadArgs> for GetProcedure {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureResponse> {
Ok(
get_check_permissions::<Procedure>(
resource::get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -45,10 +44,7 @@ impl Resolve<ReadArgs> for ListProcedures {
};
Ok(
resource::list_for_user::<Procedure>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, user, &all_tags,
)
.await?,
)
@@ -67,10 +63,7 @@ impl Resolve<ReadArgs> for ListFullProcedures {
};
Ok(
resource::list_full_for_user::<Procedure>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -85,7 +78,6 @@ impl Resolve<ReadArgs> for GetProceduresSummary {
let procedures = resource::list_full_for_user::<Procedure>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -128,10 +120,10 @@ impl Resolve<ReadArgs> for GetProcedureActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetProcedureActionStateResponse> {
let procedure = get_check_permissions::<Procedure>(
let procedure = resource::get_check_permissions::<Procedure>(
&self.procedure,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
+2 -2
View File
@@ -1,6 +1,6 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::api::read::*;
use mongo_indexed::{Document, doc};
use mongo_indexed::{doc, Document};
use mungos::{
by_id::find_one_by_id, find::find_collect,
mongodb::options::FindOptions,
+9 -19
View File
@@ -12,7 +12,6 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, github_client, repo_state_cache},
};
@@ -25,10 +24,10 @@ impl Resolve<ReadArgs> for GetRepo {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Repo> {
Ok(
get_check_permissions::<Repo>(
resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -46,13 +45,8 @@ impl Resolve<ReadArgs> for ListRepos {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Repo>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
resource::list_for_user::<Repo>(self.query, &user, &all_tags)
.await?,
)
}
}
@@ -69,10 +63,7 @@ impl Resolve<ReadArgs> for ListFullRepos {
};
Ok(
resource::list_full_for_user::<Repo>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -84,10 +75,10 @@ impl Resolve<ReadArgs> for GetRepoActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<RepoActionState> {
let repo = get_check_permissions::<Repo>(
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
@@ -108,7 +99,6 @@ impl Resolve<ReadArgs> for GetReposSummary {
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -170,10 +160,10 @@ impl Resolve<ReadArgs> for GetRepoWebhooksEnabled {
});
};
let repo = get_check_permissions::<Repo>(
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
-102
View File
@@ -1,102 +0,0 @@
use futures::future::join_all;
use komodo_client::{
api::read::*,
entities::{
ResourceTarget, action::Action, permission::PermissionLevel,
procedure::Procedure, resource::ResourceQuery,
schedule::Schedule,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::{get_all_tags, get_last_run_at},
resource::list_full_for_user,
schedule::get_schedule_item_info,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for ListSchedules {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<Vec<Schedule>> {
let all_tags = get_all_tags(None).await?;
let (actions, procedures) = tokio::try_join!(
list_full_for_user::<Action>(
ResourceQuery {
names: Default::default(),
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
specific: Default::default(),
},
&args.user,
PermissionLevel::Read.into(),
&all_tags,
),
list_full_for_user::<Procedure>(
ResourceQuery {
names: Default::default(),
tag_behavior: self.tag_behavior,
tags: self.tags.clone(),
specific: Default::default(),
},
&args.user,
PermissionLevel::Read.into(),
&all_tags,
)
)?;
let actions = actions.into_iter().map(async |action| {
let (next_scheduled_run, schedule_error) =
get_schedule_item_info(&ResourceTarget::Action(
action.id.clone(),
));
let last_run_at =
get_last_run_at::<Action>(&action.id).await.unwrap_or(None);
Schedule {
target: ResourceTarget::Action(action.id),
name: action.name,
enabled: action.config.schedule_enabled,
schedule_format: action.config.schedule_format,
schedule: action.config.schedule,
schedule_timezone: action.config.schedule_timezone,
tags: action.tags,
last_run_at,
next_scheduled_run,
schedule_error,
}
});
let procedures = procedures.into_iter().map(async |procedure| {
let (next_scheduled_run, schedule_error) =
get_schedule_item_info(&ResourceTarget::Procedure(
procedure.id.clone(),
));
let last_run_at = get_last_run_at::<Procedure>(&procedure.id)
.await
.unwrap_or(None);
Schedule {
target: ResourceTarget::Procedure(procedure.id),
name: procedure.name,
enabled: procedure.config.schedule_enabled,
schedule_format: procedure.config.schedule_format,
schedule: procedure.config.schedule,
schedule_timezone: procedure.config.schedule_timezone,
tags: procedure.tags,
last_run_at,
next_scheduled_run,
schedule_error,
}
});
let (actions, procedures) =
tokio::join!(join_all(actions), join_all(procedures));
Ok(
actions
.into_iter()
.chain(procedures)
.filter(|s| !s.schedule.is_empty())
.collect(),
)
}
}
+81 -174
View File
@@ -4,32 +4,28 @@ use std::{
sync::{Arc, OnceLock},
};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use async_timing_util::{
FIFTEEN_SECONDS_MS, get_timelength_in_ms, unix_timestamp_ms,
get_timelength_in_ms, unix_timestamp_ms, FIFTEEN_SECONDS_MS,
};
use komodo_client::{
api::read::*,
entities::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
container::{Container, ContainerListItem},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
},
komodo_timestamp,
permission::PermissionLevel,
server::{
Server, ServerActionState, ServerListItem, ServerState,
TerminalInfo,
},
stack::{Stack, StackServiceNames},
stats::{SystemInformation, SystemProcess},
update::Log,
ResourceTarget,
},
};
use mungos::{
@@ -47,11 +43,7 @@ use resolver_api::Resolve;
use tokio::sync::Mutex;
use crate::{
helpers::{
periphery_client,
query::{get_all_tags, get_system_info},
},
permission::get_check_permissions,
helpers::{periphery_client, query::get_all_tags},
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache},
@@ -67,7 +59,6 @@ impl Resolve<ReadArgs> for GetServersSummary {
let servers = resource::list_for_user::<Server>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await?;
@@ -95,10 +86,10 @@ impl Resolve<ReadArgs> for GetPeripheryVersion {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetPeripheryVersionResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let version = server_status_cache()
@@ -116,10 +107,10 @@ impl Resolve<ReadArgs> for GetServer {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Server> {
Ok(
get_check_permissions::<Server>(
resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -137,13 +128,8 @@ impl Resolve<ReadArgs> for ListServers {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Server>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?,
resource::list_for_user::<Server>(self.query, &user, &all_tags)
.await?,
)
}
}
@@ -160,10 +146,7 @@ impl Resolve<ReadArgs> for ListFullServers {
};
Ok(
resource::list_full_for_user::<Server>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -175,10 +158,10 @@ impl Resolve<ReadArgs> for GetServerState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerStateResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let status = server_status_cache()
@@ -197,10 +180,10 @@ impl Resolve<ReadArgs> for GetServerActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ServerActionState> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
@@ -213,18 +196,46 @@ impl Resolve<ReadArgs> for GetServerActionState {
}
}
// This protects the peripheries from spam requests
const SYSTEM_INFO_EXPIRY: u128 = FIFTEEN_SECONDS_MS;
type SystemInfoCache =
Mutex<HashMap<String, Arc<(SystemInformation, u128)>>>;
fn system_info_cache() -> &'static SystemInfoCache {
static SYSTEM_INFO_CACHE: OnceLock<SystemInfoCache> =
OnceLock::new();
SYSTEM_INFO_CACHE.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for GetSystemInformation {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SystemInformation> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
get_system_info(&server).await.map_err(Into::into)
let mut lock = system_info_cache().lock().await;
let res = match lock.get(&server.id) {
Some(cached) if cached.1 > unix_timestamp_ms() => {
cached.0.clone()
}
_ => {
let stats = periphery_client(&server)?
.request(periphery::stats::GetSystemInformation {})
.await?;
lock.insert(
server.id,
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
.into(),
);
stats
}
};
Ok(res)
}
}
@@ -233,10 +244,10 @@ impl Resolve<ReadArgs> for GetSystemStats {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetSystemStatsResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let status =
@@ -265,10 +276,10 @@ impl Resolve<ReadArgs> for ListSystemProcesses {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSystemProcessesResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.processes(),
PermissionLevel::Read,
)
.await?;
let mut lock = processes_cache().lock().await;
@@ -304,10 +315,10 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
granularity,
page,
} = self;
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let granularity =
@@ -352,10 +363,10 @@ impl Resolve<ReadArgs> for ListDockerContainers {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerContainersResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -376,8 +387,7 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
) -> serror::Result<ListAllDockerContainersResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
user,
PermissionLevel::Read.into(),
&user,
&[],
)
.await?
@@ -403,55 +413,15 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
}
}
impl Resolve<ReadArgs> for GetDockerContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetDockerContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
for container in containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -488,10 +458,10 @@ impl Resolve<ReadArgs> for GetContainerLog {
tail,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
PermissionLevel::Read,
)
.await?;
let res = periphery_client(&server)?
@@ -519,10 +489,10 @@ impl Resolve<ReadArgs> for SearchContainerLog {
invert,
timestamps,
} = self;
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&server,
user,
PermissionLevel::Read.logs(),
PermissionLevel::Read,
)
.await?;
let res = periphery_client(&server)?
@@ -544,10 +514,10 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetResourceMatchingContainerResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
// first check deployments
@@ -563,7 +533,7 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
let stacks =
resource::list_full_for_user_using_document::<Stack>(
doc! { "config.server_id": &server.id },
user,
&user,
)
.await?;
@@ -605,10 +575,10 @@ impl Resolve<ReadArgs> for ListDockerNetworks {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerNetworksResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -627,10 +597,10 @@ impl Resolve<ReadArgs> for InspectDockerNetwork {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Network> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -657,10 +627,10 @@ impl Resolve<ReadArgs> for ListDockerImages {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerImagesResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -679,10 +649,10 @@ impl Resolve<ReadArgs> for InspectDockerImage {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Image> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -706,10 +676,10 @@ impl Resolve<ReadArgs> for ListDockerImageHistory {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Vec<ImageHistoryResponseItem>> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -736,10 +706,10 @@ impl Resolve<ReadArgs> for ListDockerVolumes {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListDockerVolumesResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -758,10 +728,10 @@ impl Resolve<ReadArgs> for InspectDockerVolume {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Volume> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -785,10 +755,10 @@ impl Resolve<ReadArgs> for ListComposeProjects {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListComposeProjectsResponse> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let cache = server_status_cache()
@@ -801,66 +771,3 @@ impl Resolve<ReadArgs> for ListComposeProjects {
}
}
}
#[derive(Default)]
struct TerminalCacheItem {
list: Vec<TerminalInfo>,
ttl: i64,
}
const TERMINAL_CACHE_TIMEOUT: i64 = 30_000;
#[derive(Default)]
struct TerminalCache(
std::sync::Mutex<
HashMap<String, Arc<tokio::sync::Mutex<TerminalCacheItem>>>,
>,
);
impl TerminalCache {
fn get_or_insert(
&self,
server_id: String,
) -> Arc<tokio::sync::Mutex<TerminalCacheItem>> {
if let Some(cached) =
self.0.lock().unwrap().get(&server_id).cloned()
{
return cached;
}
let to_cache =
Arc::new(tokio::sync::Mutex::new(TerminalCacheItem::default()));
self.0.lock().unwrap().insert(server_id, to_cache.clone());
to_cache
}
}
fn terminals_cache() -> &'static TerminalCache {
static TERMINALS: OnceLock<TerminalCache> = OnceLock::new();
TERMINALS.get_or_init(Default::default)
}
impl Resolve<ReadArgs> for ListTerminals {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListTerminalsResponse> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.terminal(),
)
.await?;
let cache = terminals_cache().get_or_insert(server.id.clone());
let mut cache = cache.lock().await;
if self.fresh || komodo_timestamp() > cache.ttl {
cache.list = periphery_client(&server)?
.request(periphery_client::api::terminal::ListTerminals {})
.await
.context("Failed to get fresh terminal list")?;
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
Ok(cache.list.clone())
} else {
Ok(cache.list.clone())
}
}
}
+97
View File
@@ -0,0 +1,97 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
},
};
use mongo_indexed::Document;
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags, resource, state::db_client,
};
use super::ReadArgs;
impl Resolve<ReadArgs> for GetServerTemplate {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerTemplateResponse> {
Ok(
resource::get_check_permissions::<ServerTemplate>(
&self.server_template,
user,
PermissionLevel::Read,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListServerTemplates {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListServerTemplatesResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<ServerTemplate>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for ListFullServerTemplates {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListFullServerTemplatesResponse> {
let all_tags = if self.query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
Ok(
resource::list_full_for_user::<ServerTemplate>(
self.query, user, &all_tags,
)
.await?,
)
}
}
impl Resolve<ReadArgs> for GetServerTemplatesSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetServerTemplatesSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
ServerTemplate,
>(&user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let total = db_client()
.server_templates
.count_documents(query)
.await
.context("failed to count all server template documents")?;
let res = GetServerTemplatesSummaryResponse {
total: total as u32,
};
Ok(res)
}
}
+38 -136
View File
@@ -1,32 +1,25 @@
use std::collections::HashSet;
use anyhow::{Context, anyhow};
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
config::core::CoreConfig,
docker::container::Container,
permission::PermissionLevel,
server::{Server, ServerState},
stack::{Stack, StackActionState, StackListItem, StackState},
},
};
use periphery_client::api::{
compose::{GetComposeLog, GetComposeLogSearch},
container::InspectContainer,
use periphery_client::api::compose::{
GetComposeServiceLog, GetComposeServiceLogSearch,
};
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::{periphery_client, query::get_all_tags},
permission::get_check_permissions,
resource,
stack::get_stack_and_server,
state::{
action_states, github_client, server_status_cache,
stack_status_cache,
},
state::{action_states, github_client, stack_status_cache},
};
use super::ReadArgs;
@@ -37,10 +30,10 @@ impl Resolve<ReadArgs> for GetStack {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Stack> {
Ok(
get_check_permissions::<Stack>(
resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?,
)
@@ -52,10 +45,10 @@ impl Resolve<ReadArgs> for ListStackServices {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListStackServicesResponse> {
let stack = get_check_permissions::<Stack>(
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
@@ -71,122 +64,60 @@ impl Resolve<ReadArgs> for ListStackServices {
}
}
impl Resolve<ReadArgs> for GetStackLog {
impl Resolve<ReadArgs> for GetStackServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackLogResponse> {
let GetStackLog {
) -> serror::Result<GetStackServiceLogResponse> {
let GetStackServiceLog {
stack,
services,
service,
tail,
timestamps,
} = self;
let (stack, server) = get_stack_and_server(
&stack,
user,
PermissionLevel::Read.logs(),
true,
)
.await?;
let (stack, server) =
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLog {
.request(GetComposeServiceLog {
project: stack.project_name(false),
services,
service,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?;
.context("failed to get stack service log from periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchStackLog {
impl Resolve<ReadArgs> for SearchStackServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchStackLogResponse> {
let SearchStackLog {
) -> serror::Result<SearchStackServiceLogResponse> {
let SearchStackServiceLog {
stack,
services,
service,
terms,
combinator,
invert,
timestamps,
} = self;
let (stack, server) = get_stack_and_server(
&stack,
user,
PermissionLevel::Read.logs(),
true,
)
.await?;
let (stack, server) =
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLogSearch {
.request(GetComposeServiceLogSearch {
project: stack.project_name(false),
services,
service,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectStackContainer {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<Container> {
let InspectStackContainer { stack, service } = self;
let stack = get_check_permissions::<Stack>(
&stack,
user,
PermissionLevel::Read.inspect(),
)
.await?;
if stack.config.server_id.is_empty() {
return Err(
anyhow!("Cannot inspect stack, not attached to any server")
.into(),
);
}
let server =
resource::get::<Server>(&stack.config.server_id).await?;
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if cache.state != ServerState::Ok {
return Err(
anyhow!(
"Cannot inspect container: server is {:?}",
cache.state
)
.into(),
);
}
let services = &stack_status_cache()
.get(&stack.id)
.await
.unwrap_or_default()
.curr
.services;
let Some(name) = services
.iter()
.find(|s| s.service == service)
.and_then(|s| s.container.as_ref().map(|c| c.name.clone()))
else {
return Err(anyhow!(
"No service found matching '{service}'. Was the stack last deployed manually?"
).into());
};
let res = periphery_client(&server)?
.request(InspectContainer { name })
.await?;
.context("failed to get stack service log from periphery")?;
Ok(res)
}
}
@@ -202,10 +133,7 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
get_all_tags(None).await?
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
@@ -236,10 +164,7 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
get_all_tags(None).await?
};
let stacks = resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
@@ -269,29 +194,10 @@ impl Resolve<ReadArgs> for ListStacks {
} else {
get_all_tags(None).await?
};
let only_update_available = self.query.specific.update_available;
let stacks = resource::list_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
Ok(
resource::list_for_user::<Stack>(self.query, user, &all_tags)
.await?,
)
.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)
}
}
@@ -307,10 +213,7 @@ impl Resolve<ReadArgs> for ListFullStacks {
};
Ok(
resource::list_full_for_user::<Stack>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, user, &all_tags,
)
.await?,
)
@@ -322,10 +225,10 @@ impl Resolve<ReadArgs> for GetStackActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<StackActionState> {
let stack = get_check_permissions::<Stack>(
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
@@ -346,7 +249,6 @@ impl Resolve<ReadArgs> for GetStacksSummary {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -385,10 +287,10 @@ impl Resolve<ReadArgs> for GetStackWebhooksEnabled {
});
};
let stack = get_check_permissions::<Stack>(
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
+35 -29
View File
@@ -6,6 +6,7 @@ use komodo_client::{
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
ResourceSyncState,
},
},
};
@@ -14,9 +15,8 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
permission::get_check_permissions,
resource,
state::{action_states, github_client},
state::{action_states, github_client, resource_sync_state_cache},
};
use super::ReadArgs;
@@ -27,10 +27,10 @@ impl Resolve<ReadArgs> for GetResourceSync {
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSync> {
Ok(
get_check_permissions::<ResourceSync>(
resource::get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?,
)
@@ -49,10 +49,7 @@ impl Resolve<ReadArgs> for ListResourceSyncs {
};
Ok(
resource::list_for_user::<ResourceSync>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -71,10 +68,7 @@ impl Resolve<ReadArgs> for ListFullResourceSyncs {
};
Ok(
resource::list_full_for_user::<ResourceSync>(
self.query,
user,
PermissionLevel::Read.into(),
&all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -86,10 +80,10 @@ impl Resolve<ReadArgs> for GetResourceSyncActionState {
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ResourceSyncActionState> {
let sync = get_check_permissions::<ResourceSync>(
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
@@ -111,7 +105,6 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
resource::list_full_for_user::<ResourceSync>(
Default::default(),
user,
PermissionLevel::Read.into(),
&[],
)
.await
@@ -119,6 +112,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
let mut res = GetResourceSyncsSummaryResponse::default();
let cache = resource_sync_state_cache();
let action_states = action_states();
for resource_sync in resource_syncs {
@@ -137,18 +131,30 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
res.failed += 1;
continue;
}
if action_states
.resource_sync
.get(&resource_sync.id)
.await
.unwrap_or_default()
.get()?
.syncing
{
res.syncing += 1;
continue;
match (
cache.get(&resource_sync.id).await.unwrap_or_default(),
action_states
.resource_sync
.get(&resource_sync.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.syncing => {
res.syncing += 1;
}
(ResourceSyncState::Ok, _) => res.ok += 1,
(ResourceSyncState::Failed, _) => res.failed += 1,
(ResourceSyncState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the building state, since that comes from action states
(ResourceSyncState::Syncing, _) => {
unreachable!()
}
(ResourceSyncState::Pending, _) => {
unreachable!()
}
}
res.ok += 1;
}
Ok(res)
@@ -168,10 +174,10 @@ impl Resolve<ReadArgs> for GetSyncWebhooksEnabled {
});
};
let sync = get_check_permissions::<ResourceSync>(
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
user,
PermissionLevel::Read.into(),
PermissionLevel::Read,
)
.await?;
+219 -201
View File
@@ -6,11 +6,11 @@ use komodo_client::{
ListUserGroups,
},
entities::{
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment,
permission::PermissionLevel, procedure::Procedure, repo::Repo,
resource::ResourceQuery, server::Server, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, user::User,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, permission::PermissionLevel,
procedure::Procedure, repo::Repo, resource::ResourceQuery,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, ResourceTarget,
},
};
use mungos::find::find_collect;
@@ -20,172 +20,159 @@ use crate::{
helpers::query::{
get_all_tags, get_id_to_tags, get_user_user_group_ids,
},
permission::get_check_permissions,
resource,
state::db_client,
sync::{
toml::{ToToml, convert_resource},
user_groups::{convert_user_groups, user_group_to_toml},
variables::variable_to_toml,
toml::{convert_resource, ToToml, TOML_PRETTY_OPTIONS},
user_groups::convert_user_groups,
AllResourcesById,
},
};
use super::ReadArgs;
async fn get_all_targets(
tags: &[String],
user: &User,
) -> anyhow::Result<Vec<ResourceTarget>> {
let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
targets.extend(
resource::list_full_for_user::<Alerter>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Alerter(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Builder>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Builder(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Server>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Stack>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Deployment>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Build>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Build(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Repo>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Repo(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Procedure>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_full_for_user::<Action>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags).build(),
user,
PermissionLevel::Read.into(),
&all_tags,
)
.await?
.into_iter()
// These will already be filtered by [ExportResourcesToToml]
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
Ok(targets)
}
impl Resolve<ReadArgs> for ExportAllResourcesToToml {
async fn resolve(
self,
args: &ReadArgs,
) -> serror::Result<ExportAllResourcesToTomlResponse> {
let targets = if self.include_resources {
get_all_targets(&self.tags, &args.user).await?
let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if self.tags.is_empty() {
vec![]
} else {
Vec::new()
get_all_tags(None).await?
};
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?
}
let ReadArgs { user } = args;
targets.extend(
resource::list_for_user::<Alerter>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Alerter(resource.id)),
);
targets.extend(
resource::list_for_user::<Builder>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Builder(resource.id)),
);
targets.extend(
resource::list_for_user::<Server>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Server(resource.id)),
);
targets.extend(
resource::list_for_user::<Stack>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Stack(resource.id)),
);
targets.extend(
resource::list_for_user::<Deployment>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Deployment(resource.id)),
);
targets.extend(
resource::list_for_user::<Build>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Build(resource.id)),
);
targets.extend(
resource::list_for_user::<Repo>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Repo(resource.id)),
);
targets.extend(
resource::list_for_user::<Procedure>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_for_user::<Action>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::ServerTemplate(resource.id)),
);
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
// These will already be filtered by [ExportResourcesToToml]
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
let user_groups = if user.admin && self.tags.is_empty() {
find_collect(&db_client().user_groups, None, None)
.await
.context("failed to query db for user groups")?
.into_iter()
.map(|user_group| user_group.id)
.collect()
} else {
Vec::new()
get_user_user_group_ids(&user.id).await?
};
ExportResourcesToToml {
targets,
user_groups,
include_variables: self.include_variables,
include_variables: self.tags.is_empty(),
}
.resolve(args)
.await
@@ -203,18 +190,18 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
include_variables,
} = self;
let mut res = ResourcesToml::default();
let all = AllResourcesById::load().await?;
let id_to_tags = get_id_to_tags(None).await?;
let ReadArgs { user } = args;
for target in targets {
match target {
ResourceTarget::Alerter(id) => {
let mut alerter = get_check_permissions::<Alerter>(
let alerter = resource::get_check_permissions::<Alerter>(
&id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
Alerter::replace_ids(&mut alerter);
res.alerters.push(convert_resource::<Alerter>(
alerter,
false,
@@ -223,18 +210,16 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
ResourceTarget::ResourceSync(id) => {
let mut sync = get_check_permissions::<ResourceSync>(
let sync = resource::get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
if sync.config.file_contents.is_empty()
&& (sync.config.files_on_host
|| !sync.config.repo.is_empty()
|| !sync.config.linked_repo.is_empty())
|| !sync.config.repo.is_empty())
{
ResourceSync::replace_ids(&mut sync);
res.resource_syncs.push(convert_resource::<ResourceSync>(
sync,
false,
@@ -243,14 +228,29 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
}
ResourceTarget::ServerTemplate(id) => {
let template = resource::get_check_permissions::<
ServerTemplate,
>(
&id, &user, PermissionLevel::Read
)
.await?;
res.server_templates.push(
convert_resource::<ServerTemplate>(
template,
false,
vec![],
&id_to_tags,
),
)
}
ResourceTarget::Server(id) => {
let mut server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
Server::replace_ids(&mut server);
res.servers.push(convert_resource::<Server>(
server,
false,
@@ -259,13 +259,14 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
ResourceTarget::Builder(id) => {
let mut builder = get_check_permissions::<Builder>(
&id,
user,
PermissionLevel::Read.into(),
)
.await?;
Builder::replace_ids(&mut builder);
let mut builder =
resource::get_check_permissions::<Builder>(
&id,
&user,
PermissionLevel::Read,
)
.await?;
Builder::replace_ids(&mut builder, &all);
res.builders.push(convert_resource::<Builder>(
builder,
false,
@@ -274,13 +275,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
ResourceTarget::Build(id) => {
let mut build = get_check_permissions::<Build>(
let mut build = resource::get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
Build::replace_ids(&mut build);
Build::replace_ids(&mut build, &all);
res.builds.push(convert_resource::<Build>(
build,
false,
@@ -289,13 +290,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
ResourceTarget::Deployment(id) => {
let mut deployment = get_check_permissions::<Deployment>(
&id,
user,
PermissionLevel::Read.into(),
let mut deployment = resource::get_check_permissions::<
Deployment,
>(
&id, &user, PermissionLevel::Read
)
.await?;
Deployment::replace_ids(&mut deployment);
Deployment::replace_ids(&mut deployment, &all);
res.deployments.push(convert_resource::<Deployment>(
deployment,
false,
@@ -304,13 +305,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
ResourceTarget::Repo(id) => {
let mut repo = get_check_permissions::<Repo>(
let mut repo = resource::get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
Repo::replace_ids(&mut repo);
Repo::replace_ids(&mut repo, &all);
res.repos.push(convert_resource::<Repo>(
repo,
false,
@@ -319,13 +320,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
ResourceTarget::Stack(id) => {
let mut stack = get_check_permissions::<Stack>(
let mut stack = resource::get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
Stack::replace_ids(&mut stack);
Stack::replace_ids(&mut stack, &all);
res.stacks.push(convert_resource::<Stack>(
stack,
false,
@@ -334,13 +335,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
))
}
ResourceTarget::Procedure(id) => {
let mut procedure = get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Read.into(),
let mut procedure = resource::get_check_permissions::<
Procedure,
>(
&id, &user, PermissionLevel::Read
)
.await?;
Procedure::replace_ids(&mut procedure);
Procedure::replace_ids(&mut procedure, &all);
res.procedures.push(convert_resource::<Procedure>(
procedure,
false,
@@ -349,13 +350,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
));
}
ResourceTarget::Action(id) => {
let mut action = get_check_permissions::<Action>(
let mut action = resource::get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
Action::replace_ids(&mut action);
Action::replace_ids(&mut action, &all);
res.actions.push(convert_resource::<Action>(
action,
false,
@@ -367,7 +368,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
};
}
add_user_groups(user_groups, &mut res, args)
add_user_groups(user_groups, &mut res, &all, args)
.await
.context("failed to add user groups")?;
@@ -396,6 +397,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
async fn add_user_groups(
user_groups: Vec<String>,
res: &mut ResourcesToml,
all: &AllResourcesById,
args: &ReadArgs,
) -> anyhow::Result<()> {
let user_groups = ListUserGroups {}
@@ -407,7 +409,7 @@ async fn add_user_groups(
user_groups.contains(&ug.name) || user_groups.contains(&ug.id)
});
let mut ug = Vec::with_capacity(user_groups.size_hint().0);
convert_user_groups(user_groups, &mut ug).await?;
convert_user_groups(user_groups, all, &mut ug).await?;
res.user_groups = ug.into_iter().map(|ug| ug.1).collect();
Ok(())
@@ -490,6 +492,14 @@ fn serialize_resources_toml(
Builder::push_to_toml_string(builder, &mut toml)?;
}
for server_template in resources.server_templates {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[server_template]]\n");
ServerTemplate::push_to_toml_string(server_template, &mut toml)?;
}
for resource_sync in resources.resource_syncs {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
@@ -502,14 +512,22 @@ fn serialize_resources_toml(
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str(&variable_to_toml(variable)?);
toml.push_str("[[variable]]\n");
toml.push_str(
&toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS)
.context("failed to serialize variables to toml")?,
);
}
for user_group in resources.user_groups {
for user_group in &resources.user_groups {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str(&user_group_to_toml(user_group)?);
toml.push_str("[[user_group]]\n");
toml.push_str(
&toml_pretty::to_string(user_group, TOML_PRETTY_OPTIONS)
.context("failed to serialize user_groups to toml")?,
);
}
Ok(toml)
+128 -104
View File
@@ -1,10 +1,9 @@
use std::collections::HashMap;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{
ResourceTarget,
action::Action,
alerter::Alerter,
build::Build,
@@ -14,10 +13,12 @@ use komodo_client::{
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
update::{Update, UpdateListItem},
user::User,
ResourceTarget,
},
};
use mungos::{
@@ -27,11 +28,7 @@ use mungos::{
};
use resolver_api::Resolve;
use crate::{
config::core_config,
permission::{get_check_permissions, get_resource_ids_for_user},
state::db_client,
};
use crate::{config::core_config, resource, state::db_client};
use super::ReadArgs;
@@ -45,17 +42,18 @@ impl Resolve<ReadArgs> for ListUpdates {
let query = if user.admin || core_config().transparent_mode {
self.query
} else {
let server_query = get_resource_ids_for_user::<Server>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Server", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Server" });
let server_query =
resource::get_resource_ids_for_user::<Server>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Server", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Server" });
let deployment_query =
get_resource_ids_for_user::<Deployment>(user)
resource::get_resource_ids_for_user::<Deployment>(&user)
.await?
.map(|ids| {
doc! {
@@ -64,35 +62,38 @@ impl Resolve<ReadArgs> for ListUpdates {
})
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
let stack_query = get_resource_ids_for_user::<Stack>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Stack", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let stack_query =
resource::get_resource_ids_for_user::<Stack>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Stack", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let build_query = get_resource_ids_for_user::<Build>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Build", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Build" });
let build_query =
resource::get_resource_ids_for_user::<Build>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Build", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Build" });
let repo_query = get_resource_ids_for_user::<Repo>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Repo", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let repo_query =
resource::get_resource_ids_for_user::<Repo>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Repo", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let procedure_query =
get_resource_ids_for_user::<Procedure>(user)
resource::get_resource_ids_for_user::<Procedure>(&user)
.await?
.map(|ids| {
doc! {
@@ -101,43 +102,57 @@ impl Resolve<ReadArgs> for ListUpdates {
})
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query = get_resource_ids_for_user::<Action>(user)
let action_query =
resource::get_resource_ids_for_user::<Action>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Action", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query =
resource::get_resource_ids_for_user::<Builder>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Builder", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query =
resource::get_resource_ids_for_user::<Alerter>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Alerter", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let server_template_query =
resource::get_resource_ids_for_user::<ServerTemplate>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "ServerTemplate", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
let resource_sync_query =
resource::get_resource_ids_for_user::<ResourceSync>(
&user,
)
.await?
.map(|ids| {
doc! {
"target.type": "Action", "target.id": { "$in": ids }
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query = get_resource_ids_for_user::<Builder>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Builder", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query = get_resource_ids_for_user::<Alerter>(user)
.await?
.map(|ids| {
doc! {
"target.type": "Alerter", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let resource_sync_query = get_resource_ids_for_user::<
ResourceSync,
>(user)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let mut query = self.query.unwrap_or_default();
query.extend(doc! {
@@ -151,6 +166,7 @@ impl Resolve<ReadArgs> for ListUpdates {
action_query,
alerter_query,
builder_query,
server_template_query,
resource_sync_query,
]
});
@@ -226,85 +242,93 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::System(_) => {
return Err(
anyhow!("user must be admin to view system updates").into(),
);
)
}
ResourceTarget::Server(id) => {
get_check_permissions::<Server>(
resource::get_check_permissions::<Server>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Deployment(id) => {
get_check_permissions::<Deployment>(
resource::get_check_permissions::<Deployment>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Build(id) => {
get_check_permissions::<Build>(
resource::get_check_permissions::<Build>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Repo(id) => {
get_check_permissions::<Repo>(
resource::get_check_permissions::<Repo>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Builder(id) => {
get_check_permissions::<Builder>(
resource::get_check_permissions::<Builder>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Alerter(id) => {
get_check_permissions::<Alerter>(
resource::get_check_permissions::<Alerter>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Procedure(id) => {
get_check_permissions::<Procedure>(
resource::get_check_permissions::<Procedure>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Action(id) => {
get_check_permissions::<Action>(
resource::get_check_permissions::<Action>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
id,
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::ResourceSync(id) => {
get_check_permissions::<ResourceSync>(
resource::get_check_permissions::<ResourceSync>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::Stack(id) => {
get_check_permissions::<Stack>(
resource::get_check_permissions::<Stack>(
id,
user,
PermissionLevel::Read.into(),
&user,
PermissionLevel::Read,
)
.await?;
}
+2 -2
View File
@@ -1,4 +1,4 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::read::{
FindUser, FindUserResponse, GetUsername, GetUsernameResponse,
@@ -6,7 +6,7 @@ use komodo_client::{
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
ListUsers, ListUsersResponse,
},
entities::user::{UserConfig, admin_service_user},
entities::user::{admin_service_user, UserConfig},
};
use mungos::{
by_id::find_one_by_id,
+1 -1
View File
@@ -5,7 +5,7 @@ use komodo_client::api::read::*;
use mungos::{
find::find_collect,
mongodb::{
bson::{Document, doc, oid::ObjectId},
bson::{doc, oid::ObjectId, Document},
options::FindOptions,
},
};
-299
View File
@@ -1,299 +0,0 @@
use anyhow::Context;
use axum::{Extension, Router, middleware, routing::post};
use komodo_client::{
api::terminal::*,
entities::{
deployment::Deployment, permission::PermissionLevel,
server::Server, stack::Stack, user::User,
},
};
use serror::Json;
use uuid::Uuid;
use crate::{
auth::auth_request, helpers::periphery_client,
permission::get_check_permissions, resource::get,
state::stack_status_cache,
};
pub fn router() -> Router {
Router::new()
.route("/execute", post(execute_terminal))
.route("/execute/container", post(execute_container_exec))
.route("/execute/deployment", post(execute_deployment_exec))
.route("/execute/stack", post(execute_stack_exec))
.layer(middleware::from_fn(auth_request))
}
// =================
// ExecuteTerminal
// =================
async fn execute_terminal(
Extension(user): Extension<User>,
Json(request): Json<ExecuteTerminalBody>,
) -> serror::Result<axum::body::Body> {
execute_terminal_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteTerminal",
skip(user),
fields(
user_id = user.id,
)
)]
async fn execute_terminal_inner(
req_id: Uuid,
ExecuteTerminalBody {
server,
terminal,
command,
}: ExecuteTerminalBody,
user: User,
) -> serror::Result<axum::body::Body> {
info!("/terminal/execute request | user: {}", user.username);
let res = async {
let server = get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_terminal(terminal, command)
.await
.context("Failed to execute command on periphery")?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!("/terminal/execute request {req_id} error: {e:#}");
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
}
// ======================
// ExecuteContainerExec
// ======================
async fn execute_container_exec(
Extension(user): Extension<User>,
Json(request): Json<ExecuteContainerExecBody>,
) -> serror::Result<axum::body::Body> {
execute_container_exec_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteContainerExec",
skip(user),
fields(
user_id = user.id,
)
)]
async fn execute_container_exec_inner(
req_id: Uuid,
ExecuteContainerExecBody {
server,
container,
shell,
command,
}: ExecuteContainerExecBody,
user: User,
) -> serror::Result<axum::body::Body> {
info!(
"/terminal/execute/container request | user: {}",
user.username
);
let res = async {
let server = get_check_permissions::<Server>(
&server,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_container_exec(container, shell, command)
.await
.context(
"Failed to execute container exec command on periphery",
)?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!(
"/terminal/execute/container request {req_id} error: {e:#}"
);
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
}
// =======================
// ExecuteDeploymentExec
// =======================
async fn execute_deployment_exec(
Extension(user): Extension<User>,
Json(request): Json<ExecuteDeploymentExecBody>,
) -> serror::Result<axum::body::Body> {
execute_deployment_exec_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteDeploymentExec",
skip(user),
fields(
user_id = user.id,
)
)]
async fn execute_deployment_exec_inner(
req_id: Uuid,
ExecuteDeploymentExecBody {
deployment,
shell,
command,
}: ExecuteDeploymentExecBody,
user: User,
) -> serror::Result<axum::body::Body> {
info!(
"/terminal/execute/deployment request | user: {}",
user.username
);
let res = async {
let deployment = get_check_permissions::<Deployment>(
&deployment,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let server = get::<Server>(&deployment.config.server_id).await?;
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_container_exec(deployment.name, shell, command)
.await
.context(
"Failed to execute container exec command on periphery",
)?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!(
"/terminal/execute/deployment request {req_id} error: {e:#}"
);
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
}
// ==================
// ExecuteStackExec
// ==================
async fn execute_stack_exec(
Extension(user): Extension<User>,
Json(request): Json<ExecuteStackExecBody>,
) -> serror::Result<axum::body::Body> {
execute_stack_exec_inner(Uuid::new_v4(), request, user).await
}
#[instrument(
name = "ExecuteStackExec",
skip(user),
fields(
user_id = user.id,
)
)]
async fn execute_stack_exec_inner(
req_id: Uuid,
ExecuteStackExecBody {
stack,
service,
shell,
command,
}: ExecuteStackExecBody,
user: User,
) -> serror::Result<axum::body::Body> {
info!("/terminal/execute/stack request | user: {}", user.username);
let res = async {
let stack = get_check_permissions::<Stack>(
&stack,
&user,
PermissionLevel::Read.terminal(),
)
.await?;
let server = get::<Server>(&stack.config.server_id).await?;
let container = stack_status_cache()
.get(&stack.id)
.await
.context("could not get stack status")?
.curr
.services
.iter()
.find(|s| s.service == service)
.context("could not find service")?
.container
.as_ref()
.context("could not find service container")?
.name
.clone();
let periphery = periphery_client(&server)?;
let stream = periphery
.execute_container_exec(container, shell, command)
.await
.context(
"Failed to execute container exec command on periphery",
)?;
anyhow::Ok(stream)
}
.await;
let stream = match res {
Ok(stream) => stream,
Err(e) => {
warn!("/terminal/execute/stack request {req_id} error: {e:#}");
return Err(e.into());
}
};
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
}
+2 -20
View File
@@ -1,9 +1,7 @@
use std::{collections::VecDeque, time::Instant};
use anyhow::{Context, anyhow};
use axum::{
Extension, Json, Router, extract::Path, middleware, routing::post,
};
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Json, Router};
use derive_variants::EnumVariants;
use komodo_client::{
api::user::*,
@@ -14,7 +12,6 @@ use mungos::{by_id::update_one_by_id, mongodb::bson::to_bson};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use typeshare::typeshare;
use uuid::Uuid;
@@ -24,8 +21,6 @@ use crate::{
state::db_client,
};
use super::Variant;
pub struct UserArgs {
pub user: User,
}
@@ -48,22 +43,9 @@ enum UserRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: UserRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
#[instrument(name = "UserHandler", level = "debug", skip(user))]
async fn handler(
Extension(user): Extension<User>,
+9 -8
View File
@@ -6,7 +6,7 @@ use komodo_client::{
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, resource};
use crate::resource;
use super::WriteArgs;
@@ -29,14 +29,15 @@ impl Resolve<WriteArgs> for CopyAction {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Action> {
let Action { config, .. } = get_check_permissions::<Action>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
let Action { config, .. } =
resource::get_check_permissions::<Action>(
&self.id,
&user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Action>(&self.name, config.into(), user)
resource::create::<Action>(&self.name, config.into(), &user)
.await?,
)
}
+8 -7
View File
@@ -6,7 +6,7 @@ use komodo_client::{
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, resource};
use crate::resource;
use super::WriteArgs;
@@ -29,12 +29,13 @@ impl Resolve<WriteArgs> for CopyAlerter {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Alerter> {
let Alerter { config, .. } = get_check_permissions::<Alerter>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
let Alerter { config, .. } =
resource::get_check_permissions::<Alerter>(
&self.id,
&user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Alerter>(&self.name, config.into(), user)
.await?,
+56 -423
View File
@@ -1,19 +1,13 @@
use std::{path::PathBuf, str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use anyhow::{anyhow, Context};
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
CloneArgs, FileContents, NoData, Operation, all_logs_success,
build::{Build, BuildInfo, PartialBuildConfig},
builder::{Builder, BuilderConfig},
config::core::CoreConfig,
permission::PermissionLevel,
repo::Repo,
server::ServerState,
update::Update,
CloneArgs, NoData,
},
};
use mongo_indexed::doc;
@@ -21,23 +15,11 @@ use mungos::mongodb::bson::to_document;
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::{
PeripheryClient,
api::build::{
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
},
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::core_config,
helpers::{
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
permission::get_check_permissions,
helpers::git_token,
resource,
state::{db_client, github_client},
};
@@ -63,12 +45,13 @@ impl Resolve<WriteArgs> for CopyBuild {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Build> {
let Build { mut config, .. } = get_check_permissions::<Build>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
let Build { mut config, .. } =
resource::get_check_permissions::<Build>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
// reset version to 0.0.0
config.version = Default::default();
Ok(
@@ -105,223 +88,6 @@ impl Resolve<WriteArgs> for RenameBuild {
}
}
impl Resolve<WriteArgs> for WriteBuildFileContents {
#[instrument(name = "WriteBuildFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let build = get_check_permissions::<Build>(
&self.build,
&args.user,
PermissionLevel::Write.into(),
)
.await?;
if !build.config.files_on_host
&& build.config.repo.is_empty()
&& build.config.linked_repo.is_empty()
{
return Err(anyhow!(
"Build is not configured to use Files on Host or Git Repo, can't write dockerfile contents"
).into());
}
let mut update =
make_update(&build, Operation::WriteDockerfile, &args.user);
update.push_simple_log("Dockerfile to write", &self.contents);
if build.config.files_on_host {
match get_on_host_periphery(&build)
.await?
.request(WriteDockerfileContentsToHost {
name: build.name,
build_path: build.config.build_path,
dockerfile_path: build.config.dockerfile_path,
contents: self.contents,
})
.await
.context("Failed to write dockerfile contents to host")
{
Ok(log) => {
update.logs.push(log);
}
Err(e) => {
update.push_error_log(
"Write Dockerfile Contents",
format_serror(&e.into()),
);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) =
(RefreshBuildCache { build: build.id }).resolve(args).await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
} else {
write_dockerfile_contents_git(self, args, build, update).await
}
}
}
async fn write_dockerfile_contents_git(
req: WriteBuildFileContents,
args: &WriteArgs,
build: Build,
mut update: Update,
) -> serror::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut clone_args: CloneArgs = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
{
(&crate::resource::get::<Repo>(&build.config.linked_repo).await?)
.into()
} else {
(&build).into()
};
let root = clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(root.display().to_string());
let build_path = build
.config
.build_path
.parse::<PathBuf>()
.context("Invalid build path")?;
let dockerfile_path = build
.config
.dockerfile_path
.parse::<PathBuf>()
.context("Invalid dockerfile path")?;
let full_path = root.join(&build_path).join(&dockerfile_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize dockerfile parent directory {parent:?}"
)
})?;
}
let access_token = if let Some(account) = &clone_args.account {
git_token(&clone_args.provider, account, |https| clone_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
)?
} else {
None
};
// Ensure the folder is initialized as git repo.
// This allows a new file to be committed on a branch that may not exist.
if !root.join(".git").exists() {
git::init_folder_as_repo(
&root,
&clone_args,
access_token.as_deref(),
&mut update.logs,
)
.await;
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
}
// Pull latest changes to repo to ensure linear commit history
match git::pull_or_clone(
clone_args,
&core_config().repo_directory,
access_token,
Default::default(),
Default::default(),
Default::default(),
Default::default(),
)
.await
.context("Failed to pull latest changes before commit")
{
Ok(res) => update.logs.extend(res.logs),
Err(e) => {
update.push_error_log("Pull Repo", format_serror(&e.into()));
update.finalize();
return Ok(update);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})
{
update
.push_error_log("Write Dockerfile", format_serror(&e.into()));
} else {
update.push_simple_log(
"Write Dockerfile",
format!("File written to {full_path:?}"),
);
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
let commit_res = git::commit_file(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&build.config.branch,
)
.await;
update.logs.extend(commit_res.logs);
if let Err(e) = (RefreshBuildCache { build: build.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
impl Resolve<WriteArgs> for RefreshBuildCache {
#[instrument(
name = "RefreshBuildCache",
@@ -334,70 +100,62 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// build should be able to do this.
let build = get_check_permissions::<Build>(
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
let repo = if !build.config.files_on_host
&& !build.config.linked_repo.is_empty()
if build.config.repo.is_empty()
|| build.config.git_provider.is_empty()
{
crate::resource::get::<Repo>(&build.config.linked_repo)
.await?
.into()
// Nothing to do here
return Ok(NoData {});
}
let config = core_config();
let mut clone_args: CloneArgs = (&build).into();
let repo_path =
clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
clone_args.https = https
})
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
)?
} else {
None
};
let (
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
) = if build.config.files_on_host {
// =============
// FILES ON HOST
// =============
match get_on_host_dockerfile(&build).await {
Ok(FileContents { path, contents }) => {
(Some(path), Some(contents), None, None, None)
}
Err(e) => {
(None, None, Some(format_serror(&e.into())), None, None)
}
}
} else if let Some(repo) = &repo {
let Some(res) = get_git_remote(&build, repo.into()).await?
else {
// Nothing to do here
return Ok(NoData {});
};
res
} else if !build.config.repo.is_empty() {
let Some(res) = get_git_remote(&build, (&build).into()).await?
else {
// Nothing to do here
return Ok(NoData {});
};
res
} else {
// =============
// UI BASED FILE
// =============
(None, None, None, None, None)
};
let GitRes {
hash: latest_hash,
message: latest_message,
..
} = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let info = BuildInfo {
last_built_at: build.info.last_built_at,
built_hash: build.info.built_hash,
built_message: build.info.built_message,
built_contents: build.info.built_contents,
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
};
@@ -418,131 +176,6 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
}
}
async fn get_on_host_periphery(
build: &Build,
) -> anyhow::Result<PeripheryClient> {
if build.config.builder_id.is_empty() {
return Err(anyhow!("No builder associated with build"));
}
let builder = resource::get::<Builder>(&build.config.builder_id)
.await
.context("Failed to get builder")?;
match builder.config {
BuilderConfig::Aws(_) => {
Err(anyhow!("Files on host doesn't work with AWS builder"))
}
BuilderConfig::Url(config) => {
let periphery = PeripheryClient::new(
config.address,
config.passkey,
Duration::from_secs(3),
);
periphery.health_check().await?;
Ok(periphery)
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
return Err(anyhow!(
"Builder is type server, but has no server attached"
));
}
let (server, state) =
get_server_with_state(&config.server_id).await?;
if state != ServerState::Ok {
return Err(anyhow!(
"Builder server is disabled or not reachable"
));
};
periphery_client(&server)
}
}
}
/// The successful case will be included as Some(remote_contents).
/// The error case will be included as Some(remote_error)
async fn get_on_host_dockerfile(
build: &Build,
) -> anyhow::Result<FileContents> {
get_on_host_periphery(build)
.await?
.request(GetDockerfileContentsOnHost {
name: build.name.clone(),
build_path: build.config.build_path.clone(),
dockerfile_path: build.config.dockerfile_path.clone(),
})
.await
}
async fn get_git_remote(
build: &Build,
mut clone_args: CloneArgs,
) -> anyhow::Result<
Option<(
Option<String>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
)>,
> {
if clone_args.provider.is_empty() {
// Nothing to do here
return Ok(None);
}
let config = core_config();
let repo_path = clone_args.unique_path(&config.repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
clone_args.https = https
})
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
)?
} else {
None
};
let GitRes { hash, message, .. } = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let relative_path = PathBuf::from_str(&build.config.build_path)
.context("Invalid build path")?
.join(&build.config.dockerfile_path);
let full_path = repo_path.join(&relative_path);
let (contents, error) =
match fs::read_to_string(&full_path).await.with_context(|| {
format!("Failed to read dockerfile contents at {full_path:?}")
}) {
Ok(contents) => (Some(contents), None),
Err(e) => (None, Some(format_serror(&e.into()))),
};
Ok(Some((
Some(relative_path.display().to_string()),
contents,
error,
hash,
message,
)))
}
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
@@ -560,10 +193,10 @@ impl Resolve<WriteArgs> for CreateBuildWebhook {
let WriteArgs { user } = args;
let build = get_check_permissions::<Build>(
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
@@ -673,10 +306,10 @@ impl Resolve<WriteArgs> for DeleteBuildWebhook {
);
};
let build = get_check_permissions::<Build>(
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
PermissionLevel::Write.into(),
&user,
PermissionLevel::Write,
)
.await?;
+13 -9
View File
@@ -6,7 +6,7 @@ use komodo_client::{
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, resource};
use crate::resource;
use super::WriteArgs;
@@ -29,14 +29,15 @@ impl Resolve<WriteArgs> for CopyBuilder {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Builder> {
let Builder { config, .. } = get_check_permissions::<Builder>(
&self.id,
user,
PermissionLevel::Write.into(),
)
.await?;
let Builder { config, .. } =
resource::get_check_permissions::<Builder>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Builder>(&self.name, config.into(), user)
resource::create::<Builder>(&self.name, config.into(), &user)
.await?,
)
}
@@ -71,6 +72,9 @@ impl Resolve<WriteArgs> for RenameBuilder {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(resource::rename::<Builder>(&self.id, &self.name, user).await?)
Ok(
resource::rename::<Builder>(&self.id, &self.name, &user)
.await?,
)
}
}
+20 -18
View File
@@ -1,8 +1,7 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::*,
entities::{
Operation,
deployment::{
Deployment, DeploymentImage, DeploymentState,
PartialDeploymentConfig, RestartMode,
@@ -11,8 +10,9 @@ use komodo_client::{
komodo_timestamp,
permission::PermissionLevel,
server::{Server, ServerState},
to_container_compatible_name,
to_komodo_name,
update::Update,
Operation,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
@@ -25,7 +25,6 @@ use crate::{
query::get_deployment_state,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
state::{action_states, db_client, server_status_cache},
};
@@ -52,15 +51,19 @@ impl Resolve<WriteArgs> for CopyDeployment {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let Deployment { config, .. } =
get_check_permissions::<Deployment>(
resource::get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Read.into(),
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Deployment>(&self.name, config.into(), user)
.await?,
resource::create::<Deployment>(
&self.name,
config.into(),
&user,
)
.await?,
)
}
}
@@ -71,10 +74,10 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Read.inspect().attach(),
PermissionLevel::Write,
)
.await?;
let cache = server_status_cache()
@@ -154,7 +157,7 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
}
Ok(
resource::create::<Deployment>(&self.name, config, user)
resource::create::<Deployment>(&self.name, config, &user)
.await?,
)
}
@@ -177,7 +180,7 @@ impl Resolve<WriteArgs> for UpdateDeployment {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
resource::update::<Deployment>(&self.id, self.config, user)
resource::update::<Deployment>(&self.id, self.config, &user)
.await?,
)
}
@@ -189,10 +192,10 @@ impl Resolve<WriteArgs> for RenameDeployment {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let deployment = get_check_permissions::<Deployment>(
let deployment = resource::get_check_permissions::<Deployment>(
&self.id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
@@ -207,10 +210,9 @@ impl Resolve<WriteArgs> for RenameDeployment {
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_container_compatible_name(&self.name);
let name = to_komodo_name(&self.name);
let container_state =
get_deployment_state(&deployment.id).await?;
let container_state = get_deployment_state(&deployment).await?;
if container_state == DeploymentState::Unknown {
return Err(
@@ -222,7 +224,7 @@ impl Resolve<WriteArgs> for RenameDeployment {
}
let mut update =
make_update(&deployment, Operation::RenameDeployment, user);
make_update(&deployment, Operation::RenameDeployment, &user);
update_one_by_id(
&db_client().deployments,
+23 -14
View File
@@ -2,9 +2,10 @@ use anyhow::anyhow;
use komodo_client::{
api::write::{UpdateDescription, UpdateDescriptionResponse},
entities::{
ResourceTarget, action::Action, alerter::Alerter, build::Build,
builder::Builder, deployment::Deployment, procedure::Procedure,
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, ResourceTarget,
},
};
use resolver_api::Resolve;
@@ -26,13 +27,13 @@ impl Resolve<WriteArgs> for UpdateDescription {
"cannot update description of System resource target"
)
.into(),
);
)
}
ResourceTarget::Server(id) => {
resource::update_description::<Server>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -40,7 +41,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Deployment>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -48,7 +49,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Build>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -56,7 +57,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Repo>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -64,7 +65,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Builder>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -72,7 +73,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Alerter>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -80,7 +81,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Procedure>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -88,7 +89,15 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Action>(
&id,
&self.description,
user,
&user,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::update_description::<ServerTemplate>(
&id,
&self.description,
&user,
)
.await?;
}
@@ -96,7 +105,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<ResourceSync>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -104,7 +113,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Stack>(
&id,
&self.description,
user,
&user,
)
.await?;
}
+24 -36
View File
@@ -1,23 +1,18 @@
use std::time::Instant;
use anyhow::Context;
use axum::{
Extension, Router, extract::Path, middleware, routing::post,
};
use axum::{middleware, routing::post, Extension, Router};
use derive_variants::{EnumVariants, ExtractVariant};
use komodo_client::{api::write::*, entities::user::User};
use resolver_api::Resolve;
use response::Response;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serror::Json;
use typeshare::typeshare;
use uuid::Uuid;
use crate::auth::auth_request;
use super::Variant;
mod action;
mod alerter;
mod build;
@@ -29,6 +24,7 @@ mod procedure;
mod provider;
mod repo;
mod server;
mod server_template;
mod service_user;
mod stack;
mod sync;
@@ -69,7 +65,6 @@ pub enum WriteRequest {
AddUserToUserGroup(AddUserToUserGroup),
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
SetUsersInUserGroup(SetUsersInUserGroup),
SetEveryoneUserGroup(SetEveryoneUserGroup),
// ==== PERMISSIONS ====
UpdateUserAdmin(UpdateUserAdmin),
@@ -86,20 +81,6 @@ pub enum WriteRequest {
UpdateServer(UpdateServer),
RenameServer(RenameServer),
CreateNetwork(CreateNetwork),
CreateTerminal(CreateTerminal),
DeleteTerminal(DeleteTerminal),
DeleteAllTerminals(DeleteAllTerminals),
// ==== STACK ====
CreateStack(CreateStack),
CopyStack(CopyStack),
DeleteStack(DeleteStack),
UpdateStack(UpdateStack),
RenameStack(RenameStack),
WriteStackFileContents(WriteStackFileContents),
RefreshStackCache(RefreshStackCache),
CreateStackWebhook(CreateStackWebhook),
DeleteStackWebhook(DeleteStackWebhook),
// ==== DEPLOYMENT ====
CreateDeployment(CreateDeployment),
@@ -115,7 +96,6 @@ pub enum WriteRequest {
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
@@ -127,6 +107,13 @@ pub enum WriteRequest {
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== SERVER TEMPLATE ====
CreateServerTemplate(CreateServerTemplate),
CopyServerTemplate(CopyServerTemplate),
DeleteServerTemplate(DeleteServerTemplate),
UpdateServerTemplate(UpdateServerTemplate),
RenameServerTemplate(RenameServerTemplate),
// ==== REPO ====
CreateRepo(CreateRepo),
CopyRepo(CopyRepo),
@@ -170,11 +157,21 @@ pub enum WriteRequest {
CreateSyncWebhook(CreateSyncWebhook),
DeleteSyncWebhook(DeleteSyncWebhook),
// ==== STACK ====
CreateStack(CreateStack),
CopyStack(CopyStack),
DeleteStack(DeleteStack),
UpdateStack(UpdateStack),
RenameStack(RenameStack),
WriteStackFileContents(WriteStackFileContents),
RefreshStackCache(RefreshStackCache),
CreateStackWebhook(CreateStackWebhook),
DeleteStackWebhook(DeleteStackWebhook),
// ==== TAG ====
CreateTag(CreateTag),
DeleteTag(DeleteTag),
RenameTag(RenameTag),
UpdateTagColor(UpdateTagColor),
UpdateTagsOnResource(UpdateTagsOnResource),
// ==== VARIABLE ====
@@ -196,22 +193,9 @@ pub enum WriteRequest {
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.route("/{variant}", post(variant_handler))
.layer(middleware::from_fn(auth_request))
}
async fn variant_handler(
user: Extension<User>,
Path(Variant { variant }): Path<Variant>,
Json(params): Json<serde_json::Value>,
) -> serror::Result<axum::response::Response> {
let req: WriteRequest = serde_json::from_value(json!({
"type": variant,
"params": params,
}))?;
handler(user, Json(req)).await
}
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<WriteRequest>,
@@ -222,6 +206,10 @@ async fn handler(
.await
.context("failure in spawned task");
if let Err(e) = &res {
warn!("/write request {req_id} spawn error: {e:#}");
}
res?
}
+39 -31
View File
@@ -1,17 +1,17 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::*,
entities::{
ResourceTarget, ResourceTargetVariant,
permission::{UserTarget, UserTargetVariant},
ResourceTarget, ResourceTargetVariant,
},
};
use mungos::{
by_id::{find_one_by_id, update_one_by_id},
mongodb::{
bson::{Document, doc, oid::ObjectId, to_bson},
bson::{doc, oid::ObjectId, Document},
options::UpdateOptions,
},
};
@@ -65,10 +65,6 @@ impl Resolve<WriteArgs> for UpdateUserBasePermissions {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdateUserBasePermissionsResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let UpdateUserBasePermissions {
user_id,
enabled,
@@ -76,6 +72,10 @@ impl Resolve<WriteArgs> for UpdateUserBasePermissions {
create_builds,
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let user = find_one_by_id(&db_client().users, &user_id)
.await
.context("failed to query mongo for user")?
@@ -122,16 +122,16 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let Self {
let UpdatePermissionOnResourceType {
user_target,
resource_type,
permission,
} = self;
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
@@ -153,11 +153,9 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
let id = ObjectId::from_str(&user_target_id)
.context("id is not ObjectId")?;
let filter = doc! { "_id": id };
let field = format!("all.{resource_type}");
let set =
to_bson(&permission).context("permission is not Bson")?;
let update = doc! { "$set": { &field: &set } };
let filter = doc! { "_id": id };
let update = doc! { "$set": { &field: permission.as_ref() } };
match user_target_variant {
UserTargetVariant::User => {
@@ -166,7 +164,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {set} on db")
format!("failed to set {field}: {permission} on db")
})?;
}
UserTargetVariant::UserGroup => {
@@ -175,7 +173,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
.update_one(filter, update)
.await
.with_context(|| {
format!("failed to set {field}: {set} on db")
format!("failed to set {field}: {permission} on db")
})?;
}
}
@@ -190,22 +188,19 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UpdatePermissionOnTargetResponse> {
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
let UpdatePermissionOnTarget {
user_target,
resource_target,
permission,
} = self;
// Some extra checks relevant if user target is an actual User
if !admin.admin {
return Err(anyhow!("this method is admin only").into());
}
// Some extra checks if user target is an actual User
if let UserTarget::User(user_id) = &user_target {
let user = get_user(user_id).await?;
if !user.enabled {
return Err(anyhow!("user not enabled").into());
}
if user.admin {
return Err(
anyhow!(
@@ -214,6 +209,9 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
.into(),
);
}
if !user.enabled {
return Err(anyhow!("user not enabled").into());
}
}
let (user_target_variant, user_target_id) =
@@ -225,9 +223,6 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
let (user_target_variant, resource_variant) =
(user_target_variant.as_ref(), resource_variant.as_ref());
let specific = to_bson(&permission.specific)
.context("permission.specific is not valid Bson")?;
db_client()
.permissions
.update_one(
@@ -243,8 +238,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
"user_target.id": user_target_id,
"resource_target.type": resource_variant,
"resource_target.id": resource_id,
"level": permission.level.as_ref(),
"specific": specific
"level": permission.as_ref(),
}
},
)
@@ -412,6 +406,20 @@ async fn extract_resource_target_with_validation(
.id;
Ok((ResourceTargetVariant::Action, id))
}
ResourceTarget::ServerTemplate(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.server_templates
.find_one(filter)
.await
.context("failed to query db for server templates")?
.context("no matching server template found")?
.id;
Ok((ResourceTargetVariant::ServerTemplate, id))
}
ResourceTarget::ResourceSync(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
+4 -4
View File
@@ -6,7 +6,7 @@ use komodo_client::{
};
use resolver_api::Resolve;
use crate::{permission::get_check_permissions, resource};
use crate::resource;
use super::WriteArgs;
@@ -30,10 +30,10 @@ impl Resolve<WriteArgs> for CopyProcedure {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<CopyProcedureResponse> {
let Procedure { config, .. } =
get_check_permissions::<Procedure>(
resource::get_check_permissions::<Procedure>(
&self.id,
user,
PermissionLevel::Write.into(),
&user,
PermissionLevel::Write,
)
.await?;
Ok(
+10 -10
View File
@@ -1,9 +1,9 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::*,
entities::{
Operation, ResourceTarget,
provider::{DockerRegistryAccount, GitProviderAccount},
Operation, ResourceTarget,
},
};
use mungos::{
@@ -44,7 +44,7 @@ impl Resolve<WriteArgs> for CreateGitProviderAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateGitProviderAccount,
user,
&user,
);
account.id = db_client()
@@ -114,7 +114,7 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
user,
&user,
);
let account = to_document(&self.account).context(
@@ -173,7 +173,7 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
user,
&user,
);
let db = db_client();
@@ -235,7 +235,7 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateDockerRegistryAccount,
user,
&user,
);
account.id = db_client()
@@ -298,8 +298,8 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
if username.is_empty() {
return Err(
anyhow!(
"cannot update docker registry account with empty username"
)
"cannot update docker registry account with empty username"
)
.into(),
);
}
@@ -310,7 +310,7 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
user,
&user,
);
let account = to_document(&self.account).context(
@@ -373,7 +373,7 @@ impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
user,
&user,
);
let db = db_client();
+25 -25
View File
@@ -1,17 +1,17 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
CloneArgs, NoData, Operation,
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
repo::{PartialRepoConfig, Repo, RepoInfo},
server::Server,
to_path_compatible_name,
to_komodo_name,
update::{Log, Update},
CloneArgs, NoData, Operation,
},
};
use mongo_indexed::doc;
@@ -28,7 +28,6 @@ use crate::{
git_token, periphery_client,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
state::{action_states, db_client, github_client},
};
@@ -51,14 +50,15 @@ impl Resolve<WriteArgs> for CopyRepo {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Repo> {
let Repo { config, .. } = get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
let Repo { config, .. } =
resource::get_check_permissions::<Repo>(
&self.id,
&user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Repo>(&self.name, config.into(), user)
resource::create::<Repo>(&self.name, config.into(), &user)
.await?,
)
}
@@ -87,10 +87,10 @@ impl Resolve<WriteArgs> for RenameRepo {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let repo = get_check_permissions::<Repo>(
let repo = resource::get_check_permissions::<Repo>(
&self.id,
user,
PermissionLevel::Write.into(),
&user,
PermissionLevel::Write,
)
.await?;
@@ -98,7 +98,7 @@ impl Resolve<WriteArgs> for RenameRepo {
|| !repo.config.path.is_empty()
{
return Ok(
resource::rename::<Repo>(&repo.id, &self.name, user).await?,
resource::rename::<Repo>(&repo.id, &self.name, &user).await?,
);
}
@@ -111,9 +111,9 @@ impl Resolve<WriteArgs> for RenameRepo {
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_path_compatible_name(&self.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(
&db_client().repos,
@@ -131,7 +131,7 @@ impl Resolve<WriteArgs> for RenameRepo {
let log = match periphery_client(&server)?
.request(api::git::RenameRepo {
curr_name: to_path_compatible_name(&repo.name),
curr_name: to_komodo_name(&repo.name),
new_name: name.clone(),
})
.await
@@ -169,10 +169,10 @@ impl Resolve<WriteArgs> for RefreshRepoCache {
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// repo should be able to do this.
let repo = get_check_permissions::<Repo>(
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Execute.into(),
&user,
PermissionLevel::Execute,
)
.await?;
@@ -257,10 +257,10 @@ impl Resolve<WriteArgs> for CreateRepoWebhook {
);
};
let repo = get_check_permissions::<Repo>(
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
&args.user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
@@ -380,10 +380,10 @@ impl Resolve<WriteArgs> for DeleteRepoWebhook {
);
};
let repo = get_check_permissions::<Repo>(
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
+5 -86
View File
@@ -1,13 +1,11 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
NoData, Operation,
permission::PermissionLevel,
server::Server,
to_docker_compatible_name,
update::{Update, UpdateStatus},
Operation,
},
};
use periphery_client::api;
@@ -18,7 +16,6 @@ use crate::{
periphery_client,
update::{add_update, make_update, update_update},
},
permission::get_check_permissions,
resource,
};
@@ -70,23 +67,23 @@ impl Resolve<WriteArgs> for CreateNetwork {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
let server = get_check_permissions::<Server>(
let server = resource::get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
let periphery = periphery_client(&server)?;
let mut update =
make_update(&server, Operation::CreateNetwork, user);
make_update(&server, Operation::CreateNetwork, &user);
update.status = UpdateStatus::InProgress;
update.id = add_update(update.clone()).await?;
match periphery
.request(api::network::CreateNetwork {
name: to_docker_compatible_name(&self.name),
name: self.name,
driver: None,
})
.await
@@ -104,81 +101,3 @@ impl Resolve<WriteArgs> for CreateNetwork {
Ok(update)
}
}
impl Resolve<WriteArgs> for CreateTerminal {
#[instrument(name = "CreateTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::CreateTerminal {
name: self.name,
command: self.command,
recreate: self.recreate,
})
.await
.context("Failed to create terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteTerminal {
#[instrument(name = "DeleteTerminal", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteTerminal {
terminal: self.terminal,
})
.await
.context("Failed to delete terminal on periphery")?;
Ok(NoData {})
}
}
impl Resolve<WriteArgs> for DeleteAllTerminals {
#[instrument(name = "DeleteAllTerminals", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<NoData> {
let server = get_check_permissions::<Server>(
&self.server,
user,
PermissionLevel::Write.terminal(),
)
.await?;
let periphery = periphery_client(&server)?;
periphery
.request(api::terminal::DeleteAllTerminals {})
.await
.context("Failed to delete all terminals on periphery")?;
Ok(NoData {})
}
}
+92
View File
@@ -0,0 +1,92 @@
use komodo_client::{
api::write::{
CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate,
RenameServerTemplate, UpdateServerTemplate,
},
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
update::Update,
},
};
use resolver_api::Resolve;
use crate::resource;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateServerTemplate {
#[instrument(name = "CreateServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ServerTemplate> {
Ok(
resource::create::<ServerTemplate>(
&self.name,
self.config,
&user,
)
.await?,
)
}
}
impl Resolve<WriteArgs> for CopyServerTemplate {
#[instrument(name = "CopyServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ServerTemplate> {
let ServerTemplate { config, .. } =
resource::get_check_permissions::<ServerTemplate>(
&self.id,
&user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<ServerTemplate>(
&self.name,
config.into(),
&user,
)
.await?,
)
}
}
impl Resolve<WriteArgs> for DeleteServerTemplate {
#[instrument(name = "DeleteServerTemplate", skip(args))]
async fn resolve(
self,
args: &WriteArgs,
) -> serror::Result<ServerTemplate> {
Ok(resource::delete::<ServerTemplate>(&self.id, args).await?)
}
}
impl Resolve<WriteArgs> for UpdateServerTemplate {
#[instrument(name = "UpdateServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ServerTemplate> {
Ok(
resource::update::<ServerTemplate>(&self.id, self.config, user)
.await?,
)
}
}
impl Resolve<WriteArgs> for RenameServerTemplate {
#[instrument(name = "RenameServerTemplate", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(
resource::rename::<ServerTemplate>(&self.id, &self.name, user)
.await?,
)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::{user::CreateApiKey, write::*},
entities::{
+45 -65
View File
@@ -1,16 +1,15 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
FileContents, NoData, Operation,
config::core::CoreConfig,
permission::PermissionLevel,
repo::Repo,
server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo},
update::Update,
user::stack_user,
FileContents, NoData, Operation,
},
};
use mungos::mongodb::bson::{doc, to_document};
@@ -27,16 +26,14 @@ use crate::{
api::execute::pull_stack_inner,
config::core_config,
helpers::{
periphery_client,
git_token, periphery_client,
query::get_server_with_state,
stack_git_token,
update::{add_update, make_update},
},
permission::get_check_permissions,
resource,
stack::{
get_stack_and_server,
remote::{RemoteComposeContents, get_repo_compose_contents},
remote::{get_repo_compose_contents, RemoteComposeContents},
services::extract_services_into_res,
},
state::{db_client, github_client},
@@ -63,12 +60,13 @@ impl Resolve<WriteArgs> for CopyStack {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Stack> {
let Stack { config, .. } = get_check_permissions::<Stack>(
&self.id,
user,
PermissionLevel::Read.into(),
)
.await?;
let Stack { config, .. } =
resource::get_check_permissions::<Stack>(
&self.id,
user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Stack>(&self.name, config.into(), user)
.await?,
@@ -104,7 +102,6 @@ impl Resolve<WriteArgs> for RenameStack {
}
impl Resolve<WriteArgs> for WriteStackFileContents {
#[instrument(name = "WriteStackFileContents", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
@@ -116,33 +113,20 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
} = self;
let (mut stack, server) = get_stack_and_server(
&stack,
user,
PermissionLevel::Write.into(),
&user,
PermissionLevel::Write,
true,
)
.await?;
let mut repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
if !stack.config.files_on_host
&& stack.config.repo.is_empty()
&& stack.config.linked_repo.is_empty()
{
if !stack.config.files_on_host && stack.config.repo.is_empty() {
return Err(anyhow!(
"Stack is not configured to use Files on Host, Git Repo, or Linked Repo, can't write file contents"
"Stack is not configured to use Files on Host or Git Repo, can't write file contents"
).into());
}
let mut update =
make_update(&stack, Operation::WriteStackContents, user);
make_update(&stack, Operation::WriteStackContents, &user);
update.push_simple_log("File contents to write", &contents);
@@ -164,18 +148,31 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
Err(e) => {
update.push_error_log(
"Write File Contents",
"Write file contents",
format_serror(&e.into()),
);
}
};
} else {
let git_token =
stack_git_token(&mut stack, repo.as_mut()).await?;
let git_token = if !stack.config.git_account.is_empty() {
git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
)
.await
.with_context(|| {
format!(
"Failed to get git token. | {} | {}",
stack.config.git_account, stack.config.git_provider
)
})?
} else {
None
};
match periphery_client(&server)?
.request(WriteCommitComposeContents {
stack,
repo,
username: Some(user.username.clone()),
file_path,
contents,
@@ -189,7 +186,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
Err(e) => {
update.push_error_log(
"Write File Contents",
"Write file contents",
format_serror(&e.into()),
);
}
@@ -213,7 +210,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
update.finalize();
update.id = add_update(update.clone()).await?;
add_update(update.clone()).await?;
Ok(update)
}
@@ -231,26 +228,15 @@ impl Resolve<WriteArgs> for RefreshStackCache {
) -> serror::Result<NoData> {
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
// stack should be able to do this.
let stack = get_check_permissions::<Stack>(
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Execute.into(),
PermissionLevel::Execute,
)
.await?;
let repo = if !stack.config.files_on_host
&& !stack.config.linked_repo.is_empty()
{
crate::resource::get::<Repo>(&stack.config.linked_repo)
.await?
.into()
} else {
None
};
let file_contents_empty = stack.config.file_contents.is_empty();
let repo_empty =
stack.config.repo.is_empty() && repo.as_ref().is_none();
let repo_empty = stack.config.repo.is_empty();
if !stack.config.files_on_host
&& file_contents_empty
@@ -333,12 +319,8 @@ impl Resolve<WriteArgs> for RefreshStackCache {
hash: latest_hash,
message: latest_message,
..
} = get_repo_compose_contents(
&stack,
repo.as_ref(),
Some(&mut missing_files),
)
.await?;
} = get_repo_compose_contents(&stack, Some(&mut missing_files))
.await?;
let project_name = stack.project_name(true);
@@ -389,7 +371,6 @@ impl Resolve<WriteArgs> for RefreshStackCache {
deployed_services: stack.info.deployed_services.clone(),
deployed_project_name: stack.info.deployed_project_name.clone(),
deployed_contents: stack.info.deployed_contents.clone(),
deployed_config: stack.info.deployed_config.clone(),
deployed_hash: stack.info.deployed_hash.clone(),
deployed_message: stack.info.deployed_message.clone(),
latest_services,
@@ -419,8 +400,7 @@ impl Resolve<WriteArgs> for RefreshStackCache {
if state == ServerState::Ok {
let name = stack.name.clone();
if let Err(e) =
pull_stack_inner(stack, Vec::new(), &server, repo, None)
.await
pull_stack_inner(stack, None, &server, None).await
{
warn!(
"Failed to pull latest images for Stack {name} | {e:#}",
@@ -450,10 +430,10 @@ impl Resolve<WriteArgs> for CreateStackWebhook {
);
};
let stack = get_check_permissions::<Stack>(
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
@@ -570,10 +550,10 @@ impl Resolve<WriteArgs> for DeleteStackWebhook {
);
};
let stack = get_check_permissions::<Stack>(
let stack = resource::get_check_permissions::<Stack>(
&self.stack,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
File diff suppressed because it is too large Load Diff
+43 -63
View File
@@ -1,25 +1,17 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::{
CreateTag, DeleteTag, RenameTag, UpdateTagColor,
UpdateTagsOnResource, UpdateTagsOnResourceResponse,
CreateTag, DeleteTag, RenameTag, UpdateTagsOnResource,
UpdateTagsOnResourceResponse,
},
entities::{
ResourceTarget,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
stack::Stack,
sync::ResourceSync,
tag::{Tag, TagColor},
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, permission::PermissionLevel,
procedure::Procedure, repo::Repo, server::Server,
server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, tag::Tag, ResourceTarget,
},
};
use mungos::{
@@ -30,7 +22,6 @@ use resolver_api::Resolve;
use crate::{
helpers::query::{get_tag, get_tag_check_owner},
permission::get_check_permissions,
resource,
state::db_client,
};
@@ -50,7 +41,6 @@ impl Resolve<WriteArgs> for CreateTag {
let mut tag = Tag {
id: Default::default(),
name: self.name,
color: TagColor::Slate,
owner: user.id.clone(),
};
@@ -78,7 +68,7 @@ impl Resolve<WriteArgs> for RenameTag {
return Err(anyhow!("tag name cannot be ObjectId").into());
}
get_tag_check_owner(&self.id, user).await?;
get_tag_check_owner(&self.id, &user).await?;
update_one_by_id(
&db_client().tags,
@@ -93,34 +83,13 @@ impl Resolve<WriteArgs> for RenameTag {
}
}
impl Resolve<WriteArgs> for UpdateTagColor {
#[instrument(name = "UpdateTagColor", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
let tag = get_tag_check_owner(&self.tag, user).await?;
update_one_by_id(
&db_client().tags,
&tag.id,
doc! { "$set": { "color": self.color.as_ref() } },
None,
)
.await
.context("failed to rename tag on db")?;
Ok(get_tag(&self.tag).await?)
}
}
impl Resolve<WriteArgs> for DeleteTag {
#[instrument(name = "DeleteTag", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
let tag = get_tag_check_owner(&self.id, user).await?;
let tag = get_tag_check_owner(&self.id, &user).await?;
tokio::try_join!(
resource::remove_tag_from_all::<Server>(&self.id),
@@ -131,6 +100,7 @@ impl Resolve<WriteArgs> for DeleteTag {
resource::remove_tag_from_all::<Builder>(&self.id),
resource::remove_tag_from_all::<Alerter>(&self.id),
resource::remove_tag_from_all::<Procedure>(&self.id),
resource::remove_tag_from_all::<ServerTemplate>(&self.id),
)?;
delete_one_by_id(&db_client().tags, &self.id, None).await?;
@@ -148,97 +118,107 @@ impl Resolve<WriteArgs> for UpdateTagsOnResource {
let WriteArgs { user } = args;
match self.target {
ResourceTarget::System(_) => {
return Err(anyhow!("Invalid target type: System").into());
return Err(anyhow!("Invalid target type: System").into())
}
ResourceTarget::Build(id) => {
get_check_permissions::<Build>(
resource::get_check_permissions::<Build>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Build>(&id, self.tags, args).await?;
}
ResourceTarget::Builder(id) => {
get_check_permissions::<Builder>(
resource::get_check_permissions::<Builder>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Builder>(&id, self.tags, args).await?
}
ResourceTarget::Deployment(id) => {
get_check_permissions::<Deployment>(
resource::get_check_permissions::<Deployment>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Deployment>(&id, self.tags, args)
.await?
}
ResourceTarget::Server(id) => {
get_check_permissions::<Server>(
resource::get_check_permissions::<Server>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Server>(&id, self.tags, args).await?
}
ResourceTarget::Repo(id) => {
get_check_permissions::<Repo>(
resource::get_check_permissions::<Repo>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Repo>(&id, self.tags, args).await?
}
ResourceTarget::Alerter(id) => {
get_check_permissions::<Alerter>(
resource::get_check_permissions::<Alerter>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Alerter>(&id, self.tags, args).await?
}
ResourceTarget::Procedure(id) => {
get_check_permissions::<Procedure>(
resource::get_check_permissions::<Procedure>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Procedure>(&id, self.tags, args)
.await?
}
ResourceTarget::Action(id) => {
get_check_permissions::<Action>(
resource::get_check_permissions::<Action>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Action>(&id, self.tags, args).await?
}
ResourceTarget::ResourceSync(id) => {
get_check_permissions::<ResourceSync>(
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<ServerTemplate>(&id, self.tags, args)
.await?
}
ResourceTarget::ResourceSync(id) => {
resource::get_check_permissions::<ResourceSync>(
&id,
user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<ResourceSync>(&id, self.tags, args)
.await?
}
ResourceTarget::Stack(id) => {
get_check_permissions::<Stack>(
resource::get_check_permissions::<Stack>(
&id,
user,
PermissionLevel::Write.into(),
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Stack>(&id, self.tags, args).await?
+3 -25
View File
@@ -1,20 +1,18 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::{
DeleteUser, DeleteUserResponse, UpdateUserPassword,
UpdateUserPasswordResponse, UpdateUserUsername,
UpdateUserUsernameResponse,
},
entities::{NoData, user::UserConfig},
entities::{user::UserConfig, NoData},
};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
config::core_config, helpers::hash_password, state::db_client,
};
use crate::{helpers::hash_password, state::db_client};
use super::WriteArgs;
@@ -25,16 +23,6 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateUserUsernameResponse> {
for locked_username in &core_config().lock_login_credentials_for {
if locked_username == "__ALL__"
|| *locked_username == user.username
{
return Err(
anyhow!("User not allowed to update their username.")
.into(),
);
}
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty.").into());
}
@@ -68,16 +56,6 @@ impl Resolve<WriteArgs> for UpdateUserPassword {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateUserPasswordResponse> {
for locked_username in &core_config().lock_login_credentials_for {
if locked_username == "__ALL__"
|| *locked_username == user.username
{
return Err(
anyhow!("User not allowed to update their password.")
.into(),
);
}
}
let UserConfig::Local { .. } = user.config else {
return Err(anyhow!("User is not local user").into());
};
+6 -40
View File
@@ -1,8 +1,11 @@
use std::{collections::HashMap, str::FromStr};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::*,
api::write::{
AddUserToUserGroup, CreateUserGroup, DeleteUserGroup,
RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup,
},
entities::{komodo_timestamp, user_group::UserGroup},
};
use mungos::{
@@ -17,7 +20,6 @@ use crate::state::db_client;
use super::WriteArgs;
impl Resolve<WriteArgs> for CreateUserGroup {
#[instrument(name = "CreateUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
@@ -26,12 +28,11 @@ impl Resolve<WriteArgs> for CreateUserGroup {
return Err(anyhow!("This call is admin-only").into());
}
let user_group = UserGroup {
name: self.name,
id: Default::default(),
everyone: Default::default(),
users: Default::default(),
all: Default::default(),
updated_at: komodo_timestamp(),
name: self.name,
};
let db = db_client();
let id = db
@@ -52,7 +53,6 @@ impl Resolve<WriteArgs> for CreateUserGroup {
}
impl Resolve<WriteArgs> for RenameUserGroup {
#[instrument(name = "RenameUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
@@ -78,7 +78,6 @@ impl Resolve<WriteArgs> for RenameUserGroup {
}
impl Resolve<WriteArgs> for DeleteUserGroup {
#[instrument(name = "DeleteUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
@@ -111,7 +110,6 @@ impl Resolve<WriteArgs> for DeleteUserGroup {
}
impl Resolve<WriteArgs> for AddUserToUserGroup {
#[instrument(name = "AddUserToUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
@@ -155,7 +153,6 @@ impl Resolve<WriteArgs> for AddUserToUserGroup {
}
impl Resolve<WriteArgs> for RemoveUserFromUserGroup {
#[instrument(name = "RemoveUserFromUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
@@ -199,7 +196,6 @@ impl Resolve<WriteArgs> for RemoveUserFromUserGroup {
}
impl Resolve<WriteArgs> for SetUsersInUserGroup {
#[instrument(name = "SetUsersInUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
@@ -244,33 +240,3 @@ impl Resolve<WriteArgs> for SetUsersInUserGroup {
Ok(res)
}
}
impl Resolve<WriteArgs> for SetEveryoneUserGroup {
#[instrument(name = "SetEveryoneUserGroup", skip(admin), fields(admin = admin.username))]
async fn resolve(
self,
WriteArgs { user: admin }: &WriteArgs,
) -> serror::Result<UserGroup> {
if !admin.admin {
return Err(anyhow!("This call is admin-only").into());
}
let db = db_client();
let filter = match ObjectId::from_str(&self.user_group) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": &self.user_group },
};
db.user_groups
.update_one(filter.clone(), doc! { "$set": { "everyone": self.everyone } })
.await
.context("failed to set everyone on user group")?;
let res = db
.user_groups
.find_one(filter)
.await
.context("failed to query db for UserGroups")?
.context("no user group with given id")?;
Ok(res)
}
}
+4 -4
View File
@@ -1,7 +1,7 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::{
api::write::*,
entities::{Operation, ResourceTarget, variable::Variable},
entities::{variable::Variable, Operation, ResourceTarget},
};
use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
@@ -49,7 +49,7 @@ impl Resolve<WriteArgs> for CreateVariable {
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateVariable,
user,
&user,
);
update
@@ -92,7 +92,7 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateVariableValue,
user,
&user,
);
let log = if variable.is_secret {
+5 -11
View File
@@ -1,11 +1,11 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use komodo_client::entities::config::core::{
CoreConfig, OauthCredentials,
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::sync::Mutex;
use crate::{
@@ -47,21 +47,15 @@ impl GithubOauthClient {
return None;
}
if host.is_empty() {
warn!(
"github oauth is enabled, but 'config.host' is not configured"
);
warn!("github oauth is enabled, but 'config.host' is not configured");
return None;
}
if id.is_empty() {
warn!(
"github oauth is enabled, but 'config.github_oauth.id' is not configured"
);
warn!("github oauth is enabled, but 'config.github_oauth.id' is not configured");
return None;
}
if secret.is_empty() {
warn!(
"github oauth is enabled, but 'config.github_oauth.secret' is not configured"
);
warn!("github oauth is enabled, but 'config.github_oauth.secret' is not configured");
return None;
}
GithubOauthClient {
+7 -20
View File
@@ -1,6 +1,6 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use axum::{
Router, extract::Query, response::Redirect, routing::get,
extract::Query, response::Redirect, routing::get, Router,
};
use komodo_client::entities::{
komodo_timestamp,
@@ -13,7 +13,8 @@ use serde::Deserialize;
use serror::AddStatusCode;
use crate::{
config::core_config, helpers::random_string, state::{db_client, jwt_client}
config::core_config,
state::{db_client, jwt_client},
};
use self::client::github_oauth_client;
@@ -71,7 +72,7 @@ async fn callback(
.context("failed at find user query from database")?;
let jwt = match user {
Some(user) => jwt_client()
.encode(user.id)
.generate(user.id)
.context("failed to generate jwt")?,
None => {
let ts = komodo_timestamp();
@@ -81,23 +82,9 @@ async fn callback(
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
let mut username = github_user.login;
// Modify username if it already exists
if db_client
.users
.find_one(doc! { "username": &username })
.await
.context("Failed to query users collection")?
.is_some()
{
username += "-";
username += &random_string(5);
};
let user = User {
id: Default::default(),
username,
username: github_user.login,
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
@@ -122,7 +109,7 @@ async fn callback(
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id)
.generate(user_id)
.context("failed to generate jwt")?
}
};
+11 -22
View File
@@ -1,12 +1,13 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use jsonwebtoken::{DecodingKey, Validation, decode};
use anyhow::{anyhow, Context};
use jwt::Token;
use komodo_client::entities::config::core::{
CoreConfig, OauthCredentials,
};
use reqwest::StatusCode;
use serde::{Deserialize, de::DeserializeOwned};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use tokio::sync::Mutex;
use crate::{
@@ -48,21 +49,15 @@ impl GoogleOauthClient {
return None;
}
if host.is_empty() {
warn!(
"google oauth is enabled, but 'config.host' is not configured"
);
warn!("google oauth is enabled, but 'config.host' is not configured");
return None;
}
if id.is_empty() {
warn!(
"google oauth is enabled, but 'config.google_oauth.id' is not configured"
);
warn!("google oauth is enabled, but 'config.google_oauth.id' is not configured");
return None;
}
if secret.is_empty() {
warn!(
"google oauth is enabled, but 'config.google_oauth.secret' is not configured"
);
warn!("google oauth is enabled, but 'config.google_oauth.secret' is not configured");
return None;
}
let scopes = urlencoding::encode(
@@ -144,16 +139,10 @@ impl GoogleOauthClient {
&self,
id_token: &str,
) -> anyhow::Result<GoogleUser> {
let mut v = Validation::new(Default::default());
v.insecure_disable_signature_validation();
v.validate_aud = false;
let res = decode::<GoogleUser>(
id_token,
&DecodingKey::from_secret(b""),
&v,
)
.context("failed to decode google id token")?;
Ok(res.claims)
let t: Token<Value, GoogleUser, jwt::Unverified> =
Token::parse_unverified(id_token)
.context("failed to parse id_token")?;
Ok(t.claims().to_owned())
}
#[instrument(level = "debug", skip(self))]
+11 -25
View File
@@ -1,7 +1,7 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use async_timing_util::unix_timestamp_ms;
use axum::{
Router, extract::Query, response::Redirect, routing::get,
extract::Query, response::Redirect, routing::get, Router,
};
use komodo_client::entities::user::{User, UserConfig};
use mongo_indexed::Document;
@@ -12,7 +12,6 @@ use serror::AddStatusCode;
use crate::{
config::core_config,
helpers::random_string,
state::{db_client, jwt_client},
};
@@ -82,7 +81,7 @@ async fn callback(
.context("failed at find user query from mongo")?;
let jwt = match user {
Some(user) => jwt_client()
.encode(user.id)
.generate(user.id)
.context("failed to generate jwt")?,
None => {
let ts = unix_timestamp_ms() as i64;
@@ -92,28 +91,15 @@ async fn callback(
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
let mut username = google_user
.email
.split('@')
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string();
// Modify username if it already exists
if db_client
.users
.find_one(doc! { "username": &username })
.await
.context("Failed to query users collection")?
.is_some()
{
username += "-";
username += &random_string(5);
};
let user = User {
id: Default::default(),
username,
username: google_user
.email
.split('@')
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string(),
enabled: no_users_exist || core_config.enable_new_users,
admin: no_users_exist,
super_admin: no_users_exist,
@@ -138,7 +124,7 @@ async fn callback(
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id)
.generate(user_id)
.context("failed to generate jwt")?
}
};
+14 -22
View File
@@ -1,15 +1,15 @@
use std::collections::HashMap;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use async_timing_util::{
Timelength, get_timelength_in_ms, unix_timestamp_ms,
};
use jsonwebtoken::{
DecodingKey, EncodingKey, Header, Validation, decode, encode,
get_timelength_in_ms, unix_timestamp_ms, Timelength,
};
use hmac::{Hmac, Mac};
use jwt::SignWithKey;
use komodo_client::entities::config::core::CoreConfig;
use mungos::mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tokio::sync::Mutex;
use crate::helpers::random_string;
@@ -24,10 +24,7 @@ pub struct JwtClaims {
}
pub struct JwtClient {
header: Header,
validation: Validation,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
pub key: Hmac<Sha256>,
ttl_ms: u128,
exchange_tokens: ExchangeTokenMap,
}
@@ -39,11 +36,10 @@ impl JwtClient {
} else {
config.jwt_secret.clone()
};
let key = Hmac::new_from_slice(secret.as_bytes())
.context("failed at taking HmacSha256 of jwt secret")?;
Ok(JwtClient {
header: Header::default(),
validation: Validation::new(Default::default()),
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
key,
ttl_ms: get_timelength_in_ms(
config.jwt_ttl.to_string().parse()?,
),
@@ -51,7 +47,7 @@ impl JwtClient {
})
}
pub fn encode(&self, user_id: String) -> anyhow::Result<String> {
pub fn generate(&self, user_id: String) -> anyhow::Result<String> {
let iat = unix_timestamp_ms();
let exp = iat + self.ttl_ms;
let claims = JwtClaims {
@@ -59,14 +55,10 @@ impl JwtClient {
iat,
exp,
};
encode(&self.header, &claims, &self.encoding_key)
.context("failed at signing claim")
}
pub fn decode(&self, jwt: &str) -> anyhow::Result<JwtClaims> {
decode::<JwtClaims>(jwt, &self.decoding_key, &self.validation)
.map(|res| res.claims)
.context("failed to decode token claims")
let jwt = claims
.sign_with_key(&self.key)
.context("failed at signing claim")?;
Ok(jwt)
}
#[instrument(level = "debug", skip_all)]
+3 -3
View File
@@ -1,6 +1,6 @@
use std::str::FromStr;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use async_timing_util::unix_timestamp_ms;
use komodo_client::{
api::auth::{
@@ -85,7 +85,7 @@ impl Resolve<AuthArgs> for CreateLocalUser {
.to_string();
let jwt = jwt_client()
.encode(user_id)
.generate(user_id)
.context("failed to generate jwt for user")?;
Ok(CreateLocalUserResponse { jwt })
@@ -131,7 +131,7 @@ impl Resolve<AuthArgs> for LoginLocalUser {
}
let jwt = jwt_client()
.encode(user.id)
.generate(user.id)
.context("failed at generating jwt for user")?;
Ok(LoginLocalUserResponse { jwt })
+6 -5
View File
@@ -1,4 +1,5 @@
use anyhow::{Context, anyhow};
use ::jwt::VerifyWithKey;
use anyhow::{anyhow, Context};
use async_timing_util::unix_timestamp_ms;
use axum::{
extract::Request, http::HeaderMap, middleware::Next,
@@ -70,9 +71,7 @@ pub async fn get_user_id_from_headers(
}
_ => {
// AUTH FAIL
Err(anyhow!(
"must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"
))
Err(anyhow!("must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"))
}
}
}
@@ -94,7 +93,9 @@ pub async fn authenticate_check_enabled(
pub async fn auth_jwt_get_user_id(
jwt: &str,
) -> anyhow::Result<String> {
let claims: JwtClaims = jwt_client().decode(jwt)?;
let claims: JwtClaims = jwt
.verify_with_key(&jwt_client().key)
.context("failed to verify claims")?;
if claims.exp > unix_timestamp_ms() {
Ok(claims.id)
} else {
+47 -73
View File
@@ -1,93 +1,67 @@
use std::{sync::OnceLock, time::Duration};
use std::sync::OnceLock;
use anyhow::Context;
use arc_swap::ArcSwapOption;
use openidconnect::{
Client, ClientId, ClientSecret, EmptyAdditionalClaims,
EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl,
RedirectUrl, StandardErrorResponse, core::*,
core::{CoreClient, CoreProviderMetadata},
reqwest::async_http_client,
ClientId, ClientSecret, IssuerUrl, RedirectUrl,
};
use crate::config::core_config;
type OidcClient = Client<
EmptyAdditionalClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
CoreTokenResponse,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointMaybeSet,
EndpointMaybeSet,
>;
static DEFAULT_OIDC_CLIENT: OnceLock<Option<CoreClient>> =
OnceLock::new();
pub fn oidc_client() -> &'static ArcSwapOption<OidcClient> {
static OIDC_CLIENT: OnceLock<ArcSwapOption<OidcClient>> =
OnceLock::new();
OIDC_CLIENT.get_or_init(Default::default)
pub fn default_oidc_client() -> Option<&'static CoreClient> {
DEFAULT_OIDC_CLIENT
.get()
.expect("OIDC client get before init")
.as_ref()
}
/// 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() {
pub async fn init_default_oidc_client() {
let config = core_config();
if !config.oidc_enabled
|| config.oidc_provider.is_empty()
|| config.oidc_client_id.is_empty()
|| config.oidc_client_secret.is_empty()
{
DEFAULT_OIDC_CLIENT
.set(None)
.expect("Default OIDC client initialized twice");
return;
}
if let Err(e) = reset_oidc_client().await {
error!("Failed to initialize OIDC client | {e:#}");
async {
// Use OpenID Connect Discovery to fetch the provider metadata.
let provider_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new(config.oidc_provider.clone())?,
async_http_client,
)
.await
.context(
"Failed to get OIDC /.well-known/openid-configuration",
)?;
// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
// and token URL.
let client = CoreClient::from_provider_metadata(
provider_metadata,
ClientId::new(config.oidc_client_id.to_string()),
Some(ClientSecret::new(config.oidc_client_secret.to_string())),
)
// Set the URL the user will be redirected to after the authorization process.
.set_redirect_uri(RedirectUrl::new(format!(
"{}/auth/oidc/callback",
core_config().host
))?);
DEFAULT_OIDC_CLIENT
.set(Some(client))
.expect("Default OIDC client initialized twice");
anyhow::Ok(())
}
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(e) = reset_oidc_client().await {
warn!("Failed to reinitialize OIDC client | {e:#}");
}
}
});
}
async fn reset_oidc_client() -> anyhow::Result<()> {
let config = core_config();
// Use OpenID Connect Discovery to fetch the provider metadata.
let provider_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new(config.oidc_provider.clone())?,
super::reqwest_client(),
)
.await
.context("Failed to get OIDC /.well-known/openid-configuration")?;
let client = CoreClient::from_provider_metadata(
provider_metadata,
ClientId::new(config.oidc_client_id.to_string()),
// The secret may be empty / ommitted if auth provider supports PKCE
if config.oidc_client_secret.is_empty() {
None
} else {
Some(ClientSecret::new(config.oidc_client_secret.to_string()))
},
)
// Set the URL the user will be redirected to after the authorization process.
.set_redirect_uri(RedirectUrl::new(format!(
"{}/auth/oidc/callback",
core_config().host
))?);
oidc_client().store(Some(client.into()));
Ok(())
.context("Failed to init default OIDC client")
.unwrap();
}
+26 -73
View File
@@ -1,21 +1,20 @@
use std::sync::OnceLock;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use axum::{
Router, extract::Query, response::Redirect, routing::get,
extract::Query, response::Redirect, routing::get, Router,
};
use client::oidc_client;
use client::default_oidc_client;
use dashmap::DashMap;
use komodo_client::entities::{
komodo_timestamp,
user::{User, UserConfig},
};
use mungos::mongodb::bson::{Document, doc};
use mungos::mongodb::bson::{doc, Document};
use openidconnect::{
AccessTokenHash, AuthorizationCode, CsrfToken,
EmptyAdditionalClaims, Nonce, OAuth2TokenResponse,
PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
core::{CoreAuthenticationFlow, CoreGenderClaim},
core::CoreAuthenticationFlow, AccessTokenHash, AuthorizationCode,
CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeChallenge,
PkceCodeVerifier, Scope, TokenResponse,
};
use reqwest::StatusCode;
use serde::Deserialize;
@@ -23,7 +22,6 @@ use serror::AddStatusCode;
use crate::{
config::core_config,
helpers::random_string,
state::{db_client, jwt_client},
};
@@ -31,28 +29,16 @@ use super::RedirectQuery;
pub mod client;
fn reqwest_client() -> &'static reqwest::Client {
static REQWEST: OnceLock<reqwest::Client> = OnceLock::new();
REQWEST.get_or_init(|| {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("Invalid OIDC reqwest client")
})
}
/// CSRF tokens can only be used once from the callback,
/// and must be used within this timeframe
const CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in.
type RedirectUrl = Option<String>;
/// Maps the csrf secrets to other information added in the "login" method (before auth provider redirect).
/// This information is retrieved in the "callback" method (after auth provider redirect).
type VerifierMap =
type CsrfMap =
DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>;
fn verifier_tokens() -> &'static VerifierMap {
static VERIFIERS: OnceLock<VerifierMap> = OnceLock::new();
VERIFIERS.get_or_init(Default::default)
fn csrf_verifier_tokens() -> &'static CsrfMap {
static CSRF: OnceLock<CsrfMap> = OnceLock::new();
CSRF.get_or_init(Default::default)
}
pub fn router() -> Router {
@@ -75,10 +61,10 @@ pub fn router() -> Router {
async fn login(
Query(RedirectQuery { redirect }): Query<RedirectQuery>,
) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client =
client.as_ref().context("OIDC Client not configured")?;
default_oidc_client().context("OIDC Client not configured")?;
// Generate a PKCE challenge.
let (pkce_challenge, pkce_verifier) =
PkceCodeChallenge::new_random_sha256();
@@ -89,14 +75,13 @@ async fn login(
CsrfToken::new_random,
Nonce::new_random,
)
.set_pkce_challenge(pkce_challenge)
.add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("profile".to_string()))
.add_scope(Scope::new("email".to_string()))
.set_pkce_challenge(pkce_challenge)
.url();
// Data inserted here will be matched on callback side for csrf protection.
verifier_tokens().insert(
csrf_verifier_tokens().insert(
csrf_token.secret().clone(),
(
pkce_verifier,
@@ -138,9 +123,8 @@ struct CallbackQuery {
async fn callback(
Query(query): Query<CallbackQuery>,
) -> anyhow::Result<Redirect> {
let client = oidc_client().load();
let client =
client.as_ref().context("OIDC Client not initialized successfully. Is the provider properly configured?")?;
default_oidc_client().context("OIDC Client not configured")?;
if let Some(e) = query.error {
return Err(anyhow!("Provider returned error: {e}"));
@@ -152,9 +136,9 @@ async fn callback(
);
let (_, (pkce_verifier, nonce, redirect, valid_until)) =
verifier_tokens()
csrf_verifier_tokens()
.remove(state.secret())
.context("CSRF token invalid")?;
.context("CSRF Token invalid")?;
if komodo_timestamp() > valid_until {
return Err(anyhow!(
@@ -162,12 +146,11 @@ async fn callback(
));
}
let reqwest_client = reqwest_client();
let token_response = client
.exchange_code(AuthorizationCode::new(code))
.context("Failed to get Oauth token at exchange code")?
// Set the PKCE code verifier.
.set_pkce_verifier(pkce_verifier)
.request_async(reqwest_client)
.request_async(openidconnect::reqwest::async_http_client)
.await
.context("Failed to get Oauth token")?;
@@ -190,7 +173,7 @@ async fn callback(
let claims = id_token
.claims(&verifier, &nonce)
.context("Failed to verify token claims. This issue may be temporary (60 seconds max).")?;
.context("Failed to verify token claims")?;
// Verify the access token hash to ensure that the access token hasn't been substituted for
// another user's.
@@ -198,8 +181,7 @@ async fn callback(
{
let actual_access_token_hash = AccessTokenHash::from_token(
token_response.access_token(),
id_token.signing_alg()?,
id_token.signing_key(&verifier)?,
&id_token.signing_alg()?,
)?;
if actual_access_token_hash != *expected_access_token_hash {
return Err(anyhow!("Invalid access token"));
@@ -220,7 +202,7 @@ async fn callback(
let jwt = match user {
Some(user) => jwt_client()
.encode(user.id)
.generate(user.id)
.context("failed to generate jwt")?,
None => {
let ts = komodo_timestamp();
@@ -230,26 +212,12 @@ async fn callback(
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
// Fetch user info
let user_info = client
.user_info(
token_response.access_token().clone(),
claims.subject().clone().into(),
)
.context("Invalid user info request")?
.request_async::<EmptyAdditionalClaims, _, CoreGenderClaim>(
reqwest_client,
)
.await
.context("Failed to fetch user info for new user")?;
// Will use preferred_username, then email, then user_id if it isn't available.
let mut username = user_info
let username = claims
.preferred_username()
.map(|username| username.to_string())
.unwrap_or_else(|| {
let email = user_info
let email = claims
.email()
.map(|email| email.as_str())
.unwrap_or(user_id);
@@ -263,19 +231,6 @@ async fn callback(
}
.to_string()
});
// Modify username if it already exists
if db_client
.users
.find_one(doc! { "username": &username })
.await
.context("Failed to query users collection")?
.is_some()
{
username += "-";
username += &random_string(5);
};
let user = User {
id: Default::default(),
username,
@@ -293,7 +248,6 @@ async fn callback(
user_id: user_id.to_string(),
},
};
let user_id = db_client
.users
.insert_one(user)
@@ -303,9 +257,8 @@ async fn callback(
.as_object_id()
.context("inserted_id is not ObjectId")?
.to_string();
jwt_client()
.encode(user_id)
.generate(user_id)
.context("failed to generate jwt")?
}
};
+42 -56
View File
@@ -1,22 +1,22 @@
use std::time::Duration;
use std::{str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use aws_config::{BehaviorVersion, Region};
use aws_sdk_ec2::{
Client,
types::{
BlockDeviceMapping, EbsBlockDevice,
InstanceNetworkInterfaceSpecification, InstanceStateChange,
InstanceStateName, InstanceStatus, InstanceType, ResourceType,
Tag, TagSpecification,
Tag, TagSpecification, VolumeType,
},
Client,
};
use base64::Engine;
use komodo_client::entities::{
ResourceTarget,
alert::{Alert, AlertData, SeverityLevel},
builder::AwsBuilderConfig,
komodo_timestamp,
server_template::aws::AwsServerTemplateConfig,
ResourceTarget,
};
use crate::{alert::send_alerts, config::core_config};
@@ -29,40 +29,20 @@ pub struct Ec2Instance {
pub ip: String,
}
/// Provides credentials in the core config file to the AWS client
#[derive(Debug)]
struct CredentialsFromConfig;
impl aws_credential_types::provider::ProvideCredentials
for CredentialsFromConfig
{
fn provide_credentials<'a>(
&'a self,
) -> aws_credential_types::provider::future::ProvideCredentials<'a>
where
Self: 'a,
{
aws_credential_types::provider::future::ProvideCredentials::new(
async {
let config = core_config();
Ok(aws_credential_types::Credentials::new(
&config.aws.access_key_id,
&config.aws.secret_access_key,
None,
None,
"komodo-config",
))
},
)
}
}
#[instrument]
async fn create_ec2_client(region: String) -> Client {
// There may be a better way to pass these keys to client
std::env::set_var(
"AWS_ACCESS_KEY_ID",
&core_config().aws.access_key_id,
);
std::env::set_var(
"AWS_SECRET_ACCESS_KEY",
&core_config().aws.secret_access_key,
);
let region = Region::new(region);
let config = aws_config::defaults(BehaviorVersion::latest())
let config = aws_config::defaults(BehaviorVersion::v2024_03_28())
.region(region)
.credentials_provider(CredentialsFromConfig)
.load()
.await;
Client::new(&config)
@@ -71,12 +51,12 @@ async fn create_ec2_client(region: String) -> Client {
#[instrument]
pub async fn launch_ec2_instance(
name: &str,
config: &AwsBuilderConfig,
config: AwsServerTemplateConfig,
) -> anyhow::Result<Ec2Instance> {
let AwsBuilderConfig {
let AwsServerTemplateConfig {
region,
instance_type,
volume_gb,
volumes,
ami_id,
subnet_id,
security_group_ids,
@@ -86,22 +66,19 @@ pub async fn launch_ec2_instance(
user_data,
port: _,
use_https: _,
git_providers: _,
docker_registries: _,
secrets: _,
} = config;
let instance_type = handle_unknown_instance_type(
InstanceType::from(instance_type.as_str()),
)?;
let client = create_ec2_client(region.clone()).await;
let req = client
let mut req = client
.run_instances()
.image_id(ami_id)
.instance_type(instance_type)
.network_interfaces(
InstanceNetworkInterfaceSpecification::builder()
.subnet_id(subnet_id)
.associate_public_ip_address(*assign_public_ip)
.associate_public_ip_address(assign_public_ip)
.set_groups(security_group_ids.to_vec().into())
.device_index(0)
.build(),
@@ -113,17 +90,6 @@ pub async fn launch_ec2_instance(
.resource_type(ResourceType::Instance)
.build(),
)
.block_device_mappings(
BlockDeviceMapping::builder()
.set_device_name("/dev/sda1".to_string().into())
.set_ebs(
EbsBlockDevice::builder()
.volume_size(*volume_gb)
.build()
.into(),
)
.build(),
)
.min_count(1)
.max_count(1)
.user_data(
@@ -131,6 +97,26 @@ pub async fn launch_ec2_instance(
.encode(user_data),
);
for volume in volumes {
let ebs = EbsBlockDevice::builder()
.volume_size(volume.size_gb)
.volume_type(
VolumeType::from_str(volume.volume_type.as_ref())
.context("invalid volume type")?,
)
.set_iops((volume.iops != 0).then_some(volume.iops))
.set_throughput(
(volume.throughput != 0).then_some(volume.throughput),
)
.build();
req = req.block_device_mappings(
BlockDeviceMapping::builder()
.set_device_name(volume.device_name.into())
.set_ebs(ebs.into())
.build(),
)
}
let res = req
.send()
.await
@@ -150,7 +136,7 @@ pub async fn launch_ec2_instance(
let state_name =
get_ec2_instance_state_name(&client, &instance_id).await?;
if state_name == Some(InstanceStateName::Running) {
let ip = if *use_public_ip {
let ip = if use_public_ip {
get_ec2_instance_public_ip(&client, &instance_id).await?
} else {
instance
+157
View File
@@ -0,0 +1,157 @@
use anyhow::{anyhow, Context};
use axum::http::{HeaderName, HeaderValue};
use reqwest::{RequestBuilder, StatusCode};
use serde::{de::DeserializeOwned, Serialize};
use super::{
common::{
HetznerActionResponse, HetznerDatacenterResponse,
HetznerServerResponse, HetznerVolumeResponse,
},
create_server::{CreateServerBody, CreateServerResponse},
create_volume::{CreateVolumeBody, CreateVolumeResponse},
};
const BASE_URL: &str = "https://api.hetzner.cloud/v1";
pub struct HetznerClient(reqwest::Client);
impl HetznerClient {
pub fn new(token: &str) -> HetznerClient {
HetznerClient(
reqwest::ClientBuilder::new()
.default_headers(
[(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&format!("Bearer {token}"))
.unwrap(),
)]
.into_iter()
.collect(),
)
.build()
.context("failed to build Hetzner request client")
.unwrap(),
)
}
pub async fn get_server(
&self,
id: i64,
) -> anyhow::Result<HetznerServerResponse> {
self.get(&format!("/servers/{id}")).await
}
pub async fn create_server(
&self,
body: &CreateServerBody,
) -> anyhow::Result<CreateServerResponse> {
self.post("/servers", body).await
}
#[allow(unused)]
pub async fn delete_server(
&self,
id: i64,
) -> anyhow::Result<HetznerActionResponse> {
self.delete(&format!("/servers/{id}")).await
}
pub async fn get_volume(
&self,
id: i64,
) -> anyhow::Result<HetznerVolumeResponse> {
self.get(&format!("/volumes/{id}")).await
}
pub async fn create_volume(
&self,
body: &CreateVolumeBody,
) -> anyhow::Result<CreateVolumeResponse> {
self.post("/volumes", body).await
}
#[allow(unused)]
pub async fn delete_volume(&self, id: i64) -> anyhow::Result<()> {
let res = self
.0
.delete(format!("{BASE_URL}/volumes/{id}"))
.send()
.await
.context("failed at request to delete volume")?;
let status = res.status();
if status == StatusCode::NO_CONTENT {
Ok(())
} else {
let text = res
.text()
.await
.context("failed to get response body as text")?;
Err(anyhow!("{status} | {text}"))
}
}
#[allow(unused)]
pub async fn list_datacenters(
&self,
) -> anyhow::Result<HetznerDatacenterResponse> {
self.get("/datacenters").await
}
async fn get<Res: DeserializeOwned>(
&self,
path: &str,
) -> anyhow::Result<Res> {
let req = self.0.get(format!("{BASE_URL}{path}"));
handle_req(req).await.with_context(|| {
format!("failed at GET request to Hetzner | path: {path}")
})
}
async fn post<Body: Serialize, Res: DeserializeOwned>(
&self,
path: &str,
body: &Body,
) -> anyhow::Result<Res> {
let req = self.0.post(format!("{BASE_URL}{path}")).json(&body);
handle_req(req).await.with_context(|| {
format!("failed at POST request to Hetzner | path: {path}")
})
}
#[allow(unused)]
async fn delete<Res: DeserializeOwned>(
&self,
path: &str,
) -> anyhow::Result<Res> {
let req = self.0.delete(format!("{BASE_URL}{path}"));
handle_req(req).await.with_context(|| {
format!("failed at DELETE request to Hetzner | path: {path}")
})
}
}
async fn handle_req<Res: DeserializeOwned>(
req: RequestBuilder,
) -> anyhow::Result<Res> {
let res = req.send().await?;
let status = res.status();
if status.is_success() {
res.json().await.context("failed to parse response to json")
} else {
let text = res
.text()
.await
.context("failed to get response body as text")?;
if let Ok(json_error) =
serde_json::from_str::<serde_json::Value>(&text)
{
return Err(anyhow!("{status} | {json_error:?}"));
}
Err(anyhow!("{status} | {text}"))
}
}
+280
View File
@@ -0,0 +1,280 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerServerResponse {
pub server: HetznerServer,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerServer {
pub id: i64,
pub name: String,
pub primary_disk_size: f64,
pub image: Option<HetznerImage>,
pub private_net: Vec<HetznerPrivateNet>,
pub public_net: HetznerPublicNet,
pub server_type: HetznerServerTypeDetails,
pub status: HetznerServerStatus,
#[serde(default)]
pub volumes: Vec<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerServerTypeDetails {
pub architecture: String,
pub cores: i64,
pub cpu_type: String,
pub description: String,
pub disk: f64,
pub id: i64,
pub memory: f64,
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerPrivateNet {
pub alias_ips: Vec<String>,
pub ip: String,
pub mac_address: String,
pub network: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerPublicNet {
#[serde(default)]
pub firewalls: Vec<HetznerFirewall>,
pub floating_ips: Vec<i64>,
pub ipv4: Option<HetznerIpv4>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerFirewall {
pub id: i64,
pub status: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerIpv4 {
pub id: Option<i64>,
pub blocked: bool,
pub dns_ptr: String,
pub ip: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerImage {
pub id: i64,
pub description: String,
pub name: Option<String>,
pub os_flavor: String,
pub os_version: Option<String>,
pub rapid_deploy: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerActionResponse {
pub action: HetznerAction,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerAction {
pub command: String,
pub error: Option<HetznerError>,
pub finished: Option<String>,
pub id: i64,
pub progress: i32,
pub resources: Vec<HetznerResource>,
pub started: String,
pub status: HetznerActionStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerError {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerResource {
pub id: i64,
#[serde(rename = "type")]
pub ty: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerVolumeResponse {
pub volume: HetznerVolume,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerVolume {
/// Name of the Resource. Must be unique per Project.
pub name: String,
/// Point in time when the Resource was created (in ISO-8601 format).
pub created: String,
/// Filesystem of the Volume if formatted on creation, null if not formatted on creation
pub format: Option<HetznerVolumeFormat>,
/// ID of the Volume.
pub id: i64,
/// User-defined labels ( key/value pairs) for the Resource
pub labels: HashMap<String, String>,
/// Device path on the file system for the Volume
pub linux_device: String,
/// Protection configuration for the Resource.
pub protection: HetznerProtection,
/// ID of the Server the Volume is attached to, null if it is not attached at all
pub server: Option<i64>,
/// Size in GB of the Volume
pub size: i64,
/// Current status of the Volume. Allowed: `creating`, `available`
pub status: HetznerVolumeStatus,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerProtection {
/// Prevent the Resource from being deleted.
pub delete: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerDatacenterResponse {
pub datacenters: Vec<HetznerDatacenterDetails>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HetznerDatacenterDetails {
pub id: i64,
pub name: String,
pub location: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HetznerLocation {
#[serde(rename = "nbg1")]
Nuremberg1,
#[serde(rename = "hel1")]
Helsinki1,
#[serde(rename = "fsn1")]
Falkenstein1,
#[serde(rename = "ash")]
Ashburn,
#[serde(rename = "hil")]
Hillsboro,
#[serde(rename = "sin")]
Singapore,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum HetznerDatacenter {
#[serde(rename = "nbg1-dc3")]
Nuremberg1Dc3,
#[serde(rename = "hel1-dc2")]
Helsinki1Dc2,
#[serde(rename = "fsn1-dc14")]
Falkenstein1Dc14,
#[serde(rename = "ash-dc1")]
AshburnDc1,
#[serde(rename = "hil-dc1")]
HillsboroDc1,
#[serde(rename = "sin-dc1")]
SingaporeDc1,
}
impl From<HetznerDatacenter> for HetznerLocation {
fn from(value: HetznerDatacenter) -> Self {
match value {
HetznerDatacenter::Nuremberg1Dc3 => HetznerLocation::Nuremberg1,
HetznerDatacenter::Helsinki1Dc2 => HetznerLocation::Helsinki1,
HetznerDatacenter::Falkenstein1Dc14 => {
HetznerLocation::Falkenstein1
}
HetznerDatacenter::AshburnDc1 => HetznerLocation::Ashburn,
HetznerDatacenter::HillsboroDc1 => HetznerLocation::Hillsboro,
HetznerDatacenter::SingaporeDc1 => HetznerLocation::Singapore,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerVolumeFormat {
Xfs,
Ext4,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerVolumeStatus {
Creating,
Available,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerServerStatus {
Running,
Initializing,
Starting,
Stopping,
Off,
Deleting,
Migrating,
Rebuilding,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HetznerActionStatus {
Running,
Success,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[allow(clippy::enum_variant_names)]
pub enum HetznerServerType {
// Shared
#[serde(rename = "cpx11")]
SharedAmd2Core2Ram40Disk,
#[serde(rename = "cax11")]
SharedArm2Core4Ram40Disk,
#[serde(rename = "cx22")]
SharedIntel2Core4Ram40Disk,
#[serde(rename = "cpx21")]
SharedAmd3Core4Ram80Disk,
#[serde(rename = "cax21")]
SharedArm4Core8Ram80Disk,
#[serde(rename = "cx32")]
SharedIntel4Core8Ram80Disk,
#[serde(rename = "cpx31")]
SharedAmd4Core8Ram160Disk,
#[serde(rename = "cax31")]
SharedArm8Core16Ram160Disk,
#[serde(rename = "cx42")]
SharedIntel8Core16Ram160Disk,
#[serde(rename = "cpx41")]
SharedAmd8Core16Ram240Disk,
#[serde(rename = "cax41")]
SharedArm16Core32Ram320Disk,
#[serde(rename = "cx52")]
SharedIntel16Core32Ram320Disk,
#[serde(rename = "cpx51")]
SharedAmd16Core32Ram360Disk,
// Dedicated
#[serde(rename = "ccx13")]
DedicatedAmd2Core8Ram80Disk,
#[serde(rename = "ccx23")]
DedicatedAmd4Core16Ram160Disk,
#[serde(rename = "ccx33")]
DedicatedAmd8Core32Ram240Disk,
#[serde(rename = "ccx43")]
DedicatedAmd16Core64Ram360Disk,
#[serde(rename = "ccx53")]
DedicatedAmd32Core128Ram600Disk,
#[serde(rename = "ccx63")]
DedicatedAmd48Core192Ram960Disk,
}
@@ -0,0 +1,75 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::common::{
HetznerAction, HetznerDatacenter, HetznerLocation, HetznerServer,
HetznerServerType,
};
#[derive(Debug, Clone, Serialize)]
pub struct CreateServerBody {
/// Name of the Server to create (must be unique per Project and a valid hostname as per RFC 1123)
pub name: String,
/// Auto-mount Volumes after attach
#[serde(skip_serializing_if = "Option::is_none")]
pub automount: Option<bool>,
/// ID or name of Datacenter to create Server in (must not be used together with location)
#[serde(skip_serializing_if = "Option::is_none")]
pub datacenter: Option<HetznerDatacenter>,
/// ID or name of Location to create Server in (must not be used together with datacenter)
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<HetznerLocation>,
/// Firewalls which should be applied on the Server's public network interface at creation time
pub firewalls: Vec<Firewall>,
/// ID or name of the Image the Server is created from
pub image: String,
/// User-defined labels (key-value pairs) for the Resource
pub labels: HashMap<String, String>,
/// Network IDs which should be attached to the Server private network interface at the creation time
pub networks: Vec<i64>,
/// ID of the Placement Group the server should be in
#[serde(skip_serializing_if = "Option::is_none")]
pub placement_group: Option<i64>,
/// Public Network options
pub public_net: PublicNet,
/// ID or name of the Server type this Server should be created with
pub server_type: HetznerServerType,
/// SSH key IDs ( integer ) or names ( string ) which should be injected into the Server at creation time
pub ssh_keys: Vec<String>,
/// This automatically triggers a Power on a Server-Server Action after the creation is finished and is returned in the next_actions response object.
pub start_after_create: bool,
/// Cloud-Init user data to use during Server creation. This field is limited to 32KiB.
#[serde(skip_serializing_if = "Option::is_none")]
pub user_data: Option<String>,
/// Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location.
pub volumes: Vec<i64>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Firewall {
/// ID of the Firewall
pub firewall: i64,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct PublicNet {
/// Attach an IPv4 on the public NIC. If false, no IPv4 address will be attached.
pub enable_ipv4: bool,
/// Attach an IPv6 on the public NIC. If false, no IPv6 address will be attached.
pub enable_ipv6: bool,
/// ID of the ipv4 Primary IP to use. If omitted and enable_ipv4 is true, a new ipv4 Primary IP will automatically be created.
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv4: Option<i64>,
/// ID of the ipv6 Primary IP to use. If omitted and enable_ipv6 is true, a new ipv6 Primary IP will automatically be created.
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv6: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CreateServerResponse {
pub action: HetznerAction,
pub next_actions: Vec<HetznerAction>,
pub root_password: Option<String>,
pub server: HetznerServer,
}
@@ -0,0 +1,36 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::common::{
HetznerAction, HetznerLocation, HetznerVolume, HetznerVolumeFormat,
};
#[derive(Debug, Clone, Serialize)]
pub struct CreateVolumeBody {
/// Name of the volume
pub name: String,
/// Auto-mount Volume after attach. server must be provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub automount: Option<bool>,
/// Format Volume after creation. One of: xfs, ext4
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<HetznerVolumeFormat>,
/// User-defined labels (key-value pairs) for the Resource
pub labels: HashMap<String, String>,
/// Location to create the Volume in (can be omitted if Server is specified)
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<HetznerLocation>,
/// Server to which to attach the Volume once it's created (Volume will be created in the same Location as the server)
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<i64>,
/// Size of the Volume in GB
pub size: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CreateVolumeResponse {
pub action: HetznerAction,
pub next_actions: Vec<HetznerAction>,
pub volume: HetznerVolume,
}
+281
View File
@@ -0,0 +1,281 @@
use std::{
sync::{Arc, Mutex, OnceLock},
time::Duration,
};
use anyhow::{anyhow, Context};
use futures::future::join_all;
use komodo_client::entities::server_template::hetzner::{
HetznerDatacenter, HetznerServerTemplateConfig, HetznerServerType,
HetznerVolumeFormat,
};
use crate::{
cloud::hetzner::{
common::HetznerServerStatus, create_server::CreateServerBody,
create_volume::CreateVolumeBody,
},
config::core_config,
};
use self::{client::HetznerClient, common::HetznerVolumeStatus};
mod client;
mod common;
mod create_server;
mod create_volume;
fn hetzner() -> Option<&'static HetznerClient> {
static HETZNER_CLIENT: OnceLock<Option<HetznerClient>> =
OnceLock::new();
HETZNER_CLIENT
.get_or_init(|| {
let token = &core_config().hetzner.token;
(!token.is_empty()).then(|| HetznerClient::new(token))
})
.as_ref()
}
pub struct HetznerServerMinimal {
pub id: i64,
pub ip: String,
}
const POLL_RATE_SECS: u64 = 3;
const MAX_POLL_TRIES: usize = 100;
#[instrument]
pub async fn launch_hetzner_server(
name: &str,
config: HetznerServerTemplateConfig,
) -> anyhow::Result<HetznerServerMinimal> {
let hetzner =
*hetzner().as_ref().context("Hetzner token not configured")?;
let HetznerServerTemplateConfig {
image,
datacenter,
private_network_ids,
placement_group,
enable_public_ipv4,
enable_public_ipv6,
firewall_ids,
server_type,
ssh_keys,
user_data,
use_public_ip,
labels,
volumes,
port: _,
use_https: _,
} = config;
let datacenter = hetzner_datacenter(datacenter);
// Create volumes and get their ids
let mut volume_ids = Vec::new();
for volume in volumes {
let body = CreateVolumeBody {
name: volume.name,
format: Some(hetzner_format(volume.format)),
location: Some(datacenter.into()),
labels: volume.labels,
size: volume.size_gb,
automount: None,
server: None,
};
let id = hetzner
.create_volume(&body)
.await
.context("failed to create hetzner volume")?
.volume
.id;
volume_ids.push(id);
}
// Make sure volumes are available before continue
let vol_ids_poll = Arc::new(Mutex::new(volume_ids.clone()));
for _ in 0..MAX_POLL_TRIES {
if vol_ids_poll.lock().unwrap().is_empty() {
break;
}
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
let ids = vol_ids_poll.lock().unwrap().clone();
let futures = ids.into_iter().map(|id| {
let vol_ids = vol_ids_poll.clone();
async move {
let Ok(res) = hetzner.get_volume(id).await else {
return;
};
if matches!(res.volume.status, HetznerVolumeStatus::Available)
{
vol_ids.lock().unwrap().retain(|_id| *_id != id);
}
}
});
join_all(futures).await;
}
if !vol_ids_poll.lock().unwrap().is_empty() {
return Err(anyhow!("Volumes not ready after poll"));
}
let body = CreateServerBody {
name: name.to_string(),
automount: None,
datacenter: Some(datacenter),
location: None,
firewalls: firewall_ids
.into_iter()
.map(|firewall| create_server::Firewall { firewall })
.collect(),
image,
labels,
networks: private_network_ids,
placement_group: (placement_group > 0).then_some(placement_group),
public_net: create_server::PublicNet {
enable_ipv4: enable_public_ipv4,
enable_ipv6: enable_public_ipv6,
ipv4: None,
ipv6: None,
},
server_type: hetzner_server_type(server_type),
ssh_keys,
start_after_create: true,
user_data: (!user_data.is_empty()).then_some(user_data),
volumes: volume_ids,
};
let server_id = hetzner
.create_server(&body)
.await
.context("failed to create hetnzer server")?
.server
.id;
for _ in 0..MAX_POLL_TRIES {
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
let Ok(res) = hetzner.get_server(server_id).await else {
continue;
};
if matches!(res.server.status, HetznerServerStatus::Running) {
let ip = if use_public_ip {
res
.server
.public_net
.ipv4
.context("instance does not have public ipv4 attached")?
.ip
} else {
res
.server
.private_net
.first()
.context("no private networks attached")?
.ip
.to_string()
};
let server = HetznerServerMinimal { id: server_id, ip };
return Ok(server);
}
}
Err(anyhow!(
"failed to verify server running after polling status"
))
}
fn hetzner_format(
format: HetznerVolumeFormat,
) -> common::HetznerVolumeFormat {
match format {
HetznerVolumeFormat::Xfs => common::HetznerVolumeFormat::Xfs,
HetznerVolumeFormat::Ext4 => common::HetznerVolumeFormat::Ext4,
}
}
fn hetzner_datacenter(
datacenter: HetznerDatacenter,
) -> common::HetznerDatacenter {
match datacenter {
HetznerDatacenter::Nuremberg1Dc3 => {
common::HetznerDatacenter::Nuremberg1Dc3
}
HetznerDatacenter::Helsinki1Dc2 => {
common::HetznerDatacenter::Helsinki1Dc2
}
HetznerDatacenter::Falkenstein1Dc14 => {
common::HetznerDatacenter::Falkenstein1Dc14
}
HetznerDatacenter::AshburnDc1 => {
common::HetznerDatacenter::AshburnDc1
}
HetznerDatacenter::HillsboroDc1 => {
common::HetznerDatacenter::HillsboroDc1
}
HetznerDatacenter::SingaporeDc1 => {
common::HetznerDatacenter::SingaporeDc1
}
}
}
fn hetzner_server_type(
server_type: HetznerServerType,
) -> common::HetznerServerType {
match server_type {
HetznerServerType::SharedAmd2Core2Ram40Disk => {
common::HetznerServerType::SharedAmd2Core2Ram40Disk
}
HetznerServerType::SharedArm2Core4Ram40Disk => {
common::HetznerServerType::SharedArm2Core4Ram40Disk
}
HetznerServerType::SharedIntel2Core4Ram40Disk => {
common::HetznerServerType::SharedIntel2Core4Ram40Disk
}
HetznerServerType::SharedAmd3Core4Ram80Disk => {
common::HetznerServerType::SharedAmd3Core4Ram80Disk
}
HetznerServerType::SharedArm4Core8Ram80Disk => {
common::HetznerServerType::SharedArm4Core8Ram80Disk
}
HetznerServerType::SharedIntel4Core8Ram80Disk => {
common::HetznerServerType::SharedIntel4Core8Ram80Disk
}
HetznerServerType::SharedAmd4Core8Ram160Disk => {
common::HetznerServerType::SharedAmd4Core8Ram160Disk
}
HetznerServerType::SharedArm8Core16Ram160Disk => {
common::HetznerServerType::SharedArm8Core16Ram160Disk
}
HetznerServerType::SharedIntel8Core16Ram160Disk => {
common::HetznerServerType::SharedIntel8Core16Ram160Disk
}
HetznerServerType::SharedAmd8Core16Ram240Disk => {
common::HetznerServerType::SharedAmd8Core16Ram240Disk
}
HetznerServerType::SharedArm16Core32Ram320Disk => {
common::HetznerServerType::SharedArm16Core32Ram320Disk
}
HetznerServerType::SharedIntel16Core32Ram320Disk => {
common::HetznerServerType::SharedIntel16Core32Ram320Disk
}
HetznerServerType::SharedAmd16Core32Ram360Disk => {
common::HetznerServerType::SharedAmd16Core32Ram360Disk
}
HetznerServerType::DedicatedAmd2Core8Ram80Disk => {
common::HetznerServerType::DedicatedAmd2Core8Ram80Disk
}
HetznerServerType::DedicatedAmd4Core16Ram160Disk => {
common::HetznerServerType::DedicatedAmd4Core16Ram160Disk
}
HetznerServerType::DedicatedAmd8Core32Ram240Disk => {
common::HetznerServerType::DedicatedAmd8Core32Ram240Disk
}
HetznerServerType::DedicatedAmd16Core64Ram360Disk => {
common::HetznerServerType::DedicatedAmd16Core64Ram360Disk
}
HetznerServerType::DedicatedAmd32Core128Ram600Disk => {
common::HetznerServerType::DedicatedAmd32Core128Ram600Disk
}
HetznerServerType::DedicatedAmd48Core192Ram960Disk => {
common::HetznerServerType::DedicatedAmd48Core192Ram960Disk
}
}
}
+4 -3
View File
@@ -1,9 +1,10 @@
pub mod aws;
#[allow(unused)]
pub mod hetzner;
#[derive(Debug)]
pub enum BuildCleanupData {
/// Nothing to clean up
Server,
/// Clean up AWS instance
Server { repo_name: String },
Aws { instance_id: String, region: String },
}
+6 -9
View File
@@ -8,7 +8,7 @@ use komodo_client::entities::{
config::core::{
AwsCredentials, CoreConfig, DatabaseConfig, Env,
GithubWebhookAppConfig, GithubWebhookAppInstallationConfig,
OauthCredentials,
HetznerCredentials, OauthCredentials,
},
logger::LogConfig,
};
@@ -120,6 +120,11 @@ pub fn core_config() -> &'static CoreConfig {
.komodo_aws_secret_access_key)
.unwrap_or(config.aws.secret_access_key),
},
hetzner: HetznerCredentials {
token: maybe_read_item_from_file(env.komodo_hetzner_token_file, env
.komodo_hetzner_token)
.unwrap_or(config.hetzner.token),
},
github_webhook_app: GithubWebhookAppConfig {
app_id: maybe_read_item_from_file(env.komodo_github_webhook_app_app_id_file, env
.komodo_github_webhook_app_app_id)
@@ -134,8 +139,6 @@ pub fn core_config() -> &'static CoreConfig {
title: env.komodo_title.unwrap_or(config.title),
host: env.komodo_host.unwrap_or(config.host),
port: env.komodo_port.unwrap_or(config.port),
bind_ip: env.komodo_bind_ip.unwrap_or(config.bind_ip),
timezone: env.komodo_timezone.unwrap_or(config.timezone),
first_server: env.komodo_first_server.unwrap_or(config.first_server),
frontend_path: env.komodo_frontend_path.unwrap_or(config.frontend_path),
jwt_ttl: env
@@ -173,16 +176,12 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.ui_write_disabled),
disable_confirm_dialog: env.komodo_disable_confirm_dialog
.unwrap_or(config.disable_confirm_dialog),
disable_websocket_reconnect: env.komodo_disable_websocket_reconnect
.unwrap_or(config.disable_websocket_reconnect),
enable_new_users: env.komodo_enable_new_users
.unwrap_or(config.enable_new_users),
disable_user_registration: env.komodo_disable_user_registration
.unwrap_or(config.disable_user_registration),
disable_non_admin_create: env.komodo_disable_non_admin_create
.unwrap_or(config.disable_non_admin_create),
lock_login_credentials_for: env.komodo_lock_login_credentials_for
.unwrap_or(config.lock_login_credentials_for),
local_auth: env.komodo_local_auth
.unwrap_or(config.local_auth),
logging: LogConfig {
@@ -192,7 +191,6 @@ pub fn core_config() -> &'static CoreConfig {
stdio: env
.komodo_logging_stdio
.unwrap_or(config.logging.stdio),
pretty: env.komodo_logging_pretty.unwrap_or(config.logging.pretty),
otlp_endpoint: env
.komodo_logging_otlp_endpoint
.unwrap_or(config.logging.otlp_endpoint),
@@ -200,7 +198,6 @@ pub fn core_config() -> &'static CoreConfig {
.komodo_logging_opentelemetry_service_name
.unwrap_or(config.logging.opentelemetry_service_name),
},
pretty_startup_config: env.komodo_pretty_startup_config.unwrap_or(config.pretty_startup_config),
ssl_enabled: env.komodo_ssl_enabled.unwrap_or(config.ssl_enabled),
ssl_key_file: env.komodo_ssl_key_file.unwrap_or(config.ssl_key_file),
ssl_cert_file: env.komodo_ssl_cert_file.unwrap_or(config.ssl_cert_file),
+5 -3
View File
@@ -12,6 +12,7 @@ use komodo_client::entities::{
provider::{DockerRegistryAccount, GitProviderAccount},
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
stats::SystemStatsRecord,
sync::ResourceSync,
@@ -49,6 +50,7 @@ pub struct DbClient {
pub procedures: Collection<Procedure>,
pub actions: Collection<Action>,
pub alerters: Collection<Alerter>,
pub server_templates: Collection<ServerTemplate>,
pub resource_syncs: Collection<ResourceSync>,
pub stacks: Collection<Stack>,
//
@@ -87,9 +89,7 @@ impl DbClient {
client = client.address(address);
}
_ => {
error!(
"config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?"
);
error!("config.mongo not configured correctly. must pass either config.mongo.uri, or config.mongo.address + config.mongo.username? + config.mongo.password?");
std::process::exit(1)
}
}
@@ -118,6 +118,8 @@ impl DbClient {
alerters: resource_collection(&db, "Alerter").await?,
procedures: resource_collection(&db, "Procedure").await?,
actions: resource_collection(&db, "Action").await?,
server_templates: resource_collection(&db, "ServerTemplate")
.await?,
resource_syncs: resource_collection(&db, "ResourceSync")
.await?,
stacks: resource_collection(&db, "Stack").await?,
+2 -2
View File
@@ -84,8 +84,8 @@ pub struct UpdateGuard<'a, States: Default + Send + 'static>(
&'a Mutex<States>,
);
impl<States: Default + Send + 'static> Drop
for UpdateGuard<'_, States>
impl<'a, States: Default + Send + 'static> Drop
for UpdateGuard<'a, States>
{
fn drop(&mut self) {
let mut lock = match self.0.lock() {
-73
View File
@@ -1,73 +0,0 @@
use std::collections::HashMap;
use komodo_client::entities::{
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, stack::Stack, sync::ResourceSync,
};
#[derive(Debug, Default)]
pub struct AllResourcesById {
pub servers: HashMap<String, Server>,
pub deployments: HashMap<String, Deployment>,
pub stacks: HashMap<String, Stack>,
pub builds: HashMap<String, Build>,
pub repos: HashMap<String, Repo>,
pub procedures: HashMap<String, Procedure>,
pub actions: HashMap<String, Action>,
pub builders: HashMap<String, Builder>,
pub alerters: HashMap<String, Alerter>,
pub syncs: HashMap<String, ResourceSync>,
}
impl AllResourcesById {
/// Use `match_tags` to filter resources by tag.
pub async fn load() -> anyhow::Result<Self> {
let map = HashMap::new();
let id_to_tags = &map;
let match_tags = &[];
Ok(Self {
servers: crate::resource::get_id_to_resource_map::<Server>(
id_to_tags, match_tags,
)
.await?,
deployments: crate::resource::get_id_to_resource_map::<
Deployment,
>(id_to_tags, match_tags)
.await?,
builds: crate::resource::get_id_to_resource_map::<Build>(
id_to_tags, match_tags,
)
.await?,
repos: crate::resource::get_id_to_resource_map::<Repo>(
id_to_tags, match_tags,
)
.await?,
procedures:
crate::resource::get_id_to_resource_map::<Procedure>(
id_to_tags, match_tags,
)
.await?,
actions: crate::resource::get_id_to_resource_map::<Action>(
id_to_tags, match_tags,
)
.await?,
builders: crate::resource::get_id_to_resource_map::<Builder>(
id_to_tags, match_tags,
)
.await?,
alerters: crate::resource::get_id_to_resource_map::<Alerter>(
id_to_tags, match_tags,
)
.await?,
syncs: crate::resource::get_id_to_resource_map::<ResourceSync>(
id_to_tags, match_tags,
)
.await?,
stacks: crate::resource::get_id_to_resource_map::<Stack>(
id_to_tags, match_tags,
)
.await?,
})
}
}
+30 -13
View File
@@ -1,26 +1,27 @@
use std::time::Duration;
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use formatting::muted;
use komodo_client::entities::{
Version,
builder::{AwsBuilderConfig, Builder, BuilderConfig},
komodo_timestamp,
server::Server,
server_template::aws::AwsServerTemplateConfig,
update::{Log, Update},
Version,
};
use periphery_client::{
PeripheryClient,
api::{self, GetVersionResponse},
PeripheryClient,
};
use crate::{
cloud::{
BuildCleanupData,
aws::ec2::{
Ec2Instance, launch_ec2_instance,
terminate_ec2_instance_with_retry,
launch_ec2_instance, terminate_ec2_instance_with_retry,
Ec2Instance,
},
BuildCleanupData,
},
config::core_config,
helpers::update::update_update,
@@ -60,7 +61,12 @@ pub async fn get_builder_periphery(
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((periphery, BuildCleanupData::Server))
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
@@ -68,7 +74,12 @@ pub async fn get_builder_periphery(
}
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server)?;
Ok((periphery, BuildCleanupData::Server))
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
@@ -87,8 +98,11 @@ async fn get_aws_builder(
let version = version.map(|v| format!("-v{v}")).unwrap_or_default();
let instance_name = format!("BUILDER-{resource_name}{version}");
let Ec2Instance { instance_id, ip } =
launch_ec2_instance(&instance_name, &config).await?;
let Ec2Instance { instance_id, ip } = launch_ec2_instance(
&instance_name,
AwsServerTemplateConfig::from_builder_config(&config),
)
.await?;
info!("ec2 instance launched");
@@ -165,14 +179,17 @@ async fn get_aws_builder(
)
}
#[instrument(skip(update))]
#[instrument(skip(periphery, update))]
pub async fn cleanup_builder_instance(
periphery: PeripheryClient,
cleanup_data: BuildCleanupData,
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server => {
// Nothing to clean up
BuildCleanupData::Server { repo_name } => {
let _ = periphery
.request(api::git::DeleteRepo { name: repo_name })
.await;
}
BuildCleanupData::Aws {
instance_id,
+6 -6
View File
@@ -9,9 +9,9 @@ pub struct Cache<K: PartialEq + Eq + Hash, T: Clone + Default> {
}
impl<
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default,
> Cache<K, T>
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default,
> Cache<K, T>
{
#[instrument(level = "debug", skip(self))]
pub async fn get(&self, key: &K) -> Option<T> {
@@ -70,9 +70,9 @@ impl<
}
impl<
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default + Busy,
> Cache<K, T>
K: PartialEq + Eq + Hash + std::fmt::Debug + Clone,
T: Clone + Default + Busy,
> Cache<K, T>
{
#[instrument(level = "debug", skip(self))]
pub async fn busy(&self, id: &K) -> bool {
+5 -5
View File
@@ -1,11 +1,11 @@
use std::sync::OnceLock;
use komodo_client::entities::update::{Update, UpdateListItem};
use tokio::sync::{Mutex, broadcast};
use tokio::sync::{broadcast, Mutex};
/// A channel sending (build_id, update_id)
pub fn build_cancel_channel()
-> &'static BroadcastChannel<(String, Update)> {
pub fn build_cancel_channel(
) -> &'static BroadcastChannel<(String, Update)> {
static BUILD_CANCEL_CHANNEL: OnceLock<
BroadcastChannel<(String, Update)>,
> = OnceLock::new();
@@ -13,8 +13,8 @@ pub fn build_cancel_channel()
}
/// A channel sending (repo_id, update_id)
pub fn repo_cancel_channel()
-> &'static BroadcastChannel<(String, Update)> {
pub fn repo_cancel_channel(
) -> &'static BroadcastChannel<(String, Update)> {
static REPO_CANCEL_CHANNEL: OnceLock<
BroadcastChannel<(String, Update)>,
> = OnceLock::new();
+1 -1
View File
@@ -1,7 +1,7 @@
use std::collections::HashSet;
use anyhow::Context;
use komodo_client::entities::{SystemCommand, update::Update};
use komodo_client::entities::{update::Update, SystemCommand};
use super::query::VariablesAndSecrets;
-114
View File
@@ -1,114 +0,0 @@
use std::str::FromStr;
use anyhow::Context;
use chrono::{Datelike, Local};
use komodo_client::entities::{
DayOfWeek, MaintenanceScheduleType, MaintenanceWindow,
};
use crate::config::core_config;
/// Check if a timestamp is currently in a maintenance window, given a list of windows.
pub fn is_in_maintenance(
windows: &[MaintenanceWindow],
timestamp: i64,
) -> bool {
windows
.iter()
.any(|window| is_maintenance_window_active(window, timestamp))
}
/// Check if the current timestamp falls within this maintenance window
pub fn is_maintenance_window_active(
window: &MaintenanceWindow,
timestamp: i64,
) -> bool {
if !window.enabled {
return false;
}
let dt = chrono::DateTime::from_timestamp(timestamp / 1000, 0)
.unwrap_or_else(chrono::Utc::now);
let (local_time, local_weekday, local_date) =
match (window.timezone.as_str(), core_config().timezone.as_str())
{
("", "") => {
let local_dt = dt.with_timezone(&Local);
(local_dt.time(), local_dt.weekday(), local_dt.date_naive())
}
("", timezone) | (timezone, _) => {
let tz: chrono_tz::Tz = match timezone
.parse()
.context("Failed to parse timezone")
{
Ok(tz) => tz,
Err(e) => {
warn!(
"Failed to parse maintenance window timezone: {e:#}"
);
return false;
}
};
let local_dt = dt.with_timezone(&tz);
(local_dt.time(), local_dt.weekday(), local_dt.date_naive())
}
};
match window.schedule_type {
MaintenanceScheduleType::Daily => {
is_time_in_window(window, local_time)
}
MaintenanceScheduleType::Weekly => {
let day_of_week =
DayOfWeek::from_str(&window.day_of_week).unwrap_or_default();
convert_day_of_week(local_weekday) == day_of_week
&& is_time_in_window(window, local_time)
}
MaintenanceScheduleType::OneTime => {
// Parse the date string and check if it matches current date
if let Ok(maintenance_date) =
chrono::NaiveDate::parse_from_str(&window.date, "%Y-%m-%d")
{
local_date == maintenance_date
&& is_time_in_window(window, local_time)
} else {
false
}
}
}
}
fn is_time_in_window(
window: &MaintenanceWindow,
current_time: chrono::NaiveTime,
) -> bool {
let start_time = chrono::NaiveTime::from_hms_opt(
window.hour as u32,
window.minute as u32,
0,
)
.unwrap_or(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap());
let end_time = start_time
+ chrono::Duration::minutes(window.duration_minutes as i64);
// Handle case where maintenance window crosses midnight
if end_time < start_time {
current_time >= start_time || current_time <= end_time
} else {
current_time >= start_time && current_time <= end_time
}
}
fn convert_day_of_week(value: chrono::Weekday) -> DayOfWeek {
match value {
chrono::Weekday::Mon => DayOfWeek::Monday,
chrono::Weekday::Tue => DayOfWeek::Tuesday,
chrono::Weekday::Wed => DayOfWeek::Wednesday,
chrono::Weekday::Thu => DayOfWeek::Thursday,
chrono::Weekday::Fri => DayOfWeek::Friday,
chrono::Weekday::Sat => DayOfWeek::Saturday,
chrono::Weekday::Sun => DayOfWeek::Sunday,
}
}
-32
View File
@@ -1,32 +0,0 @@
use anyhow::Context;
pub enum Matcher<'a> {
Wildcard(wildcard::Wildcard<'a>),
Regex(regex::Regex),
}
impl<'a> Matcher<'a> {
pub fn new(pattern: &'a str) -> anyhow::Result<Self> {
if pattern.starts_with('\\') && pattern.ends_with('\\') {
let inner = &pattern[1..(pattern.len() - 1)];
let regex = regex::Regex::new(inner)
.with_context(|| format!("invalid regex. got: {inner}"))?;
Ok(Self::Regex(regex))
} else {
let wildcard = wildcard::Wildcard::new(pattern.as_bytes())
.with_context(|| {
format!("invalid wildcard. got: {pattern}")
})?;
Ok(Self::Wildcard(wildcard))
}
}
pub fn is_match(&self, source: &str) -> bool {
match self {
Matcher::Wildcard(wildcard) => {
wildcard.is_match(source.as_bytes())
}
Matcher::Regex(regex) => regex.is_match(source),
}
}
}
+172 -108
View File
@@ -1,33 +1,39 @@
use std::{fmt::Write, time::Duration};
use std::{str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use indexmap::IndexSet;
use komodo_client::entities::{
ResourceTarget,
build::Build,
permission::{
Permission, PermissionLevel, SpecificPermission, UserTarget,
use anyhow::{anyhow, Context};
use futures::future::join_all;
use komodo_client::{
api::write::{CreateBuilder, CreateServer},
entities::{
builder::{PartialBuilderConfig, PartialServerBuilderConfig},
komodo_timestamp,
permission::{Permission, PermissionLevel, UserTarget},
server::{PartialServerConfig, Server},
sync::ResourceSync,
update::Log,
user::{system_user, User},
ResourceTarget,
},
repo::Repo,
server::Server,
stack::Stack,
user::User,
};
use mongo_indexed::Document;
use mungos::mongodb::bson::{Bson, doc};
use mungos::{
find::find_collect,
mongodb::bson::{doc, oid::ObjectId, to_document, Bson},
};
use periphery_client::PeripheryClient;
use rand::Rng;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use resolver_api::Resolve;
use crate::{config::core_config, state::db_client};
use crate::{
api::write::WriteArgs, config::core_config, resource,
state::db_client,
};
pub mod action_state;
pub mod all_resources;
pub mod builder;
pub mod cache;
pub mod channel;
pub mod interpolate;
pub mod maintenance;
pub mod matcher;
pub mod procedure;
pub mod prune;
pub mod query;
@@ -48,8 +54,8 @@ pub fn empty_or_only_spaces(word: &str) -> bool {
}
pub fn random_string(length: usize) -> String {
rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
thread_rng()
.sample_iter(&Alphanumeric)
.take(length)
.map(char::from)
.collect()
@@ -72,9 +78,6 @@ pub async fn git_token(
account_username: &str,
mut on_https_found: impl FnMut(bool),
) -> anyhow::Result<Option<String>> {
if provider_domain.is_empty() || account_username.is_empty() {
return Ok(None);
}
let db_provider = db_client()
.git_accounts
.find_one(doc! { "domain": provider_domain, "username": account_username })
@@ -100,70 +103,6 @@ pub async fn git_token(
)
}
pub async fn stack_git_token(
stack: &mut Stack,
repo: Option<&mut Repo>,
) -> anyhow::Result<Option<String>> {
if let Some(repo) = repo {
return git_token(
&repo.config.git_provider,
&repo.config.git_account,
|https| repo.config.git_https = https,
)
.await
.with_context(|| {
format!(
"Failed to get git token. Stopping run. | {} | {}",
repo.config.git_provider, repo.config.git_account
)
});
}
git_token(
&stack.config.git_provider,
&stack.config.git_account,
|https| stack.config.git_https = https,
)
.await
.with_context(|| {
format!(
"Failed to get git token. Stopping run. | {} | {}",
stack.config.git_provider, stack.config.git_account
)
})
}
pub async fn build_git_token(
build: &mut Build,
repo: Option<&mut Repo>,
) -> anyhow::Result<Option<String>> {
if let Some(repo) = repo {
return git_token(
&repo.config.git_provider,
&repo.config.git_account,
|https| repo.config.git_https = https,
)
.await
.with_context(|| {
format!(
"Failed to get git token. Stopping run. | {} | {}",
repo.config.git_provider, repo.config.git_account
)
});
}
git_token(
&build.config.git_provider,
&build.config.git_account,
|https| build.config.git_https = https,
)
.await
.with_context(|| {
format!(
"Failed to get git token. Stopping run. | {} | {}",
build.config.git_provider, build.config.git_account
)
})
}
/// First checks db for token, then checks core config.
/// Only errors if db call errors.
pub async fn registry_token(
@@ -204,11 +143,7 @@ pub fn periphery_client(
let client = PeripheryClient::new(
&server.config.address,
if server.config.passkey.is_empty() {
&core_config().passkey
} else {
&server.config.passkey
},
&core_config().passkey,
Duration::from_secs(server.config.timeout_seconds as u64),
);
@@ -220,7 +155,6 @@ pub async fn create_permission<T>(
user: &User,
target: T,
level: PermissionLevel,
specific: IndexSet<SpecificPermission>,
) where
T: Into<ResourceTarget> + std::fmt::Debug,
{
@@ -236,7 +170,6 @@ pub async fn create_permission<T>(
user_target: UserTarget::User(user.id.clone()),
resource_target: target.clone(),
level,
specific,
})
.await
{
@@ -264,20 +197,151 @@ pub fn flatten_document(doc: Document) -> Document {
target
}
pub fn repo_link(
provider: &str,
repo: &str,
branch: &str,
https: bool,
) -> String {
let mut res = format!(
"http{}://{provider}/{repo}",
if https { "s" } else { "" }
pub async fn startup_cleanup() {
tokio::join!(
startup_in_progress_update_cleanup(),
startup_open_alert_cleanup(),
);
// Each provider uses a different link format to get to branches.
// At least can support github for branch aware link.
if provider == "github.com" {
let _ = write!(&mut res, "/tree/{branch}");
}
/// Run on startup, as no updates should be in progress on startup
async fn startup_in_progress_update_cleanup() {
let log = Log::error(
"Komodo shutdown",
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.
let log = to_document(&log).unwrap();
if let Err(e) = db_client()
.updates
.update_many(
doc! { "status": "InProgress" },
doc! {
"$set": {
"status": "Complete",
"success": false,
},
"$push": {
"logs": log
}
},
)
.await
{
error!("failed to cleanup in progress updates on startup | {e:#}")
}
res
}
/// Run on startup, ensure open alerts pointing to invalid resources are closed.
async fn startup_open_alert_cleanup() {
let db = db_client();
let Ok(alerts) =
find_collect(&db.alerts, doc! { "resolved": false }, None)
.await
.inspect_err(|e| {
error!(
"failed to list all alerts for startup open alert cleanup | {e:?}"
)
})
else {
return;
};
let futures = alerts.into_iter().map(|alert| async move {
match alert.target {
ResourceTarget::Server(id) => {
resource::get::<Server>(&id)
.await
.is_err()
.then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok()).flatten()
}
ResourceTarget::ResourceSync(id) => {
resource::get::<ResourceSync>(&id)
.await
.is_err()
.then(|| ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok()).flatten()
}
// No other resources should have open alerts.
_ => ObjectId::from_str(&alert.id).inspect_err(|e| warn!("failed to clean up alert - id is invalid ObjectId | {e:?}")).ok(),
}
});
let to_update_ids = join_all(futures)
.await
.into_iter()
.flatten()
.collect::<Vec<_>>();
if let Err(e) = db
.alerts
.update_many(
doc! { "_id": { "$in": to_update_ids } },
doc! { "$set": {
"resolved": true,
"resolved_ts": komodo_timestamp()
} },
)
.await
{
error!(
"failed to clean up invalid open alerts on startup | {e:#}"
)
}
}
/// Ensures a default server / builder exists with the defined address
pub async fn ensure_first_server_and_builder() {
let first_server = &core_config().first_server;
if first_server.is_empty() {
return;
}
let db = db_client();
let Ok(server) = db
.servers
.find_one(Document::new())
.await
.inspect_err(|e| error!("Failed to initialize 'first_server'. Failed to query db. {e:?}"))
else {
return;
};
let server = if let Some(server) = server {
server
} else {
match (CreateServer {
name: format!("server-{}", random_string(5)),
config: PartialServerConfig {
address: Some(first_server.to_string()),
enabled: Some(true),
..Default::default()
},
})
.resolve(&WriteArgs {
user: system_user().to_owned(),
})
.await
{
Ok(server) => server,
Err(e) => {
error!("Failed to initialize 'first_server'. Failed to CreateServer. {:#}", e.error);
return;
}
}
};
let Ok(None) = db.builders
.find_one(Document::new()).await
.inspect_err(|e| error!("Failed to initialize 'first_builder' | Failed to query db | {e:?}")) else {
return;
};
if let Err(e) = (CreateBuilder {
name: String::from("local"),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id),
},
),
})
.resolve(&WriteArgs {
user: system_user().to_owned(),
})
.await
{
error!("Failed to initialize 'first_builder' | Failed to CreateBuilder | {:#}", e.error);
}
}
+6 -31
View File
@@ -1,7 +1,7 @@
use std::time::{Duration, Instant};
use anyhow::{Context, anyhow};
use formatting::{Color, bold, colored, format_serror, muted};
use anyhow::{anyhow, Context};
use formatting::{bold, colored, format_serror, muted, Color};
use futures::future::join_all;
use komodo_client::{
api::execute::*,
@@ -9,7 +9,6 @@ use komodo_client::{
action::Action,
build::Build,
deployment::Deployment,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
stack::Stack,
@@ -26,7 +25,7 @@ use crate::{
execute::{ExecuteArgs, ExecuteRequest},
write::WriteArgs,
},
resource::{KomodoResource, list_full_for_user_using_pattern},
resource::{list_full_for_user_using_pattern, KomodoResource},
state::db_client,
};
@@ -167,13 +166,6 @@ async fn execute_stage(
)
.await?;
}
Execution::BatchPullStack(exec) => {
extend_batch_exection::<BatchPullStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyStack(exec) => {
extend_batch_exection::<BatchDestroyStack>(
&exec.pattern,
@@ -214,7 +206,7 @@ async fn execute_stage(
join_all(futures)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
.collect::<anyhow::Result<_>>()?;
Ok(())
}
@@ -993,12 +985,6 @@ async fn execute_execution(
)
.await?
}
Execution::BatchPullStack(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchPullStack not implemented correctly"
));
}
Execution::StartStack(req) => {
let req = ExecuteRequest::StartStack(req);
let update = init_execution_update(&req, &user).await?;
@@ -1190,7 +1176,6 @@ async fn extend_batch_exection<E: ExtendBatch>(
pattern,
Default::default(),
procedure_user(),
PermissionLevel::Read.into(),
&[],
)
.await?
@@ -1274,7 +1259,7 @@ impl ExtendBatch for BatchDeployStack {
fn single_execution(stack: String) -> Execution {
Execution::DeployStack(DeployStack {
stack,
services: Vec::new(),
service: None,
stop_time: None,
})
}
@@ -1290,22 +1275,12 @@ impl ExtendBatch for BatchDeployStackIfChanged {
}
}
impl ExtendBatch for BatchPullStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::PullStack(PullStack {
stack,
services: Vec::new(),
})
}
}
impl ExtendBatch for BatchDestroyStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DestroyStack(DestroyStack {
stack,
services: Vec::new(),
service: None,
remove_orphans: false,
stop_time: None,
})

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