forked from github-starred/komodo
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
118ae9b92c | ||
|
|
2205a81e79 | ||
|
|
e2280f38df | ||
|
|
545196d7eb | ||
|
|
23f8ecc1d9 | ||
|
|
4d401d7f20 | ||
|
|
4165e25332 | ||
|
|
4cc0817b0f | ||
|
|
51cf1e2b05 | ||
|
|
5309c70929 | ||
|
|
1278c62859 | ||
|
|
6d6acdbc0b | ||
|
|
d22000331e | ||
|
|
31034e5b34 | ||
|
|
a43e1f3f52 | ||
|
|
7a3b2b542d | ||
|
|
8d516d6d5f | ||
|
|
3e0d1befbd | ||
|
|
5dc609b206 | ||
|
|
f1127007c3 | ||
|
|
765e5a0df1 | ||
|
|
76f2f61be5 | ||
|
|
b43e2918da | ||
|
|
f45205011e | ||
|
|
5b211fb8f0 | ||
|
|
3f04614303 | ||
|
|
4c3210f70a | ||
|
|
2c37ac26a5 | ||
|
|
db1cf786ac | ||
|
|
9c841e5bdc |
@@ -23,7 +23,7 @@ services:
|
||||
|
||||
db:
|
||||
extends:
|
||||
file: ../test.compose.yaml
|
||||
file: ../dev.compose.yaml
|
||||
service: ferretdb
|
||||
|
||||
volumes:
|
||||
|
||||
Generated
+1087
-790
File diff suppressed because it is too large
Load Diff
+48
-33
@@ -8,8 +8,8 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.17.0-dev-4"
|
||||
edition = "2021"
|
||||
version = "1.18.4"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/moghtech/komodo"
|
||||
@@ -20,6 +20,8 @@ homepage = "https://komo.do"
|
||||
komodo_client = { path = "client/core/rs" }
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
environment_file = { path = "lib/environment_file" }
|
||||
environment = { path = "lib/environment" }
|
||||
interpolate = { path = "lib/interpolate" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
response = { path = "lib/response" }
|
||||
command = { path = "lib/command" }
|
||||
@@ -30,7 +32,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.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
merge_config_files = "0.1.5"
|
||||
@@ -41,76 +43,89 @@ mongo_indexed = "2.0.1"
|
||||
resolver_api = "3.0.0"
|
||||
toml_pretty = "1.1.2"
|
||||
mungos = "3.2.0"
|
||||
svi = "1.0.1"
|
||||
svi = "1.1.0"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls-native-roots"] }
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
tokio-util = "0.7.13"
|
||||
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"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
arc-swap = "1.7.1"
|
||||
|
||||
# SERVER
|
||||
axum-extra = { version = "0.10.0", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
|
||||
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
|
||||
tokio-tungstenite = "0.26.2"
|
||||
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"] }
|
||||
|
||||
# SER/DE
|
||||
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
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"
|
||||
serde_yaml = "0.9.34"
|
||||
toml = "0.8.20"
|
||||
serde_qs = "0.15.0"
|
||||
toml = "0.8.23"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.97"
|
||||
anyhow = "1.0.98"
|
||||
thiserror = "2.0.12"
|
||||
|
||||
# LOGGING
|
||||
opentelemetry-otlp = { version = "0.28.0", features = ["tls-roots", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.28.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.30.0", features = ["tls-roots", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.19", features = ["json"] }
|
||||
opentelemetry-semantic-conventions = "0.28.0"
|
||||
tracing-opentelemetry = "0.29.0"
|
||||
opentelemetry = "0.28.0"
|
||||
opentelemetry-semantic-conventions = "0.30.0"
|
||||
tracing-opentelemetry = "0.31.0"
|
||||
opentelemetry = "0.30.0"
|
||||
tracing = "0.1.41"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.31", features = ["derive"] }
|
||||
clap = { version = "4.5.40", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.15.1", features = ["v4", "fast-rng", "serde"] }
|
||||
uuid = { version = "1.17.0", features = ["v4", "fast-rng", "serde"] }
|
||||
jsonwebtoken = { version = "9.3.1", default-features = false }
|
||||
openidconnect = "4.0.0"
|
||||
urlencoding = "2.1.3"
|
||||
nom_pem = "4.0.0"
|
||||
bcrypt = "0.17.0"
|
||||
base64 = "0.22.1"
|
||||
rustls = "0.23.23"
|
||||
rustls = "0.23.27"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
rand = "0.9.0"
|
||||
jwt = "0.16.0"
|
||||
sha2 = "0.10.9"
|
||||
rand = "0.9.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
bollard = "0.18.1"
|
||||
sysinfo = "0.33.1"
|
||||
portable-pty = "0.9.0"
|
||||
bollard = "0.19.1"
|
||||
sysinfo = "0.35.2"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.5.17"
|
||||
aws-sdk-ec2 = "1.114.0"
|
||||
aws-config = "1.8.0"
|
||||
aws-sdk-ec2 = "1.139.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"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.2"
|
||||
typeshare = "1.0.4"
|
||||
octorust = "0.9.0"
|
||||
octorust = "0.10.0"
|
||||
dashmap = "6.1.0"
|
||||
wildcard = "0.3.0"
|
||||
colored = "3.0.0"
|
||||
regex = "1.11.1"
|
||||
bson = "2.13.0"
|
||||
bytes = "1.10.1"
|
||||
bson = "2.15.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Builds the Komodo Core and Periphery binaries
|
||||
## Builds the Komodo Core, Periphery, and Util binaries
|
||||
## for a specific architecture.
|
||||
|
||||
FROM rust:1.84.1-bullseye AS builder
|
||||
FROM rust:1.87.0-bullseye AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@@ -10,18 +10,21 @@ 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_periphery --release && \
|
||||
cargo build -p komodo_util --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 Periphery"
|
||||
LABEL org.opencontainers.image.description="Komodo Binaries"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
+57
-50
@@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub enum ExecutionResult {
|
||||
Single(Update),
|
||||
Single(Box<Update>),
|
||||
Batch(BatchExecutionResponse),
|
||||
}
|
||||
|
||||
@@ -185,6 +185,9 @@ 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())
|
||||
}
|
||||
@@ -224,7 +227,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::RunAction(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchRunAction(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -232,7 +235,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::RunProcedure(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchRunProcedure(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -240,7 +243,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::RunBuild(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchRunBuild(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -248,11 +251,11 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::CancelBuild(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::Deploy(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDeploy(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -260,31 +263,31 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::PullDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StartDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DestroyDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDestroyDeployment(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -292,7 +295,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::CloneRepo(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchCloneRepo(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -300,7 +303,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::PullRepo(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchPullRepo(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -308,7 +311,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::BuildRepo(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchBuildRepo(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -316,103 +319,103 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::CancelRepoBuild(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StartContainer(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartContainer(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseContainer(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseContainer(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopContainer(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DestroyContainer(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StartAllContainers(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartAllContainers(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseAllContainers(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseAllContainers(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopAllContainers(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneContainers(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeleteNetwork(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneNetworks(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeleteImage(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneImages(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeleteVolume(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneVolumes(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneDockerBuilders(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneBuildx(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PruneSystem(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RunSync(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::CommitSync(request) => komodo_client()
|
||||
.write(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DeployStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDeployStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -420,7 +423,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::DeployStackIfChanged(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDeployStackIfChanged(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -428,31 +431,35 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::PullStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchPullStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Batch),
|
||||
Execution::StartStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RestartStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::PauseStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::UnpauseStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::StopStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::DestroyStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::BatchDestroyStack(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
@@ -460,7 +467,7 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::TestAlerter(request) => komodo_client()
|
||||
.execute(request)
|
||||
.await
|
||||
.map(ExecutionResult::Single),
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::Sleep(request) => {
|
||||
let duration =
|
||||
Duration::from_millis(request.duration_ms as u64);
|
||||
|
||||
+11
-3
@@ -18,6 +18,7 @@ path = "src/main.rs"
|
||||
komodo_client = { workspace = true, features = ["mongo"] }
|
||||
periphery_client.workspace = true
|
||||
environment_file.workspace = true
|
||||
interpolate.workspace = true
|
||||
formatting.workspace = true
|
||||
response.workspace = true
|
||||
command.workspace = true
|
||||
@@ -37,9 +38,12 @@ mungos.workspace = true
|
||||
slack.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
axum-server.workspace = true
|
||||
ordered_hash_map.workspace = true
|
||||
aws-credential-types.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
openidconnect.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
axum-server.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
aws-config.workspace = true
|
||||
@@ -49,8 +53,11 @@ 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
|
||||
@@ -58,6 +65,8 @@ 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
|
||||
@@ -71,5 +80,4 @@ envy.workspace = true
|
||||
rand.workspace = true
|
||||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
jwt.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
# Build Core
|
||||
FROM rust:1.84.1-bullseye AS core-builder
|
||||
FROM rust:1.87.0-bullseye AS core-builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@@ -24,10 +24,9 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Setup an application directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/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
|
||||
|
||||
@@ -15,10 +15,9 @@ FROM ${FRONTEND_IMAGE} AS frontend
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && \
|
||||
apt install -y git ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ./bin/core/starship.toml /config/starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
RUN sh ./debian-deps.sh && rm ./debian-deps.sh
|
||||
|
||||
# Copy
|
||||
COPY ./config/core.config.toml /config/config.toml
|
||||
|
||||
@@ -94,7 +94,9 @@ 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,
|
||||
@@ -104,7 +106,9 @@ 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,
|
||||
@@ -114,7 +118,9 @@ 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,
|
||||
@@ -126,7 +132,9 @@ 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,
|
||||
@@ -137,7 +145,9 @@ 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,
|
||||
@@ -150,13 +160,17 @@ 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 =
|
||||
@@ -167,16 +181,58 @@ 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() {
|
||||
send_message(url, &content).await?;
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
send_message(&url_interpolated, &content)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let replacers = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {}",
|
||||
sanitized_error
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+51
-22
@@ -1,23 +1,28 @@
|
||||
use std::collections::HashSet;
|
||||
use ::slack::types::Block;
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use derive_variants::ExtractVariant;
|
||||
use futures::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::entities::{
|
||||
ResourceTargetVariant,
|
||||
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
|
||||
alerter::*,
|
||||
deployment::DeploymentState,
|
||||
komodo_timestamp,
|
||||
stack::StackState,
|
||||
ResourceTargetVariant,
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
use crate::helpers::interpolate::interpolate_variables_secrets_into_string;
|
||||
use crate::helpers::query::get_variables_and_secrets;
|
||||
use crate::helpers::{
|
||||
maintenance::is_in_maintenance, query::VariablesAndSecrets,
|
||||
};
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
mod discord;
|
||||
mod ntfy;
|
||||
mod pushover;
|
||||
mod slack;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
@@ -78,6 +83,13 @@ 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
|
||||
@@ -128,6 +140,24 @@ 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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,19 +166,14 @@ async fn send_custom_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
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 mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
let res = reqwest::Client::new()
|
||||
.post(url_interpolated)
|
||||
@@ -156,9 +181,16 @@ 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 = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with request: {}",
|
||||
sanitized_error
|
||||
))
|
||||
})
|
||||
.context("failed at post request to alerter")?;
|
||||
let status = res.status();
|
||||
@@ -238,9 +270,6 @@ fn resource_link(
|
||||
ResourceTargetVariant::Action => {
|
||||
format!("/actions/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ServerTemplate => {
|
||||
format!("/server-templates/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ResourceSync => {
|
||||
format!("/resource-syncs/{id}")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let level = fmt_level(alert.level);
|
||||
let content = match &alert.data {
|
||||
AlertData::Test { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Alerter, id);
|
||||
format!(
|
||||
"{level} | If you see this message, then Alerter {} is working\n{link}",
|
||||
name,
|
||||
)
|
||||
}
|
||||
AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | {}{} is now reachable\n{link}",
|
||||
name, region
|
||||
)
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {:#?}", e))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{level} | {}{} is unreachable ❌\n{link}{err}",
|
||||
name, region
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
format!(
|
||||
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
|
||||
name, region,
|
||||
)
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
|
||||
name, region,
|
||||
)
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
|
||||
name, region, path,
|
||||
)
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let to_state = fmt_docker_container_state(to);
|
||||
format!(
|
||||
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
|
||||
name, to_state, server_name, from,
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
format!(
|
||||
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
|
||||
name, server_name, image,
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
format!(
|
||||
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
|
||||
name, server_name, image,
|
||||
)
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let to_state = fmt_stack_state(to);
|
||||
format!(
|
||||
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
|
||||
name, to_state, server_name, from,
|
||||
)
|
||||
}
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
format!(
|
||||
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
|
||||
name, server_name, service, image,
|
||||
)
|
||||
}
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images_str = images.join(", ");
|
||||
format!(
|
||||
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
|
||||
name, server_name, images_label, images_str,
|
||||
)
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
instance_id,
|
||||
message,
|
||||
} => {
|
||||
format!(
|
||||
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
|
||||
instance_id, message,
|
||||
)
|
||||
}
|
||||
AlertData::ResourceSyncPendingUpdates { id, name } => {
|
||||
let link =
|
||||
resource_link(ResourceTargetVariant::ResourceSync, id);
|
||||
format!(
|
||||
"{level} | Pending resource sync updates on {}\n{link}",
|
||||
name,
|
||||
)
|
||||
}
|
||||
AlertData::BuildFailed { id, name, version } => {
|
||||
let link = resource_link(ResourceTargetVariant::Build, id);
|
||||
format!(
|
||||
"{level} | Build {name} failed\nversion: v{version}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::RepoBuildFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Repo, id);
|
||||
format!("{level} | Repo build for {} failed\n{link}", name,)
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Procedure, id);
|
||||
format!("{level} | Procedure {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Action, id);
|
||||
format!("{level} | Action {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let link = resource_link(*resource_type, id);
|
||||
format!(
|
||||
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
|
||||
if !content.is_empty() {
|
||||
send_message(url, content).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
url: &str,
|
||||
content: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// pushover needs all information to be encoded in the URL. At minimum they need
|
||||
// the user key, the application token, and the message (url encoded).
|
||||
// other optional params here: https://pushover.net/api (just add them to the
|
||||
// webhook url along with the application token and the user key).
|
||||
let content = [("message", content)];
|
||||
|
||||
let response = http_client()
|
||||
.post(url)
|
||||
.form(&content)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send message")?;
|
||||
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
debug!("pushover alert sent successfully: {}", status);
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send message to pushover | {} | failed to get response text",
|
||||
status
|
||||
)
|
||||
})?;
|
||||
Err(anyhow!(
|
||||
"Failed to send message to pushover | {} | {}",
|
||||
status,
|
||||
text
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
+85
-14
@@ -73,7 +73,9 @@ 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!(
|
||||
@@ -87,7 +89,9 @@ 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!(
|
||||
@@ -113,7 +117,9 @@ 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!(
|
||||
@@ -130,7 +136,9 @@ 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!(
|
||||
@@ -160,7 +168,9 @@ 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!(
|
||||
@@ -169,12 +179,17 @@ 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!(
|
||||
@@ -183,7 +198,10 @@ 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())
|
||||
}
|
||||
@@ -355,9 +373,7 @@ pub async fn send_alert(
|
||||
let text = format!("{level} | Build {name} has failed");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"build name: *{name}*\nversion: *v{version}*",
|
||||
)),
|
||||
Block::section(format!("version: *v{version}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Build,
|
||||
id,
|
||||
@@ -370,7 +386,6 @@ 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,
|
||||
@@ -378,11 +393,67 @@ 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 slack = ::slack::Client::new(url);
|
||||
slack.send_message(text, blocks).await?;
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolator.interpolate_string(&mut url_interpolated)?;
|
||||
|
||||
let slack = ::slack::Client::new(url_interpolated);
|
||||
slack.send_message(text, blocks).await.map_err(|e| {
|
||||
let replacers = interpolator
|
||||
.secret_replacers
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {}",
|
||||
sanitized_error
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+23
-13
@@ -1,11 +1,12 @@
|
||||
use std::{sync::OnceLock, time::Instant};
|
||||
|
||||
use axum::{http::HeaderMap, routing::post, Router};
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::auth::*, entities::user::User};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
@@ -15,13 +16,15 @@ use crate::{
|
||||
get_user_id_from_headers,
|
||||
github::{self, client::github_oauth_client},
|
||||
google::{self, client::google_oauth_client},
|
||||
oidc,
|
||||
oidc::{self, client::oidc_client},
|
||||
},
|
||||
config::core_config,
|
||||
helpers::query::get_user,
|
||||
state::jwt_client,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub struct AuthArgs {
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
@@ -45,7 +48,9 @@ pub enum AuthRequest {
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
let mut router = Router::new().route("/", post(handler));
|
||||
let mut router = Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler));
|
||||
|
||||
if core_config().local_auth {
|
||||
info!("🔑 Local Login Enabled");
|
||||
@@ -69,6 +74,18 @@ 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,
|
||||
@@ -97,16 +114,9 @@ fn login_options_reponse() -> &'static GetLoginOptionsResponse {
|
||||
let config = core_config();
|
||||
GetLoginOptionsResponse {
|
||||
local: config.local_auth,
|
||||
github: config.github_oauth.enabled
|
||||
&& !config.github_oauth.id.is_empty()
|
||||
&& !config.github_oauth.secret.is_empty(),
|
||||
google: config.google_oauth.enabled
|
||||
&& !config.google_oauth.id.is_empty()
|
||||
&& !config.google_oauth.secret.is_empty(),
|
||||
oidc: config.oidc_enabled
|
||||
&& !config.oidc_provider.is_empty()
|
||||
&& !config.oidc_client_id.is_empty()
|
||||
&& !config.oidc_client_secret.is_empty(),
|
||||
github: github_oauth_client().is_some(),
|
||||
google: google_oauth_client().is_some(),
|
||||
oidc: oidc_client().load().is_some(),
|
||||
registration_disabled: config.disable_user_registration,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,14 +7,20 @@ use std::{
|
||||
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_command;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
|
||||
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
|
||||
},
|
||||
entities::{
|
||||
action::Action, config::core::CoreConfig,
|
||||
permission::PermissionLevel, update::Update, user::action_user,
|
||||
action::Action,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
config::core::CoreConfig,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
update::Update,
|
||||
user::action_user,
|
||||
},
|
||||
};
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
@@ -22,18 +28,16 @@ use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::{execute::ExecuteRequest, user::UserArgs},
|
||||
config::core_config,
|
||||
helpers::{
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_string,
|
||||
},
|
||||
query::get_variables_and_secrets,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
random_string,
|
||||
update::update_update,
|
||||
},
|
||||
resource::{self, refresh_action_state_cache},
|
||||
permission::get_check_permissions,
|
||||
resource::refresh_action_state_cache,
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
@@ -65,10 +69,10 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let mut action = resource::get_check_permissions::<Action>(
|
||||
let mut action = get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -120,11 +124,28 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
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 {}", path.display()),
|
||||
format!(
|
||||
"deno run --allow-all{https_cert_flag}{reload} {}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -167,6 +188,26 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -177,28 +218,22 @@ async fn interpolate(
|
||||
key: String,
|
||||
secret: String,
|
||||
) -> serror::Result<HashSet<(String, String)>> {
|
||||
let mut vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let VariablesAndSecrets {
|
||||
variables,
|
||||
mut secrets,
|
||||
} = get_variables_and_secrets().await?;
|
||||
|
||||
vars_and_secrets
|
||||
.secrets
|
||||
.insert(String::from("ACTION_API_KEY"), key);
|
||||
vars_and_secrets
|
||||
.secrets
|
||||
.insert(String::from("ACTION_API_SECRET"), secret);
|
||||
secrets.insert(String::from("ACTION_API_KEY"), key);
|
||||
secrets.insert(String::from("ACTION_API_SECRET"), secret);
|
||||
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
contents,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
interpolator
|
||||
.interpolate_string(contents)?
|
||||
.push_logs(&mut update.logs);
|
||||
|
||||
add_interp_update_log(update, &global_replacers, &secret_replacers);
|
||||
|
||||
Ok(secret_replacers)
|
||||
Ok(interpolator.secret_replacers)
|
||||
}
|
||||
|
||||
fn full_contents(contents: &str, key: &str, secret: &str) -> String {
|
||||
@@ -208,7 +243,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 }} from '{base_url}/client/lib.js';
|
||||
"import {{ KomodoClient, Types }} from '{base_url}/client/lib.js';
|
||||
import * as __YAML__ from 'jsr:@std/yaml';
|
||||
import * as __TOML__ from 'jsr:@std/toml';
|
||||
|
||||
@@ -244,7 +279,7 @@ main()
|
||||
console.error('Status:', error.status);
|
||||
console.error(JSON.stringify(error.result, null, 2));
|
||||
}} else {{
|
||||
console.error(JSON.stringify(error, null, 2));
|
||||
console.error(error);
|
||||
}}
|
||||
Deno.exit(1)
|
||||
}});"
|
||||
@@ -306,8 +341,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;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
alert::send_alert_to_alerter, helpers::update::update_update,
|
||||
resource::get_check_permissions,
|
||||
permission::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,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
+109
-130
@@ -1,8 +1,9 @@
|
||||
use std::{collections::HashSet, future::IntoFuture, time::Duration};
|
||||
use std::{future::IntoFuture, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use futures::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
|
||||
@@ -16,6 +17,7 @@ use komodo_client::{
|
||||
deployment::DeploymentState,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
update::{Log, Update},
|
||||
user::auto_redeploy_user,
|
||||
},
|
||||
@@ -35,19 +37,17 @@ 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,
|
||||
interpolate_variables_secrets_into_string,
|
||||
interpolate_variables_secrets_into_system_command,
|
||||
query::{
|
||||
VariablesAndSecrets, get_deployment_state,
|
||||
get_variables_and_secrets,
|
||||
},
|
||||
query::{get_deployment_state, get_variables_and_secrets},
|
||||
registry_token,
|
||||
update::{init_execution_update, update_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource::{self, refresh_build_state_cache},
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
@@ -80,13 +80,33 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let mut build = resource::get_check_permissions::<Build>(
|
||||
let mut build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
let mut vars_and_secrets = get_variables_and_secrets().await?;
|
||||
|
||||
let mut repo = if !build.config.files_on_host
|
||||
&& !build.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&build.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let VariablesAndSecrets {
|
||||
mut variables,
|
||||
secrets,
|
||||
} = get_variables_and_secrets().await?;
|
||||
|
||||
// Add the $VERSION to variables. Use with [[$VERSION]]
|
||||
variables.insert(
|
||||
String::from("$VERSION"),
|
||||
build.config.version.to_string(),
|
||||
);
|
||||
|
||||
if build.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("Must attach builder to RunBuild").into());
|
||||
@@ -110,23 +130,8 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).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 git_token =
|
||||
build_git_token(&mut build, repo.as_mut()).await?;
|
||||
|
||||
let registry_token =
|
||||
validate_account_extract_registry_token(&build).await?;
|
||||
@@ -177,7 +182,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
});
|
||||
|
||||
// GET BUILDER PERIPHERY
|
||||
|
||||
let (periphery, cleanup_data) = match get_builder_periphery(
|
||||
build.name.clone(),
|
||||
Some(build.config.version),
|
||||
@@ -203,112 +207,83 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
}
|
||||
};
|
||||
|
||||
// CLONE REPO
|
||||
// INTERPOLATE VARIABLES
|
||||
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();
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolate_variables_secrets_into_system_command(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.pre_build,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
interpolator.interpolate_build(&mut build)?;
|
||||
|
||||
add_interp_update_log(
|
||||
&mut update,
|
||||
&global_replacers,
|
||||
&secret_replacers,
|
||||
);
|
||||
if let Some(repo) = repo.as_mut() {
|
||||
interpolator.interpolate_repo(repo)?;
|
||||
}
|
||||
|
||||
secret_replacers
|
||||
interpolator.push_logs(&mut update.logs);
|
||||
|
||||
interpolator.secret_replacers
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
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) {
|
||||
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 commit_message = if !build.config.files_on_host
|
||||
&& (!build.config.repo.is_empty()
|
||||
|| !build.config.linked_repo.is_empty())
|
||||
{
|
||||
// PULL OR CLONE REPO
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::git::PullOrCloneRepo {
|
||||
args: repo.as_ref().map(Into::into).unwrap_or((&build).into()),
|
||||
git_token,
|
||||
environment: Default::default(),
|
||||
env_file_path: Default::default(),
|
||||
on_clone: None,
|
||||
on_pull: None,
|
||||
skip_secret_interp: Default::default(),
|
||||
replacers: Default::default(),
|
||||
}) => res,
|
||||
_ = cancel.cancelled() => {
|
||||
debug!("build cancelled during clone, cleaning up builder");
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
|
||||
cleanup_builder_instance(cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
},
|
||||
};
|
||||
|
||||
let commit_message = match res {
|
||||
Ok(res) => {
|
||||
debug!("finished repo clone");
|
||||
update.logs.extend(res.res.logs);
|
||||
update.commit_hash =
|
||||
res.res.commit_hash.unwrap_or_default().to_string();
|
||||
res.res.commit_message.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed build at clone repo | {e:#}");
|
||||
update.push_error_log(
|
||||
"Clone Repo",
|
||||
format_serror(&e.context("Failed to clone repo").into()),
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Some(commit_message)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
// RUN BUILD
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::build::Build {
|
||||
build: build.clone(),
|
||||
repo,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
// Push a commit hash tagged image
|
||||
@@ -321,7 +296,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(periphery, cleanup_data, &mut update)
|
||||
cleanup_builder_instance(cleanup_data, &mut update)
|
||||
.await;
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
},
|
||||
@@ -365,8 +340,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
// stop the cancel listening task from going forever
|
||||
cancel.cancel();
|
||||
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
// If building on temporary cloud server (AWS),
|
||||
// this will terminate the server.
|
||||
cleanup_builder_instance(cleanup_data, &mut update).await;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
@@ -515,10 +491,10 @@ impl Resolve<ExecuteArgs> for CancelBuild {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let build = resource::get_check_permissions::<Build>(
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -560,7 +536,9 @@ 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:#}"
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -587,8 +565,9 @@ async fn handle_post_build_redeploy(build_id: &str) {
|
||||
redeploy_deployments
|
||||
.into_iter()
|
||||
.map(|deployment| async move {
|
||||
let state =
|
||||
get_deployment_state(&deployment).await.unwrap_or_default();
|
||||
let state = get_deployment_state(&deployment.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if state == DeploymentState::Running {
|
||||
let req = super::ExecuteRequest::Deploy(Deploy {
|
||||
deployment: deployment.id.clone(),
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use std::{collections::HashSet, sync::OnceLock};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use cache::TimeoutCache;
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
Version,
|
||||
build::{Build, ImageRegistryConfig},
|
||||
deployment::{
|
||||
extract_registry_domain, Deployment, DeploymentImage,
|
||||
Deployment, DeploymentImage, extract_registry_domain,
|
||||
},
|
||||
get_image_name, komodo_timestamp, optional_string,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Log, Update},
|
||||
user::User,
|
||||
Version,
|
||||
},
|
||||
};
|
||||
use periphery_client::api;
|
||||
@@ -23,17 +24,13 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_extra_args,
|
||||
interpolate_variables_secrets_into_string,
|
||||
},
|
||||
periphery_client,
|
||||
query::get_variables_and_secrets,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
registry_token,
|
||||
update::update_update,
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::action_states,
|
||||
};
|
||||
@@ -68,10 +65,10 @@ async fn setup_deployment_execution(
|
||||
deployment: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(Deployment, Server)> {
|
||||
let deployment = resource::get_check_permissions::<Deployment>(
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -179,53 +176,17 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
// interpolate variables / secrets, returning the sanitizing replacers to send to
|
||||
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
|
||||
let secret_replacers = if !deployment.config.skip_secret_interp {
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.environment,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
interpolator
|
||||
.interpolate_deployment(&mut deployment)?
|
||||
.push_logs(&mut update.logs);
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.ports,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.volumes,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_extra_args(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.extra_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut deployment.config.command,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
add_interp_update_log(
|
||||
&mut update,
|
||||
&global_replacers,
|
||||
&secret_replacers,
|
||||
);
|
||||
|
||||
secret_replacers
|
||||
interpolator.secret_replacers
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
use std::{pin::Pin, time::Instant};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use axum_extra::{headers::ContentType, TypedHeader};
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers::ContentType};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use formatting::format_serror;
|
||||
use futures::future::join_all;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
Operation,
|
||||
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;
|
||||
@@ -25,7 +29,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
auth::auth_request,
|
||||
helpers::update::{init_execution_update, update_update},
|
||||
resource::{list_full_for_user_using_pattern, KomodoResource},
|
||||
resource::{KomodoResource, list_full_for_user_using_pattern},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
@@ -36,10 +40,11 @@ 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,
|
||||
};
|
||||
@@ -82,6 +87,21 @@ 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),
|
||||
@@ -94,20 +114,6 @@ 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),
|
||||
@@ -130,9 +136,6 @@ pub enum ExecuteRequest {
|
||||
RunAction(RunAction),
|
||||
BatchRunAction(BatchRunAction),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
LaunchServer(LaunchServer),
|
||||
|
||||
// ==== ALERTER ====
|
||||
TestAlerter(TestAlerter),
|
||||
|
||||
@@ -143,9 +146,22 @@ 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>,
|
||||
@@ -158,8 +174,11 @@ async fn handler(
|
||||
Ok((TypedHeader(ContentType::json()), res))
|
||||
}
|
||||
|
||||
#[typeshare(serialized_as = "Update")]
|
||||
type BoxUpdate = Box<Update>;
|
||||
|
||||
pub enum ExecutionResult {
|
||||
Single(Update),
|
||||
Single(BoxUpdate),
|
||||
/// The batch contents will be pre serialized here
|
||||
Batch(String),
|
||||
}
|
||||
@@ -229,7 +248,7 @@ pub fn inner_handler(
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ExecutionResult::Single(update))
|
||||
Ok(ExecutionResult::Single(update.into()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -283,6 +302,7 @@ async fn batch_execute<E: BatchExecute>(
|
||||
pattern,
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use formatting::{bold, colored, format_serror, muted, Color};
|
||||
use formatting::{Color, bold, colored, format_serror, muted};
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
|
||||
},
|
||||
entities::{
|
||||
permission::PermissionLevel, procedure::Procedure,
|
||||
update::Update, user::User,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
update::Update,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
@@ -15,8 +19,10 @@ use resolver_api::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
helpers::{procedure::execute_procedure, update::update_update},
|
||||
resource::{self, refresh_procedure_state_cache},
|
||||
permission::get_check_permissions,
|
||||
resource::refresh_procedure_state_cache,
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
@@ -65,10 +71,10 @@ fn resolve_inner(
|
||||
>,
|
||||
> {
|
||||
Box::pin(async move {
|
||||
let procedure = resource::get_check_permissions::<Procedure>(
|
||||
let procedure = get_check_permissions::<Procedure>(
|
||||
&procedure,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -137,6 +143,26 @@ 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::{collections::HashSet, future::IntoFuture, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{execute::*, write::RefreshRepoCache},
|
||||
entities::{
|
||||
@@ -31,16 +32,11 @@ use crate::{
|
||||
helpers::{
|
||||
builder::{cleanup_builder_instance, get_builder_periphery},
|
||||
channel::repo_cancel_channel,
|
||||
git_token,
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_string,
|
||||
interpolate_variables_secrets_into_system_command,
|
||||
},
|
||||
periphery_client,
|
||||
query::get_variables_and_secrets,
|
||||
git_token, periphery_client,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
update::update_update,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource::{self, refresh_repo_state_cache},
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
@@ -73,10 +69,10 @@ impl Resolve<ExecuteArgs> for CloneRepo {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let mut repo = resource::get_check_permissions::<Repo>(
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -122,16 +118,18 @@ impl Resolve<ExecuteArgs> for CloneRepo {
|
||||
git_token,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
on_clone: repo.config.on_clone.into(),
|
||||
on_pull: repo.config.on_pull.into(),
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(res) => res.logs,
|
||||
Ok(res) => 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()),
|
||||
)]
|
||||
}
|
||||
};
|
||||
@@ -155,14 +153,14 @@ impl Resolve<ExecuteArgs> for CloneRepo {
|
||||
);
|
||||
};
|
||||
|
||||
handle_server_update_return(update).await
|
||||
handle_repo_update_return(update).await
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BatchExecute for BatchPullRepo {
|
||||
type Resource = Repo;
|
||||
fn single_request(repo: String) -> ExecuteRequest {
|
||||
ExecuteRequest::CloneRepo(CloneRepo { repo })
|
||||
ExecuteRequest::PullRepo(PullRepo { repo })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,10 +183,10 @@ impl Resolve<ExecuteArgs> for PullRepo {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let mut repo = resource::get_check_permissions::<Repo>(
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -235,14 +233,15 @@ impl Resolve<ExecuteArgs> for PullRepo {
|
||||
git_token,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
on_pull: repo.config.on_pull.into(),
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
update.commit_hash = res.commit_hash.unwrap_or_default();
|
||||
res.logs
|
||||
update.commit_hash = res.res.commit_hash.unwrap_or_default();
|
||||
res.res.logs
|
||||
}
|
||||
Err(e) => {
|
||||
vec![Log::error(
|
||||
@@ -272,12 +271,12 @@ impl Resolve<ExecuteArgs> for PullRepo {
|
||||
);
|
||||
};
|
||||
|
||||
handle_server_update_return(update).await
|
||||
handle_repo_update_return(update).await
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(update_id = update.id))]
|
||||
async fn handle_server_update_return(
|
||||
async fn handle_repo_update_return(
|
||||
update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
// Need to manually update the update before cache refresh,
|
||||
@@ -340,10 +339,10 @@ impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let mut repo = resource::get_check_permissions::<Repo>(
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -456,13 +455,15 @@ impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
git_token,
|
||||
environment: repo.config.env_vars()?,
|
||||
env_file_path: repo.config.env_file_path,
|
||||
on_clone: repo.config.on_clone.into(),
|
||||
on_pull: repo.config.on_pull.into(),
|
||||
skip_secret_interp: repo.config.skip_secret_interp,
|
||||
replacers: secret_replacers.into_iter().collect()
|
||||
}) => res,
|
||||
_ = cancel.cancelled() => {
|
||||
debug!("build cancelled during clone, cleaning up builder");
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
cleanup_builder_instance(cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
return handle_builder_early_return(update, repo.id, repo.name, true).await
|
||||
@@ -472,14 +473,15 @@ impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
let commit_message = match res {
|
||||
Ok(res) => {
|
||||
debug!("finished repo clone");
|
||||
update.logs.extend(res.logs);
|
||||
update.commit_hash = res.commit_hash.unwrap_or_default();
|
||||
res.commit_message.unwrap_or_default()
|
||||
update.logs.extend(res.res.logs);
|
||||
update.commit_hash = res.res.commit_hash.unwrap_or_default();
|
||||
|
||||
res.res.commit_message.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"clone repo",
|
||||
format_serror(&e.context("failed to clone repo").into()),
|
||||
"Clone Repo",
|
||||
format_serror(&e.context("Failed to clone repo").into()),
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
@@ -506,8 +508,9 @@ impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
// stop the cancel listening task from going forever
|
||||
cancel.cancel();
|
||||
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
// If building on temporary cloud server (AWS),
|
||||
// this will terminate the server.
|
||||
cleanup_builder_instance(cleanup_data, &mut update).await;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
@@ -650,10 +653,10 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let repo = resource::get_check_permissions::<Repo>(
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -695,7 +698,9 @@ 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:#}"
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -708,39 +713,17 @@ async fn interpolate(
|
||||
update: &mut Update,
|
||||
) -> anyhow::Result<HashSet<(String, String)>> {
|
||||
if !repo.config.skip_secret_interp {
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut repo.config.environment,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
interpolator
|
||||
.interpolate_repo(repo)?
|
||||
.push_logs(&mut update.logs);
|
||||
|
||||
interpolate_variables_secrets_into_system_command(
|
||||
&vars_and_secrets,
|
||||
&mut repo.config.on_clone,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_system_command(
|
||||
&vars_and_secrets,
|
||||
&mut repo.config.on_pull,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
add_interp_update_log(
|
||||
update,
|
||||
&global_replacers,
|
||||
&secret_replacers,
|
||||
);
|
||||
|
||||
Ok(secret_replacers)
|
||||
Ok(interpolator.secret_replacers)
|
||||
} else {
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
helpers::{periphery_client, update::update_update},
|
||||
monitor::update_cache_for_server,
|
||||
resource,
|
||||
permission::get_check_permissions,
|
||||
state::action_states,
|
||||
};
|
||||
|
||||
@@ -27,10 +27,10 @@ impl Resolve<ExecuteArgs> for StartContainer {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -81,10 +81,10 @@ impl Resolve<ExecuteArgs> for RestartContainer {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -137,10 +137,10 @@ impl Resolve<ExecuteArgs> for PauseContainer {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -191,10 +191,10 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -247,10 +247,10 @@ impl Resolve<ExecuteArgs> for StopContainer {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -309,10 +309,10 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
|
||||
signal,
|
||||
time,
|
||||
} = self;
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -365,10 +365,10 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -415,10 +415,10 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -467,10 +467,10 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -517,10 +517,10 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -569,10 +569,10 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -619,10 +619,10 @@ impl Resolve<ExecuteArgs> for PruneContainers {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -675,10 +675,10 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -726,10 +726,10 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -780,10 +780,10 @@ impl Resolve<ExecuteArgs> for DeleteImage {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -828,10 +828,10 @@ impl Resolve<ExecuteArgs> for PruneImages {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -880,10 +880,10 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -931,10 +931,10 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -983,10 +983,10 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1035,10 +1035,10 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1087,10 +1087,10 @@ impl Resolve<ExecuteArgs> for PruneSystem {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{execute::*, write::RefreshStackCache},
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
stack::{Stack, StackInfo},
|
||||
update::{Log, Update},
|
||||
@@ -18,17 +18,13 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
api::write::WriteArgs,
|
||||
helpers::{
|
||||
interpolate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_extra_args,
|
||||
interpolate_variables_secrets_into_string,
|
||||
interpolate_variables_secrets_into_system_command,
|
||||
},
|
||||
periphery_client,
|
||||
query::get_variables_and_secrets,
|
||||
query::{VariablesAndSecrets, 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},
|
||||
@@ -69,11 +65,21 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
let (mut stack, server) = get_stack_and_server(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
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;
|
||||
@@ -89,7 +95,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
|
||||
if !self.services.is_empty() {
|
||||
update.logs.push(Log::simple(
|
||||
&format!("Service/s",),
|
||||
"Service/s",
|
||||
format!(
|
||||
"Execution requested for Stack service/s {}",
|
||||
self.services.join(", ")
|
||||
@@ -97,13 +103,8 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
))
|
||||
}
|
||||
|
||||
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 git_token =
|
||||
stack_git_token(&mut stack, repo.as_mut()).await?;
|
||||
|
||||
let registry_token = crate::helpers::registry_token(
|
||||
&stack.config.registry_provider,
|
||||
@@ -115,60 +116,21 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
// interpolate variables / secrets, returning the sanitizing replacers to send to
|
||||
// periphery so it may sanitize the final command for safe logging (avoids exposing secret values)
|
||||
let secret_replacers = if !stack.config.skip_secret_interp {
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.file_contents,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
interpolator.interpolate_stack(&mut stack)?;
|
||||
if let Some(repo) = repo.as_mut() {
|
||||
if !repo.config.skip_secret_interp {
|
||||
interpolator.interpolate_repo(repo)?;
|
||||
}
|
||||
}
|
||||
interpolator.push_logs(&mut update.logs);
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.environment,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_extra_args(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.extra_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_extra_args(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.build_extra_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_system_command(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.pre_deploy,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_system_command(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.post_deploy,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
add_interp_update_log(
|
||||
&mut update,
|
||||
&global_replacers,
|
||||
&secret_replacers,
|
||||
);
|
||||
|
||||
secret_replacers
|
||||
interpolator.secret_replacers
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
@@ -187,6 +149,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
.request(ComposeUp {
|
||||
stack: stack.clone(),
|
||||
services: self.services,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
@@ -320,10 +283,10 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
|
||||
self,
|
||||
ExecuteArgs { user, update }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
RefreshStackCache {
|
||||
@@ -385,30 +348,49 @@ 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>,
|
||||
server: &Server,
|
||||
mut repo: Option<Repo>,
|
||||
mut update: Option<&mut Update>,
|
||||
) -> anyhow::Result<ComposePullResponse> {
|
||||
if let Some(update) = update.as_mut() {
|
||||
if !services.is_empty() {}
|
||||
update.logs.push(Log::simple(
|
||||
&format!("Service/s"),
|
||||
format!(
|
||||
"Execution requested for Stack service/s {}",
|
||||
services.join(", ")
|
||||
),
|
||||
))
|
||||
if !services.is_empty() {
|
||||
update.logs.push(Log::simple(
|
||||
"Service/s",
|
||||
format!(
|
||||
"Execution requested for Stack service/s {}",
|
||||
services.join(", ")
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let git_token = crate::helpers::git_token(
|
||||
&stack.config.git_provider,
|
||||
&stack.config.git_account,
|
||||
|https| stack.config.git_https = https,
|
||||
).await.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {}", stack.config.git_provider, stack.config.git_account),
|
||||
)?;
|
||||
let git_token = stack_git_token(&mut stack, repo.as_mut()).await?;
|
||||
|
||||
let registry_token = crate::helpers::registry_token(
|
||||
&stack.config.registry_provider,
|
||||
@@ -418,41 +400,35 @@ pub async fn pull_stack_inner(
|
||||
)?;
|
||||
|
||||
// interpolate variables / secrets
|
||||
if !stack.config.skip_secret_interp {
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let secret_replacers = if !stack.config.skip_secret_interp {
|
||||
let VariablesAndSecrets { variables, secrets } =
|
||||
get_variables_and_secrets().await?;
|
||||
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let mut interpolator =
|
||||
Interpolator::new(Some(&variables), &secrets);
|
||||
|
||||
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,
|
||||
);
|
||||
interpolator.interpolate_stack(&mut stack)?;
|
||||
if let Some(repo) = repo.as_mut() {
|
||||
if !repo.config.skip_secret_interp {
|
||||
interpolator.interpolate_repo(repo)?;
|
||||
}
|
||||
}
|
||||
if let Some(update) = update {
|
||||
interpolator.push_logs(&mut update.logs);
|
||||
}
|
||||
interpolator.secret_replacers
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let res = periphery_client(server)?
|
||||
.request(ComposePull {
|
||||
stack,
|
||||
services,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -471,11 +447,21 @@ impl Resolve<ExecuteArgs> for PullStack {
|
||||
let (stack, server) = get_stack_and_server(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
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;
|
||||
@@ -492,6 +478,7 @@ impl Resolve<ExecuteArgs> for PullStack {
|
||||
stack,
|
||||
self.services,
|
||||
&server,
|
||||
repo,
|
||||
Some(&mut update),
|
||||
)
|
||||
.await?;
|
||||
|
||||
+128
-207
@@ -1,11 +1,11 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{colored, format_serror, Color};
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::{Color, colored, format_serror};
|
||||
use komodo_client::{
|
||||
api::{execute::RunSync, write::RefreshResourceSyncPending},
|
||||
entities::{
|
||||
self,
|
||||
self, ResourceTargetVariant,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
@@ -16,12 +16,10 @@ 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;
|
||||
@@ -30,16 +28,19 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs,
|
||||
helpers::{query::get_id_to_tags, update::update_update},
|
||||
resource,
|
||||
helpers::{
|
||||
all_resources::AllResourcesById, query::get_id_to_tags,
|
||||
update::update_update,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
state::{action_states, db_client},
|
||||
sync::{
|
||||
ResourceSyncTrait,
|
||||
deploy::{
|
||||
build_deploy_cache, deploy_from_cache, SyncDeployParams,
|
||||
SyncDeployParams, build_deploy_cache, deploy_from_cache,
|
||||
},
|
||||
execute::{get_updates_for_execution, ExecuteResourceSync},
|
||||
execute::{ExecuteResourceSync, get_updates_for_execution},
|
||||
remote::RemoteResources,
|
||||
AllResourcesById, ResourceSyncTrait,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,11 +57,23 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
resource_type: match_resource_type,
|
||||
resources: match_resources,
|
||||
} = self;
|
||||
let sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&sync, user, PermissionLevel::Execute)
|
||||
let sync = get_check_permissions::<entities::sync::ResourceSync>(
|
||||
&sync,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let repo = if !sync.config.files_on_host
|
||||
&& !sync.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&sync.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// get the action state for the sync (or insert default).
|
||||
let action_state = action_states()
|
||||
.resource_sync
|
||||
@@ -84,9 +97,10 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
message,
|
||||
file_errors,
|
||||
..
|
||||
} = crate::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
} =
|
||||
crate::sync::remote::get_remote_resources(&sync, repo.as_ref())
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
update_update(update.clone()).await?;
|
||||
@@ -143,10 +157,6 @@ 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)
|
||||
@@ -201,145 +211,141 @@ 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 (servers_to_create, servers_to_update, servers_to_delete) =
|
||||
let server_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Server>(
|
||||
resources.servers,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
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) =
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let stack_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Stack>(
|
||||
resources.stacks,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (builds_to_create, builds_to_update, builds_to_delete) =
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let deployment_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Deployment>(
|
||||
resources.deployments,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let build_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Build>(
|
||||
resources.builds,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (repos_to_create, repos_to_update, repos_to_delete) =
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let repo_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Repo>(
|
||||
resources.repos,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
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) =
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let procedure_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Procedure>(
|
||||
resources.procedures,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let action_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Action>(
|
||||
resources.actions,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (builders_to_create, builders_to_update, builders_to_delete) =
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let builder_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Builder>(
|
||||
resources.builders,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
let (alerters_to_create, alerters_to_update, alerters_to_delete) =
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let alerter_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<Alerter>(
|
||||
resources.alerters,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
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?;
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let resource_sync_deltas = if sync.config.include_resources {
|
||||
get_updates_for_execution::<entities::sync::ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
delete,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let (
|
||||
variables_to_create,
|
||||
@@ -347,12 +353,11 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
variables_to_delete,
|
||||
) = if match_resource_type.is_none()
|
||||
&& match_resources.is_none()
|
||||
&& sync.config.match_tags.is_empty()
|
||||
&& sync.config.include_variables
|
||||
{
|
||||
crate::sync::variables::get_updates_for_execution(
|
||||
resources.variables,
|
||||
// Delete doesn't work with variables when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
delete,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -364,13 +369,11 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
user_groups_to_delete,
|
||||
) = if match_resource_type.is_none()
|
||||
&& match_resources.is_none()
|
||||
&& sync.config.match_tags.is_empty()
|
||||
&& sync.config.include_user_groups
|
||||
{
|
||||
crate::sync::user_groups::get_updates_for_execution(
|
||||
resources.user_groups,
|
||||
// Delete doesn't work with user groups when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
&all_resources,
|
||||
delete,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -378,39 +381,16 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
};
|
||||
|
||||
if deploy_cache.is_empty()
|
||||
&& 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()
|
||||
&& resource_sync_deltas.no_changes()
|
||||
&& server_deltas.no_changes()
|
||||
&& deployment_deltas.no_changes()
|
||||
&& stack_deltas.no_changes()
|
||||
&& build_deltas.no_changes()
|
||||
&& builder_deltas.no_changes()
|
||||
&& alerter_deltas.no_changes()
|
||||
&& repo_deltas.no_changes()
|
||||
&& procedure_deltas.no_changes()
|
||||
&& action_deltas.no_changes()
|
||||
&& user_groups_to_create.is_empty()
|
||||
&& user_groups_to_update.is_empty()
|
||||
&& user_groups_to_delete.is_empty()
|
||||
@@ -453,111 +433,52 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
ResourceSync::execute_sync_updates(
|
||||
resource_syncs_to_create,
|
||||
resource_syncs_to_update,
|
||||
resource_syncs_to_delete,
|
||||
)
|
||||
.await,
|
||||
ResourceSync::execute_sync_updates(resource_sync_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
ServerTemplate::execute_sync_updates(
|
||||
server_templates_to_create,
|
||||
server_templates_to_update,
|
||||
server_templates_to_delete,
|
||||
)
|
||||
.await,
|
||||
Server::execute_sync_updates(server_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Server::execute_sync_updates(
|
||||
servers_to_create,
|
||||
servers_to_update,
|
||||
servers_to_delete,
|
||||
)
|
||||
.await,
|
||||
Alerter::execute_sync_updates(alerter_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
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,
|
||||
Action::execute_sync_updates(action_deltas).await,
|
||||
);
|
||||
|
||||
// Dependent on server
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Builder::execute_sync_updates(
|
||||
builders_to_create,
|
||||
builders_to_update,
|
||||
builders_to_delete,
|
||||
)
|
||||
.await,
|
||||
Builder::execute_sync_updates(builder_deltas).await,
|
||||
);
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Repo::execute_sync_updates(
|
||||
repos_to_create,
|
||||
repos_to_update,
|
||||
repos_to_delete,
|
||||
)
|
||||
.await,
|
||||
Repo::execute_sync_updates(repo_deltas).await,
|
||||
);
|
||||
|
||||
// Dependant on builder
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Build::execute_sync_updates(
|
||||
builds_to_create,
|
||||
builds_to_update,
|
||||
builds_to_delete,
|
||||
)
|
||||
.await,
|
||||
Build::execute_sync_updates(build_deltas).await,
|
||||
);
|
||||
|
||||
// Dependant on server / build
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Deployment::execute_sync_updates(
|
||||
deployments_to_create,
|
||||
deployments_to_update,
|
||||
deployments_to_delete,
|
||||
)
|
||||
.await,
|
||||
Deployment::execute_sync_updates(deployment_deltas).await,
|
||||
);
|
||||
// stack only depends on server, but maybe will depend on build later.
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Stack::execute_sync_updates(
|
||||
stacks_to_create,
|
||||
stacks_to_update,
|
||||
stacks_to_delete,
|
||||
)
|
||||
.await,
|
||||
Stack::execute_sync_updates(stack_deltas).await,
|
||||
);
|
||||
|
||||
// Dependant on everything
|
||||
maybe_extend(
|
||||
&mut update.logs,
|
||||
Procedure::execute_sync_updates(
|
||||
procedures_to_create,
|
||||
procedures_to_update,
|
||||
procedures_to_delete,
|
||||
)
|
||||
.await,
|
||||
Procedure::execute_sync_updates(procedure_deltas).await,
|
||||
);
|
||||
|
||||
// Execute the deploy cache
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod terminal;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Variant {
|
||||
variant: String,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_state_cache, action_states},
|
||||
};
|
||||
@@ -24,10 +25,10 @@ impl Resolve<ReadArgs> for GetAction {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Action> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Action>(
|
||||
get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -45,8 +46,13 @@ impl Resolve<ReadArgs> for ListActions {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Action>(self.query, user, &all_tags)
|
||||
.await?,
|
||||
resource::list_for_user::<Action>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +69,10 @@ impl Resolve<ReadArgs> for ListFullActions {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Action>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -75,10 +84,10 @@ impl Resolve<ReadArgs> for GetActionActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ActionActionState> {
|
||||
let action = resource::get_check_permissions::<Action>(
|
||||
let action = get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
@@ -99,6 +108,7 @@ impl Resolve<ReadArgs> for GetActionsSummary {
|
||||
let actions = resource::list_full_for_user::<Action>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -16,7 +16,7 @@ use mungos::{
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config, resource::get_resource_ids_for_user,
|
||||
config::core_config, permission::get_resource_ids_for_user,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, resource, state::db_client,
|
||||
helpers::query::get_all_tags, permission::get_check_permissions,
|
||||
resource, state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -22,10 +23,10 @@ impl Resolve<ReadArgs> for GetAlerter {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Alerter>(
|
||||
get_check_permissions::<Alerter>(
|
||||
&self.alerter,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -43,8 +44,13 @@ impl Resolve<ReadArgs> for ListAlerters {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Alerter>(self.query, user, &all_tags)
|
||||
.await?,
|
||||
resource::list_for_user::<Alerter>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +67,10 @@ impl Resolve<ReadArgs> for ListFullAlerters {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Alerter>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
|
||||
@@ -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,6 +22,7 @@ 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,
|
||||
@@ -36,10 +37,10 @@ impl Resolve<ReadArgs> for GetBuild {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Build> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Build>(
|
||||
get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -57,8 +58,13 @@ impl Resolve<ReadArgs> for ListBuilds {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Build>(self.query, user, &all_tags)
|
||||
.await?,
|
||||
resource::list_for_user::<Build>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +81,10 @@ impl Resolve<ReadArgs> for ListFullBuilds {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Build>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -87,10 +96,10 @@ impl Resolve<ReadArgs> for GetBuildActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<BuildActionState> {
|
||||
let build = resource::get_check_permissions::<Build>(
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
@@ -111,6 +120,7 @@ impl Resolve<ReadArgs> for GetBuildsSummary {
|
||||
let builds = resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -218,10 +228,10 @@ impl Resolve<ReadArgs> for ListBuildVersions {
|
||||
patch,
|
||||
limit,
|
||||
} = self;
|
||||
let build = resource::get_check_permissions::<Build>(
|
||||
let build = get_check_permissions::<Build>(
|
||||
&build,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -274,7 +284,10 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let builds = resource::list_full_for_user::<Build>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
@@ -306,10 +319,10 @@ impl Resolve<ReadArgs> for GetBuildWebhookEnabled {
|
||||
});
|
||||
};
|
||||
|
||||
let build = resource::get_check_permissions::<Build>(
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, resource, state::db_client,
|
||||
helpers::query::get_all_tags, permission::get_check_permissions,
|
||||
resource, state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -22,10 +23,10 @@ impl Resolve<ReadArgs> for GetBuilder {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Builder>(
|
||||
get_check_permissions::<Builder>(
|
||||
&self.builder,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -43,8 +44,13 @@ impl Resolve<ReadArgs> for ListBuilders {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Builder>(self.query, user, &all_tags)
|
||||
.await?,
|
||||
resource::list_for_user::<Builder>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +67,10 @@ impl Resolve<ReadArgs> for ListFullBuilders {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Builder>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{cmp, collections::HashSet};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
@@ -8,19 +8,22 @@ use komodo_client::{
|
||||
Deployment, DeploymentActionState, DeploymentConfig,
|
||||
DeploymentListItem, DeploymentState,
|
||||
},
|
||||
docker::container::ContainerStats,
|
||||
docker::container::{Container, ContainerStats},
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
server::{Server, ServerState},
|
||||
update::Log,
|
||||
},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use periphery_client::api::{self, container::InspectContainer};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, deployment_status_cache},
|
||||
state::{
|
||||
action_states, deployment_status_cache, server_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -31,10 +34,10 @@ impl Resolve<ReadArgs> for GetDeployment {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Deployment>(
|
||||
get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -51,12 +54,23 @@ impl Resolve<ReadArgs> for ListDeployments {
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Deployment>(
|
||||
self.query, user, &all_tags,
|
||||
)
|
||||
.await?,
|
||||
let only_update_available = self.query.specific.update_available;
|
||||
let deployments = resource::list_for_user::<Deployment>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?;
|
||||
let deployments = if only_update_available {
|
||||
deployments
|
||||
.into_iter()
|
||||
.filter(|deployment| deployment.info.update_available)
|
||||
.collect()
|
||||
} else {
|
||||
deployments
|
||||
};
|
||||
Ok(deployments)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +86,10 @@ impl Resolve<ReadArgs> for ListFullDeployments {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -84,10 +101,10 @@ impl Resolve<ReadArgs> for GetDeploymentContainer {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetDeploymentContainerResponse> {
|
||||
let deployment = resource::get_check_permissions::<Deployment>(
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let status = deployment_status_cache()
|
||||
@@ -118,10 +135,10 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = resource::get_check_permissions::<Deployment>(
|
||||
} = get_check_permissions::<Deployment>(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
@@ -156,10 +173,10 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = resource::get_check_permissions::<Deployment>(
|
||||
} = get_check_permissions::<Deployment>(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
@@ -180,6 +197,50 @@ 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,
|
||||
@@ -189,10 +250,10 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = resource::get_check_permissions::<Deployment>(
|
||||
} = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
@@ -214,10 +275,10 @@ impl Resolve<ReadArgs> for GetDeploymentActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<DeploymentActionState> {
|
||||
let deployment = resource::get_check_permissions::<Deployment>(
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
@@ -238,6 +299,7 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -281,7 +343,10 @@ impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
use std::{collections::HashSet, sync::OnceLock, time::Instant};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
build::Build,
|
||||
builder::{Builder, BuilderConfig},
|
||||
config::{DockerRegistry, GitProvider},
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
sync::ResourceSync,
|
||||
user::User,
|
||||
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;
|
||||
@@ -27,6 +31,8 @@ use crate::{
|
||||
resource,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
mod action;
|
||||
mod alert;
|
||||
mod alerter;
|
||||
@@ -37,8 +43,8 @@ mod permission;
|
||||
mod procedure;
|
||||
mod provider;
|
||||
mod repo;
|
||||
mod schedule;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod stack;
|
||||
mod sync;
|
||||
mod tag;
|
||||
@@ -67,7 +73,7 @@ enum ReadRequest {
|
||||
|
||||
// ==== USER ====
|
||||
GetUsername(GetUsername),
|
||||
GetPermissionLevel(GetPermissionLevel),
|
||||
GetPermission(GetPermission),
|
||||
FindUser(FindUser),
|
||||
ListUsers(ListUsers),
|
||||
ListApiKeys(ListApiKeys),
|
||||
@@ -93,11 +99,8 @@ enum ReadRequest {
|
||||
ListActions(ListActions),
|
||||
ListFullActions(ListFullActions),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
GetServerTemplate(GetServerTemplate),
|
||||
GetServerTemplatesSummary(GetServerTemplatesSummary),
|
||||
ListServerTemplates(ListServerTemplates),
|
||||
ListFullServerTemplates(ListFullServerTemplates),
|
||||
// ==== SCHEDULE ====
|
||||
ListSchedules(ListSchedules),
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary(GetServersSummary),
|
||||
@@ -116,12 +119,33 @@ 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),
|
||||
@@ -131,6 +155,7 @@ enum ReadRequest {
|
||||
GetDeploymentStats(GetDeploymentStats),
|
||||
GetDeploymentLog(GetDeploymentLog),
|
||||
SearchDeploymentLog(SearchDeploymentLog),
|
||||
InspectDeploymentContainer(InspectDeploymentContainer),
|
||||
ListDeployments(ListDeployments),
|
||||
ListFullDeployments(ListFullDeployments),
|
||||
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
|
||||
@@ -162,19 +187,6 @@ enum ReadRequest {
|
||||
ListResourceSyncs(ListResourceSyncs),
|
||||
ListFullResourceSyncs(ListFullResourceSyncs),
|
||||
|
||||
// ==== STACK ====
|
||||
GetStacksSummary(GetStacksSummary),
|
||||
GetStack(GetStack),
|
||||
GetStackActionState(GetStackActionState),
|
||||
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
|
||||
GetStackLog(GetStackLog),
|
||||
SearchStackLog(SearchStackLog),
|
||||
ListStacks(ListStacks),
|
||||
ListFullStacks(ListFullStacks),
|
||||
ListStackServices(ListStackServices),
|
||||
ListCommonStackExtraArgs(ListCommonStackExtraArgs),
|
||||
ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs),
|
||||
|
||||
// ==== BUILDER ====
|
||||
GetBuildersSummary(GetBuildersSummary),
|
||||
GetBuilder(GetBuilder),
|
||||
@@ -203,11 +215,6 @@ enum ReadRequest {
|
||||
ListAlerts(ListAlerts),
|
||||
GetAlert(GetAlert),
|
||||
|
||||
// ==== SERVER STATS ====
|
||||
GetSystemInformation(GetSystemInformation),
|
||||
GetSystemStats(GetSystemStats),
|
||||
ListSystemProcesses(ListSystemProcesses),
|
||||
|
||||
// ==== VARIABLE ====
|
||||
GetVariable(GetVariable),
|
||||
ListVariables(ListVariables),
|
||||
@@ -222,9 +229,22 @@ 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>,
|
||||
@@ -269,12 +289,14 @@ 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(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -315,7 +337,7 @@ impl Resolve<ReadArgs> for ListSecrets {
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`").into(),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Some(id) = server_id {
|
||||
@@ -373,7 +395,7 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`").into(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,16 +404,19 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
|
||||
resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[]
|
||||
),
|
||||
)?;
|
||||
@@ -473,7 +498,7 @@ impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`").into(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
GetPermissionLevel, GetPermissionLevelResponse, ListPermissions,
|
||||
GetPermission, GetPermissionResponse, ListPermissions,
|
||||
ListPermissionsResponse, ListUserTargetPermissions,
|
||||
ListUserTargetPermissionsResponse,
|
||||
},
|
||||
@@ -35,13 +35,13 @@ impl Resolve<ReadArgs> for ListPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetPermissionLevel {
|
||||
impl Resolve<ReadArgs> for GetPermission {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetPermissionLevelResponse> {
|
||||
) -> serror::Result<GetPermissionResponse> {
|
||||
if user.admin {
|
||||
return Ok(PermissionLevel::Write);
|
||||
return Ok(PermissionLevel::Write.all());
|
||||
}
|
||||
Ok(get_user_permission_on_target(user, &self.target).await?)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, procedure_state_cache},
|
||||
};
|
||||
@@ -22,10 +23,10 @@ impl Resolve<ReadArgs> for GetProcedure {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetProcedureResponse> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Procedure>(
|
||||
get_check_permissions::<Procedure>(
|
||||
&self.procedure,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -44,7 +45,10 @@ impl Resolve<ReadArgs> for ListProcedures {
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Procedure>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -63,7 +67,10 @@ impl Resolve<ReadArgs> for ListFullProcedures {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Procedure>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -78,6 +85,7 @@ impl Resolve<ReadArgs> for GetProceduresSummary {
|
||||
let procedures = resource::list_full_for_user::<Procedure>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -120,10 +128,10 @@ impl Resolve<ReadArgs> for GetProcedureActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetProcedureActionStateResponse> {
|
||||
let procedure = resource::get_check_permissions::<Procedure>(
|
||||
let procedure = get_check_permissions::<Procedure>(
|
||||
&self.procedure,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::api::read::*;
|
||||
use mongo_indexed::{doc, Document};
|
||||
use mongo_indexed::{Document, doc};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id, find::find_collect,
|
||||
mongodb::options::FindOptions,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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},
|
||||
};
|
||||
@@ -24,10 +25,10 @@ impl Resolve<ReadArgs> for GetRepo {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Repo>(
|
||||
get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -45,8 +46,13 @@ impl Resolve<ReadArgs> for ListRepos {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Repo>(self.query, user, &all_tags)
|
||||
.await?,
|
||||
resource::list_for_user::<Repo>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +69,10 @@ impl Resolve<ReadArgs> for ListFullRepos {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Repo>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -75,10 +84,10 @@ impl Resolve<ReadArgs> for GetRepoActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<RepoActionState> {
|
||||
let repo = resource::get_check_permissions::<Repo>(
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
@@ -99,6 +108,7 @@ impl Resolve<ReadArgs> for GetReposSummary {
|
||||
let repos = resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -160,10 +170,10 @@ impl Resolve<ReadArgs> for GetRepoWebhooksEnabled {
|
||||
});
|
||||
};
|
||||
|
||||
let repo = resource::get_check_permissions::<Repo>(
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use futures::future::join_all;
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
action::Action,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
resource::{ResourceQuery, TemplatesQueryBehavior},
|
||||
schedule::Schedule,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::{get_all_tags, get_last_run_at},
|
||||
resource::list_full_for_user,
|
||||
schedule::get_schedule_item_info,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for ListSchedules {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> serror::Result<Vec<Schedule>> {
|
||||
let all_tags = get_all_tags(None).await?;
|
||||
let (actions, procedures) = tokio::try_join!(
|
||||
list_full_for_user::<Action>(
|
||||
ResourceQuery {
|
||||
names: Default::default(),
|
||||
templates: TemplatesQueryBehavior::Include,
|
||||
tag_behavior: self.tag_behavior,
|
||||
tags: self.tags.clone(),
|
||||
specific: Default::default(),
|
||||
},
|
||||
&args.user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
),
|
||||
list_full_for_user::<Procedure>(
|
||||
ResourceQuery {
|
||||
names: Default::default(),
|
||||
templates: TemplatesQueryBehavior::Include,
|
||||
tag_behavior: self.tag_behavior,
|
||||
tags: self.tags.clone(),
|
||||
specific: Default::default(),
|
||||
},
|
||||
&args.user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
)?;
|
||||
let actions = actions.into_iter().map(async |action| {
|
||||
let (next_scheduled_run, schedule_error) =
|
||||
get_schedule_item_info(&ResourceTarget::Action(
|
||||
action.id.clone(),
|
||||
));
|
||||
let last_run_at =
|
||||
get_last_run_at::<Action>(&action.id).await.unwrap_or(None);
|
||||
Schedule {
|
||||
target: ResourceTarget::Action(action.id),
|
||||
name: action.name,
|
||||
enabled: action.config.schedule_enabled,
|
||||
schedule_format: action.config.schedule_format,
|
||||
schedule: action.config.schedule,
|
||||
schedule_timezone: action.config.schedule_timezone,
|
||||
tags: action.tags,
|
||||
last_run_at,
|
||||
next_scheduled_run,
|
||||
schedule_error,
|
||||
}
|
||||
});
|
||||
let procedures = procedures.into_iter().map(async |procedure| {
|
||||
let (next_scheduled_run, schedule_error) =
|
||||
get_schedule_item_info(&ResourceTarget::Procedure(
|
||||
procedure.id.clone(),
|
||||
));
|
||||
let last_run_at = get_last_run_at::<Procedure>(&procedure.id)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
Schedule {
|
||||
target: ResourceTarget::Procedure(procedure.id),
|
||||
name: procedure.name,
|
||||
enabled: procedure.config.schedule_enabled,
|
||||
schedule_format: procedure.config.schedule_format,
|
||||
schedule: procedure.config.schedule,
|
||||
schedule_timezone: procedure.config.schedule_timezone,
|
||||
tags: procedure.tags,
|
||||
last_run_at,
|
||||
next_scheduled_run,
|
||||
schedule_error,
|
||||
}
|
||||
});
|
||||
let (actions, procedures) =
|
||||
tokio::join!(join_all(actions), join_all(procedures));
|
||||
|
||||
Ok(
|
||||
actions
|
||||
.into_iter()
|
||||
.chain(procedures)
|
||||
.filter(|s| !s.schedule.is_empty())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+172
-79
@@ -4,28 +4,32 @@ use std::{
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::{
|
||||
get_timelength_in_ms, unix_timestamp_ms, FIFTEEN_SECONDS_MS,
|
||||
FIFTEEN_SECONDS_MS, get_timelength_in_ms, unix_timestamp_ms,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
deployment::Deployment,
|
||||
docker::{
|
||||
container::{Container, ContainerListItem},
|
||||
container::{
|
||||
Container, ContainerListItem, ContainerStateStatusEnum,
|
||||
},
|
||||
image::{Image, ImageHistoryResponseItem},
|
||||
network::Network,
|
||||
volume::Volume,
|
||||
},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerState,
|
||||
TerminalInfo,
|
||||
},
|
||||
stack::{Stack, StackServiceNames},
|
||||
stats::{SystemInformation, SystemProcess},
|
||||
update::Log,
|
||||
ResourceTarget,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -43,7 +47,11 @@ use resolver_api::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::{get_all_tags, get_system_info},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
stack::compose_container_match_regex,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
@@ -59,6 +67,7 @@ impl Resolve<ReadArgs> for GetServersSummary {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
@@ -86,10 +95,10 @@ impl Resolve<ReadArgs> for GetPeripheryVersion {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetPeripheryVersionResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let version = server_status_cache()
|
||||
@@ -107,10 +116,10 @@ impl Resolve<ReadArgs> for GetServer {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Server> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Server>(
|
||||
get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -128,8 +137,13 @@ impl Resolve<ReadArgs> for ListServers {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Server>(self.query, user, &all_tags)
|
||||
.await?,
|
||||
resource::list_for_user::<Server>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -146,7 +160,10 @@ impl Resolve<ReadArgs> for ListFullServers {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Server>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -158,10 +175,10 @@ impl Resolve<ReadArgs> for GetServerState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetServerStateResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let status = server_status_cache()
|
||||
@@ -180,10 +197,10 @@ impl Resolve<ReadArgs> for GetServerActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ServerActionState> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
@@ -196,46 +213,18 @@ 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 = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut lock = system_info_cache().lock().await;
|
||||
let res = match lock.get(&server.id) {
|
||||
Some(cached) if cached.1 > unix_timestamp_ms() => {
|
||||
cached.0.clone()
|
||||
}
|
||||
_ => {
|
||||
let stats = periphery_client(&server)?
|
||||
.request(periphery::stats::GetSystemInformation {})
|
||||
.await?;
|
||||
lock.insert(
|
||||
server.id,
|
||||
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
|
||||
.into(),
|
||||
);
|
||||
stats
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
get_system_info(&server).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,10 +233,10 @@ impl Resolve<ReadArgs> for GetSystemStats {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetSystemStatsResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let status =
|
||||
@@ -276,10 +265,10 @@ impl Resolve<ReadArgs> for ListSystemProcesses {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSystemProcessesResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.processes(),
|
||||
)
|
||||
.await?;
|
||||
let mut lock = processes_cache().lock().await;
|
||||
@@ -315,10 +304,10 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
|
||||
granularity,
|
||||
page,
|
||||
} = self;
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let granularity =
|
||||
@@ -363,10 +352,10 @@ impl Resolve<ReadArgs> for ListDockerContainers {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerContainersResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -388,6 +377,7 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
@@ -413,15 +403,55 @@ 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 = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -458,10 +488,10 @@ impl Resolve<ReadArgs> for GetContainerLog {
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)?
|
||||
@@ -489,10 +519,10 @@ impl Resolve<ReadArgs> for SearchContainerLog {
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)?
|
||||
@@ -514,10 +544,10 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetResourceMatchingContainerResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
// first check deployments
|
||||
@@ -575,10 +605,10 @@ impl Resolve<ReadArgs> for ListDockerNetworks {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerNetworksResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -597,10 +627,10 @@ impl Resolve<ReadArgs> for InspectDockerNetwork {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Network> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -627,10 +657,10 @@ impl Resolve<ReadArgs> for ListDockerImages {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerImagesResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -649,10 +679,10 @@ impl Resolve<ReadArgs> for InspectDockerImage {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Image> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -676,10 +706,10 @@ impl Resolve<ReadArgs> for ListDockerImageHistory {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<ImageHistoryResponseItem>> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -706,10 +736,10 @@ impl Resolve<ReadArgs> for ListDockerVolumes {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerVolumesResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -728,10 +758,10 @@ impl Resolve<ReadArgs> for InspectDockerVolume {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Volume> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -755,10 +785,10 @@ impl Resolve<ReadArgs> for ListComposeProjects {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListComposeProjectsResponse> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -771,3 +801,66 @@ 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
+122
-24
@@ -1,25 +1,32 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
config::core::CoreConfig,
|
||||
docker::container::Container,
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
stack::{Stack, StackActionState, StackListItem, StackState},
|
||||
},
|
||||
};
|
||||
use periphery_client::api::compose::{
|
||||
GetComposeLog, GetComposeLogSearch,
|
||||
use periphery_client::api::{
|
||||
compose::{GetComposeLog, GetComposeLogSearch},
|
||||
container::InspectContainer,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
stack::get_stack_and_server,
|
||||
state::{action_states, github_client, stack_status_cache},
|
||||
state::{
|
||||
action_states, github_client, server_status_cache,
|
||||
stack_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -30,10 +37,10 @@ impl Resolve<ReadArgs> for GetStack {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<Stack>(
|
||||
get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -45,10 +52,10 @@ impl Resolve<ReadArgs> for ListStackServices {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListStackServicesResponse> {
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -75,9 +82,13 @@ impl Resolve<ReadArgs> for GetStackLog {
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, server) =
|
||||
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
|
||||
.await?;
|
||||
let (stack, server) = get_stack_and_server(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)?
|
||||
.request(GetComposeLog {
|
||||
project: stack.project_name(false),
|
||||
@@ -104,9 +115,13 @@ impl Resolve<ReadArgs> for SearchStackLog {
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, server) =
|
||||
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
|
||||
.await?;
|
||||
let (stack, server) = get_stack_and_server(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)?
|
||||
.request(GetComposeLogSearch {
|
||||
project: stack.project_name(false),
|
||||
@@ -122,6 +137,60 @@ impl Resolve<ReadArgs> for SearchStackLog {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectStackContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Container> {
|
||||
let InspectStackContainer { stack, service } = self;
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
if stack.config.server_id.is_empty() {
|
||||
return Err(
|
||||
anyhow!("Cannot inspect stack, not attached to any server")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let server =
|
||||
resource::get::<Server>(&stack.config.server_id).await?;
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if cache.state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot inspect container: server is {:?}",
|
||||
cache.state
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let services = &stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
.services;
|
||||
let Some(name) = services
|
||||
.iter()
|
||||
.find(|s| s.service == service)
|
||||
.and_then(|s| s.container.as_ref().map(|c| c.name.clone()))
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"No service found matching '{service}'. Was the stack last deployed manually?"
|
||||
).into());
|
||||
};
|
||||
let res = periphery_client(&server)?
|
||||
.request(InspectContainer { name })
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
@@ -133,7 +202,10 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
@@ -164,7 +236,10 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await
|
||||
.context("failed to get resources matching query")?;
|
||||
@@ -194,10 +269,29 @@ impl Resolve<ReadArgs> for ListStacks {
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Stack>(self.query, user, &all_tags)
|
||||
.await?,
|
||||
let only_update_available = self.query.specific.update_available;
|
||||
let stacks = resource::list_for_user::<Stack>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?;
|
||||
let stacks = if only_update_available {
|
||||
stacks
|
||||
.into_iter()
|
||||
.filter(|stack| {
|
||||
stack
|
||||
.info
|
||||
.services
|
||||
.iter()
|
||||
.any(|service| service.update_available)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
stacks
|
||||
};
|
||||
Ok(stacks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +307,10 @@ impl Resolve<ReadArgs> for ListFullStacks {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Stack>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -225,10 +322,10 @@ impl Resolve<ReadArgs> for GetStackActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<StackActionState> {
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
@@ -249,6 +346,7 @@ impl Resolve<ReadArgs> for GetStacksSummary {
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -287,10 +385,10 @@ impl Resolve<ReadArgs> for GetStackWebhooksEnabled {
|
||||
});
|
||||
};
|
||||
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, github_client},
|
||||
};
|
||||
@@ -26,10 +27,10 @@ impl Resolve<ReadArgs> for GetResourceSync {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
Ok(
|
||||
resource::get_check_permissions::<ResourceSync>(
|
||||
get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -48,7 +49,10 @@ impl Resolve<ReadArgs> for ListResourceSyncs {
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<ResourceSync>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -67,7 +71,10 @@ impl Resolve<ReadArgs> for ListFullResourceSyncs {
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -79,10 +86,10 @@ impl Resolve<ReadArgs> for GetResourceSyncActionState {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ResourceSyncActionState> {
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
@@ -104,6 +111,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -138,7 +146,9 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
|
||||
.syncing
|
||||
{
|
||||
res.syncing += 1;
|
||||
continue;
|
||||
}
|
||||
res.ok += 1;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
@@ -158,10 +168,10 @@ impl Resolve<ReadArgs> for GetSyncWebhooksEnabled {
|
||||
});
|
||||
};
|
||||
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
+194
-210
@@ -6,11 +6,11 @@ use komodo_client::{
|
||||
ListUserGroups,
|
||||
},
|
||||
entities::{
|
||||
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,
|
||||
ResourceTarget, action::Action, alerter::Alerter, build::Build,
|
||||
builder::Builder, deployment::Deployment,
|
||||
permission::PermissionLevel, procedure::Procedure, repo::Repo,
|
||||
resource::ResourceQuery, server::Server, stack::Stack,
|
||||
sync::ResourceSync, toml::ResourcesToml, user::User,
|
||||
},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
@@ -20,159 +20,172 @@ 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::{convert_resource, ToToml, TOML_PRETTY_OPTIONS},
|
||||
user_groups::convert_user_groups,
|
||||
AllResourcesById,
|
||||
toml::{ToToml, convert_resource},
|
||||
user_groups::{convert_user_groups, user_group_to_toml},
|
||||
variables::variable_to_toml,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
async fn get_all_targets(
|
||||
tags: &[String],
|
||||
user: &User,
|
||||
) -> anyhow::Result<Vec<ResourceTarget>> {
|
||||
let mut targets = Vec::<ResourceTarget>::new();
|
||||
let all_tags = if tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Alerter>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Alerter(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Builder>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Builder(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Server>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Server(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Stack>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Stack(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Deployment(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Build>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Build(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Repo>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Repo(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Procedure>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Procedure(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<Action>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resource| ResourceTarget::Action(resource.id)),
|
||||
);
|
||||
targets.extend(
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
// These will already be filtered by [ExportResourcesToToml]
|
||||
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
|
||||
);
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ExportAllResourcesToToml {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> serror::Result<ExportAllResourcesToTomlResponse> {
|
||||
let mut targets = Vec::<ResourceTarget>::new();
|
||||
|
||||
let all_tags = if self.tags.is_empty() {
|
||||
vec![]
|
||||
let targets = if self.include_resources {
|
||||
get_all_targets(&self.tags, &args.user).await?
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
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()
|
||||
let user_groups = if self.include_user_groups {
|
||||
if args.user.admin {
|
||||
find_collect(&db_client().user_groups, None, None)
|
||||
.await
|
||||
.context("failed to query db for user groups")?
|
||||
.into_iter()
|
||||
.map(|user_group| user_group.id)
|
||||
.collect()
|
||||
} else {
|
||||
get_user_user_group_ids(&args.user.id).await?
|
||||
}
|
||||
} else {
|
||||
get_user_user_group_ids(&user.id).await?
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
ExportResourcesToToml {
|
||||
targets,
|
||||
user_groups,
|
||||
include_variables: self.tags.is_empty(),
|
||||
include_variables: self.include_variables,
|
||||
}
|
||||
.resolve(args)
|
||||
.await
|
||||
@@ -190,18 +203,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 alerter = resource::get_check_permissions::<Alerter>(
|
||||
let mut alerter = get_check_permissions::<Alerter>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Alerter::replace_ids(&mut alerter);
|
||||
res.alerters.push(convert_resource::<Alerter>(
|
||||
alerter,
|
||||
false,
|
||||
@@ -210,16 +223,18 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
let mut sync = get_check_permissions::<ResourceSync>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
if sync.config.file_contents.is_empty()
|
||||
&& (sync.config.files_on_host
|
||||
|| !sync.config.repo.is_empty())
|
||||
|| !sync.config.repo.is_empty()
|
||||
|| !sync.config.linked_repo.is_empty())
|
||||
{
|
||||
ResourceSync::replace_ids(&mut sync);
|
||||
res.resource_syncs.push(convert_resource::<ResourceSync>(
|
||||
sync,
|
||||
false,
|
||||
@@ -228,27 +243,14 @@ 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 server = resource::get_check_permissions::<Server>(
|
||||
let mut server = get_check_permissions::<Server>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Server::replace_ids(&mut server);
|
||||
res.servers.push(convert_resource::<Server>(
|
||||
server,
|
||||
false,
|
||||
@@ -257,14 +259,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
let mut builder =
|
||||
resource::get_check_permissions::<Builder>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
Builder::replace_ids(&mut builder, &all);
|
||||
let mut builder = get_check_permissions::<Builder>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Builder::replace_ids(&mut builder);
|
||||
res.builders.push(convert_resource::<Builder>(
|
||||
builder,
|
||||
false,
|
||||
@@ -273,13 +274,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
let mut build = resource::get_check_permissions::<Build>(
|
||||
let mut build = get_check_permissions::<Build>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Build::replace_ids(&mut build, &all);
|
||||
Build::replace_ids(&mut build);
|
||||
res.builds.push(convert_resource::<Build>(
|
||||
build,
|
||||
false,
|
||||
@@ -288,13 +289,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
let mut deployment = resource::get_check_permissions::<
|
||||
Deployment,
|
||||
>(
|
||||
&id, user, PermissionLevel::Read
|
||||
let mut deployment = get_check_permissions::<Deployment>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Deployment::replace_ids(&mut deployment, &all);
|
||||
Deployment::replace_ids(&mut deployment);
|
||||
res.deployments.push(convert_resource::<Deployment>(
|
||||
deployment,
|
||||
false,
|
||||
@@ -303,13 +304,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
let mut repo = resource::get_check_permissions::<Repo>(
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Repo::replace_ids(&mut repo, &all);
|
||||
Repo::replace_ids(&mut repo);
|
||||
res.repos.push(convert_resource::<Repo>(
|
||||
repo,
|
||||
false,
|
||||
@@ -318,13 +319,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
let mut stack = resource::get_check_permissions::<Stack>(
|
||||
let mut stack = get_check_permissions::<Stack>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Stack::replace_ids(&mut stack, &all);
|
||||
Stack::replace_ids(&mut stack);
|
||||
res.stacks.push(convert_resource::<Stack>(
|
||||
stack,
|
||||
false,
|
||||
@@ -333,13 +334,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
))
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
let mut procedure = resource::get_check_permissions::<
|
||||
Procedure,
|
||||
>(
|
||||
&id, user, PermissionLevel::Read
|
||||
let mut procedure = get_check_permissions::<Procedure>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Procedure::replace_ids(&mut procedure, &all);
|
||||
Procedure::replace_ids(&mut procedure);
|
||||
res.procedures.push(convert_resource::<Procedure>(
|
||||
procedure,
|
||||
false,
|
||||
@@ -348,13 +349,13 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
));
|
||||
}
|
||||
ResourceTarget::Action(id) => {
|
||||
let mut action = resource::get_check_permissions::<Action>(
|
||||
let mut action = get_check_permissions::<Action>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Action::replace_ids(&mut action, &all);
|
||||
Action::replace_ids(&mut action);
|
||||
res.actions.push(convert_resource::<Action>(
|
||||
action,
|
||||
false,
|
||||
@@ -366,7 +367,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
};
|
||||
}
|
||||
|
||||
add_user_groups(user_groups, &mut res, &all, args)
|
||||
add_user_groups(user_groups, &mut res, args)
|
||||
.await
|
||||
.context("failed to add user groups")?;
|
||||
|
||||
@@ -395,7 +396,6 @@ 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 +407,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, all, &mut ug).await?;
|
||||
convert_user_groups(user_groups, &mut ug).await?;
|
||||
res.user_groups = ug.into_iter().map(|ug| ug.1).collect();
|
||||
|
||||
Ok(())
|
||||
@@ -490,14 +490,6 @@ 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");
|
||||
@@ -510,22 +502,14 @@ fn serialize_resources_toml(
|
||||
if !toml.is_empty() {
|
||||
toml.push_str("\n\n##\n\n");
|
||||
}
|
||||
toml.push_str("[[variable]]\n");
|
||||
toml.push_str(
|
||||
&toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS)
|
||||
.context("failed to serialize variables to toml")?,
|
||||
);
|
||||
toml.push_str(&variable_to_toml(variable)?);
|
||||
}
|
||||
|
||||
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]]\n");
|
||||
toml.push_str(
|
||||
&toml_pretty::to_string(user_group, TOML_PRETTY_OPTIONS)
|
||||
.context("failed to serialize user_groups to toml")?,
|
||||
);
|
||||
toml.push_str(&user_group_to_toml(user_group)?);
|
||||
}
|
||||
|
||||
Ok(toml)
|
||||
|
||||
+94
-118
@@ -1,9 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
@@ -13,12 +14,10 @@ use komodo_client::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
update::{Update, UpdateListItem},
|
||||
user::User,
|
||||
ResourceTarget,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -28,7 +27,11 @@ use mungos::{
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{config::core_config, resource, state::db_client};
|
||||
use crate::{
|
||||
config::core_config,
|
||||
permission::{get_check_permissions, get_resource_ids_for_user},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
@@ -42,18 +45,17 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
let query = if user.admin || core_config().transparent_mode {
|
||||
self.query
|
||||
} else {
|
||||
let server_query =
|
||||
resource::get_resource_ids_for_user::<Server>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Server", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Server" });
|
||||
let server_query = get_resource_ids_for_user::<Server>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Server", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Server" });
|
||||
|
||||
let deployment_query =
|
||||
resource::get_resource_ids_for_user::<Deployment>(user)
|
||||
get_resource_ids_for_user::<Deployment>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
@@ -62,38 +64,35 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
|
||||
|
||||
let stack_query =
|
||||
resource::get_resource_ids_for_user::<Stack>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Stack", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Stack" });
|
||||
let stack_query = get_resource_ids_for_user::<Stack>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Stack", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Stack" });
|
||||
|
||||
let build_query =
|
||||
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 build_query = get_resource_ids_for_user::<Build>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Build", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Build" });
|
||||
|
||||
let repo_query =
|
||||
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 repo_query = get_resource_ids_for_user::<Repo>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Repo", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Repo" });
|
||||
|
||||
let procedure_query =
|
||||
resource::get_resource_ids_for_user::<Procedure>(user)
|
||||
get_resource_ids_for_user::<Procedure>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
@@ -102,57 +101,43 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
|
||||
|
||||
let action_query =
|
||||
resource::get_resource_ids_for_user::<Action>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Action", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Action" });
|
||||
|
||||
let builder_query =
|
||||
resource::get_resource_ids_for_user::<Builder>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Builder", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Builder" });
|
||||
|
||||
let alerter_query =
|
||||
resource::get_resource_ids_for_user::<Alerter>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Alerter", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
|
||||
|
||||
let server_template_query =
|
||||
resource::get_resource_ids_for_user::<ServerTemplate>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "ServerTemplate", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
|
||||
|
||||
let resource_sync_query =
|
||||
resource::get_resource_ids_for_user::<ResourceSync>(
|
||||
user,
|
||||
)
|
||||
let action_query = get_resource_ids_for_user::<Action>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "ResourceSync", "target.id": { "$in": ids }
|
||||
"target.type": "Action", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
|
||||
.unwrap_or_else(|| doc! { "target.type": "Action" });
|
||||
|
||||
let builder_query = get_resource_ids_for_user::<Builder>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Builder", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Builder" });
|
||||
|
||||
let alerter_query = get_resource_ids_for_user::<Alerter>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Alerter", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
|
||||
|
||||
let resource_sync_query = get_resource_ids_for_user::<
|
||||
ResourceSync,
|
||||
>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "ResourceSync", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
|
||||
|
||||
let mut query = self.query.unwrap_or_default();
|
||||
query.extend(doc! {
|
||||
@@ -166,7 +151,6 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
action_query,
|
||||
alerter_query,
|
||||
builder_query,
|
||||
server_template_query,
|
||||
resource_sync_query,
|
||||
]
|
||||
});
|
||||
@@ -242,93 +226,85 @@ impl Resolve<ReadArgs> for GetUpdate {
|
||||
ResourceTarget::System(_) => {
|
||||
return Err(
|
||||
anyhow!("user must be admin to view system updates").into(),
|
||||
)
|
||||
);
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
resource::get_check_permissions::<Server>(
|
||||
get_check_permissions::<Server>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
resource::get_check_permissions::<Deployment>(
|
||||
get_check_permissions::<Deployment>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
resource::get_check_permissions::<Build>(
|
||||
get_check_permissions::<Build>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
resource::get_check_permissions::<Repo>(
|
||||
get_check_permissions::<Repo>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
resource::get_check_permissions::<Builder>(
|
||||
get_check_permissions::<Builder>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
resource::get_check_permissions::<Alerter>(
|
||||
get_check_permissions::<Alerter>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
resource::get_check_permissions::<Procedure>(
|
||||
get_check_permissions::<Procedure>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Action(id) => {
|
||||
resource::get_check_permissions::<Action>(
|
||||
get_check_permissions::<Action>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
resource::get_check_permissions::<ServerTemplate>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::get_check_permissions::<ResourceSync>(
|
||||
get_check_permissions::<ResourceSync>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
resource::get_check_permissions::<Stack>(
|
||||
get_check_permissions::<Stack>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::{
|
||||
FindUser, FindUserResponse, GetUsername, GetUsernameResponse,
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
|
||||
ListUsers, ListUsersResponse,
|
||||
},
|
||||
entities::user::{admin_service_user, UserConfig},
|
||||
entities::user::{UserConfig, admin_service_user},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::find_one_by_id,
|
||||
|
||||
@@ -5,7 +5,7 @@ use komodo_client::api::read::*;
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{
|
||||
bson::{doc, oid::ObjectId, Document},
|
||||
bson::{Document, doc, oid::ObjectId},
|
||||
options::FindOptions,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use komodo_client::{
|
||||
api::terminal::*,
|
||||
entities::{
|
||||
deployment::Deployment, permission::PermissionLevel,
|
||||
server::Server, stack::Stack, user::User,
|
||||
},
|
||||
};
|
||||
use serror::Json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::auth_request, helpers::periphery_client,
|
||||
permission::get_check_permissions, resource::get,
|
||||
state::stack_status_cache,
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/execute", post(execute_terminal))
|
||||
.route("/execute/container", post(execute_container_exec))
|
||||
.route("/execute/deployment", post(execute_deployment_exec))
|
||||
.route("/execute/stack", post(execute_stack_exec))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
// =================
|
||||
// ExecuteTerminal
|
||||
// =================
|
||||
|
||||
async fn execute_terminal(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteTerminalBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
execute_terminal_inner(Uuid::new_v4(), request, user).await
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteTerminal",
|
||||
skip(user),
|
||||
fields(
|
||||
user_id = user.id,
|
||||
)
|
||||
)]
|
||||
async fn execute_terminal_inner(
|
||||
req_id: Uuid,
|
||||
ExecuteTerminalBody {
|
||||
server,
|
||||
terminal,
|
||||
command,
|
||||
}: ExecuteTerminalBody,
|
||||
user: User,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("/terminal/execute request | user: {}", user.username);
|
||||
|
||||
let res = async {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_terminal(terminal, command)
|
||||
.await
|
||||
.context("Failed to execute command on periphery")?;
|
||||
|
||||
anyhow::Ok(stream)
|
||||
}
|
||||
.await;
|
||||
|
||||
let stream = match res {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
warn!("/terminal/execute request {req_id} error: {e:#}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
|
||||
}
|
||||
|
||||
// ======================
|
||||
// ExecuteContainerExec
|
||||
// ======================
|
||||
|
||||
async fn execute_container_exec(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteContainerExecBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
execute_container_exec_inner(Uuid::new_v4(), request, user).await
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteContainerExec",
|
||||
skip(user),
|
||||
fields(
|
||||
user_id = user.id,
|
||||
)
|
||||
)]
|
||||
async fn execute_container_exec_inner(
|
||||
req_id: Uuid,
|
||||
ExecuteContainerExecBody {
|
||||
server,
|
||||
container,
|
||||
shell,
|
||||
command,
|
||||
}: ExecuteContainerExecBody,
|
||||
user: User,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!(
|
||||
"/terminal/execute/container request | user: {}",
|
||||
user.username
|
||||
);
|
||||
|
||||
let res = async {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(container, shell, command)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
)?;
|
||||
|
||||
anyhow::Ok(stream)
|
||||
}
|
||||
.await;
|
||||
|
||||
let stream = match res {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"/terminal/execute/container request {req_id} error: {e:#}"
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
|
||||
}
|
||||
|
||||
// =======================
|
||||
// ExecuteDeploymentExec
|
||||
// =======================
|
||||
|
||||
async fn execute_deployment_exec(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteDeploymentExecBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
execute_deployment_exec_inner(Uuid::new_v4(), request, user).await
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteDeploymentExec",
|
||||
skip(user),
|
||||
fields(
|
||||
user_id = user.id,
|
||||
)
|
||||
)]
|
||||
async fn execute_deployment_exec_inner(
|
||||
req_id: Uuid,
|
||||
ExecuteDeploymentExecBody {
|
||||
deployment,
|
||||
shell,
|
||||
command,
|
||||
}: ExecuteDeploymentExecBody,
|
||||
user: User,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!(
|
||||
"/terminal/execute/deployment request | user: {}",
|
||||
user.username
|
||||
);
|
||||
|
||||
let res = async {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&deployment,
|
||||
&user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server = get::<Server>(&deployment.config.server_id).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(deployment.name, shell, command)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
)?;
|
||||
|
||||
anyhow::Ok(stream)
|
||||
}
|
||||
.await;
|
||||
|
||||
let stream = match res {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"/terminal/execute/deployment request {req_id} error: {e:#}"
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
|
||||
}
|
||||
|
||||
// ==================
|
||||
// ExecuteStackExec
|
||||
// ==================
|
||||
|
||||
async fn execute_stack_exec(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteStackExecBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
execute_stack_exec_inner(Uuid::new_v4(), request, user).await
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteStackExec",
|
||||
skip(user),
|
||||
fields(
|
||||
user_id = user.id,
|
||||
)
|
||||
)]
|
||||
async fn execute_stack_exec_inner(
|
||||
req_id: Uuid,
|
||||
ExecuteStackExecBody {
|
||||
stack,
|
||||
service,
|
||||
shell,
|
||||
command,
|
||||
}: ExecuteStackExecBody,
|
||||
user: User,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("/terminal/execute/stack request | user: {}", user.username);
|
||||
|
||||
let res = async {
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&stack,
|
||||
&user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server = get::<Server>(&stack.config.server_id).await?;
|
||||
|
||||
let container = stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.context("could not get stack status")?
|
||||
.curr
|
||||
.services
|
||||
.iter()
|
||||
.find(|s| s.service == service)
|
||||
.context("could not find service")?
|
||||
.container
|
||||
.as_ref()
|
||||
.context("could not find service container")?
|
||||
.name
|
||||
.clone();
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(container, shell, command)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
)?;
|
||||
|
||||
anyhow::Ok(stream)
|
||||
}
|
||||
.await;
|
||||
|
||||
let stream = match res {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
warn!("/terminal/execute/stack request {req_id} error: {e:#}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream.into_line_stream()))
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::{collections::VecDeque, time::Instant};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Json, Router};
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Extension, Json, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use derive_variants::EnumVariants;
|
||||
use komodo_client::{
|
||||
api::user::*,
|
||||
@@ -12,6 +14,7 @@ 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;
|
||||
|
||||
@@ -21,6 +24,8 @@ use crate::{
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub struct UserArgs {
|
||||
pub user: User,
|
||||
}
|
||||
@@ -43,9 +48,22 @@ 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>,
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::resource;
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -29,13 +29,12 @@ impl Resolve<WriteArgs> for CopyAction {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Action> {
|
||||
let Action { config, .. } =
|
||||
resource::get_check_permissions::<Action>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let Action { config, .. } = get_check_permissions::<Action>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Action>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::resource;
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -29,13 +29,12 @@ impl Resolve<WriteArgs> for CopyAlerter {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
let Alerter { config, .. } =
|
||||
resource::get_check_permissions::<Alerter>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let Alerter { config, .. } = get_check_permissions::<Alerter>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Alerter>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
|
||||
+414
-56
@@ -1,13 +1,19 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use git::GitRes;
|
||||
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
FileContents, NoData, Operation, RepoExecutionArgs,
|
||||
all_logs_success,
|
||||
build::{Build, BuildInfo, PartialBuildConfig},
|
||||
builder::{Builder, BuilderConfig},
|
||||
config::core::CoreConfig,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::ServerState,
|
||||
update::Update,
|
||||
CloneArgs, NoData,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
@@ -15,11 +21,23 @@ 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,
|
||||
helpers::{
|
||||
git_token, periphery_client,
|
||||
query::get_server_with_state,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{db_client, github_client},
|
||||
};
|
||||
@@ -45,13 +63,12 @@ impl Resolve<WriteArgs> for CopyBuild {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Build> {
|
||||
let Build { mut config, .. } =
|
||||
resource::get_check_permissions::<Build>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let Build { mut config, .. } = get_check_permissions::<Build>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
// reset version to 0.0.0
|
||||
config.version = Default::default();
|
||||
Ok(
|
||||
@@ -88,6 +105,221 @@ 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: RepoExecutionArgs = if !build
|
||||
.config
|
||||
.files_on_host
|
||||
&& !build.config.linked_repo.is_empty()
|
||||
{
|
||||
(&crate::resource::get::<Repo>(&build.config.linked_repo).await?)
|
||||
.into()
|
||||
} else {
|
||||
(&build).into()
|
||||
};
|
||||
let root = 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,
|
||||
)
|
||||
.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",
|
||||
@@ -100,62 +332,70 @@ 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 = resource::get_check_permissions::<Build>(
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if build.config.repo.is_empty()
|
||||
|| build.config.git_provider.is_empty()
|
||||
let repo = if !build.config.files_on_host
|
||||
&& !build.config.linked_repo.is_empty()
|
||||
{
|
||||
// Nothing to do here
|
||||
return Ok(NoData {});
|
||||
}
|
||||
|
||||
let config = core_config();
|
||||
|
||||
let mut clone_args: CloneArgs = (&build).into();
|
||||
let repo_path =
|
||||
clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
// Don't want to run these on core.
|
||||
clone_args.on_clone = None;
|
||||
clone_args.on_pull = None;
|
||||
|
||||
let access_token = if let Some(username) = &clone_args.account {
|
||||
git_token(&clone_args.provider, username, |https| {
|
||||
clone_args.https = https
|
||||
})
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
|
||||
)?
|
||||
crate::resource::get::<Repo>(&build.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
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 (
|
||||
remote_path,
|
||||
remote_contents,
|
||||
remote_error,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
) = if build.config.files_on_host {
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
match get_on_host_dockerfile(&build).await {
|
||||
Ok(FileContents { path, contents }) => {
|
||||
(Some(path), Some(contents), None, None, None)
|
||||
}
|
||||
Err(e) => {
|
||||
(None, None, Some(format_serror(&e.into())), None, None)
|
||||
}
|
||||
}
|
||||
} else if let Some(repo) = &repo {
|
||||
let Some(res) = get_git_remote(&build, repo.into()).await?
|
||||
else {
|
||||
// Nothing to do here
|
||||
return Ok(NoData {});
|
||||
};
|
||||
res
|
||||
} else if !build.config.repo.is_empty() {
|
||||
let Some(res) = get_git_remote(&build, (&build).into()).await?
|
||||
else {
|
||||
// Nothing to do here
|
||||
return Ok(NoData {});
|
||||
};
|
||||
res
|
||||
} else {
|
||||
// =============
|
||||
// UI BASED FILE
|
||||
// =============
|
||||
(None, None, None, None, None)
|
||||
};
|
||||
|
||||
let info = BuildInfo {
|
||||
last_built_at: build.info.last_built_at,
|
||||
built_hash: build.info.built_hash,
|
||||
built_message: build.info.built_message,
|
||||
built_contents: build.info.built_contents,
|
||||
remote_path,
|
||||
remote_contents,
|
||||
remote_error,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
};
|
||||
@@ -176,6 +416,124 @@ 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: RepoExecutionArgs,
|
||||
) -> anyhow::Result<
|
||||
Option<(
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)>,
|
||||
> {
|
||||
if clone_args.provider.is_empty() {
|
||||
// Nothing to do here
|
||||
return Ok(None);
|
||||
}
|
||||
let config = core_config();
|
||||
let repo_path = clone_args.unique_path(&config.repo_directory)?;
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
|
||||
let access_token = if let Some(username) = &clone_args.account {
|
||||
git_token(&clone_args.provider, username, |https| {
|
||||
clone_args.https = https
|
||||
})
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (res, _) = git::pull_or_clone(
|
||||
clone_args,
|
||||
&config.repo_directory,
|
||||
access_token,
|
||||
)
|
||||
.await
|
||||
.context("failed to clone build repo")?;
|
||||
|
||||
let relative_path = PathBuf::from_str(&build.config.build_path)
|
||||
.context("Invalid build path")?
|
||||
.join(&build.config.dockerfile_path);
|
||||
|
||||
let full_path = repo_path.join(&relative_path);
|
||||
let (contents, error) =
|
||||
match fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!("Failed to read dockerfile contents at {full_path:?}")
|
||||
}) {
|
||||
Ok(contents) => (Some(contents), None),
|
||||
Err(e) => (None, Some(format_serror(&e.into()))),
|
||||
};
|
||||
Ok(Some((
|
||||
Some(relative_path.display().to_string()),
|
||||
contents,
|
||||
error,
|
||||
res.commit_hash,
|
||||
res.commit_message,
|
||||
)))
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CreateBuildWebhook {
|
||||
#[instrument(name = "CreateBuildWebhook", skip(args))]
|
||||
async fn resolve(
|
||||
@@ -193,10 +551,10 @@ impl Resolve<WriteArgs> for CreateBuildWebhook {
|
||||
|
||||
let WriteArgs { user } = args;
|
||||
|
||||
let build = resource::get_check_permissions::<Build>(
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -306,10 +664,10 @@ impl Resolve<WriteArgs> for DeleteBuildWebhook {
|
||||
);
|
||||
};
|
||||
|
||||
let build = resource::get_check_permissions::<Build>(
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::resource;
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -29,13 +29,12 @@ impl Resolve<WriteArgs> for CopyBuilder {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
let Builder { config, .. } =
|
||||
resource::get_check_permissions::<Builder>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let Builder { config, .. } = get_check_permissions::<Builder>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Builder>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
@@ -72,9 +71,6 @@ 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?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
Operation,
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, DeploymentState,
|
||||
PartialDeploymentConfig, RestartMode,
|
||||
@@ -10,9 +11,8 @@ use komodo_client::{
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
to_komodo_name,
|
||||
to_container_compatible_name,
|
||||
update::Update,
|
||||
Operation,
|
||||
},
|
||||
};
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
|
||||
@@ -25,6 +25,7 @@ use crate::{
|
||||
query::get_deployment_state,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
};
|
||||
@@ -51,19 +52,15 @@ impl Resolve<WriteArgs> for CopyDeployment {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
let Deployment { config, .. } =
|
||||
resource::get_check_permissions::<Deployment>(
|
||||
get_check_permissions::<Deployment>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Deployment>(
|
||||
&self.name,
|
||||
config.into(),
|
||||
user,
|
||||
)
|
||||
.await?,
|
||||
resource::create::<Deployment>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -74,10 +71,10 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Read.inspect().attach(),
|
||||
)
|
||||
.await?;
|
||||
let cache = server_status_cache()
|
||||
@@ -192,10 +189,10 @@ impl Resolve<WriteArgs> for RenameDeployment {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let deployment = resource::get_check_permissions::<Deployment>(
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -210,9 +207,10 @@ impl Resolve<WriteArgs> for RenameDeployment {
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.renaming = true)?;
|
||||
|
||||
let name = to_komodo_name(&self.name);
|
||||
let name = to_container_compatible_name(&self.name);
|
||||
|
||||
let container_state = get_deployment_state(&deployment).await?;
|
||||
let container_state =
|
||||
get_deployment_state(&deployment.id).await?;
|
||||
|
||||
if container_state == DeploymentState::Unknown {
|
||||
return Err(
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use komodo_client::{
|
||||
api::write::{UpdateDescription, UpdateDescriptionResponse},
|
||||
entities::{
|
||||
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;
|
||||
|
||||
use crate::resource;
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateDescription {
|
||||
#[instrument(name = "UpdateDescription", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateDescriptionResponse> {
|
||||
match self.target {
|
||||
ResourceTarget::System(_) => {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"cannot update description of System resource target"
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
resource::update_description::<Server>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
resource::update_description::<Deployment>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
resource::update_description::<Build>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
resource::update_description::<Repo>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
resource::update_description::<Builder>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
resource::update_description::<Alerter>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
resource::update_description::<Procedure>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Action(id) => {
|
||||
resource::update_description::<Action>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
resource::update_description::<ServerTemplate>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::update_description::<ResourceSync>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
resource::update_description::<Stack>(
|
||||
&id,
|
||||
&self.description,
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(UpdateDescriptionResponse {})
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,34 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::write::*, entities::user::User};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::auth_request;
|
||||
|
||||
use super::Variant;
|
||||
|
||||
mod action;
|
||||
mod alerter;
|
||||
mod build;
|
||||
mod builder;
|
||||
mod deployment;
|
||||
mod description;
|
||||
mod permissions;
|
||||
mod procedure;
|
||||
mod provider;
|
||||
mod repo;
|
||||
mod resource;
|
||||
mod server;
|
||||
mod server_template;
|
||||
mod service_user;
|
||||
mod stack;
|
||||
mod sync;
|
||||
@@ -65,6 +69,7 @@ pub enum WriteRequest {
|
||||
AddUserToUserGroup(AddUserToUserGroup),
|
||||
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
|
||||
SetUsersInUserGroup(SetUsersInUserGroup),
|
||||
SetEveryoneUserGroup(SetEveryoneUserGroup),
|
||||
|
||||
// ==== PERMISSIONS ====
|
||||
UpdateUserAdmin(UpdateUserAdmin),
|
||||
@@ -72,15 +77,30 @@ pub enum WriteRequest {
|
||||
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
|
||||
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
|
||||
|
||||
// ==== DESCRIPTION ====
|
||||
UpdateDescription(UpdateDescription),
|
||||
// ==== RESOURCE ====
|
||||
UpdateResourceMeta(UpdateResourceMeta),
|
||||
|
||||
// ==== SERVER ====
|
||||
CreateServer(CreateServer),
|
||||
CopyServer(CopyServer),
|
||||
DeleteServer(DeleteServer),
|
||||
UpdateServer(UpdateServer),
|
||||
RenameServer(RenameServer),
|
||||
CreateNetwork(CreateNetwork),
|
||||
CreateTerminal(CreateTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
|
||||
// ==== STACK ====
|
||||
CreateStack(CreateStack),
|
||||
CopyStack(CopyStack),
|
||||
DeleteStack(DeleteStack),
|
||||
UpdateStack(UpdateStack),
|
||||
RenameStack(RenameStack),
|
||||
WriteStackFileContents(WriteStackFileContents),
|
||||
RefreshStackCache(RefreshStackCache),
|
||||
CreateStackWebhook(CreateStackWebhook),
|
||||
DeleteStackWebhook(DeleteStackWebhook),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment(CreateDeployment),
|
||||
@@ -96,6 +116,7 @@ pub enum WriteRequest {
|
||||
DeleteBuild(DeleteBuild),
|
||||
UpdateBuild(UpdateBuild),
|
||||
RenameBuild(RenameBuild),
|
||||
WriteBuildFileContents(WriteBuildFileContents),
|
||||
RefreshBuildCache(RefreshBuildCache),
|
||||
CreateBuildWebhook(CreateBuildWebhook),
|
||||
DeleteBuildWebhook(DeleteBuildWebhook),
|
||||
@@ -107,13 +128,6 @@ pub enum WriteRequest {
|
||||
UpdateBuilder(UpdateBuilder),
|
||||
RenameBuilder(RenameBuilder),
|
||||
|
||||
// ==== SERVER TEMPLATE ====
|
||||
CreateServerTemplate(CreateServerTemplate),
|
||||
CopyServerTemplate(CopyServerTemplate),
|
||||
DeleteServerTemplate(DeleteServerTemplate),
|
||||
UpdateServerTemplate(UpdateServerTemplate),
|
||||
RenameServerTemplate(RenameServerTemplate),
|
||||
|
||||
// ==== REPO ====
|
||||
CreateRepo(CreateRepo),
|
||||
CopyRepo(CopyRepo),
|
||||
@@ -157,22 +171,11 @@ 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),
|
||||
UpdateTagsOnResource(UpdateTagsOnResource),
|
||||
UpdateTagColor(UpdateTagColor),
|
||||
|
||||
// ==== VARIABLE ====
|
||||
CreateVariable(CreateVariable),
|
||||
@@ -193,9 +196,22 @@ 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>,
|
||||
@@ -206,10 +222,6 @@ async fn handler(
|
||||
.await
|
||||
.context("failure in spawned task");
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/write request {req_id} spawn error: {e:#}");
|
||||
}
|
||||
|
||||
res?
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
permission::{UserTarget, UserTargetVariant},
|
||||
ResourceTarget, ResourceTargetVariant,
|
||||
permission::{UserTarget, UserTargetVariant},
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
by_id::{find_one_by_id, update_one_by_id},
|
||||
mongodb::{
|
||||
bson::{doc, oid::ObjectId, Document},
|
||||
bson::{Document, doc, oid::ObjectId, to_bson},
|
||||
options::UpdateOptions,
|
||||
},
|
||||
};
|
||||
@@ -65,6 +65,10 @@ 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,
|
||||
@@ -72,10 +76,6 @@ 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> {
|
||||
let UpdatePermissionOnResourceType {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("this method is admin only").into());
|
||||
}
|
||||
|
||||
let Self {
|
||||
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,9 +153,11 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
|
||||
|
||||
let id = ObjectId::from_str(&user_target_id)
|
||||
.context("id is not ObjectId")?;
|
||||
let field = format!("all.{resource_type}");
|
||||
let filter = doc! { "_id": id };
|
||||
let update = doc! { "$set": { &field: permission.as_ref() } };
|
||||
let field = format!("all.{resource_type}");
|
||||
let set =
|
||||
to_bson(&permission).context("permission is not Bson")?;
|
||||
let update = doc! { "$set": { &field: &set } };
|
||||
|
||||
match user_target_variant {
|
||||
UserTargetVariant::User => {
|
||||
@@ -164,7 +166,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
|
||||
.update_one(filter, update)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to set {field}: {permission} on db")
|
||||
format!("failed to set {field}: {set} on db")
|
||||
})?;
|
||||
}
|
||||
UserTargetVariant::UserGroup => {
|
||||
@@ -173,7 +175,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
|
||||
.update_one(filter, update)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to set {field}: {permission} on db")
|
||||
format!("failed to set {field}: {set} on db")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -188,19 +190,22 @@ 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;
|
||||
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("this method is admin only").into());
|
||||
}
|
||||
|
||||
// Some extra checks if user target is an actual User
|
||||
// Some extra checks relevant if user target is an actual User
|
||||
if let UserTarget::User(user_id) = &user_target {
|
||||
let user = get_user(user_id).await?;
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("user not enabled").into());
|
||||
}
|
||||
if user.admin {
|
||||
return Err(
|
||||
anyhow!(
|
||||
@@ -209,9 +214,6 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("user not enabled").into());
|
||||
}
|
||||
}
|
||||
|
||||
let (user_target_variant, user_target_id) =
|
||||
@@ -223,6 +225,9 @@ 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(
|
||||
@@ -238,7 +243,8 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
|
||||
"user_target.id": user_target_id,
|
||||
"resource_target.type": resource_variant,
|
||||
"resource_target.id": resource_id,
|
||||
"level": permission.as_ref(),
|
||||
"level": permission.level.as_ref(),
|
||||
"specific": specific
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -406,20 +412,6 @@ 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 },
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::resource;
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -30,10 +30,10 @@ impl Resolve<WriteArgs> for CopyProcedure {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CopyProcedureResponse> {
|
||||
let Procedure { config, .. } =
|
||||
resource::get_check_permissions::<Procedure>(
|
||||
get_check_permissions::<Procedure>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
provider::{DockerRegistryAccount, GitProviderAccount},
|
||||
Operation, ResourceTarget,
|
||||
provider::{DockerRegistryAccount, GitProviderAccount},
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use git::GitRes;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
NoData, Operation, RepoExecutionArgs,
|
||||
config::core::CoreConfig,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
repo::{PartialRepoConfig, Repo, RepoInfo},
|
||||
server::Server,
|
||||
to_komodo_name,
|
||||
to_path_compatible_name,
|
||||
update::{Log, Update},
|
||||
CloneArgs, NoData, Operation,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
@@ -28,6 +27,7 @@ use crate::{
|
||||
git_token, periphery_client,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, db_client, github_client},
|
||||
};
|
||||
@@ -50,13 +50,12 @@ impl Resolve<WriteArgs> for CopyRepo {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
let Repo { config, .. } =
|
||||
resource::get_check_permissions::<Repo>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let Repo { config, .. } = get_check_permissions::<Repo>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Repo>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
@@ -87,10 +86,10 @@ impl Resolve<WriteArgs> for RenameRepo {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let repo = resource::get_check_permissions::<Repo>(
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -111,7 +110,7 @@ impl Resolve<WriteArgs> for RenameRepo {
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.renaming = true)?;
|
||||
|
||||
let name = to_komodo_name(&self.name);
|
||||
let name = to_path_compatible_name(&self.name);
|
||||
|
||||
let mut update = make_update(&repo, Operation::RenameRepo, user);
|
||||
|
||||
@@ -131,7 +130,7 @@ impl Resolve<WriteArgs> for RenameRepo {
|
||||
|
||||
let log = match periphery_client(&server)?
|
||||
.request(api::git::RenameRepo {
|
||||
curr_name: to_komodo_name(&repo.name),
|
||||
curr_name: to_path_compatible_name(&repo.name),
|
||||
new_name: name.clone(),
|
||||
})
|
||||
.await
|
||||
@@ -169,10 +168,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 = resource::get_check_permissions::<Repo>(
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -183,13 +182,10 @@ impl Resolve<WriteArgs> for RefreshRepoCache {
|
||||
return Ok(NoData {});
|
||||
}
|
||||
|
||||
let mut clone_args: CloneArgs = (&repo).into();
|
||||
let mut clone_args: RepoExecutionArgs = (&repo).into();
|
||||
let repo_path =
|
||||
clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
// 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| {
|
||||
@@ -203,14 +199,10 @@ impl Resolve<WriteArgs> for RefreshRepoCache {
|
||||
None
|
||||
};
|
||||
|
||||
let GitRes { hash, message, .. } = git::pull_or_clone(
|
||||
let (res, _) = git::pull_or_clone(
|
||||
clone_args,
|
||||
&core_config().repo_directory,
|
||||
access_token,
|
||||
&[],
|
||||
"",
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
@@ -222,8 +214,8 @@ impl Resolve<WriteArgs> for RefreshRepoCache {
|
||||
last_built_at: repo.info.last_built_at,
|
||||
built_hash: repo.info.built_hash,
|
||||
built_message: repo.info.built_message,
|
||||
latest_hash: hash,
|
||||
latest_message: message,
|
||||
latest_hash: res.commit_hash,
|
||||
latest_message: res.commit_message,
|
||||
};
|
||||
|
||||
let info = to_document(&info)
|
||||
@@ -257,10 +249,10 @@ impl Resolve<WriteArgs> for CreateRepoWebhook {
|
||||
);
|
||||
};
|
||||
|
||||
let repo = resource::get_check_permissions::<Repo>(
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
&args.user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -380,10 +372,10 @@ impl Resolve<WriteArgs> for DeleteRepoWebhook {
|
||||
);
|
||||
};
|
||||
|
||||
let repo = resource::get_check_permissions::<Repo>(
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use anyhow::anyhow;
|
||||
use komodo_client::{
|
||||
api::write::{UpdateResourceMeta, UpdateResourceMetaResponse},
|
||||
entities::{
|
||||
ResourceTarget, action::Action, alerter::Alerter, build::Build,
|
||||
builder::Builder, deployment::Deployment, procedure::Procedure,
|
||||
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::resource::{self, ResourceMetaUpdate};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateResourceMeta {
|
||||
#[instrument(name = "UpdateResourceMeta", skip(args))]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &WriteArgs,
|
||||
) -> serror::Result<UpdateResourceMetaResponse> {
|
||||
let meta = ResourceMetaUpdate {
|
||||
description: self.description,
|
||||
template: self.template,
|
||||
tags: self.tags,
|
||||
};
|
||||
match self.target {
|
||||
ResourceTarget::System(_) => {
|
||||
return Err(
|
||||
anyhow!("cannot update meta of System resource target")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
resource::update_meta::<Server>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
resource::update_meta::<Deployment>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
resource::update_meta::<Build>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
resource::update_meta::<Repo>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
resource::update_meta::<Builder>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
resource::update_meta::<Alerter>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
resource::update_meta::<Procedure>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Action(id) => {
|
||||
resource::update_meta::<Action>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::update_meta::<ResourceSync>(&id, meta, args)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
resource::update_meta::<Stack>(&id, meta, args).await?;
|
||||
}
|
||||
}
|
||||
Ok(UpdateResourceMetaResponse {})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
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;
|
||||
@@ -16,6 +18,7 @@ use crate::{
|
||||
periphery_client,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
};
|
||||
|
||||
@@ -34,6 +37,25 @@ impl Resolve<WriteArgs> for CreateServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CopyServer {
|
||||
#[instrument(name = "CopyServer", skip(user))]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Server> {
|
||||
let Server { config, .. } = get_check_permissions::<Server>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Server>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteServer {
|
||||
#[instrument(name = "DeleteServer", skip(args))]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Server> {
|
||||
@@ -67,10 +89,10 @@ impl Resolve<WriteArgs> for CreateNetwork {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let server = resource::get_check_permissions::<Server>(
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -83,7 +105,7 @@ impl Resolve<WriteArgs> for CreateNetwork {
|
||||
|
||||
match periphery
|
||||
.request(api::network::CreateNetwork {
|
||||
name: self.name,
|
||||
name: to_docker_compatible_name(&self.name),
|
||||
driver: None,
|
||||
})
|
||||
.await
|
||||
@@ -101,3 +123,81 @@ 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 {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
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,6 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::{user::CreateApiKey, write::*},
|
||||
entities::{
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
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};
|
||||
@@ -26,14 +27,16 @@ use crate::{
|
||||
api::execute::pull_stack_inner,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
git_token, periphery_client,
|
||||
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::{get_repo_compose_contents, RemoteComposeContents},
|
||||
remote::{RemoteComposeContents, get_repo_compose_contents},
|
||||
services::extract_services_into_res,
|
||||
},
|
||||
state::{db_client, github_client},
|
||||
@@ -60,13 +63,12 @@ impl Resolve<WriteArgs> for CopyStack {
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
let Stack { config, .. } =
|
||||
resource::get_check_permissions::<Stack>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
let Stack { config, .. } = get_check_permissions::<Stack>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
resource::create::<Stack>(&self.name, config.into(), user)
|
||||
.await?,
|
||||
@@ -115,14 +117,27 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
|
||||
let (mut stack, server) = get_stack_and_server(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !stack.config.files_on_host && stack.config.repo.is_empty() {
|
||||
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()
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Stack is not configured to use Files on Host or Git Repo, can't write file contents"
|
||||
"Stack is not configured to use Files on Host, Git Repo, or Linked Repo, can't write file contents"
|
||||
).into());
|
||||
}
|
||||
|
||||
@@ -155,25 +170,12 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let git_token = if !stack.config.git_account.is_empty() {
|
||||
git_token(
|
||||
&stack.config.git_provider,
|
||||
&stack.config.git_account,
|
||||
|https| stack.config.git_https = https,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to get git token. | {} | {}",
|
||||
stack.config.git_account, stack.config.git_provider
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let git_token =
|
||||
stack_git_token(&mut stack, repo.as_mut()).await?;
|
||||
match periphery_client(&server)?
|
||||
.request(WriteCommitComposeContents {
|
||||
stack,
|
||||
repo,
|
||||
username: Some(user.username.clone()),
|
||||
file_path,
|
||||
contents,
|
||||
@@ -211,7 +213,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
add_update(update.clone()).await?;
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
@@ -229,15 +231,26 @@ 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 = resource::get_check_permissions::<Stack>(
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let repo = if !stack.config.files_on_host
|
||||
&& !stack.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&stack.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let file_contents_empty = stack.config.file_contents.is_empty();
|
||||
let repo_empty = stack.config.repo.is_empty();
|
||||
let repo_empty =
|
||||
stack.config.repo.is_empty() && repo.as_ref().is_none();
|
||||
|
||||
if !stack.config.files_on_host
|
||||
&& file_contents_empty
|
||||
@@ -320,8 +333,12 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
hash: latest_hash,
|
||||
message: latest_message,
|
||||
..
|
||||
} = get_repo_compose_contents(&stack, Some(&mut missing_files))
|
||||
.await?;
|
||||
} = get_repo_compose_contents(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
Some(&mut missing_files),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
@@ -402,7 +419,8 @@ 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, None).await
|
||||
pull_stack_inner(stack, Vec::new(), &server, repo, None)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to pull latest images for Stack {name} | {e:#}",
|
||||
@@ -432,10 +450,10 @@ impl Resolve<WriteArgs> for CreateStackWebhook {
|
||||
);
|
||||
};
|
||||
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -552,10 +570,10 @@ impl Resolve<WriteArgs> for DeleteStackWebhook {
|
||||
);
|
||||
};
|
||||
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
+395
-268
@@ -1,11 +1,14 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::{read::ExportAllResourcesToToml, write::*},
|
||||
entities::{
|
||||
self,
|
||||
self, NoData, Operation, RepoExecutionArgs, ResourceTarget,
|
||||
action::Action,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
alerter::Alerter,
|
||||
@@ -19,15 +22,14 @@ use komodo_client::{
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
sync::{
|
||||
PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,
|
||||
SyncDeployUpdate,
|
||||
},
|
||||
to_komodo_name,
|
||||
to_path_compatible_name,
|
||||
update::{Log, Update},
|
||||
user::sync_user,
|
||||
CloneArgs, NoData, Operation, ResourceTarget,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -45,15 +47,17 @@ use crate::{
|
||||
api::read::ReadArgs,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
all_resources::AllResourcesById,
|
||||
git_token,
|
||||
query::get_id_to_tags,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{db_client, github_client},
|
||||
sync::{
|
||||
deploy::SyncDeployParams, remote::RemoteResources,
|
||||
view::push_updates_for_view, AllResourcesById,
|
||||
view::push_updates_for_view,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -79,10 +83,10 @@ impl Resolve<WriteArgs> for CopyResourceSync {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
let ResourceSync { config, .. } =
|
||||
resource::get_check_permissions::<ResourceSync>(
|
||||
get_check_permissions::<ResourceSync>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
@@ -132,6 +136,52 @@ impl Resolve<WriteArgs> for RenameResourceSync {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for WriteSyncFileContents {
|
||||
#[instrument(name = "WriteSyncFileContents", skip(args))]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
&args.user,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let repo = if !sync.config.files_on_host
|
||||
&& !sync.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&sync.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !sync.config.files_on_host
|
||||
&& sync.config.repo.is_empty()
|
||||
&& sync.config.linked_repo.is_empty()
|
||||
{
|
||||
return Err(
|
||||
anyhow!(
|
||||
"This method is only for 'files on host' or 'repo' based syncs."
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&sync, Operation::WriteSyncContents, &args.user);
|
||||
|
||||
update.push_simple_log("File contents", &self.contents);
|
||||
|
||||
if sync.config.files_on_host {
|
||||
write_sync_file_contents_on_host(self, args, sync, update).await
|
||||
} else {
|
||||
write_sync_file_contents_git(self, args, sync, repo, update)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_sync_file_contents_on_host(
|
||||
req: WriteSyncFileContents,
|
||||
args: &WriteArgs,
|
||||
@@ -147,7 +197,7 @@ async fn write_sync_file_contents_on_host(
|
||||
|
||||
let root = core_config()
|
||||
.sync_directory
|
||||
.join(to_komodo_name(&sync.name));
|
||||
.join(to_path_compatible_name(&sync.name));
|
||||
let file_path =
|
||||
file_path.parse::<PathBuf>().context("Invalid file path")?;
|
||||
let resource_path = resource_path
|
||||
@@ -165,7 +215,9 @@ async fn write_sync_file_contents_on_host(
|
||||
|
||||
if let Err(e) =
|
||||
fs::write(&full_path, &contents).await.with_context(|| {
|
||||
format!("Failed to write resource file contents to {full_path:?}")
|
||||
format!(
|
||||
"Failed to write resource file contents to {full_path:?}"
|
||||
)
|
||||
})
|
||||
{
|
||||
update.push_error_log("Write File", format_serror(&e.into()));
|
||||
@@ -203,6 +255,7 @@ async fn write_sync_file_contents_git(
|
||||
req: WriteSyncFileContents,
|
||||
args: &WriteArgs,
|
||||
sync: ResourceSync,
|
||||
repo: Option<Repo>,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
let WriteSyncFileContents {
|
||||
@@ -212,15 +265,34 @@ async fn write_sync_file_contents_git(
|
||||
contents,
|
||||
} = req;
|
||||
|
||||
let mut clone_args: CloneArgs = (&sync).into();
|
||||
let mut clone_args: RepoExecutionArgs = if let Some(repo) = &repo {
|
||||
repo.into()
|
||||
} else {
|
||||
(&sync).into()
|
||||
};
|
||||
let root = clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(root.display().to_string());
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
let file_path =
|
||||
file_path.parse::<PathBuf>().context("Invalid file path")?;
|
||||
let resource_path = resource_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
let full_path = root.join(&resource_path).join(&file_path);
|
||||
let full_path = root
|
||||
.join(&resource_path)
|
||||
.join(&file_path)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
|
||||
if let Some(parent) = full_path.parent() {
|
||||
fs::create_dir_all(parent).await.with_context(|| {
|
||||
@@ -233,16 +305,6 @@ async fn write_sync_file_contents_git(
|
||||
// Ensure the folder is initialized as git repo.
|
||||
// This allows a new file to be committed on a branch that may not exist.
|
||||
if !root.join(".git").exists() {
|
||||
let access_token = if let Some(account) = &clone_args.account {
|
||||
git_token(&clone_args.provider, account, |https| clone_args.https = https)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
git::init_folder_as_repo(
|
||||
&root,
|
||||
&clone_args,
|
||||
@@ -254,14 +316,38 @@ async fn write_sync_file_contents_git(
|
||||
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,
|
||||
)
|
||||
.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 resource file contents to {full_path:?}")
|
||||
format!(
|
||||
"Failed to write resource file contents to {full_path:?}"
|
||||
)
|
||||
})
|
||||
{
|
||||
update.push_error_log("Write File", format_serror(&e.into()));
|
||||
@@ -294,7 +380,7 @@ async fn write_sync_file_contents_git(
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh failed",
|
||||
"Refresh sync pending",
|
||||
format_serror(&e.error.into()),
|
||||
);
|
||||
}
|
||||
@@ -305,51 +391,33 @@ async fn write_sync_file_contents_git(
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for WriteSyncFileContents {
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
&args.user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !sync.config.files_on_host && sync.config.repo.is_empty() {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"This method is only for 'files on host' or 'repo' based syncs."
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&sync, Operation::WriteSyncContents, &args.user);
|
||||
|
||||
update.push_simple_log("File contents", &self.contents);
|
||||
|
||||
if sync.config.files_on_host {
|
||||
write_sync_file_contents_on_host(self, args, sync, update).await
|
||||
} else {
|
||||
write_sync_file_contents_git(self, args, sync, update).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CommitSync {
|
||||
#[instrument(name = "CommitSync", skip(args))]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
let WriteArgs { user } = args;
|
||||
|
||||
let sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&self.sync, user, PermissionLevel::Write)
|
||||
let sync = get_check_permissions::<entities::sync::ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let repo = if !sync.config.files_on_host
|
||||
&& !sync.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&sync.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let file_contents_empty = sync.config.file_contents_empty();
|
||||
|
||||
let fresh_sync = !sync.config.files_on_host
|
||||
&& sync.config.repo.is_empty()
|
||||
&& repo.is_none()
|
||||
&& file_contents_empty;
|
||||
|
||||
if !sync.config.managed && !fresh_sync {
|
||||
@@ -360,32 +428,37 @@ impl Resolve<WriteArgs> for CommitSync {
|
||||
}
|
||||
|
||||
// Get this here so it can fail before update created.
|
||||
let resource_path =
|
||||
if sync.config.files_on_host || !sync.config.repo.is_empty() {
|
||||
let resource_path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.first()
|
||||
.context("Sync does not have resource path configured.")?
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
let resource_path = if sync.config.files_on_host
|
||||
|| !sync.config.repo.is_empty()
|
||||
|| repo.is_some()
|
||||
{
|
||||
let resource_path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.first()
|
||||
.context("Sync does not have resource path configured.")?
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
|
||||
if resource_path
|
||||
.extension()
|
||||
.context("Resource path missing '.toml' extension")?
|
||||
!= "toml"
|
||||
{
|
||||
return Err(
|
||||
anyhow!("Resource path missing '.toml' extension").into(),
|
||||
);
|
||||
}
|
||||
Some(resource_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if resource_path
|
||||
.extension()
|
||||
.context("Resource path missing '.toml' extension")?
|
||||
!= "toml"
|
||||
{
|
||||
return Err(
|
||||
anyhow!("Resource path missing '.toml' extension").into(),
|
||||
);
|
||||
}
|
||||
Some(resource_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let res = ExportAllResourcesToToml {
|
||||
include_resources: sync.config.include_resources,
|
||||
tags: sync.config.match_tags.clone(),
|
||||
include_variables: sync.config.include_variables,
|
||||
include_user_groups: sync.config.include_user_groups,
|
||||
}
|
||||
.resolve(&ReadArgs {
|
||||
user: sync_user().to_owned(),
|
||||
@@ -404,7 +477,7 @@ impl Resolve<WriteArgs> for CommitSync {
|
||||
};
|
||||
let file_path = core_config()
|
||||
.sync_directory
|
||||
.join(to_komodo_name(&sync.name))
|
||||
.join(to_path_compatible_name(&sync.name))
|
||||
.join(&resource_path);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
@@ -430,34 +503,43 @@ impl Resolve<WriteArgs> for CommitSync {
|
||||
format!("File contents written to {file_path:?}"),
|
||||
);
|
||||
}
|
||||
} else if let Some(repo) = &repo {
|
||||
let Some(resource_path) = resource_path else {
|
||||
// Resource path checked above for repo mode.
|
||||
unreachable!()
|
||||
};
|
||||
let args: RepoExecutionArgs = repo.into();
|
||||
if let Err(e) =
|
||||
commit_git_sync(args, &resource_path, &res.toml, &mut update)
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
} else if !sync.config.repo.is_empty() {
|
||||
let Some(resource_path) = resource_path else {
|
||||
// Resource path checked above for repo mode.
|
||||
unreachable!()
|
||||
};
|
||||
// GIT REPO
|
||||
let args: CloneArgs = (&sync).into();
|
||||
let root = args.unique_path(&core_config().repo_directory)?;
|
||||
match git::write_commit_file(
|
||||
"Commit Sync",
|
||||
&root,
|
||||
&resource_path,
|
||||
&res.toml,
|
||||
&sync.config.branch,
|
||||
)
|
||||
.await
|
||||
let args: RepoExecutionArgs = (&sync).into();
|
||||
if let Err(e) =
|
||||
commit_git_sync(args, &resource_path, &res.toml, &mut update)
|
||||
.await
|
||||
{
|
||||
Ok(res) => update.logs.extend(res.logs),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
// ===========
|
||||
// UI DEFINED
|
||||
} else if let Err(e) = db_client()
|
||||
@@ -495,6 +577,49 @@ impl Resolve<WriteArgs> for CommitSync {
|
||||
}
|
||||
}
|
||||
|
||||
async fn commit_git_sync(
|
||||
mut args: RepoExecutionArgs,
|
||||
resource_path: &Path,
|
||||
toml: &str,
|
||||
update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
let root = args.unique_path(&core_config().repo_directory)?;
|
||||
args.destination = Some(root.display().to_string());
|
||||
|
||||
let access_token = if let Some(account) = &args.account {
|
||||
git_token(&args.provider, account, |https| args.https = https)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", args.provider),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (pull_res, _) = git::pull_or_clone(
|
||||
args.clone(),
|
||||
&core_config().repo_directory,
|
||||
access_token,
|
||||
)
|
||||
.await?;
|
||||
update.logs.extend(pull_res.logs);
|
||||
if !all_logs_success(&update.logs) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let res = git::write_commit_file(
|
||||
"Commit Sync",
|
||||
&root,
|
||||
resource_path,
|
||||
toml,
|
||||
&args.branch,
|
||||
)
|
||||
.await?;
|
||||
update.logs.extend(res.logs);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
#[instrument(
|
||||
name = "RefreshResourceSyncPending",
|
||||
@@ -507,15 +632,29 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
) -> serror::Result<ResourceSync> {
|
||||
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
|
||||
// sync should be able to do this.
|
||||
let mut sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&self.sync, user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
let mut sync =
|
||||
get_check_permissions::<entities::sync::ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let repo = if !sync.config.files_on_host
|
||||
&& !sync.config.linked_repo.is_empty()
|
||||
{
|
||||
crate::resource::get::<Repo>(&sync.config.linked_repo)
|
||||
.await?
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !sync.config.managed
|
||||
&& !sync.config.files_on_host
|
||||
&& sync.config.file_contents.is_empty()
|
||||
&& sync.config.repo.is_empty()
|
||||
&& sync.config.linked_repo.is_empty()
|
||||
{
|
||||
// Sync not configured, nothing to refresh
|
||||
return Ok(sync);
|
||||
@@ -529,9 +668,12 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
hash,
|
||||
message,
|
||||
..
|
||||
} = crate::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
} = crate::sync::remote::get_remote_resources(
|
||||
&sync,
|
||||
repo.as_ref(),
|
||||
)
|
||||
.await
|
||||
.context("failed to get remote resources")?;
|
||||
|
||||
sync.info.remote_contents = files;
|
||||
sync.info.remote_errors = file_errors;
|
||||
@@ -545,162 +687,146 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
}
|
||||
|
||||
let resources = resources?;
|
||||
|
||||
let id_to_tags = get_id_to_tags(None).await?;
|
||||
let delete = sync.config.managed || sync.config.delete;
|
||||
let all_resources = AllResourcesById::load().await?;
|
||||
|
||||
let deployments_by_name = all_resources
|
||||
.deployments
|
||||
.values()
|
||||
.map(|deployment| {
|
||||
(deployment.name.clone(), deployment.clone())
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
let stacks_by_name = all_resources
|
||||
.stacks
|
||||
.values()
|
||||
.map(|stack| (stack.name.clone(), stack.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let (resource_updates, deploy_updates) =
|
||||
if sync.config.include_resources {
|
||||
let id_to_tags = get_id_to_tags(None).await?;
|
||||
|
||||
let deploy_updates =
|
||||
crate::sync::deploy::get_updates_for_view(SyncDeployParams {
|
||||
deployments: &resources.deployments,
|
||||
deployment_map: &deployments_by_name,
|
||||
stacks: &resources.stacks,
|
||||
stack_map: &stacks_by_name,
|
||||
all_resources: &all_resources,
|
||||
})
|
||||
.await;
|
||||
let deployments_by_name = all_resources
|
||||
.deployments
|
||||
.values()
|
||||
.map(|deployment| {
|
||||
(deployment.name.clone(), deployment.clone())
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
let stacks_by_name = all_resources
|
||||
.stacks
|
||||
.values()
|
||||
.map(|stack| (stack.name.clone(), stack.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let delete = sync.config.managed || sync.config.delete;
|
||||
let deploy_updates =
|
||||
crate::sync::deploy::get_updates_for_view(
|
||||
SyncDeployParams {
|
||||
deployments: &resources.deployments,
|
||||
deployment_map: &deployments_by_name,
|
||||
stacks: &resources.stacks,
|
||||
stack_map: &stacks_by_name,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut diffs = Vec::new();
|
||||
let mut diffs = Vec::new();
|
||||
|
||||
{
|
||||
push_updates_for_view::<Server>(
|
||||
resources.servers,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Stack>(
|
||||
resources.stacks,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Deployment>(
|
||||
resources.deployments,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Build>(
|
||||
resources.builds,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Repo>(
|
||||
resources.repos,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Procedure>(
|
||||
resources.procedures,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Action>(
|
||||
resources.actions,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Builder>(
|
||||
resources.builders,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Alerter>(
|
||||
resources.alerters,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<ServerTemplate>(
|
||||
resources.server_templates,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
push_updates_for_view::<Server>(
|
||||
resources.servers,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Stack>(
|
||||
resources.stacks,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Deployment>(
|
||||
resources.deployments,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Build>(
|
||||
resources.builds,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Repo>(
|
||||
resources.repos,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Procedure>(
|
||||
resources.procedures,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Action>(
|
||||
resources.actions,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Builder>(
|
||||
resources.builders,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<Alerter>(
|
||||
resources.alerters,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
push_updates_for_view::<ResourceSync>(
|
||||
resources.resource_syncs,
|
||||
delete,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let variable_updates = if sync.config.match_tags.is_empty() {
|
||||
(diffs, deploy_updates)
|
||||
} else {
|
||||
(Vec::new(), SyncDeployUpdate::default())
|
||||
};
|
||||
|
||||
let variable_updates = if sync.config.include_variables {
|
||||
crate::sync::variables::get_updates_for_view(
|
||||
&resources.variables,
|
||||
delete,
|
||||
@@ -710,11 +836,10 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let user_group_updates = if sync.config.match_tags.is_empty() {
|
||||
let user_group_updates = if sync.config.include_user_groups {
|
||||
crate::sync::user_groups::get_updates_for_view(
|
||||
resources.user_groups,
|
||||
delete,
|
||||
&all_resources,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -722,7 +847,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
};
|
||||
|
||||
anyhow::Ok((
|
||||
diffs,
|
||||
resource_updates,
|
||||
deploy_updates,
|
||||
variable_updates,
|
||||
user_group_updates,
|
||||
@@ -815,7 +940,9 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
.context("failed to open existing pending resource sync updates alert")
|
||||
.inspect_err(|e| warn!("{e:#}"))
|
||||
.ok();
|
||||
send_alerts(&[alert]).await;
|
||||
if sync.config.pending_alert {
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
}
|
||||
// CLOSE ALERT
|
||||
(Some(existing), false) => {
|
||||
@@ -860,10 +987,10 @@ impl Resolve<WriteArgs> for CreateSyncWebhook {
|
||||
);
|
||||
};
|
||||
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -980,10 +1107,10 @@ impl Resolve<WriteArgs> for DeleteSyncWebhook {
|
||||
);
|
||||
};
|
||||
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
PermissionLevel::Write.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
+39
-132
@@ -1,17 +1,20 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
CreateTag, DeleteTag, RenameTag, UpdateTagsOnResource,
|
||||
UpdateTagsOnResourceResponse,
|
||||
},
|
||||
api::write::{CreateTag, DeleteTag, RenameTag, UpdateTagColor},
|
||||
entities::{
|
||||
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,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
deployment::Deployment,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
tag::{Tag, TagColor},
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -41,6 +44,7 @@ impl Resolve<WriteArgs> for CreateTag {
|
||||
let mut tag = Tag {
|
||||
id: Default::default(),
|
||||
name: self.name,
|
||||
color: TagColor::Slate,
|
||||
owner: user.id.clone(),
|
||||
};
|
||||
|
||||
@@ -83,6 +87,27 @@ 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(
|
||||
@@ -93,14 +118,15 @@ impl Resolve<WriteArgs> for DeleteTag {
|
||||
|
||||
tokio::try_join!(
|
||||
resource::remove_tag_from_all::<Server>(&self.id),
|
||||
resource::remove_tag_from_all::<Deployment>(&self.id),
|
||||
resource::remove_tag_from_all::<Stack>(&self.id),
|
||||
resource::remove_tag_from_all::<Deployment>(&self.id),
|
||||
resource::remove_tag_from_all::<Build>(&self.id),
|
||||
resource::remove_tag_from_all::<Repo>(&self.id),
|
||||
resource::remove_tag_from_all::<Procedure>(&self.id),
|
||||
resource::remove_tag_from_all::<Action>(&self.id),
|
||||
resource::remove_tag_from_all::<ResourceSync>(&self.id),
|
||||
resource::remove_tag_from_all::<Builder>(&self.id),
|
||||
resource::remove_tag_from_all::<Alerter>(&self.id),
|
||||
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?;
|
||||
@@ -108,122 +134,3 @@ impl Resolve<WriteArgs> for DeleteTag {
|
||||
Ok(tag)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateTagsOnResource {
|
||||
#[instrument(name = "UpdateTagsOnResource", skip(args))]
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &WriteArgs,
|
||||
) -> serror::Result<UpdateTagsOnResourceResponse> {
|
||||
let WriteArgs { user } = args;
|
||||
match self.target {
|
||||
ResourceTarget::System(_) => {
|
||||
return Err(anyhow!("Invalid target type: System").into())
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
resource::get_check_permissions::<Build>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Build>(&id, self.tags, args).await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
resource::get_check_permissions::<Builder>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Builder>(&id, self.tags, args).await?
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
resource::get_check_permissions::<Deployment>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Deployment>(&id, self.tags, args)
|
||||
.await?
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
resource::get_check_permissions::<Server>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Server>(&id, self.tags, args).await?
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
resource::get_check_permissions::<Repo>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Repo>(&id, self.tags, args).await?
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
resource::get_check_permissions::<Alerter>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Alerter>(&id, self.tags, args).await?
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
resource::get_check_permissions::<Procedure>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Procedure>(&id, self.tags, args)
|
||||
.await?
|
||||
}
|
||||
ResourceTarget::Action(id) => {
|
||||
resource::get_check_permissions::<Action>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Action>(&id, self.tags, args).await?
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
resource::get_check_permissions::<ServerTemplate>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<ServerTemplate>(&id, self.tags, args)
|
||||
.await?
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
resource::get_check_permissions::<ResourceSync>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<ResourceSync>(&id, self.tags, args)
|
||||
.await?
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
resource::get_check_permissions::<Stack>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
resource::update_tags::<Stack>(&id, self.tags, args).await?
|
||||
}
|
||||
};
|
||||
Ok(UpdateTagsOnResourceResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
DeleteUser, DeleteUserResponse, UpdateUserPassword,
|
||||
UpdateUserPasswordResponse, UpdateUserUsername,
|
||||
UpdateUserUsernameResponse,
|
||||
},
|
||||
entities::{user::UserConfig, NoData},
|
||||
entities::{NoData, user::UserConfig},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
AddUserToUserGroup, CreateUserGroup, DeleteUserGroup,
|
||||
RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup,
|
||||
},
|
||||
api::write::*,
|
||||
entities::{komodo_timestamp, user_group::UserGroup},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -20,6 +17,7 @@ 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,
|
||||
@@ -28,11 +26,12 @@ 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
|
||||
@@ -53,6 +52,7 @@ 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,6 +78,7 @@ 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,
|
||||
@@ -110,6 +111,7 @@ 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,
|
||||
@@ -153,6 +155,7 @@ 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,
|
||||
@@ -196,6 +199,7 @@ 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,
|
||||
@@ -240,3 +244,36 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{variable::Variable, Operation, ResourceTarget},
|
||||
entities::{Operation, ResourceTarget, variable::Variable},
|
||||
};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::entities::config::core::{
|
||||
CoreConfig, OauthCredentials,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
@@ -47,15 +47,21 @@ 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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
Router, extract::Query, response::Redirect, routing::get,
|
||||
};
|
||||
use komodo_client::entities::{
|
||||
komodo_timestamp,
|
||||
@@ -14,6 +14,7 @@ use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::random_string,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
@@ -72,7 +73,7 @@ async fn callback(
|
||||
.context("failed at find user query from database")?;
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.generate(user.id)
|
||||
.encode(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = komodo_timestamp();
|
||||
@@ -82,9 +83,23 @@ 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: github_user.login,
|
||||
username,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
@@ -109,7 +124,7 @@ async fn callback(
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
jwt_client()
|
||||
.generate(user_id)
|
||||
.encode(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use jwt::Token;
|
||||
use anyhow::{Context, anyhow};
|
||||
use jsonwebtoken::{DecodingKey, Validation, decode};
|
||||
use komodo_client::entities::config::core::{
|
||||
CoreConfig, OauthCredentials,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
use serde::{Deserialize, de::DeserializeOwned};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
@@ -49,15 +48,21 @@ 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(
|
||||
@@ -139,10 +144,16 @@ impl GoogleOauthClient {
|
||||
&self,
|
||||
id_token: &str,
|
||||
) -> anyhow::Result<GoogleUser> {
|
||||
let t: Token<Value, GoogleUser, jwt::Unverified> =
|
||||
Token::parse_unverified(id_token)
|
||||
.context("failed to parse id_token")?;
|
||||
Ok(t.claims().to_owned())
|
||||
let mut v = Validation::new(Default::default());
|
||||
v.insecure_disable_signature_validation();
|
||||
v.validate_aud = false;
|
||||
let res = decode::<GoogleUser>(
|
||||
id_token,
|
||||
&DecodingKey::from_secret(b""),
|
||||
&v,
|
||||
)
|
||||
.context("failed to decode google id token")?;
|
||||
Ok(res.claims)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
Router, extract::Query, response::Redirect, routing::get,
|
||||
};
|
||||
use komodo_client::entities::user::{User, UserConfig};
|
||||
use mongo_indexed::Document;
|
||||
@@ -12,6 +12,7 @@ use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::random_string,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
@@ -81,7 +82,7 @@ async fn callback(
|
||||
.context("failed at find user query from mongo")?;
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.generate(user.id)
|
||||
.encode(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
@@ -91,15 +92,28 @@ 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: google_user
|
||||
.email
|
||||
.split('@')
|
||||
.collect::<Vec<&str>>()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
username,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
@@ -124,7 +138,7 @@ async fn callback(
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
jwt_client()
|
||||
.generate(user_id)
|
||||
.encode(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
}
|
||||
};
|
||||
|
||||
+22
-14
@@ -1,15 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::{
|
||||
get_timelength_in_ms, unix_timestamp_ms, Timelength,
|
||||
Timelength, get_timelength_in_ms, unix_timestamp_ms,
|
||||
};
|
||||
use jsonwebtoken::{
|
||||
DecodingKey, EncodingKey, Header, Validation, decode, encode,
|
||||
};
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::SignWithKey;
|
||||
use komodo_client::entities::config::core::CoreConfig;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::helpers::random_string;
|
||||
@@ -24,7 +24,10 @@ pub struct JwtClaims {
|
||||
}
|
||||
|
||||
pub struct JwtClient {
|
||||
pub key: Hmac<Sha256>,
|
||||
header: Header,
|
||||
validation: Validation,
|
||||
encoding_key: EncodingKey,
|
||||
decoding_key: DecodingKey,
|
||||
ttl_ms: u128,
|
||||
exchange_tokens: ExchangeTokenMap,
|
||||
}
|
||||
@@ -36,10 +39,11 @@ 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 {
|
||||
key,
|
||||
header: Header::default(),
|
||||
validation: Validation::new(Default::default()),
|
||||
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
|
||||
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
|
||||
ttl_ms: get_timelength_in_ms(
|
||||
config.jwt_ttl.to_string().parse()?,
|
||||
),
|
||||
@@ -47,7 +51,7 @@ impl JwtClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate(&self, user_id: String) -> anyhow::Result<String> {
|
||||
pub fn encode(&self, user_id: String) -> anyhow::Result<String> {
|
||||
let iat = unix_timestamp_ms();
|
||||
let exp = iat + self.ttl_ms;
|
||||
let claims = JwtClaims {
|
||||
@@ -55,10 +59,14 @@ impl JwtClient {
|
||||
iat,
|
||||
exp,
|
||||
};
|
||||
let jwt = claims
|
||||
.sign_with_key(&self.key)
|
||||
.context("failed at signing claim")?;
|
||||
Ok(jwt)
|
||||
encode(&self.header, &claims, &self.encoding_key)
|
||||
.context("failed at signing claim")
|
||||
}
|
||||
|
||||
pub fn decode(&self, jwt: &str) -> anyhow::Result<JwtClaims> {
|
||||
decode::<JwtClaims>(jwt, &self.decoding_key, &self.validation)
|
||||
.map(|res| res.claims)
|
||||
.context("failed to decode token claims")
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
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()
|
||||
.generate(user_id)
|
||||
.encode(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()
|
||||
.generate(user.id)
|
||||
.encode(user.id)
|
||||
.context("failed at generating jwt for user")?;
|
||||
|
||||
Ok(LoginLocalUserResponse { jwt })
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use ::jwt::VerifyWithKey;
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::Request, http::HeaderMap, middleware::Next,
|
||||
@@ -71,7 +70,9 @@ 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"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,9 +94,7 @@ pub async fn authenticate_check_enabled(
|
||||
pub async fn auth_jwt_get_user_id(
|
||||
jwt: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let claims: JwtClaims = jwt
|
||||
.verify_with_key(&jwt_client().key)
|
||||
.context("failed to verify claims")?;
|
||||
let claims: JwtClaims = jwt_client().decode(jwt)?;
|
||||
if claims.exp > unix_timestamp_ms() {
|
||||
Ok(claims.id)
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::{sync::OnceLock, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use arc_swap::ArcSwapOption;
|
||||
use openidconnect::{
|
||||
core::*, Client, ClientId, ClientSecret, EmptyAdditionalClaims,
|
||||
Client, ClientId, ClientSecret, EmptyAdditionalClaims,
|
||||
EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl,
|
||||
RedirectUrl, StandardErrorResponse,
|
||||
RedirectUrl, StandardErrorResponse, core::*,
|
||||
};
|
||||
|
||||
use crate::config::core_config;
|
||||
@@ -29,59 +30,64 @@ type OidcClient = Client<
|
||||
EndpointMaybeSet,
|
||||
>;
|
||||
|
||||
static DEFAULT_OIDC_CLIENT: OnceLock<Option<OidcClient>> =
|
||||
OnceLock::new();
|
||||
|
||||
pub fn default_oidc_client() -> Option<&'static OidcClient> {
|
||||
DEFAULT_OIDC_CLIENT
|
||||
.get()
|
||||
.expect("OIDC client get before init")
|
||||
.as_ref()
|
||||
pub fn oidc_client() -> &'static ArcSwapOption<OidcClient> {
|
||||
static OIDC_CLIENT: OnceLock<ArcSwapOption<OidcClient>> =
|
||||
OnceLock::new();
|
||||
OIDC_CLIENT.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn init_default_oidc_client() {
|
||||
/// The OIDC client must be reinitialized to
|
||||
/// pick up the latest provider JWKs. This
|
||||
/// function spawns a management thread to do this
|
||||
/// on a loop.
|
||||
pub async fn spawn_oidc_client_management() {
|
||||
let config = core_config();
|
||||
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;
|
||||
}
|
||||
async {
|
||||
// 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",
|
||||
)?;
|
||||
|
||||
// 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(())
|
||||
if let Err(e) = reset_oidc_client().await {
|
||||
error!("Failed to initialize OIDC client | {e:#}");
|
||||
}
|
||||
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 init default OIDC client")
|
||||
.unwrap();
|
||||
.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(())
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
extract::Query, response::Redirect, routing::get, Router,
|
||||
Router, extract::Query, response::Redirect, routing::get,
|
||||
};
|
||||
use client::default_oidc_client;
|
||||
use client::oidc_client;
|
||||
use dashmap::DashMap;
|
||||
use komodo_client::entities::{
|
||||
komodo_timestamp,
|
||||
user::{User, UserConfig},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, Document};
|
||||
use mungos::mongodb::bson::{Document, doc};
|
||||
use openidconnect::{
|
||||
core::CoreAuthenticationFlow, AuthorizationCode, CsrfToken, Nonce,
|
||||
Scope, TokenResponse,
|
||||
AccessTokenHash, AuthorizationCode, CsrfToken,
|
||||
EmptyAdditionalClaims, Nonce, OAuth2TokenResponse,
|
||||
PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
|
||||
core::{CoreAuthenticationFlow, CoreGenderClaim},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
@@ -21,6 +23,7 @@ use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::random_string,
|
||||
state::{db_client, jwt_client},
|
||||
};
|
||||
|
||||
@@ -43,10 +46,13 @@ fn reqwest_client() -> &'static reqwest::Client {
|
||||
const CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in.
|
||||
|
||||
type RedirectUrl = Option<String>;
|
||||
type CsrfMap = DashMap<String, (Nonce, RedirectUrl, i64)>;
|
||||
fn csrf_verifier_tokens() -> &'static CsrfMap {
|
||||
static CSRF: OnceLock<CsrfMap> = OnceLock::new();
|
||||
CSRF.get_or_init(Default::default)
|
||||
/// Maps the csrf secrets to other information added in the "login" method (before auth provider redirect).
|
||||
/// This information is retrieved in the "callback" method (after auth provider redirect).
|
||||
type VerifierMap =
|
||||
DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>;
|
||||
fn verifier_tokens() -> &'static VerifierMap {
|
||||
static VERIFIERS: OnceLock<VerifierMap> = OnceLock::new();
|
||||
VERIFIERS.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
@@ -69,8 +75,12 @@ pub fn router() -> Router {
|
||||
async fn login(
|
||||
Query(RedirectQuery { redirect }): Query<RedirectQuery>,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client = oidc_client().load();
|
||||
let client =
|
||||
default_oidc_client().context("OIDC Client not configured")?;
|
||||
client.as_ref().context("OIDC Client not configured")?;
|
||||
|
||||
let (pkce_challenge, pkce_verifier) =
|
||||
PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
// Generate the authorization URL.
|
||||
let (auth_url, csrf_token, nonce) = client
|
||||
@@ -79,14 +89,21 @@ 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()))
|
||||
.url();
|
||||
|
||||
// Data inserted here will be matched on callback side for csrf protection.
|
||||
csrf_verifier_tokens().insert(
|
||||
verifier_tokens().insert(
|
||||
csrf_token.secret().clone(),
|
||||
(nonce, redirect, komodo_timestamp() + CSRF_VALID_FOR_MS),
|
||||
(
|
||||
pkce_verifier,
|
||||
nonce,
|
||||
redirect,
|
||||
komodo_timestamp() + CSRF_VALID_FOR_MS,
|
||||
),
|
||||
);
|
||||
|
||||
let config = core_config();
|
||||
@@ -121,8 +138,9 @@ struct CallbackQuery {
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client = oidc_client().load();
|
||||
let client =
|
||||
default_oidc_client().context("OIDC Client not configured")?;
|
||||
client.as_ref().context("OIDC Client not initialized successfully. Is the provider properly configured?")?;
|
||||
|
||||
if let Some(e) = query.error {
|
||||
return Err(anyhow!("Provider returned error: {e}"));
|
||||
@@ -133,9 +151,10 @@ async fn callback(
|
||||
query.state.context("Provider did not return state")?,
|
||||
);
|
||||
|
||||
let (_, (nonce, redirect, valid_until)) = csrf_verifier_tokens()
|
||||
.remove(state.secret())
|
||||
.context("CSRF Token invalid")?;
|
||||
let (_, (pkce_verifier, nonce, redirect, valid_until)) =
|
||||
verifier_tokens()
|
||||
.remove(state.secret())
|
||||
.context("CSRF token invalid")?;
|
||||
|
||||
if komodo_timestamp() > valid_until {
|
||||
return Err(anyhow!(
|
||||
@@ -143,10 +162,12 @@ 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")?
|
||||
.request_async(reqwest_client())
|
||||
.set_pkce_verifier(pkce_verifier)
|
||||
.request_async(reqwest_client)
|
||||
.await
|
||||
.context("Failed to get Oauth token")?;
|
||||
|
||||
@@ -169,7 +190,21 @@ async fn callback(
|
||||
|
||||
let claims = id_token
|
||||
.claims(&verifier, &nonce)
|
||||
.context("Failed to verify token claims")?;
|
||||
.context("Failed to verify token claims. This issue may be temporary (60 seconds max).")?;
|
||||
|
||||
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||
// another user's.
|
||||
if let Some(expected_access_token_hash) = claims.access_token_hash()
|
||||
{
|
||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||
token_response.access_token(),
|
||||
id_token.signing_alg()?,
|
||||
id_token.signing_key(&verifier)?,
|
||||
)?;
|
||||
if actual_access_token_hash != *expected_access_token_hash {
|
||||
return Err(anyhow!("Invalid access token"));
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = claims.subject().as_str();
|
||||
|
||||
@@ -185,7 +220,7 @@ async fn callback(
|
||||
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.generate(user.id)
|
||||
.encode(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = komodo_timestamp();
|
||||
@@ -195,12 +230,26 @@ 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 username = claims
|
||||
let mut username = user_info
|
||||
.preferred_username()
|
||||
.map(|username| username.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
let email = claims
|
||||
let email = user_info
|
||||
.email()
|
||||
.map(|email| email.as_str())
|
||||
.unwrap_or(user_id);
|
||||
@@ -214,6 +263,19 @@ 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,
|
||||
@@ -231,6 +293,7 @@ async fn callback(
|
||||
user_id: user_id.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user)
|
||||
@@ -240,8 +303,9 @@ async fn callback(
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
jwt_client()
|
||||
.generate(user_id)
|
||||
.encode(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
use aws_sdk_ec2::{
|
||||
Client,
|
||||
types::{
|
||||
BlockDeviceMapping, EbsBlockDevice,
|
||||
InstanceNetworkInterfaceSpecification, InstanceStateChange,
|
||||
InstanceStateName, InstanceStatus, InstanceType, ResourceType,
|
||||
Tag, TagSpecification, VolumeType,
|
||||
Tag, TagSpecification,
|
||||
},
|
||||
Client,
|
||||
};
|
||||
use base64::Engine;
|
||||
use komodo_client::entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
komodo_timestamp,
|
||||
server_template::aws::AwsServerTemplateConfig,
|
||||
ResourceTarget,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
builder::AwsBuilderConfig,
|
||||
komodo_timestamp,
|
||||
};
|
||||
|
||||
use crate::{alert::send_alerts, config::core_config};
|
||||
@@ -29,20 +29,40 @@ 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::v2024_03_28())
|
||||
let config = aws_config::defaults(BehaviorVersion::latest())
|
||||
.region(region)
|
||||
.credentials_provider(CredentialsFromConfig)
|
||||
.load()
|
||||
.await;
|
||||
Client::new(&config)
|
||||
@@ -51,12 +71,12 @@ async fn create_ec2_client(region: String) -> Client {
|
||||
#[instrument]
|
||||
pub async fn launch_ec2_instance(
|
||||
name: &str,
|
||||
config: AwsServerTemplateConfig,
|
||||
config: &AwsBuilderConfig,
|
||||
) -> anyhow::Result<Ec2Instance> {
|
||||
let AwsServerTemplateConfig {
|
||||
let AwsBuilderConfig {
|
||||
region,
|
||||
instance_type,
|
||||
volumes,
|
||||
volume_gb,
|
||||
ami_id,
|
||||
subnet_id,
|
||||
security_group_ids,
|
||||
@@ -66,19 +86,22 @@ 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 mut req = client
|
||||
let 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(),
|
||||
@@ -90,6 +113,17 @@ 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(
|
||||
@@ -97,26 +131,6 @@ 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
|
||||
@@ -136,7 +150,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
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
pub mod aws;
|
||||
|
||||
#[allow(unused)]
|
||||
pub mod hetzner;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BuildCleanupData {
|
||||
Server { repo_name: String },
|
||||
/// Nothing to clean up
|
||||
Server,
|
||||
/// Clean up AWS instance
|
||||
Aws { instance_id: String, region: String },
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use komodo_client::entities::{
|
||||
config::core::{
|
||||
AwsCredentials, CoreConfig, DatabaseConfig, Env,
|
||||
GithubWebhookAppConfig, GithubWebhookAppInstallationConfig,
|
||||
HetznerCredentials, OauthCredentials,
|
||||
OauthCredentials,
|
||||
},
|
||||
logger::LogConfig,
|
||||
};
|
||||
@@ -120,11 +120,6 @@ 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)
|
||||
@@ -139,6 +134,8 @@ 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
|
||||
@@ -176,6 +173,8 @@ 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
|
||||
@@ -193,6 +192,7 @@ 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,6 +200,7 @@ 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),
|
||||
|
||||
+3
-5
@@ -12,7 +12,6 @@ use komodo_client::entities::{
|
||||
provider::{DockerRegistryAccount, GitProviderAccount},
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
server_template::ServerTemplate,
|
||||
stack::Stack,
|
||||
stats::SystemStatsRecord,
|
||||
sync::ResourceSync,
|
||||
@@ -50,7 +49,6 @@ 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>,
|
||||
//
|
||||
@@ -89,7 +87,9 @@ 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,8 +118,6 @@ 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?,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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 = ↦
|
||||
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?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
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::{
|
||||
api::{self, GetVersionResponse},
|
||||
PeripheryClient,
|
||||
api::{self, GetVersionResponse},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cloud::{
|
||||
aws::ec2::{
|
||||
launch_ec2_instance, terminate_ec2_instance_with_retry,
|
||||
Ec2Instance,
|
||||
},
|
||||
BuildCleanupData,
|
||||
aws::ec2::{
|
||||
Ec2Instance, launch_ec2_instance,
|
||||
terminate_ec2_instance_with_retry,
|
||||
},
|
||||
},
|
||||
config::core_config,
|
||||
helpers::update::update_update,
|
||||
@@ -61,12 +60,7 @@ pub async fn get_builder_periphery(
|
||||
.health_check()
|
||||
.await
|
||||
.context("Url Builder failed health check")?;
|
||||
Ok((
|
||||
periphery,
|
||||
BuildCleanupData::Server {
|
||||
repo_name: resource_name,
|
||||
},
|
||||
))
|
||||
Ok((periphery, BuildCleanupData::Server))
|
||||
}
|
||||
BuilderConfig::Server(config) => {
|
||||
if config.server_id.is_empty() {
|
||||
@@ -74,12 +68,7 @@ pub async fn get_builder_periphery(
|
||||
}
|
||||
let server = resource::get::<Server>(&config.server_id).await?;
|
||||
let periphery = periphery_client(&server)?;
|
||||
Ok((
|
||||
periphery,
|
||||
BuildCleanupData::Server {
|
||||
repo_name: resource_name,
|
||||
},
|
||||
))
|
||||
Ok((periphery, BuildCleanupData::Server))
|
||||
}
|
||||
BuilderConfig::Aws(config) => {
|
||||
get_aws_builder(&resource_name, version, config, update).await
|
||||
@@ -98,11 +87,8 @@ 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,
|
||||
AwsServerTemplateConfig::from_builder_config(&config),
|
||||
)
|
||||
.await?;
|
||||
let Ec2Instance { instance_id, ip } =
|
||||
launch_ec2_instance(&instance_name, &config).await?;
|
||||
|
||||
info!("ec2 instance launched");
|
||||
|
||||
@@ -179,17 +165,14 @@ async fn get_aws_builder(
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip(periphery, update))]
|
||||
#[instrument(skip(update))]
|
||||
pub async fn cleanup_builder_instance(
|
||||
periphery: PeripheryClient,
|
||||
cleanup_data: BuildCleanupData,
|
||||
update: &mut Update,
|
||||
) {
|
||||
match cleanup_data {
|
||||
BuildCleanupData::Server { repo_name } => {
|
||||
let _ = periphery
|
||||
.request(api::git::DeleteRepo { name: repo_name })
|
||||
.await;
|
||||
BuildCleanupData::Server => {
|
||||
// Nothing to clean up
|
||||
}
|
||||
BuildCleanupData::Aws {
|
||||
instance_id,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use komodo_client::entities::update::{Update, UpdateListItem};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
|
||||
/// 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,164 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{update::Update, SystemCommand};
|
||||
|
||||
use super::query::VariablesAndSecrets;
|
||||
|
||||
pub fn interpolate_variables_secrets_into_extra_args(
|
||||
VariablesAndSecrets { variables, secrets }: &VariablesAndSecrets,
|
||||
extra_args: &mut Vec<String>,
|
||||
global_replacers: &mut HashSet<(String, String)>,
|
||||
secret_replacers: &mut HashSet<(String, String)>,
|
||||
) -> anyhow::Result<()> {
|
||||
for arg in extra_args {
|
||||
if arg.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// first pass - global variables
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
arg,
|
||||
variables,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate global variables into extra arg '{arg}'",
|
||||
)
|
||||
})?;
|
||||
global_replacers.extend(more_replacers);
|
||||
|
||||
// second pass - core secrets
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
&res,
|
||||
secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate core secrets into extra arg '{arg}'",
|
||||
)
|
||||
})?;
|
||||
secret_replacers.extend(more_replacers);
|
||||
|
||||
// set arg with the result
|
||||
*arg = res;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn interpolate_variables_secrets_into_string(
|
||||
VariablesAndSecrets { variables, secrets }: &VariablesAndSecrets,
|
||||
target: &mut String,
|
||||
global_replacers: &mut HashSet<(String, String)>,
|
||||
secret_replacers: &mut HashSet<(String, String)>,
|
||||
) -> anyhow::Result<()> {
|
||||
if target.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// first pass - global variables
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
target,
|
||||
variables,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.context("Failed to interpolate core variables")?;
|
||||
global_replacers.extend(more_replacers);
|
||||
|
||||
// second pass - core secrets
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
&res,
|
||||
secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.context("Failed to interpolate core secrets")?;
|
||||
secret_replacers.extend(more_replacers);
|
||||
|
||||
// set command with the result
|
||||
*target = res;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn interpolate_variables_secrets_into_system_command(
|
||||
VariablesAndSecrets { variables, secrets }: &VariablesAndSecrets,
|
||||
command: &mut SystemCommand,
|
||||
global_replacers: &mut HashSet<(String, String)>,
|
||||
secret_replacers: &mut HashSet<(String, String)>,
|
||||
) -> anyhow::Result<()> {
|
||||
if command.command.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// first pass - global variables
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
&command.command,
|
||||
variables,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate global variables into command '{}'",
|
||||
command.command
|
||||
)
|
||||
})?;
|
||||
global_replacers.extend(more_replacers);
|
||||
|
||||
// second pass - core secrets
|
||||
let (res, more_replacers) = svi::interpolate_variables(
|
||||
&res,
|
||||
secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
false,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to interpolate core secrets into command '{}'",
|
||||
command.command
|
||||
)
|
||||
})?;
|
||||
secret_replacers.extend(more_replacers);
|
||||
|
||||
// set command with the result
|
||||
command.command = res;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_interp_update_log(
|
||||
update: &mut Update,
|
||||
global_replacers: &HashSet<(String, String)>,
|
||||
secret_replacers: &HashSet<(String, String)>,
|
||||
) {
|
||||
// Show which variables were interpolated
|
||||
if !global_replacers.is_empty() {
|
||||
update.push_simple_log(
|
||||
"interpolate global variables",
|
||||
global_replacers
|
||||
.iter()
|
||||
.map(|(value, variable)| format!("<span class=\"text-muted-foreground\">{variable} =></span> {value}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
// Only show names of interpolated secrets
|
||||
if !secret_replacers.is_empty() {
|
||||
update.push_simple_log(
|
||||
"interpolate core secrets",
|
||||
secret_replacers
|
||||
.iter()
|
||||
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
-170
@@ -1,39 +1,32 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use std::{fmt::Write, time::Duration};
|
||||
|
||||
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,
|
||||
use anyhow::{Context, anyhow};
|
||||
use indexmap::IndexSet;
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget,
|
||||
build::Build,
|
||||
permission::{
|
||||
Permission, PermissionLevel, SpecificPermission, UserTarget,
|
||||
},
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
user::User,
|
||||
};
|
||||
use mongo_indexed::Document;
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, oid::ObjectId, to_document, Bson},
|
||||
};
|
||||
use mungos::mongodb::bson::{Bson, doc};
|
||||
use periphery_client::PeripheryClient;
|
||||
use rand::Rng;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs, config::core_config, resource,
|
||||
state::db_client,
|
||||
};
|
||||
use crate::{config::core_config, 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;
|
||||
@@ -78,6 +71,9 @@ 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 })
|
||||
@@ -103,6 +99,70 @@ 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(
|
||||
@@ -143,7 +203,11 @@ pub fn periphery_client(
|
||||
|
||||
let client = PeripheryClient::new(
|
||||
&server.config.address,
|
||||
&core_config().passkey,
|
||||
if server.config.passkey.is_empty() {
|
||||
&core_config().passkey
|
||||
} else {
|
||||
&server.config.passkey
|
||||
},
|
||||
Duration::from_secs(server.config.timeout_seconds as u64),
|
||||
);
|
||||
|
||||
@@ -155,6 +219,7 @@ pub async fn create_permission<T>(
|
||||
user: &User,
|
||||
target: T,
|
||||
level: PermissionLevel,
|
||||
specific: IndexSet<SpecificPermission>,
|
||||
) where
|
||||
T: Into<ResourceTarget> + std::fmt::Debug,
|
||||
{
|
||||
@@ -170,6 +235,7 @@ pub async fn create_permission<T>(
|
||||
user_target: UserTarget::User(user.id.clone()),
|
||||
resource_target: target.clone(),
|
||||
level,
|
||||
specific,
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -197,151 +263,20 @@ pub fn flatten_document(doc: Document) -> Document {
|
||||
target
|
||||
}
|
||||
|
||||
pub async fn startup_cleanup() {
|
||||
tokio::join!(
|
||||
startup_in_progress_update_cleanup(),
|
||||
startup_open_alert_cleanup(),
|
||||
pub fn repo_link(
|
||||
provider: &str,
|
||||
repo: &str,
|
||||
branch: &str,
|
||||
https: bool,
|
||||
) -> String {
|
||||
let mut res = format!(
|
||||
"http{}://{provider}/{repo}",
|
||||
if https { "s" } else { "" }
|
||||
);
|
||||
}
|
||||
|
||||
/// 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:#}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
// 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}");
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{bold, colored, format_serror, muted, Color};
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::{Color, bold, colored, format_serror, muted};
|
||||
use futures::future::join_all;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
@@ -9,6 +9,7 @@ use komodo_client::{
|
||||
action::Action,
|
||||
build::Build,
|
||||
deployment::Deployment,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
stack::Stack,
|
||||
@@ -25,7 +26,7 @@ use crate::{
|
||||
execute::{ExecuteArgs, ExecuteRequest},
|
||||
write::WriteArgs,
|
||||
},
|
||||
resource::{list_full_for_user_using_pattern, KomodoResource},
|
||||
resource::{KomodoResource, list_full_for_user_using_pattern},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
@@ -166,6 +167,13 @@ 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,
|
||||
@@ -206,7 +214,7 @@ async fn execute_stage(
|
||||
join_all(futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -985,6 +993,12 @@ 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?;
|
||||
@@ -1176,6 +1190,7 @@ async fn extend_batch_exection<E: ExtendBatch>(
|
||||
pattern,
|
||||
Default::default(),
|
||||
procedure_user(),
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
@@ -1275,6 +1290,16 @@ 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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Context;
|
||||
use async_timing_util::{
|
||||
unix_timestamp_ms, wait_until_timelength, Timelength, ONE_DAY_MS,
|
||||
ONE_DAY_MS, Timelength, unix_timestamp_ms, wait_until_timelength,
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
|
||||
+133
-22
@@ -1,42 +1,56 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
str::FromStr,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::{ONE_MIN_MS, unix_timestamp_ms};
|
||||
use komodo_client::entities::{
|
||||
action::Action,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
action::{Action, ActionState},
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
deployment::{Deployment, DeploymentState},
|
||||
docker::container::{ContainerListItem, ContainerStateStatusEnum},
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
permission::{PermissionLevel, PermissionLevelAndSpecifics},
|
||||
procedure::{Procedure, ProcedureState},
|
||||
repo::Repo,
|
||||
server::{Server, ServerState},
|
||||
server_template::ServerTemplate,
|
||||
stack::{Stack, StackServiceNames, StackState},
|
||||
stats::SystemInformation,
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
update::Update,
|
||||
user::{admin_service_user, User},
|
||||
user::{User, admin_service_user},
|
||||
user_group::UserGroup,
|
||||
variable::Variable,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{
|
||||
bson::{doc, oid::ObjectId, Document},
|
||||
bson::{Document, doc, oid::ObjectId},
|
||||
options::FindOneOptions,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::stats;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
resource::{self, get_user_permission_on_resource},
|
||||
permission::get_user_permission_on_resource,
|
||||
resource::{self, KomodoResource},
|
||||
stack::compose_container_match_regex,
|
||||
state::{db_client, deployment_status_cache, stack_status_cache},
|
||||
state::{
|
||||
action_state_cache, action_states, db_client,
|
||||
deployment_status_cache, procedure_state_cache,
|
||||
stack_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
use super::periphery_client;
|
||||
|
||||
// user: Id or username
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_user(user: &str) -> anyhow::Result<User> {
|
||||
@@ -78,10 +92,22 @@ pub async fn get_server_state(server: &Server) -> ServerState {
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_deployment_state(
|
||||
deployment: &Deployment,
|
||||
id: &String,
|
||||
) -> anyhow::Result<DeploymentState> {
|
||||
if action_states()
|
||||
.deployment
|
||||
.get(id)
|
||||
.await
|
||||
.map(|s| s.get().map(|s| s.deploying))
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return Ok(DeploymentState::Deploying);
|
||||
}
|
||||
let state = deployment_status_cache()
|
||||
.get(&deployment.id)
|
||||
.get(id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
@@ -229,7 +255,10 @@ pub async fn get_user_user_groups(
|
||||
find_collect(
|
||||
&db_client().user_groups,
|
||||
doc! {
|
||||
"users": user_id
|
||||
"$or": [
|
||||
{ "everyone": true },
|
||||
{ "users": user_id },
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
@@ -268,9 +297,9 @@ pub fn user_target_query(
|
||||
pub async fn get_user_permission_on_target(
|
||||
user: &User,
|
||||
target: &ResourceTarget,
|
||||
) -> anyhow::Result<PermissionLevel> {
|
||||
) -> anyhow::Result<PermissionLevelAndSpecifics> {
|
||||
match target {
|
||||
ResourceTarget::System(_) => Ok(PermissionLevel::None),
|
||||
ResourceTarget::System(_) => Ok(PermissionLevel::None.into()),
|
||||
ResourceTarget::Build(id) => {
|
||||
get_user_permission_on_resource::<Build>(user, id).await
|
||||
}
|
||||
@@ -295,10 +324,6 @@ pub async fn get_user_permission_on_target(
|
||||
ResourceTarget::Action(id) => {
|
||||
get_user_permission_on_resource::<Action>(user, id).await
|
||||
}
|
||||
ResourceTarget::ServerTemplate(id) => {
|
||||
get_user_permission_on_resource::<ServerTemplate>(user, id)
|
||||
.await
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
get_user_permission_on_resource::<ResourceSync>(user, id).await
|
||||
}
|
||||
@@ -359,8 +384,8 @@ pub struct VariablesAndSecrets {
|
||||
pub secrets: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub async fn get_variables_and_secrets(
|
||||
) -> anyhow::Result<VariablesAndSecrets> {
|
||||
pub async fn get_variables_and_secrets()
|
||||
-> anyhow::Result<VariablesAndSecrets> {
|
||||
let variables = find_collect(&db_client().variables, None, None)
|
||||
.await
|
||||
.context("failed to get all variables from db")?;
|
||||
@@ -382,3 +407,89 @@ pub async fn get_variables_and_secrets(
|
||||
|
||||
Ok(VariablesAndSecrets { variables, secrets })
|
||||
}
|
||||
|
||||
// This protects the peripheries from spam requests
|
||||
const SYSTEM_INFO_EXPIRY: u128 = ONE_MIN_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)
|
||||
}
|
||||
|
||||
pub async fn get_system_info(
|
||||
server: &Server,
|
||||
) -> anyhow::Result<SystemInformation> {
|
||||
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(stats::GetSystemInformation {})
|
||||
.await?;
|
||||
lock.insert(
|
||||
server.id.clone(),
|
||||
(stats.clone(), unix_timestamp_ms() + SYSTEM_INFO_EXPIRY)
|
||||
.into(),
|
||||
);
|
||||
stats
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get last time procedure / action was run using Update query.
|
||||
/// Ignored whether run was successful.
|
||||
pub async fn get_last_run_at<R: KomodoResource>(
|
||||
id: &String,
|
||||
) -> anyhow::Result<Option<i64>> {
|
||||
let resource_type = R::resource_type();
|
||||
let res = db_client()
|
||||
.updates
|
||||
.find_one(doc! {
|
||||
"target.type": resource_type.as_ref(),
|
||||
"target.id": id,
|
||||
"operation": format!("Run{resource_type}"),
|
||||
"status": "Complete"
|
||||
})
|
||||
.sort(doc! { "start_ts": -1 })
|
||||
.await
|
||||
.context("Failed to query updates collection for last run time")?
|
||||
.map(|u| u.start_ts);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get_action_state(id: &String) -> ActionState {
|
||||
if action_states()
|
||||
.action
|
||||
.get(id)
|
||||
.await
|
||||
.map(|s| s.get().map(|s| s.running))
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return ActionState::Running;
|
||||
}
|
||||
action_state_cache().get(id).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn get_procedure_state(id: &String) -> ProcedureState {
|
||||
if action_states()
|
||||
.procedure
|
||||
.get(id)
|
||||
.await
|
||||
.map(|s| s.get().map(|s| s.running))
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return ProcedureState::Running;
|
||||
}
|
||||
procedure_state_cache().get(id).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user