forked from github-starred/komodo
Compare commits
136
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973aae82fd | ||
|
|
c3626e0949 | ||
|
|
5ba96a58e4 | ||
|
|
c6a1cf2867 | ||
|
|
bd39aec1fd | ||
|
|
079895a64d | ||
|
|
5af1c646a5 | ||
|
|
747e03b5c4 | ||
|
|
78c6761eca | ||
|
|
a949891d26 | ||
|
|
9d256e420a | ||
|
|
b460504b5a | ||
|
|
e60316f2c1 | ||
|
|
c45e3eab16 | ||
|
|
ec12f63272 | ||
|
|
f2f77b4eb6 | ||
|
|
677a65f8b0 | ||
|
|
a5a1090177 | ||
|
|
ccb18fc88a | ||
|
|
0ca583fe5e | ||
|
|
4013cd3f1e | ||
|
|
5854a7de99 | ||
|
|
8c52232852 | ||
|
|
fb9e5b1860 | ||
|
|
a1c00a8d32 | ||
|
|
775f5bf703 | ||
|
|
effe737ffe | ||
|
|
436959cbcd | ||
|
|
9957287443 | ||
|
|
c77de8683c | ||
|
|
087bcb7044 | ||
|
|
0dfcaaf06a | ||
|
|
6624a821a5 | ||
|
|
c32af84e02 | ||
|
|
9d9add7b34 | ||
|
|
db099dbe1e | ||
|
|
413a8abc84 | ||
|
|
36243e83c1 | ||
|
|
076113a1de | ||
|
|
c998edaa73 | ||
|
|
4de32b08e5 | ||
|
|
262999c58f | ||
|
|
be57f52e6e | ||
|
|
3591789316 | ||
|
|
83f2bd65fe | ||
|
|
5e5cdb81f8 | ||
|
|
721038a1df | ||
|
|
7de98ad519 | ||
|
|
8c62f2b5c5 | ||
|
|
85787781ee | ||
|
|
25e2d4e926 | ||
|
|
b9b8d45cbc | ||
|
|
2f2e863dbf | ||
|
|
a3a01f1625 | ||
|
|
aec8fa2bf1 | ||
|
|
7ff2dba82f | ||
|
|
9c86b2d239 | ||
|
|
b1fec7c663 | ||
|
|
8341c6b802 | ||
|
|
73285c4374 | ||
|
|
32d48cdb02 | ||
|
|
8e081fd09c | ||
|
|
04531f1dea | ||
|
|
80f439d472 | ||
|
|
d5e03d6d16 | ||
|
|
a9f55bb8e6 | ||
|
|
9e765f93f5 | ||
|
|
b3aa8e906f | ||
|
|
03fe442aa0 | ||
|
|
d268009a6a | ||
|
|
f0697e812a | ||
|
|
78766463d6 | ||
|
|
0fa1edba2c | ||
|
|
bbd968cac3 | ||
|
|
5f24fc1be3 | ||
|
|
7ecd2b0b0b | ||
|
|
7bf44d2e04 | ||
|
|
24e0672384 | ||
|
|
04f081631f | ||
|
|
b1af956b63 | ||
|
|
370712b29f | ||
|
|
2b6c552964 | ||
|
|
434a1d8ea9 | ||
|
|
0b7f28360f | ||
|
|
3c8ef0ab29 | ||
|
|
930b2423c3 | ||
|
|
546747b5f2 | ||
|
|
c6df866755 | ||
|
|
ea5e684915 | ||
|
|
64db8933de | ||
|
|
7a5580de57 | ||
|
|
b1656bb174 | ||
|
|
559ce103da | ||
|
|
75e278a57b | ||
|
|
430f3ddc34 | ||
|
|
6c30c202e9 | ||
|
|
c5401de1c5 | ||
|
|
7a3d9e0ef6 | ||
|
|
595e3ece42 | ||
|
|
a3bc895755 | ||
|
|
3e3def03ec | ||
|
|
bc672d9649 | ||
|
|
ea6dee4d51 | ||
|
|
b985f18c74 | ||
|
|
45909b2f04 | ||
|
|
2b5a54ce89 | ||
|
|
a18f33b95e | ||
|
|
f35b00ea95 | ||
|
|
70fab08520 | ||
|
|
0331780a5f | ||
|
|
06cdfd2bbc | ||
|
|
1555202569 | ||
|
|
5139622aad | ||
|
|
61ce2ee3db | ||
|
|
3171c14f2b | ||
|
|
521db748d8 | ||
|
|
35bf224080 | ||
|
|
e0b31cfe51 | ||
|
|
0a890078b0 | ||
|
|
df97ced7a4 | ||
|
|
d4e5e2e6d8 | ||
|
|
19aa60dcb5 | ||
|
|
fc19c53e6f | ||
|
|
4f0af960db | ||
|
|
e2ec5258fb | ||
|
|
49b6545a02 | ||
|
|
0aabaa9e62 | ||
|
|
dc65986eab | ||
|
|
1d8f28437d | ||
|
|
c1502e89c2 | ||
|
|
0bd15fc442 | ||
|
|
5a3621b02e | ||
|
|
38192e2dac | ||
|
|
5d271d5547 | ||
|
|
11fb67a35b | ||
|
|
a80499dcc4 |
Generated
+686
-236
File diff suppressed because it is too large
Load Diff
+34
-27
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-72"
|
||||
version = "2.0.0-dev-98"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -26,7 +26,9 @@ environment_file = { path = "lib/environment_file" }
|
||||
environment = { path = "lib/environment" }
|
||||
interpolate = { path = "lib/interpolate" }
|
||||
secret_file = { path = "lib/secret_file" }
|
||||
validations = { path = "lib/validations" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
rate_limit = { path = "lib/rate_limit" }
|
||||
transport = { path = "lib/transport" }
|
||||
database = { path = "lib/database" }
|
||||
encoding = { path = "lib/encoding" }
|
||||
@@ -39,7 +41,6 @@ noise = { path = "lib/noise" }
|
||||
git = { path = "lib/git" }
|
||||
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.5.3", default-features = false }
|
||||
slack = { version = "2.0.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
derive_default_builder = "0.1.8"
|
||||
@@ -49,14 +50,14 @@ partial_derive2 = "0.4.3"
|
||||
derive_variants = "1.0.0"
|
||||
mongo_indexed = "2.0.2"
|
||||
resolver_api = "3.0.0"
|
||||
toml_pretty = "1.2.0"
|
||||
toml_pretty = "2.0.0"
|
||||
mungos = "3.2.2"
|
||||
svi = "1.2.0"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.24", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.16", features = ["io", "codec"] }
|
||||
tokio-util = { version = "0.7.17", features = ["io", "codec"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["sync"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
futures-util = "0.3.31"
|
||||
@@ -64,14 +65,15 @@ arc-swap = "1.7.1"
|
||||
|
||||
# SERVER
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
|
||||
axum-extra = { version = "0.10.3", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.6", features = ["fs", "cors"] }
|
||||
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
|
||||
axum = { version = "0.8.6", features = ["ws", "json", "macros"] }
|
||||
tower-http = { version = "0.6.8", features = ["fs", "cors", "set-header"] }
|
||||
axum = { version = "0.8.7", features = ["ws", "json", "macros"] }
|
||||
axum-extra = { version = "0.12.2", features = ["typed-header"] }
|
||||
axum-server = { version = "0.8.0", features = ["tls-rustls"] }
|
||||
tower-sessions = "0.14.0"
|
||||
|
||||
# SER/DE
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
indexmap = { version = "2.12.0", features = ["serde"] }
|
||||
indexmap = { version = "2.12.1", features = ["serde"] }
|
||||
serde = { version = "1.0.227", features = ["derive"] }
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
bson = { version = "2.15.0" } # must keep in sync with mongodb version
|
||||
@@ -88,62 +90,67 @@ thiserror = "2.0.17"
|
||||
# LOGGING
|
||||
opentelemetry-otlp = { version = "0.31.0", features = ["tls-roots", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.20", features = ["json"] }
|
||||
tracing-subscriber = { version = "0.3.22", features = ["json"] }
|
||||
opentelemetry-semantic-conventions = "0.31.0"
|
||||
tracing-opentelemetry = "0.32.0"
|
||||
opentelemetry = "0.31.0"
|
||||
tracing = "0.1.41"
|
||||
tracing = "0.1.43"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.49", features = ["derive"] }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO / AUTH
|
||||
uuid = { version = "1.18.1", features = ["v4", "fast-rng", "serde"] }
|
||||
jsonwebtoken = { version = "10.0.0", features = ["aws_lc_rs"] } # locked back with octorust
|
||||
rustls = { version = "0.23.33", features = ["aws-lc-rs"] }
|
||||
pem-rfc7468 = { version = "0.7.0", features = ["alloc"] }
|
||||
openidconnect = "4.0.1"
|
||||
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation"] }
|
||||
openidconnect = { version = "4.0.1", features = ["accept-rfc3339-timestamps"] }
|
||||
uuid = { version = "1.19.0", features = ["v4", "fast-rng", "serde"] }
|
||||
jsonwebtoken = { version = "10.2.0", features = ["aws_lc_rs"] } # locked back with octorust
|
||||
rustls = { version = "0.23.35", features = ["aws-lc-rs"] }
|
||||
pem-rfc7468 = { version = "1.0.0", features = ["alloc"] }
|
||||
totp-rs = { version = "5.7.0", features = ["qr"] }
|
||||
webauthn-rs-proto = "0.5.4"
|
||||
base64urlsafedata = "0.5.4"
|
||||
data-encoding = "2.9.0"
|
||||
urlencoding = "2.1.3"
|
||||
bcrypt = "0.17.1"
|
||||
base64 = "0.22.1"
|
||||
pkcs8 = "0.10.2"
|
||||
snow = "0.10.0"
|
||||
hmac = "0.12.1"
|
||||
sha1 = "0.10.6"
|
||||
sha2 = "0.10.9"
|
||||
rand = "0.9.2"
|
||||
hex = "0.4.3"
|
||||
spki = "0.7.3"
|
||||
der = "0.7.10"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
hickory-resolver = "0.25.2"
|
||||
portable-pty = "0.9.0"
|
||||
shell-escape = "0.1.5"
|
||||
crossterm = "0.29.0"
|
||||
bollard = "0.19.3"
|
||||
bollard = "0.19.4"
|
||||
sysinfo = "0.37.1"
|
||||
shlex = "1.3.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.8.8"
|
||||
aws-sdk-ec2 = "1.175.1"
|
||||
aws-credential-types = "1.2.8"
|
||||
aws-config = "1.8.12"
|
||||
aws-sdk-ec2 = "1.196.0"
|
||||
aws-credential-types = "1.2.11"
|
||||
|
||||
## CRON
|
||||
english-to-cron = "0.1.6"
|
||||
english-to-cron = "0.1.7"
|
||||
chrono-tz = "0.10.4"
|
||||
chrono = "0.4.42"
|
||||
croner = "3.0.0"
|
||||
croner = "3.0.1"
|
||||
|
||||
# MISC
|
||||
async-compression = { version = "0.4.32", features = ["tokio", "gzip"] }
|
||||
async-compression = { version = "0.4.35", features = ["tokio", "gzip"] }
|
||||
derive_builder = "0.20.2"
|
||||
comfy-table = "7.2.1"
|
||||
typeshare = "1.0.4"
|
||||
dashmap = "6.1.0"
|
||||
wildcard = "0.3.0"
|
||||
colored = "3.0.0"
|
||||
bytes = "1.11.0"
|
||||
regex = "1.12.2"
|
||||
bytes = "1.10.1"
|
||||
@@ -1,7 +1,7 @@
|
||||
## Builds the Komodo Core, Periphery, and Util binaries
|
||||
## for a specific architecture.
|
||||
## for a specific architecture. Requires OpenSSL 3 or later.
|
||||
|
||||
FROM rust:1.90.0-bullseye AS builder
|
||||
FROM rust:1.91.1-bookworm AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
## Builds the Komodo Core, Periphery, and Util binaries
|
||||
## for a specific architecture.
|
||||
## for a specific architecture. Requires OpenSSL 3 or later.
|
||||
|
||||
## Uses chef for dependency caching to help speed up back-to-back builds.
|
||||
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.90.0-bullseye AS chef
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.90.0-bookworm AS chef
|
||||
WORKDIR /builder
|
||||
|
||||
# Plan just the RECIPE to see if things have changed
|
||||
|
||||
+1
-2
@@ -21,9 +21,9 @@ config.workspace = true
|
||||
logger.workspace = true
|
||||
noise.workspace = true
|
||||
# external
|
||||
tokio-tungstenite.workspace = true
|
||||
futures-util.workspace = true
|
||||
comfy-table.workspace = true
|
||||
tokio-util.workspace = true
|
||||
serde_json.workspace = true
|
||||
crossterm.workspace = true
|
||||
serde_qs.workspace = true
|
||||
@@ -33,7 +33,6 @@ colored.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
bytes.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
clap.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.90.0-bullseye AS builder
|
||||
FROM rust:1.91.1-bullseye AS builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
@@ -61,7 +61,8 @@ async fn list_containers(
|
||||
.map(|s| (s.id.clone(), s))
|
||||
.collect::<HashMap<_, _>>())),
|
||||
client.read(ListAllDockerContainers {
|
||||
servers: Default::default()
|
||||
servers: Default::default(),
|
||||
containers: Default::default(),
|
||||
}),
|
||||
)?;
|
||||
|
||||
@@ -145,7 +146,8 @@ pub async fn inspect_container(
|
||||
.map(|s| (s.id.clone(), s))
|
||||
.collect::<HashMap<_, _>>())),
|
||||
client.read(ListAllDockerContainers {
|
||||
servers: Default::default()
|
||||
servers: Default::default(),
|
||||
containers: Default::default()
|
||||
}),
|
||||
)?;
|
||||
|
||||
|
||||
@@ -221,6 +221,21 @@ pub async fn handle(
|
||||
Execution::SendAlert(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmNodes(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmStacks(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmServices(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::ClearRepoCache(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
@@ -488,6 +503,26 @@ pub async fn handle(
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmNodes(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmStacks(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmServices(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmConfigs(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::RemoveSwarmSecrets(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
.map(|u| ExecutionResult::Single(u.into())),
|
||||
Execution::ClearRepoCache(request) => client
|
||||
.execute(request)
|
||||
.await
|
||||
|
||||
+19
-38
@@ -5,10 +5,9 @@ use futures_util::{FutureExt, try_join};
|
||||
use komodo_client::{
|
||||
KomodoClient,
|
||||
api::read::{
|
||||
ListActions, ListAlerters, ListAllTerminals, ListBuilders,
|
||||
ListBuilds, ListDeployments, ListProcedures, ListRepos,
|
||||
ListResourceSyncs, ListSchedules, ListServers, ListStacks,
|
||||
ListTags,
|
||||
ListActions, ListAlerters, ListBuilders, ListBuilds,
|
||||
ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
|
||||
ListSchedules, ListServers, ListStacks, ListTags, ListTerminals,
|
||||
},
|
||||
entities::{
|
||||
ResourceTargetVariant,
|
||||
@@ -27,20 +26,16 @@ use komodo_client::{
|
||||
ProcedureListItem, ProcedureListItemInfo, ProcedureState,
|
||||
},
|
||||
repo::{RepoListItem, RepoListItemInfo, RepoState},
|
||||
resource::{
|
||||
ResourceListItem, ResourceQuery, TemplatesQueryBehavior,
|
||||
},
|
||||
resource::{ResourceListItem, ResourceQuery},
|
||||
resource_link,
|
||||
schedule::Schedule,
|
||||
server::{
|
||||
ServerListItem, ServerListItemInfo, ServerState,
|
||||
TerminalInfoWithServer,
|
||||
},
|
||||
server::{ServerListItem, ServerListItemInfo, ServerState},
|
||||
stack::{StackListItem, StackListItemInfo, StackState},
|
||||
sync::{
|
||||
ResourceSyncListItem, ResourceSyncListItemInfo,
|
||||
ResourceSyncState,
|
||||
},
|
||||
terminal::Terminal,
|
||||
},
|
||||
};
|
||||
use serde::Serialize;
|
||||
@@ -202,30 +197,16 @@ async fn list_terminals(
|
||||
filters: &ResourceFilters,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = crate::command::komodo_client().await?;
|
||||
let query = ResourceQuery::builder()
|
||||
.tags(filters.tags.clone())
|
||||
.templates(TemplatesQueryBehavior::Exclude)
|
||||
.build();
|
||||
let (mut terminals, servers) = tokio::try_join!(
|
||||
client.read(ListAllTerminals {
|
||||
query: query.clone(),
|
||||
fresh: true,
|
||||
}),
|
||||
client
|
||||
.read(ListServers { query })
|
||||
.map(|res| res.map(|res| res
|
||||
.into_iter()
|
||||
.map(|t| (t.id, t.name))
|
||||
.collect::<HashMap<_, _>>()))
|
||||
)?;
|
||||
// Fix server ids -> names
|
||||
terminals.iter_mut().for_each(|terminal| {
|
||||
let Some(name) = servers.get(&terminal.server_id) else {
|
||||
terminal.server_id = String::new();
|
||||
return;
|
||||
};
|
||||
terminal.server_id.clone_from(name);
|
||||
});
|
||||
// let query = ResourceQuery::builder()
|
||||
// .tags(filters.tags.clone())
|
||||
// .templates(TemplatesQueryBehavior::Exclude)
|
||||
// .build();
|
||||
let terminals = client
|
||||
.read(ListTerminals {
|
||||
target: None,
|
||||
use_names: true,
|
||||
})
|
||||
.await?;
|
||||
if !terminals.is_empty() {
|
||||
print_items(terminals, filters.format, filters.links)?;
|
||||
}
|
||||
@@ -1177,14 +1158,14 @@ impl PrintTable for ResourceListItem<AlerterListItemInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for TerminalInfoWithServer {
|
||||
impl PrintTable for Terminal {
|
||||
fn header(_links: bool) -> &'static [&'static str] {
|
||||
&["Terminal", "Server", "Command", "Size", "Created"]
|
||||
&["Terminal", "Target", "Command", "Size", "Created"]
|
||||
}
|
||||
fn row(self, _links: bool) -> Vec<comfy_table::Cell> {
|
||||
vec![
|
||||
Cell::new(self.name).add_attribute(Attribute::Bold),
|
||||
Cell::new(self.server_id),
|
||||
Cell::new(format!("{:?}", self.target)),
|
||||
Cell::new(self.command),
|
||||
Cell::new(if self.stored_size_kb < 1.0 {
|
||||
format!("{:.1} KiB", self.stored_size_kb)
|
||||
|
||||
@@ -18,7 +18,7 @@ pub mod container;
|
||||
pub mod database;
|
||||
pub mod execute;
|
||||
pub mod list;
|
||||
pub mod ssh;
|
||||
pub mod terminal;
|
||||
pub mod update;
|
||||
|
||||
async fn komodo_client() -> anyhow::Result<&'static KomodoClient> {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use colored::Colorize;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use komodo_client::{
|
||||
api::write::{CreateTerminal, TerminalRecreateMode},
|
||||
entities::config::cli::args::ssh::Ssh,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
|
||||
use tokio_tungstenite::tungstenite;
|
||||
|
||||
pub async fn handle(
|
||||
Ssh {
|
||||
server,
|
||||
name,
|
||||
command,
|
||||
recreate,
|
||||
}: &Ssh,
|
||||
) -> anyhow::Result<()> {
|
||||
// Need to forward multiple sources into ws write
|
||||
let (write_tx, mut write_rx) =
|
||||
tokio::sync::mpsc::channel::<Bytes>(1024);
|
||||
|
||||
// ================
|
||||
// SETUP RESIZING
|
||||
// ================
|
||||
|
||||
// Subscribe to SIGWINCH for resize messages
|
||||
let mut sigwinch = tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::window_change(),
|
||||
)
|
||||
.context("failed to register SIGWINCH handler")?;
|
||||
|
||||
// Send first resize messsage, bailing if it fails to get the size.
|
||||
write_tx.send(resize_message()?).await?;
|
||||
|
||||
let forward_resize = async {
|
||||
while sigwinch.recv().await.is_some() {
|
||||
if let Ok(resize_message) = resize_message()
|
||||
&& write_tx.send(resize_message).await.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let forward_stdin = async {
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
// Read into buffer starting from index 1,
|
||||
// leaving first byte to represent 'data' message.
|
||||
let n = match stdin.read(&mut buf[1..]).await {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => n,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Check for disconnect sequence (alt + q)
|
||||
if buf[1..(n + 1)] == [197, 147] {
|
||||
break;
|
||||
}
|
||||
let bytes = Bytes::copy_from_slice(&buf[..(n + 1)]);
|
||||
if write_tx.send(bytes).await.is_err() {
|
||||
break;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// =====================
|
||||
// CONNECT AND FORWARD
|
||||
// =====================
|
||||
|
||||
let client = super::komodo_client().await?;
|
||||
|
||||
// Init the terminal if it doesn't exist already.
|
||||
client
|
||||
.write(CreateTerminal {
|
||||
server: server.to_string(),
|
||||
name: name.to_string(),
|
||||
command: command.clone(),
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
let (mut ws_write, mut ws_read) = client
|
||||
.connect_terminal_websocket(server, name)
|
||||
.await?
|
||||
.split();
|
||||
|
||||
let forward_write = async {
|
||||
while let Some(bytes) = write_rx.recv().await {
|
||||
if let Err(e) =
|
||||
ws_write.send(tungstenite::Message::Binary(bytes)).await
|
||||
{
|
||||
return Some(e);
|
||||
};
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
let forward_read = async {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
loop {
|
||||
match ws_read.next().await {
|
||||
Some(Ok(tungstenite::Message::Binary(bytes))) => {
|
||||
if let Err(e) =
|
||||
tokio::io::copy(&mut bytes.as_ref(), &mut stdout)
|
||||
.await
|
||||
.context("Failed to copy bytes to stdout")
|
||||
{
|
||||
return Some(e);
|
||||
}
|
||||
let _ = stdout.flush().await;
|
||||
}
|
||||
Some(Ok(tungstenite::Message::Text(text))) => {
|
||||
if let Err(e) =
|
||||
tokio::io::copy(&mut text.as_ref(), &mut stdout)
|
||||
.await
|
||||
.context("Failed to copy text to stdout")
|
||||
{
|
||||
return Some(e);
|
||||
}
|
||||
let _ = stdout.flush().await;
|
||||
}
|
||||
Some(Ok(tungstenite::Message::Close(_))) => break,
|
||||
Some(Err(e)) => {
|
||||
return Some(
|
||||
anyhow::Error::from(e).context("Websocket read error"),
|
||||
);
|
||||
}
|
||||
None => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
let guard = RawModeGuard::enable_raw_mode()?;
|
||||
|
||||
tokio::select! {
|
||||
_ = forward_resize => drop(guard),
|
||||
_ = forward_stdin => drop(guard),
|
||||
e = forward_write => {
|
||||
drop(guard);
|
||||
if let Some(e) = e {
|
||||
eprintln!("\nFailed to forward stdin | {e:#}");
|
||||
}
|
||||
},
|
||||
e = forward_read => {
|
||||
drop(guard);
|
||||
if let Some(e) = e {
|
||||
eprintln!("\nFailed to forward stdout | {e:#}");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
println!("\n\n{} {}", "connection".bold(), "closed".red().bold());
|
||||
|
||||
// It doesn't seem to exit by itself after the raw mode stuff.
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
fn resize_message() -> anyhow::Result<Bytes> {
|
||||
let (cols, rows) = crossterm::terminal::size()
|
||||
.context("Failed to get terminal size")?;
|
||||
let bytes: Vec<u8> =
|
||||
format!(r#"{{"rows":{rows},"cols":{cols}}}"#).into();
|
||||
let mut msg = Vec::with_capacity(bytes.len() + 1);
|
||||
msg.push(0xff); // resize prefix
|
||||
msg.extend(bytes);
|
||||
Ok(msg.into())
|
||||
}
|
||||
|
||||
struct RawModeGuard;
|
||||
|
||||
impl RawModeGuard {
|
||||
fn enable_raw_mode() -> anyhow::Result<Self> {
|
||||
crossterm::terminal::enable_raw_mode()
|
||||
.context("Failed to enable terminal raw mode")?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
impl Drop for RawModeGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = crossterm::terminal::disable_raw_mode() {
|
||||
eprintln!("Failed to disable terminal raw mode | {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use colored::Colorize;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
read::{ListAllDockerContainers, ListServers},
|
||||
terminal::InitTerminal,
|
||||
},
|
||||
entities::{
|
||||
config::cli::args::terminal::{Attach, Connect, Exec},
|
||||
server::ServerQuery,
|
||||
terminal::{
|
||||
ContainerTerminalMode, TerminalRecreateMode,
|
||||
TerminalResizeMessage, TerminalStdinMessage,
|
||||
},
|
||||
},
|
||||
ws::terminal::TerminalWebsocket,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn handle_connect(
|
||||
Connect {
|
||||
server,
|
||||
name,
|
||||
command,
|
||||
recreate,
|
||||
}: &Connect,
|
||||
) -> anyhow::Result<()> {
|
||||
handle_terminal_forwarding(async {
|
||||
super::komodo_client()
|
||||
.await?
|
||||
.connect_server_terminal(
|
||||
server.to_string(),
|
||||
Some(name.to_string()),
|
||||
Some(InitTerminal {
|
||||
command: command.clone(),
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
mode: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_exec(
|
||||
Exec {
|
||||
server,
|
||||
container,
|
||||
shell,
|
||||
recreate,
|
||||
}: &Exec,
|
||||
) -> anyhow::Result<()> {
|
||||
let server = get_server(server.clone(), container).await?;
|
||||
handle_terminal_forwarding(async {
|
||||
super::komodo_client()
|
||||
.await?
|
||||
.connect_container_terminal(
|
||||
server,
|
||||
container.to_string(),
|
||||
None,
|
||||
Some(InitTerminal {
|
||||
command: Some(shell.to_string()),
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
mode: Some(ContainerTerminalMode::Exec),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_attach(
|
||||
Attach {
|
||||
server,
|
||||
container,
|
||||
recreate,
|
||||
}: &Attach,
|
||||
) -> anyhow::Result<()> {
|
||||
let server = get_server(server.clone(), container).await?;
|
||||
handle_terminal_forwarding(async {
|
||||
super::komodo_client()
|
||||
.await?
|
||||
.connect_container_terminal(
|
||||
server,
|
||||
container.to_string(),
|
||||
None,
|
||||
Some(InitTerminal {
|
||||
command: None,
|
||||
recreate: if *recreate {
|
||||
TerminalRecreateMode::Always
|
||||
} else {
|
||||
TerminalRecreateMode::DifferentCommand
|
||||
},
|
||||
mode: Some(ContainerTerminalMode::Attach),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_server(
|
||||
server: Option<String>,
|
||||
container: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
if let Some(server) = server {
|
||||
return Ok(server);
|
||||
}
|
||||
|
||||
let client = super::komodo_client().await?;
|
||||
|
||||
let mut containers = client
|
||||
.read(ListAllDockerContainers {
|
||||
servers: Default::default(),
|
||||
containers: vec![container.to_string()],
|
||||
})
|
||||
.await?;
|
||||
|
||||
if containers.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Did not find any container matching {container}"
|
||||
));
|
||||
}
|
||||
|
||||
if containers.len() == 1 {
|
||||
return containers
|
||||
.pop()
|
||||
.context("Shouldn't happen")?
|
||||
.server_id
|
||||
.context("Container doesn't have server_id");
|
||||
}
|
||||
|
||||
let servers = containers
|
||||
.into_iter()
|
||||
.flat_map(|container| container.server_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let servers = client
|
||||
.read(ListServers {
|
||||
query: ServerQuery::builder().names(servers).build(),
|
||||
})
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|server| format!("\t- {}", server.name.bold()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
Err(anyhow!(
|
||||
"Multiple containers matching '{}' on Servers:\n{servers}",
|
||||
container.bold(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn handle_terminal_forwarding<
|
||||
C: Future<Output = anyhow::Result<TerminalWebsocket>>,
|
||||
>(
|
||||
connect: C,
|
||||
) -> anyhow::Result<()> {
|
||||
// Need to forward multiple sources into ws write
|
||||
let (write_tx, mut write_rx) =
|
||||
tokio::sync::mpsc::channel::<TerminalStdinMessage>(1024);
|
||||
|
||||
// ================
|
||||
// SETUP RESIZING
|
||||
// ================
|
||||
|
||||
// Subscribe to SIGWINCH for resize messages
|
||||
let mut sigwinch = tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::window_change(),
|
||||
)
|
||||
.context("failed to register SIGWINCH handler")?;
|
||||
|
||||
// Send first resize messsage, bailing if it fails to get the size.
|
||||
write_tx.send(resize_message()?).await?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
let forward_resize = async {
|
||||
while future_or_cancel(sigwinch.recv(), &cancel)
|
||||
.await
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
if let Ok(resize_message) = resize_message()
|
||||
&& write_tx.send(resize_message).await.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
cancel.cancel();
|
||||
};
|
||||
|
||||
let forward_stdin = async {
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut buf = [0u8; 8192];
|
||||
while let Some(Ok(n)) =
|
||||
future_or_cancel(stdin.read(&mut buf), &cancel).await
|
||||
{
|
||||
// EOF
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let bytes = &buf[..n];
|
||||
// Check for disconnect sequence (alt + q)
|
||||
if bytes == [197, 147] {
|
||||
break;
|
||||
}
|
||||
// Forward bytes
|
||||
if write_tx
|
||||
.send(TerminalStdinMessage::Forward(bytes.to_vec()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
};
|
||||
}
|
||||
cancel.cancel();
|
||||
};
|
||||
|
||||
// =====================
|
||||
// CONNECT AND FORWARD
|
||||
// =====================
|
||||
|
||||
let (mut ws_write, mut ws_read) = connect.await?.split();
|
||||
|
||||
let forward_write = async {
|
||||
while let Some(message) =
|
||||
future_or_cancel(write_rx.recv(), &cancel).await.flatten()
|
||||
{
|
||||
if let Err(e) = ws_write.send_stdin_message(message).await {
|
||||
cancel.cancel();
|
||||
return Some(e);
|
||||
};
|
||||
}
|
||||
cancel.cancel();
|
||||
None
|
||||
};
|
||||
|
||||
let forward_read = async {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
while let Some(msg) =
|
||||
future_or_cancel(ws_read.receive_stdout(), &cancel).await
|
||||
{
|
||||
let bytes = match msg {
|
||||
Ok(Some(bytes)) => bytes,
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
cancel.cancel();
|
||||
return Some(e.context("Websocket read error"));
|
||||
}
|
||||
};
|
||||
if let Err(e) = stdout
|
||||
.write_all(&bytes)
|
||||
.await
|
||||
.context("Failed to write text to stdout")
|
||||
{
|
||||
cancel.cancel();
|
||||
return Some(e);
|
||||
}
|
||||
let _ = stdout.flush().await;
|
||||
}
|
||||
cancel.cancel();
|
||||
None
|
||||
};
|
||||
|
||||
let guard = RawModeGuard::enable_raw_mode()?;
|
||||
|
||||
let (_, _, write_error, read_error) = tokio::join!(
|
||||
forward_resize,
|
||||
forward_stdin,
|
||||
forward_write,
|
||||
forward_read
|
||||
);
|
||||
|
||||
drop(guard);
|
||||
|
||||
if let Some(e) = write_error {
|
||||
eprintln!("\nFailed to forward stdin | {e:#}");
|
||||
}
|
||||
|
||||
if let Some(e) = read_error {
|
||||
eprintln!("\nFailed to forward stdout | {e:#}");
|
||||
}
|
||||
|
||||
println!("\n\n{} {}", "connection".bold(), "closed".red().bold());
|
||||
|
||||
// It doesn't seem to exit by itself after the raw mode stuff.
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
fn resize_message() -> anyhow::Result<TerminalStdinMessage> {
|
||||
let (cols, rows) = crossterm::terminal::size()
|
||||
.context("Failed to get terminal size")?;
|
||||
Ok(TerminalStdinMessage::Resize(TerminalResizeMessage {
|
||||
rows,
|
||||
cols,
|
||||
}))
|
||||
}
|
||||
|
||||
struct RawModeGuard;
|
||||
|
||||
impl RawModeGuard {
|
||||
fn enable_raw_mode() -> anyhow::Result<Self> {
|
||||
crossterm::terminal::enable_raw_mode()
|
||||
.context("Failed to enable terminal raw mode")?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
impl Drop for RawModeGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = crossterm::terminal::disable_raw_mode() {
|
||||
eprintln!("Failed to disable terminal raw mode | {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn future_or_cancel<T, F: Future<Output = T>>(
|
||||
fut: F,
|
||||
cancel: &CancellationToken,
|
||||
) -> Option<T> {
|
||||
tokio::select! {
|
||||
res = fut => Some(res),
|
||||
_ = cancel.cancelled() => None
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ pub async fn update(
|
||||
UpdateUserCommand::SuperAdmin { enabled, yes } => {
|
||||
update_super_admin(username, *enabled, *yes).await
|
||||
}
|
||||
UpdateUserCommand::Clear2fa { yes } => {
|
||||
clear_2fa(username, *yes).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,3 +123,20 @@ async fn update_super_admin(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_2fa(username: &str, yes: bool) -> anyhow::Result<()> {
|
||||
println!("\n{}: Clear 2FA Methods\n", "Mode".dimmed());
|
||||
println!(" - {}: {username}", "Username".dimmed());
|
||||
|
||||
crate::command::wait_for_enter("clear user 2FA methods", yes)?;
|
||||
|
||||
info!("Clearing 2FA methods...");
|
||||
|
||||
let db = database::Client::new(&cli_config().database).await?;
|
||||
|
||||
db.clear_user_2fa_methods(username).await?;
|
||||
|
||||
info!("2FA methods cleared ✅");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+23
-3
@@ -2,6 +2,7 @@
|
||||
extern crate tracing;
|
||||
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use komodo_client::entities::config::cli::args;
|
||||
|
||||
use crate::config::cli_config;
|
||||
@@ -54,7 +55,15 @@ async fn app() -> anyhow::Result<()> {
|
||||
args::Command::Update { command } => {
|
||||
command::update::handle(command).await
|
||||
}
|
||||
args::Command::Ssh(ssh) => command::ssh::handle(ssh).await,
|
||||
args::Command::Connect(connect) => {
|
||||
command::terminal::handle_connect(connect).await
|
||||
}
|
||||
args::Command::Exec(exec) => {
|
||||
command::terminal::handle_exec(exec).await
|
||||
}
|
||||
args::Command::Attach(attach) => {
|
||||
command::terminal::handle_attach(attach).await
|
||||
}
|
||||
args::Command::Key { command } => {
|
||||
noise::key::command::handle(command).await
|
||||
}
|
||||
@@ -70,7 +79,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
tokio::signal::unix::SignalKind::terminate(),
|
||||
)?;
|
||||
tokio::select! {
|
||||
res = tokio::spawn(app()) => res?,
|
||||
_ = term_signal.recv() => Ok(()),
|
||||
res = tokio::spawn(app()) => match res {
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("{}: {e}", "ERROR".red());
|
||||
std::process::exit(1)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", "ERROR".red());
|
||||
std::process::exit(1)
|
||||
},
|
||||
Ok(_) => {}
|
||||
},
|
||||
_ = term_signal.recv() => {},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+9
-3
@@ -20,7 +20,9 @@ periphery_client.workspace = true
|
||||
environment_file.workspace = true
|
||||
interpolate.workspace = true
|
||||
secret_file.workspace = true
|
||||
validations.workspace = true
|
||||
formatting.workspace = true
|
||||
rate_limit.workspace = true
|
||||
transport.workspace = true
|
||||
database.workspace = true
|
||||
encoding.workspace = true
|
||||
@@ -43,33 +45,37 @@ svi.workspace = true
|
||||
# external
|
||||
aws-credential-types.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
openidconnect.workspace = true
|
||||
data-encoding.workspace = true
|
||||
serde_yaml_ng.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
futures-util.workspace = true
|
||||
axum-server.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
urlencoding.workspace = true
|
||||
webauthn-rs.workspace = true
|
||||
aws-config.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum-extra.workspace = true
|
||||
tower-http.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml_ng.workspace = true
|
||||
typeshare.workspace = true
|
||||
chrono-tz.workspace = true
|
||||
indexmap.workspace = true
|
||||
wildcard.workspace = true
|
||||
arc-swap.workspace = true
|
||||
serde_qs.workspace = true
|
||||
colored.workspace = true
|
||||
dashmap.workspace = true
|
||||
tracing.workspace = true
|
||||
reqwest.workspace = true
|
||||
dotenvy.workspace = true
|
||||
totp-rs.workspace = true
|
||||
anyhow.workspace = true
|
||||
croner.workspace = true
|
||||
chrono.workspace = true
|
||||
bcrypt.workspace = true
|
||||
base64.workspace = true
|
||||
rustls.workspace = true
|
||||
bytes.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
# Build Core
|
||||
FROM rust:1.90.0-bullseye AS core-builder
|
||||
FROM rust:1.91.1-trixie AS core-builder
|
||||
RUN cargo install cargo-strip
|
||||
|
||||
WORKDIR /builder
|
||||
@@ -26,7 +26,7 @@ RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
COPY ./bin/core/starship.toml /starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
|
||||
@@ -13,7 +13,7 @@ FROM ${AARCH64_BINARIES} AS aarch64
|
||||
FROM ${FRONTEND_IMAGE} AS frontend
|
||||
|
||||
# Final Image
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
COPY ./bin/core/starship.toml /starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
|
||||
@@ -14,7 +14,7 @@ COPY ./client/core/ts ./client
|
||||
RUN cd client && yarn && yarn build && yarn link
|
||||
RUN cd frontend && yarn link komodo_client && yarn && yarn build
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
COPY ./bin/core/starship.toml /starship.toml
|
||||
COPY ./bin/core/debian-deps.sh .
|
||||
|
||||
@@ -232,9 +232,9 @@ pub async fn send_alert(
|
||||
format!(
|
||||
"{level} | {message}{}",
|
||||
if details.is_empty() {
|
||||
format_args!("")
|
||||
String::new()
|
||||
} else {
|
||||
format_args!("\n{details}")
|
||||
format!("\n{details}")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -466,9 +466,9 @@ fn standard_alert_content(alert: &Alert) -> String {
|
||||
format!(
|
||||
"{level} | {message}{}",
|
||||
if details.is_empty() {
|
||||
format_args!("")
|
||||
String::new()
|
||||
} else {
|
||||
format_args!("\n{details}")
|
||||
format!("\n{details}")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+222
-19
@@ -1,34 +1,61 @@
|
||||
use std::{sync::OnceLock, time::Instant};
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::OnceLock,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Path},
|
||||
http::HeaderMap,
|
||||
routing::post,
|
||||
};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use database::{
|
||||
bson::{doc, to_bson},
|
||||
mungos::by_id::update_one_by_id,
|
||||
};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::auth::*, entities::user::User};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::{AddStatusCode, Json};
|
||||
use serror::{AddStatusCode, AddStatusCodeError, Json};
|
||||
use tower_sessions::Session;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::PasskeyAuthentication;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
SESSION_KEY_PASSKEY_LOGIN, SESSION_KEY_TOTP_LOGIN,
|
||||
SESSION_KEY_USER_ID, memory_session_layer,
|
||||
},
|
||||
auth::{
|
||||
get_user_id_from_headers,
|
||||
github::{self, client::github_oauth_client},
|
||||
google::{self, client::google_oauth_client},
|
||||
oidc::{self, client::oidc_client},
|
||||
totp::make_totp,
|
||||
},
|
||||
config::core_config,
|
||||
helpers::query::get_user,
|
||||
state::jwt_client,
|
||||
state::{auth_rate_limiter, db_client, jwt_client, webauthn},
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AuthArgs {
|
||||
pub headers: HeaderMap,
|
||||
/// Prefer extracting IP from headers.
|
||||
/// This IP will be the IP of reverse proxy itself.
|
||||
pub ip: IpAddr,
|
||||
/// Per-client session state
|
||||
pub session: Option<Session>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -46,6 +73,8 @@ pub enum AuthRequest {
|
||||
SignUpLocalUser(SignUpLocalUser),
|
||||
LoginLocalUser(LoginLocalUser),
|
||||
ExchangeForJwt(ExchangeForJwt),
|
||||
CompleteTotpLogin(CompleteTotpLogin),
|
||||
CompletePasskeyLogin(CompletePasskeyLogin),
|
||||
GetUser(GetUser),
|
||||
}
|
||||
|
||||
@@ -73,11 +102,13 @@ pub fn router() -> Router {
|
||||
router = router.nest("/oidc", oidc::router())
|
||||
}
|
||||
|
||||
router
|
||||
router.layer(memory_session_layer(60))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
headers: HeaderMap,
|
||||
session: Session,
|
||||
info: ConnectInfo<SocketAddr>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
@@ -85,11 +116,13 @@ async fn variant_handler(
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(headers, Json(req)).await
|
||||
handler(headers, session, info, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
headers: HeaderMap,
|
||||
session: Session,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
Json(request): Json<AuthRequest>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let timer = Instant::now();
|
||||
@@ -98,7 +131,13 @@ async fn handler(
|
||||
"/auth request {req_id} | METHOD: {:?}",
|
||||
request.extract_variant()
|
||||
);
|
||||
let res = request.resolve(&AuthArgs { headers }).await;
|
||||
let res = request
|
||||
.resolve(&AuthArgs {
|
||||
headers,
|
||||
ip: info.ip(),
|
||||
session: Some(session),
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = &res {
|
||||
debug!("/auth request {req_id} | error: {:#}", e.error);
|
||||
}
|
||||
@@ -135,25 +174,189 @@ impl Resolve<AuthArgs> for GetLoginOptions {
|
||||
impl Resolve<AuthArgs> for ExchangeForJwt {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &AuthArgs,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<ExchangeForJwtResponse> {
|
||||
jwt_client()
|
||||
.redeem_exchange_token(&self.token)
|
||||
async {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let user_id = session
|
||||
.remove::<String>(SESSION_KEY_USER_ID)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.context("Internal session type error")?
|
||||
.context("Authentication steps must be completed before JWT can be retrieved")?;
|
||||
|
||||
jwt_client().encode(user_id).map_err(Into::into)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for CompleteTotpLogin {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<CompleteTotpLoginResponse> {
|
||||
async {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let user_id = session
|
||||
.get::<String>(SESSION_KEY_TOTP_LOGIN)
|
||||
.await
|
||||
.context("Internal session type error")?
|
||||
.context(
|
||||
"Totp login has not been initiated for this session",
|
||||
)?;
|
||||
|
||||
let user = get_user(&user_id)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if user.totp.secret.is_empty() {
|
||||
return Err(
|
||||
anyhow!("User is not enrolled in totp")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
|
||||
let secret_bytes = BASE32_NOPAD
|
||||
.decode(user.totp.secret.as_bytes())
|
||||
.context("Failed to decode totp secret to bytes")?;
|
||||
|
||||
let totp = make_totp(secret_bytes, None)?;
|
||||
|
||||
let valid = totp
|
||||
.check_current(&self.code)
|
||||
.context("Failed to check TOTP code validity")?;
|
||||
|
||||
if !valid {
|
||||
return Err(
|
||||
anyhow!("Invalid totp code")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
jwt_client().encode(user_id).map_err(Into::into)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for CompletePasskeyLogin {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<CompletePasskeyLoginResponse> {
|
||||
async {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
|
||||
let (user_id, server_state) = session
|
||||
.get::<(String, PasskeyAuthentication)>(
|
||||
SESSION_KEY_PASSKEY_LOGIN,
|
||||
)
|
||||
.await
|
||||
.context("Internal session type error")?
|
||||
.context(
|
||||
"Passkey login has not been initiated for this session",
|
||||
)?;
|
||||
|
||||
// The result of this call must be used to
|
||||
// update the stored passkey info on database.
|
||||
let update = webauthn
|
||||
.finish_passkey_authentication(
|
||||
&self.credential,
|
||||
&server_state,
|
||||
)
|
||||
.context("Failed to validate passkey")?;
|
||||
|
||||
let mut passkey = get_user(&user_id)
|
||||
.await?
|
||||
.passkey
|
||||
.passkey
|
||||
.context("Could not find passkey on database.")?;
|
||||
|
||||
passkey.update_credential(&update);
|
||||
|
||||
let passkey = to_bson(&passkey)
|
||||
.context("Failed to serialize passkey to BSON")?;
|
||||
|
||||
let update = doc! { "$set": { "passkey.passkey": passkey } };
|
||||
|
||||
let _ =
|
||||
update_one_by_id(&db_client().users, &user_id, update, None)
|
||||
.await
|
||||
.context(
|
||||
"Failed to update user passkey on database after login",
|
||||
)
|
||||
.inspect_err(|e| warn!("{e:#}"));
|
||||
|
||||
jwt_client().encode(user_id).map_err(Into::into)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for GetUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs { headers }: &AuthArgs,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session: _,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<User> {
|
||||
let user_id = get_user_id_from_headers(headers)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
get_user(&user_id)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)
|
||||
async {
|
||||
let user_id = get_user_id_from_headers(headers)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
let mut user = get_user(&user_id)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
// Sanitize before sending to client.
|
||||
user.sanitize();
|
||||
Ok(user)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_command;
|
||||
use command::run_komodo_standard_command;
|
||||
use config::merge_objects;
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id, mongodb::bson::to_document,
|
||||
@@ -141,6 +141,7 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
}
|
||||
.resolve(&UserArgs {
|
||||
user: action_user().to_owned(),
|
||||
session: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -178,7 +179,7 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
""
|
||||
};
|
||||
|
||||
let mut res = run_komodo_command(
|
||||
let mut res = run_komodo_standard_command(
|
||||
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
|
||||
"Execute Action",
|
||||
None,
|
||||
@@ -199,6 +200,7 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
if let Err(e) = (DeleteApiKey { key })
|
||||
.resolve(&UserArgs {
|
||||
user: action_user().to_owned(),
|
||||
session: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -104,22 +104,23 @@ impl Resolve<ExecuteArgs> for SendAlert {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let alerters =
|
||||
list_full_for_user::<Alerter>(Default::default(), user, &[])
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|a| {
|
||||
a.config.enabled
|
||||
&& (self.alerters.is_empty()
|
||||
|| self.alerters.contains(&a.name)
|
||||
|| self.alerters.contains(&a.id))
|
||||
&& (a.config.alert_types.is_empty()
|
||||
|| a
|
||||
.config
|
||||
.alert_types
|
||||
.contains(&AlertDataVariant::Custom))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let alerters = list_full_for_user::<Alerter>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|a| {
|
||||
a.config.enabled
|
||||
&& (self.alerters.is_empty()
|
||||
|| self.alerters.contains(&a.name)
|
||||
|| self.alerters.contains(&a.id))
|
||||
&& (a.config.alert_types.is_empty()
|
||||
|| a.config.alert_types.contains(&AlertDataVariant::Custom))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let alerters = if user.admin {
|
||||
alerters
|
||||
|
||||
@@ -17,9 +17,12 @@ use formatting::format_serror;
|
||||
use futures_util::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
|
||||
RunBuild,
|
||||
api::{
|
||||
execute::{
|
||||
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
|
||||
RunBuild,
|
||||
},
|
||||
write::RefreshBuildCache,
|
||||
},
|
||||
entities::{
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
@@ -41,6 +44,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::write::WriteArgs,
|
||||
helpers::{
|
||||
build_git_token,
|
||||
builder::{cleanup_builder_instance, connect_builder_periphery},
|
||||
@@ -186,7 +190,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
update.finalize();
|
||||
let id = update.id.clone();
|
||||
if let Err(e) = update_update(update).await {
|
||||
warn!("failed to modify Update {id} on db | {e:#}");
|
||||
warn!("Failed to modify Update {id} on db | {e:#}");
|
||||
}
|
||||
if !is_server_builder {
|
||||
cancel_clone.cancel();
|
||||
@@ -215,12 +219,12 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
Ok(builder) => builder,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"failed to get builder for build {} | {e:#}",
|
||||
"Failed to get Builder for Build {} | {e:#}",
|
||||
build.name
|
||||
);
|
||||
update.logs.push(Log::error(
|
||||
"get builder",
|
||||
format_serror(&e.context("failed to get builder").into()),
|
||||
"Get Builder",
|
||||
format_serror(&e.context("Failed to get Builder").into()),
|
||||
));
|
||||
return handle_early_return(
|
||||
update, build.id, build.name, false,
|
||||
@@ -276,7 +280,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
|
||||
let commit_message = match res {
|
||||
Ok(res) => {
|
||||
debug!("finished repo clone");
|
||||
debug!("Finished repo clone");
|
||||
update.logs.extend(res.res.logs);
|
||||
update.commit_hash =
|
||||
res.res.commit_hash.unwrap_or_default().to_string();
|
||||
@@ -312,10 +316,10 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
commit_hash: optional_string(&update.commit_hash),
|
||||
// Unused for now
|
||||
additional_tags: Default::default(),
|
||||
}) => res.context("failed at call to periphery to build"),
|
||||
}) => res.context("Failed at call to Periphery to build"),
|
||||
_ = cancel.cancelled() => {
|
||||
info!("Build cancelled during build, cleaning up builder");
|
||||
update.push_error_log("Build cancelled", String::from("user cancelled build during docker build"));
|
||||
update.push_error_log("Build cancelled", String::from("User cancelled build during docker build"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
@@ -328,10 +332,10 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
update.logs.extend(logs);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("error in build | {e:#}");
|
||||
warn!("Error in build | {e:#}");
|
||||
update.push_error_log(
|
||||
"Build Error",
|
||||
format_serror(&e.context("failed to build").into()),
|
||||
format_serror(&e.context("Failed to build").into()),
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -382,12 +386,15 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let Build { id, name, .. } = build;
|
||||
|
||||
if update.success {
|
||||
// don't hold response up for user
|
||||
tokio::spawn(async move {
|
||||
handle_post_build_redeploy(&build.id).await;
|
||||
handle_post_build_redeploy(&id).await;
|
||||
});
|
||||
} else {
|
||||
let name = name.clone();
|
||||
let target = update.target.clone();
|
||||
let version = update.version;
|
||||
tokio::spawn(async move {
|
||||
@@ -398,16 +405,22 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::BuildFailed {
|
||||
id: build.id,
|
||||
name: build.name,
|
||||
version,
|
||||
},
|
||||
data: AlertData::BuildFailed { id, name, version },
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(e) = (RefreshBuildCache { build: name })
|
||||
.resolve(&WriteArgs { user: user.clone() })
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh build cache",
|
||||
format_serror(&e.error.into()),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(update.clone())
|
||||
}
|
||||
}
|
||||
@@ -565,7 +578,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"failed to set CancelBuild Update status Complete after timeout | {e:#}"
|
||||
"Failed to set CancelBuild Update status Complete after timeout | {e:#}"
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
Version,
|
||||
SwarmOrServer, Version,
|
||||
build::{Build, ImageRegistryConfig},
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, extract_registry_domain,
|
||||
@@ -16,22 +16,23 @@ use komodo_client::{
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
update::{Log, Update},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
registry_token,
|
||||
swarm::swarm_request,
|
||||
update::update_update,
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
monitor::{update_cache_for_server, update_cache_for_swarm},
|
||||
resource::{self, setup_deployment_execution},
|
||||
state::action_states,
|
||||
};
|
||||
|
||||
@@ -69,32 +70,6 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument("SetupDeploy", skip_all)]
|
||||
async fn setup_deployment_execution(
|
||||
deployment: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(Deployment, Server)> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if deployment.config.server_id.is_empty() {
|
||||
return Err(anyhow!("Deployment has no Server configured"));
|
||||
}
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&deployment.config.server_id).await?;
|
||||
|
||||
if !server.config.enabled {
|
||||
return Err(anyhow!("Attached Server is not enabled"));
|
||||
}
|
||||
|
||||
Ok((deployment, server))
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for Deploy {
|
||||
#[instrument(
|
||||
"Deploy",
|
||||
@@ -112,8 +87,13 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (mut deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (mut deployment, swarm_or_server) =
|
||||
setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -223,27 +203,55 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
update.version = version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::Deploy {
|
||||
deployment,
|
||||
stop_signal: self.stop_signal,
|
||||
stop_time: self.stop_time,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Deploy Container",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
api::swarm::CreateSwarmService {
|
||||
deployment,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(logs) => {
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
update.logs.extend(logs)
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Create Swarm Service",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
update_cache_for_server(&server, true).await;
|
||||
SwarmOrServer::Server(server) => {
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::RunContainer {
|
||||
deployment,
|
||||
stop_signal: self.stop_signal,
|
||||
stop_time: self.stop_time,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update_cache_for_server(&server, true).await;
|
||||
update.logs.push(log)
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Deploy Container",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
@@ -400,8 +408,19 @@ impl Resolve<ExecuteArgs> for PullDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("PullDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -443,8 +462,19 @@ impl Resolve<ExecuteArgs> for StartDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("StartDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -500,8 +530,19 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("RestartDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -559,8 +600,19 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("PauseDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -616,8 +668,19 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("UnpauseDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -677,8 +740,19 @@ impl Resolve<ExecuteArgs> for StopDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!("StopDeployment should not be called for Deployment in Swarm Mode")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -779,8 +853,12 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&self.deployment, user).await?;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
@@ -798,31 +876,61 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::RemoveContainer {
|
||||
name: deployment.name,
|
||||
signal: self
|
||||
.signal
|
||||
.unwrap_or(deployment.config.termination_signal)
|
||||
.into(),
|
||||
time: self
|
||||
.time
|
||||
.unwrap_or(deployment.config.termination_timeout)
|
||||
.into(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
),
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
api::swarm::RemoveSwarmServices {
|
||||
services: vec![deployment.name],
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
log
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"Remove Swarm Service",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm service").into(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::RemoveContainer {
|
||||
name: deployment.name,
|
||||
signal: self
|
||||
.signal
|
||||
.unwrap_or(deployment.config.termination_signal)
|
||||
.into(),
|
||||
time: self
|
||||
.time
|
||||
.unwrap_or(deployment.config.termination_timeout)
|
||||
.into(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(log) => {
|
||||
update_cache_for_server(&server, true).await;
|
||||
log
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"Destroy Container",
|
||||
format_serror(
|
||||
&e.context("Failed to destroy container").into(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_cache_for_server(&server, true).await;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{fmt::Write as _, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::run_komodo_command;
|
||||
use command::run_komodo_standard_command;
|
||||
use database::{
|
||||
bson::{Document, doc},
|
||||
mungos::find::find_collect,
|
||||
@@ -151,7 +151,7 @@ impl Resolve<ExecuteArgs> for BackupCoreDatabase {
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let res = run_komodo_command(
|
||||
let res = run_komodo_standard_command(
|
||||
"Backup Core Database",
|
||||
None,
|
||||
"km database backup --yes",
|
||||
|
||||
@@ -8,9 +8,7 @@ use axum_extra::{TypedHeader, headers::ContentType};
|
||||
use database::mungos::by_id::find_one_by_id;
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use formatting::format_serror;
|
||||
use futures_util::{
|
||||
StreamExt as _, future::join_all, stream::FuturesUnordered,
|
||||
};
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
@@ -32,7 +30,6 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
auth::auth_request,
|
||||
helpers::update::{init_execution_update, update_update},
|
||||
permission::get_check_permissions,
|
||||
resource::{KomodoResource, list_full_for_user_using_pattern},
|
||||
state::db_client,
|
||||
};
|
||||
@@ -46,6 +43,7 @@ mod procedure;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod stack;
|
||||
mod swarm;
|
||||
mod sync;
|
||||
|
||||
use super::Variant;
|
||||
@@ -72,29 +70,6 @@ pub struct ExecuteArgs {
|
||||
#[error(serror::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum ExecuteRequest {
|
||||
// ==== SERVER ====
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
UnpauseContainer(UnpauseContainer),
|
||||
StopContainer(StopContainer),
|
||||
DestroyContainer(DestroyContainer),
|
||||
StartAllContainers(StartAllContainers),
|
||||
RestartAllContainers(RestartAllContainers),
|
||||
PauseAllContainers(PauseAllContainers),
|
||||
UnpauseAllContainers(UnpauseAllContainers),
|
||||
StopAllContainers(StopAllContainers),
|
||||
PruneContainers(PruneContainers),
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
PruneNetworks(PruneNetworks),
|
||||
DeleteImage(DeleteImage),
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// ==== STACK ====
|
||||
DeployStack(DeployStack),
|
||||
BatchDeployStack(BatchDeployStack),
|
||||
@@ -145,12 +120,42 @@ pub enum ExecuteRequest {
|
||||
RunAction(RunAction),
|
||||
BatchRunAction(BatchRunAction),
|
||||
|
||||
// ==== SYNC ====
|
||||
RunSync(RunSync),
|
||||
|
||||
// ==== ALERTER ====
|
||||
TestAlerter(TestAlerter),
|
||||
SendAlert(SendAlert),
|
||||
|
||||
// ==== SYNC ====
|
||||
RunSync(RunSync),
|
||||
// ==== SERVER ====
|
||||
StartContainer(StartContainer),
|
||||
RestartContainer(RestartContainer),
|
||||
PauseContainer(PauseContainer),
|
||||
UnpauseContainer(UnpauseContainer),
|
||||
StopContainer(StopContainer),
|
||||
DestroyContainer(DestroyContainer),
|
||||
StartAllContainers(StartAllContainers),
|
||||
RestartAllContainers(RestartAllContainers),
|
||||
PauseAllContainers(PauseAllContainers),
|
||||
UnpauseAllContainers(UnpauseAllContainers),
|
||||
StopAllContainers(StopAllContainers),
|
||||
PruneContainers(PruneContainers),
|
||||
DeleteNetwork(DeleteNetwork),
|
||||
PruneNetworks(PruneNetworks),
|
||||
DeleteImage(DeleteImage),
|
||||
PruneImages(PruneImages),
|
||||
DeleteVolume(DeleteVolume),
|
||||
PruneVolumes(PruneVolumes),
|
||||
PruneDockerBuilders(PruneDockerBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// ==== SWARM ====
|
||||
RemoveSwarmNodes(RemoveSwarmNodes),
|
||||
RemoveSwarmStacks(RemoveSwarmStacks),
|
||||
RemoveSwarmServices(RemoveSwarmServices),
|
||||
RemoveSwarmConfigs(RemoveSwarmConfigs),
|
||||
RemoveSwarmSecrets(RemoveSwarmSecrets),
|
||||
|
||||
// ==== MAINTENANCE ====
|
||||
ClearRepoCache(ClearRepoCache),
|
||||
@@ -324,32 +329,11 @@ async fn batch_execute<E: BatchExecute>(
|
||||
pattern,
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let resources = if user.admin {
|
||||
resources
|
||||
} else {
|
||||
// Only keep resources with execute permissions
|
||||
resources
|
||||
.into_iter()
|
||||
.map(|resource| async move {
|
||||
get_check_permissions::<E::Resource>(
|
||||
&resource.id,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
};
|
||||
|
||||
let futures = resources.into_iter().map(|resource| {
|
||||
let user = user.clone();
|
||||
async move {
|
||||
|
||||
@@ -70,8 +70,8 @@ impl Resolve<ExecuteArgs> for StartContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"start container",
|
||||
format_serror(&e.context("failed to start container").into()),
|
||||
"Start Container",
|
||||
format_serror(&e.context("Failed to start container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -134,9 +134,9 @@ impl Resolve<ExecuteArgs> for RestartContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"restart container",
|
||||
"Restart Container",
|
||||
format_serror(
|
||||
&e.context("failed to restart container").into(),
|
||||
&e.context("Failed to restart container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
@@ -200,8 +200,8 @@ impl Resolve<ExecuteArgs> for PauseContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"pause container",
|
||||
format_serror(&e.context("failed to pause container").into()),
|
||||
"Pause Container",
|
||||
format_serror(&e.context("Failed to pause container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -264,9 +264,9 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"unpause container",
|
||||
"Unpause Container",
|
||||
format_serror(
|
||||
&e.context("failed to unpause container").into(),
|
||||
&e.context("Failed to unpause container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
@@ -334,8 +334,8 @@ impl Resolve<ExecuteArgs> for StopContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
"Stop Container",
|
||||
format_serror(&e.context("Failed to stop container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -408,8 +408,10 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
"Remove Container",
|
||||
format_serror(
|
||||
&e.context("Failed to remove container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -464,13 +466,13 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
|
||||
.await?
|
||||
.request(api::container::StartAllContainers {})
|
||||
.await
|
||||
.context("failed to start all containers on host")?;
|
||||
.context("Failed to start all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"start all containers",
|
||||
"Start All Containers",
|
||||
String::from("All containers have been started on the host."),
|
||||
);
|
||||
}
|
||||
@@ -524,13 +526,13 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
|
||||
.await?
|
||||
.request(api::container::RestartAllContainers {})
|
||||
.await
|
||||
.context("failed to restart all containers on host")?;
|
||||
.context("Failed to restart all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"restart all containers",
|
||||
"Restart All Containers",
|
||||
String::from(
|
||||
"All containers have been restarted on the host.",
|
||||
),
|
||||
@@ -586,13 +588,13 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
|
||||
.await?
|
||||
.request(api::container::PauseAllContainers {})
|
||||
.await
|
||||
.context("failed to pause all containers on host")?;
|
||||
.context("Failed to pause all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"pause all containers",
|
||||
"Pause All Containers",
|
||||
String::from("All containers have been paused on the host."),
|
||||
);
|
||||
}
|
||||
@@ -646,13 +648,13 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
|
||||
.await?
|
||||
.request(api::container::UnpauseAllContainers {})
|
||||
.await
|
||||
.context("failed to unpause all containers on host")?;
|
||||
.context("Failed to unpause all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"unpause all containers",
|
||||
"Unpause All Containers",
|
||||
String::from(
|
||||
"All containers have been unpaused on the host.",
|
||||
),
|
||||
@@ -708,13 +710,13 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
|
||||
.await?
|
||||
.request(api::container::StopAllContainers {})
|
||||
.await
|
||||
.context("failed to stop all containers on host")?;
|
||||
.context("Failed to stop all containers on host")?;
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
update.push_simple_log(
|
||||
"stop all containers",
|
||||
"Stop All Containers",
|
||||
String::from("All containers have been stopped on the host."),
|
||||
);
|
||||
}
|
||||
@@ -770,14 +772,14 @@ impl Resolve<ExecuteArgs> for PruneContainers {
|
||||
.request(api::container::PruneContainers {})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to prune containers on server {}",
|
||||
"Failed to prune containers on server {}",
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune containers",
|
||||
"Prune Containers",
|
||||
format_serror(
|
||||
&e.context("failed to prune containers").into(),
|
||||
&e.context("Failed to prune containers").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
@@ -827,15 +829,15 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
|
||||
})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to delete network {} on server {}",
|
||||
"Failed to delete network {} on server {}",
|
||||
self.name, server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"delete network",
|
||||
"Delete Network",
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"failed to delete network {}",
|
||||
"Failed to delete network {}",
|
||||
self.name
|
||||
))
|
||||
.into(),
|
||||
@@ -896,13 +898,13 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
|
||||
.request(api::docker::PruneNetworks {})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to prune networks on server {}",
|
||||
"Failed to prune networks on server {}",
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune networks",
|
||||
format_serror(&e.context("failed to prune networks").into()),
|
||||
"Prune Networks",
|
||||
format_serror(&e.context("Failed to prune networks").into()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -951,14 +953,14 @@ impl Resolve<ExecuteArgs> for DeleteImage {
|
||||
})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to delete image {} on server {}",
|
||||
"Failed to delete image {} on server {}",
|
||||
self.name, server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"delete image",
|
||||
format_serror(
|
||||
&e.context(format!("failed to delete image {}", self.name))
|
||||
&e.context(format!("Failed to delete image {}", self.name))
|
||||
.into(),
|
||||
),
|
||||
),
|
||||
@@ -1017,9 +1019,9 @@ impl Resolve<ExecuteArgs> for PruneImages {
|
||||
match periphery.request(api::docker::PruneImages {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune images",
|
||||
"Prune Images",
|
||||
format!(
|
||||
"failed to prune images on server {} | {e:#?}",
|
||||
"Failed to prune images on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1070,7 +1072,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
|
||||
})
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to delete volume {} on server {}",
|
||||
"Failed to delete volume {} on server {}",
|
||||
self.name, server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
@@ -1078,7 +1080,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
|
||||
"delete volume",
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"failed to delete volume {}",
|
||||
"Failed to delete volume {}",
|
||||
self.name
|
||||
))
|
||||
.into(),
|
||||
@@ -1139,9 +1141,9 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
|
||||
match periphery.request(api::docker::PruneVolumes {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune volumes",
|
||||
"Prune Volumes",
|
||||
format!(
|
||||
"failed to prune volumes on server {} | {e:#?}",
|
||||
"Failed to prune volumes on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1200,9 +1202,9 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
|
||||
match periphery.request(api::build::PruneBuilders {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune builders",
|
||||
"Prune Builders",
|
||||
format!(
|
||||
"failed to docker builder prune on server {} | {e:#?}",
|
||||
"Failed to docker builder prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1261,9 +1263,9 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
|
||||
match periphery.request(api::build::PruneBuildx {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune buildx",
|
||||
"Prune Buildx",
|
||||
format!(
|
||||
"failed to docker buildx prune on server {} | {e:#?}",
|
||||
"Failed to docker buildx prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
@@ -1321,9 +1323,9 @@ impl Resolve<ExecuteArgs> for PruneSystem {
|
||||
let log = match periphery.request(api::PruneSystem {}).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error(
|
||||
"prune system",
|
||||
"Prune System",
|
||||
format!(
|
||||
"failed to docker system prune on server {} | {e:#?}",
|
||||
"Failed to docker system prune on server {} | {e:#?}",
|
||||
server.name
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::mongodb::bson::{
|
||||
doc, oid::ObjectId, to_bson, to_document,
|
||||
};
|
||||
@@ -9,7 +9,7 @@ use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{execute::*, write::RefreshStackCache},
|
||||
entities::{
|
||||
FileContents,
|
||||
FileContents, SwarmOrServer,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
@@ -20,8 +20,12 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::compose::*;
|
||||
use periphery_client::api::{
|
||||
DeployStackResponse, compose::*, swarm::DeploySwarmStack,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -30,14 +34,20 @@ use crate::{
|
||||
periphery_client,
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
stack_git_token,
|
||||
swarm::swarm_request,
|
||||
update::{
|
||||
add_update_without_send, init_execution_update, update_update,
|
||||
},
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
monitor::{update_cache_for_server, update_cache_for_swarm},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
stack::{execute::execute_compose, get_stack_and_server},
|
||||
stack::{
|
||||
execute::{
|
||||
execute_compose, execute_compose_with_stack_and_server,
|
||||
},
|
||||
setup_stack_execution,
|
||||
},
|
||||
state::{action_states, db_client},
|
||||
};
|
||||
|
||||
@@ -92,11 +102,10 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (mut stack, server) = get_stack_and_server(
|
||||
let (mut stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -165,27 +174,44 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let ComposeUpResponse {
|
||||
let DeployStackResponse {
|
||||
logs,
|
||||
deployed,
|
||||
services,
|
||||
file_contents,
|
||||
missing_files,
|
||||
remote_errors,
|
||||
compose_config,
|
||||
merged_config,
|
||||
commit_hash,
|
||||
commit_message,
|
||||
} = periphery_client(&server)
|
||||
.await?
|
||||
.request(ComposeUp {
|
||||
stack: stack.clone(),
|
||||
services: self.services,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await?;
|
||||
} = match &swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
DeploySwarmStack {
|
||||
stack: stack.clone(),
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
periphery_client(server)
|
||||
.await?
|
||||
.request(ComposeUp {
|
||||
stack: stack.clone(),
|
||||
services: self.services,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
update.logs.extend(logs);
|
||||
|
||||
@@ -219,7 +245,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
compose_config,
|
||||
merged_config,
|
||||
commit_hash.clone(),
|
||||
commit_message.clone(),
|
||||
)
|
||||
@@ -257,7 +283,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
};
|
||||
|
||||
let info = to_document(&info)
|
||||
.context("failed to serialize stack info to bson")?;
|
||||
.context("Failed to serialize stack info to bson")?;
|
||||
|
||||
db_client()
|
||||
.stacks
|
||||
@@ -266,22 +292,29 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
doc! { "$set": { "info": info } },
|
||||
)
|
||||
.await
|
||||
.context("failed to update stack info on db")?;
|
||||
.context("Failed to update stack info on db")?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
// This will be weird with single service deploys. Come back to it.
|
||||
if let Err(e) = update_info.await {
|
||||
update.push_error_log(
|
||||
"refresh stack info",
|
||||
"Refresh Stack Info",
|
||||
format_serror(
|
||||
&e.context("failed to refresh stack info on db").into(),
|
||||
&e.context("Failed to refresh stack info on db").into(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Ensure cached stack state up to date by updating server cache
|
||||
update_cache_for_server(&server, true).await;
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
update_cache_for_server(&server, true).await;
|
||||
}
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
@@ -862,14 +895,22 @@ impl Resolve<ExecuteArgs> for PullStack {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"PullStack should not be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let repo = if !stack.config.files_on_host
|
||||
&& !stack.config.linked_repo.is_empty()
|
||||
{
|
||||
@@ -1106,16 +1147,77 @@ impl Resolve<ExecuteArgs> for DestroyStack {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
execute_compose::<DestroyStack>(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
self.services,
|
||||
user,
|
||||
|state| state.destroying = true,
|
||||
update.clone(),
|
||||
(self.stop_time, self.remove_orphans),
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.await?;
|
||||
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
if !self.services.is_empty() {
|
||||
return Err(
|
||||
anyhow!("Cannot destroy specific Stack services when in Swarm mode.")
|
||||
.status_code(StatusCode::BAD_REQUEST)
|
||||
);
|
||||
}
|
||||
|
||||
// get the action state for the stack (or insert default).
|
||||
let action_state = action_states()
|
||||
.stack
|
||||
.get_or_insert_default(&stack.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure stack not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.destroying = true)?;
|
||||
|
||||
let mut update = update.clone();
|
||||
// Send update here for frontend to recheck action state
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmStacks {
|
||||
stacks: vec![stack.project_name(false)],
|
||||
detach: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Destroy Stack",
|
||||
format_serror(
|
||||
&e.context("Failed to 'docker stack rm' on swarm")
|
||||
.into(),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
// Ensure cached stack state up to date by updating swarm cache
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
execute_compose_with_stack_and_server::<DestroyStack>(
|
||||
stack,
|
||||
server,
|
||||
self.services,
|
||||
|state| state.destroying = true,
|
||||
update.clone(),
|
||||
(self.stop_time, self.remove_orphans),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,14 +1238,22 @@ impl Resolve<ExecuteArgs> for RunStackService {
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
let (mut stack, server) = get_stack_and_server(
|
||||
let (mut stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"RunStackService should not be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let mut repo = if !stack.config.files_on_host
|
||||
&& !stack.config.linked_repo.is_empty()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::{
|
||||
RemoveSwarmConfigs, RemoveSwarmNodes, RemoveSwarmSecrets,
|
||||
RemoveSwarmServices, RemoveSwarmStacks,
|
||||
},
|
||||
entities::{permission::PermissionLevel, swarm::Swarm},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteArgs,
|
||||
helpers::{swarm::swarm_request, update::update_update},
|
||||
permission::get_check_permissions,
|
||||
};
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmNodes {
|
||||
#[instrument(
|
||||
"RemoveSwarmNodes",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
nodes = serde_json::to_string(&self.nodes).unwrap_or_else(|e| e.to_string()),
|
||||
force = self.force,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmNodes {
|
||||
nodes: self.nodes,
|
||||
force: self.force,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Nodes",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm nodes").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmStacks {
|
||||
#[instrument(
|
||||
"RemoveSwarmStacks",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
stacks = serde_json::to_string(&self.stacks).unwrap_or_else(|e| e.to_string()),
|
||||
detach = self.detach,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmStacks {
|
||||
stacks: self.stacks,
|
||||
detach: self.detach,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Stacks",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm stacks").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmServices {
|
||||
#[instrument(
|
||||
"RemoveSwarmServices",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
services = serde_json::to_string(&self.services).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmServices {
|
||||
services: self.services,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Services",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm services").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmConfigs {
|
||||
#[instrument(
|
||||
"RemoveSwarmConfigs",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
configs = serde_json::to_string(&self.configs).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmConfigs {
|
||||
configs: self.configs,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Configs",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm configs").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ExecuteArgs> for RemoveSwarmSecrets {
|
||||
#[instrument(
|
||||
"RemoveSwarmSecrets",
|
||||
skip_all,
|
||||
fields(
|
||||
id = id.to_string(),
|
||||
operator = user.id,
|
||||
update_id = update.id,
|
||||
swarm = self.swarm,
|
||||
secrets = serde_json::to_string(&self.secrets).unwrap_or_else(|e| e.to_string()),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let mut update = update.clone();
|
||||
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::RemoveSwarmSecrets {
|
||||
secrets: self.secrets,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"Remove Swarm Secrets",
|
||||
format_serror(
|
||||
&e.context("Failed to remove swarm secrets").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
@@ -136,34 +136,10 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
};
|
||||
match ObjectId::from_str(&name_or_id) {
|
||||
Ok(_) => match resource_type {
|
||||
ResourceTargetVariant::Alerter => all_resources
|
||||
.alerters
|
||||
ResourceTargetVariant::Swarm => all_resources
|
||||
.swarms
|
||||
.get(&name_or_id)
|
||||
.map(|a| a.name.clone()),
|
||||
ResourceTargetVariant::Build => all_resources
|
||||
.builds
|
||||
.get(&name_or_id)
|
||||
.map(|b| b.name.clone()),
|
||||
ResourceTargetVariant::Builder => all_resources
|
||||
.builders
|
||||
.get(&name_or_id)
|
||||
.map(|b| b.name.clone()),
|
||||
ResourceTargetVariant::Deployment => all_resources
|
||||
.deployments
|
||||
.get(&name_or_id)
|
||||
.map(|d| d.name.clone()),
|
||||
ResourceTargetVariant::Procedure => all_resources
|
||||
.procedures
|
||||
.get(&name_or_id)
|
||||
.map(|p| p.name.clone()),
|
||||
ResourceTargetVariant::Action => all_resources
|
||||
.actions
|
||||
.get(&name_or_id)
|
||||
.map(|p| p.name.clone()),
|
||||
ResourceTargetVariant::Repo => all_resources
|
||||
.repos
|
||||
.get(&name_or_id)
|
||||
.map(|r| r.name.clone()),
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::Server => all_resources
|
||||
.servers
|
||||
.get(&name_or_id)
|
||||
@@ -172,10 +148,38 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
.stacks
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::Deployment => all_resources
|
||||
.deployments
|
||||
.get(&name_or_id)
|
||||
.map(|d| d.name.clone()),
|
||||
ResourceTargetVariant::Build => all_resources
|
||||
.builds
|
||||
.get(&name_or_id)
|
||||
.map(|b| b.name.clone()),
|
||||
ResourceTargetVariant::Repo => all_resources
|
||||
.repos
|
||||
.get(&name_or_id)
|
||||
.map(|r| r.name.clone()),
|
||||
ResourceTargetVariant::Procedure => all_resources
|
||||
.procedures
|
||||
.get(&name_or_id)
|
||||
.map(|p| p.name.clone()),
|
||||
ResourceTargetVariant::Action => all_resources
|
||||
.actions
|
||||
.get(&name_or_id)
|
||||
.map(|p| p.name.clone()),
|
||||
ResourceTargetVariant::ResourceSync => all_resources
|
||||
.syncs
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::Builder => all_resources
|
||||
.builders
|
||||
.get(&name_or_id)
|
||||
.map(|b| b.name.clone()),
|
||||
ResourceTargetVariant::Alerter => all_resources
|
||||
.alerters
|
||||
.get(&name_or_id)
|
||||
.map(|a| a.name.clone()),
|
||||
ResourceTargetVariant::System => None,
|
||||
},
|
||||
Err(_) => Some(name_or_id),
|
||||
|
||||
+4
-5
@@ -5,10 +5,9 @@ use hmac::{Hmac, Mac};
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
listener::{ExtractBranch, VerifySecret},
|
||||
};
|
||||
use crate::config::core_config;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
@@ -18,7 +17,7 @@ pub struct Github;
|
||||
impl VerifySecret for Github {
|
||||
#[instrument("VerifyGithubSecret", skip_all)]
|
||||
fn verify_secret(
|
||||
headers: HeaderMap,
|
||||
headers: &HeaderMap,
|
||||
body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::http::HeaderMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
listener::{ExtractBranch, VerifySecret},
|
||||
};
|
||||
use crate::config::core_config;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
|
||||
/// Listener implementation for Gitlab type API
|
||||
pub struct Gitlab;
|
||||
@@ -12,7 +12,7 @@ pub struct Gitlab;
|
||||
impl VerifySecret for Gitlab {
|
||||
#[instrument("VerifyGitlabSecret", skip_all)]
|
||||
fn verify_secret(
|
||||
headers: axum::http::HeaderMap,
|
||||
headers: &HeaderMap,
|
||||
_body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
|
||||
use super::{ExtractBranch, VerifySecret};
|
||||
@@ -32,7 +32,7 @@ trait CustomSecret: KomodoResource {
|
||||
/// Implemented on the integration struct, eg [integrations::github::Github]
|
||||
trait VerifySecret {
|
||||
fn verify_secret(
|
||||
headers: HeaderMap,
|
||||
headers: &HeaderMap,
|
||||
body: &str,
|
||||
custom_secret: &str,
|
||||
) -> anyhow::Result<()>;
|
||||
@@ -1,14 +1,22 @@
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Path},
|
||||
http::HeaderMap,
|
||||
routing::post,
|
||||
};
|
||||
use komodo_client::entities::{
|
||||
action::Action, build::Build, procedure::Procedure, repo::Repo,
|
||||
resource::Resource, stack::Stack, sync::ResourceSync,
|
||||
};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::resource::KomodoResource;
|
||||
use crate::{resource::KomodoResource, state::auth_rate_limiter};
|
||||
|
||||
use super::{
|
||||
CustomSecret, ExtractBranch, VerifySecret,
|
||||
@@ -47,9 +55,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/build/{id}",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
|Path(Id { id }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let build =
|
||||
auth_webhook::<P, Build>(&id, headers, &body).await?;
|
||||
auth_webhook::<P, Build>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("BuildWebhook", id);
|
||||
async {
|
||||
@@ -73,9 +81,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/repo/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let repo =
|
||||
auth_webhook::<P, Repo>(&id, headers, &body).await?;
|
||||
auth_webhook::<P, Repo>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("RepoWebhook", id);
|
||||
async {
|
||||
@@ -99,9 +107,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/stack/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let stack =
|
||||
auth_webhook::<P, Stack>(&id, headers, &body).await?;
|
||||
auth_webhook::<P, Stack>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("StackWebhook", id);
|
||||
async {
|
||||
@@ -125,9 +133,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/sync/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let sync =
|
||||
auth_webhook::<P, ResourceSync>(&id, headers, &body).await?;
|
||||
auth_webhook::<P, ResourceSync>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ResourceSyncWebhook", id);
|
||||
async {
|
||||
@@ -151,9 +159,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/procedure/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let procedure =
|
||||
auth_webhook::<P, Procedure>(&id, headers, &body).await?;
|
||||
auth_webhook::<P, Procedure>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ProcedureWebhook", id);
|
||||
async {
|
||||
@@ -177,9 +185,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/action/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, body: String| async move {
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
let action =
|
||||
auth_webhook::<P, Action>(&id, headers, &body).await?;
|
||||
auth_webhook::<P, Action>(&id, &headers, info.ip(), &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ActionWebhook", id);
|
||||
async {
|
||||
@@ -204,17 +212,26 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
|
||||
async fn auth_webhook<P, R>(
|
||||
id: &str,
|
||||
headers: HeaderMap,
|
||||
headers: &HeaderMap,
|
||||
ip: IpAddr,
|
||||
body: &str,
|
||||
) -> serror::Result<Resource<R::Config, R::Info>>
|
||||
where
|
||||
P: VerifySecret,
|
||||
R: KomodoResource + CustomSecret,
|
||||
{
|
||||
let resource = crate::resource::get::<R>(id)
|
||||
.await
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
P::verify_secret(headers, body, R::custom_secret(&resource))
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
Ok(resource)
|
||||
async {
|
||||
let resource = crate::resource::get::<R>(id)
|
||||
.await
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
P::verify_secret(headers, body, R::custom_secret(&resource))
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
serror::Result::Ok(resource)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
+82
-1
@@ -1,11 +1,92 @@
|
||||
use axum::{
|
||||
Router,
|
||||
http::{HeaderName, HeaderValue},
|
||||
routing::get,
|
||||
};
|
||||
use tower_http::{
|
||||
services::{ServeDir, ServeFile},
|
||||
set_header::SetResponseHeaderLayer,
|
||||
};
|
||||
use tower_sessions::{
|
||||
Expiry, MemoryStore, SessionManagerLayer, cookie::time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{core_config, core_host, cors_layer},
|
||||
ts_client,
|
||||
};
|
||||
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod terminal;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
mod listener;
|
||||
mod terminal;
|
||||
mod ws;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Variant {
|
||||
variant: String,
|
||||
}
|
||||
|
||||
pub fn app() -> Router {
|
||||
let config = core_config();
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = &config.frontend_path;
|
||||
let frontend_index =
|
||||
ServeFile::new(format!("{frontend_path}/index.html"));
|
||||
let serve_frontend = ServeDir::new(frontend_path)
|
||||
.not_found_service(frontend_index.clone());
|
||||
|
||||
Router::new()
|
||||
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
|
||||
.nest("/auth", auth::router())
|
||||
.nest("/user", user::router())
|
||||
.nest("/read", read::router())
|
||||
.nest("/write", write::router())
|
||||
.nest("/execute", execute::router())
|
||||
.nest("/terminal", terminal::router())
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
.fallback_service(serve_frontend)
|
||||
.layer(cors_layer())
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-content-type-options"),
|
||||
HeaderValue::from_static("nosniff"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-frame-options"),
|
||||
HeaderValue::from_static("DENY"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-xss-protection"),
|
||||
HeaderValue::from_static("1; mode=block"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("referrer-policy"),
|
||||
HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
))
|
||||
}
|
||||
|
||||
fn memory_session_layer(
|
||||
expiry: i64,
|
||||
) -> SessionManagerLayer<MemoryStore> {
|
||||
let config = core_config();
|
||||
let mut layer = SessionManagerLayer::new(MemoryStore::default())
|
||||
.with_expiry(Expiry::OnInactivity(Duration::seconds(expiry)))
|
||||
.with_secure(config.host.starts_with("https://"));
|
||||
if let Some(domain) = core_host().and_then(|url| url.domain()) {
|
||||
layer = layer.with_domain(domain);
|
||||
}
|
||||
layer
|
||||
}
|
||||
|
||||
pub const SESSION_KEY_USER_ID: &str = "user-id";
|
||||
pub const SESSION_KEY_TOTP_LOGIN: &str = "totp-user-id";
|
||||
pub const SESSION_KEY_TOTP_ENROLLMENT: &str = "totp-enrollment";
|
||||
pub const SESSION_KEY_PASSKEY_LOGIN: &str = "passkey-user-id";
|
||||
pub const SESSION_KEY_PASSKEY_ENROLLMENT: &str = "passkey-enrollment";
|
||||
|
||||
@@ -46,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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -64,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?,
|
||||
)
|
||||
@@ -100,6 +108,7 @@ impl Resolve<ReadArgs> for GetActionsSummary {
|
||||
let actions = resource::list_full_for_user::<Action>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -8,15 +8,15 @@ use komodo_client::{
|
||||
api::read::{
|
||||
GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,
|
||||
},
|
||||
entities::{
|
||||
deployment::Deployment, server::Server, stack::Stack,
|
||||
sync::ResourceSync,
|
||||
},
|
||||
entities::permission::PermissionLevel,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config, permission::get_resource_ids_for_user,
|
||||
config::core_config,
|
||||
permission::{
|
||||
check_user_target_access, user_resource_target_query,
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
@@ -29,25 +29,10 @@ impl Resolve<ReadArgs> for ListAlerts {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListAlertsResponse> {
|
||||
let mut query = self.query.unwrap_or_default();
|
||||
if !user.admin && !core_config().transparent_mode {
|
||||
let server_ids =
|
||||
get_resource_ids_for_user::<Server>(user).await?;
|
||||
let stack_ids =
|
||||
get_resource_ids_for_user::<Stack>(user).await?;
|
||||
let deployment_ids =
|
||||
get_resource_ids_for_user::<Deployment>(user).await?;
|
||||
let sync_ids =
|
||||
get_resource_ids_for_user::<ResourceSync>(user).await?;
|
||||
query.extend(doc! {
|
||||
"$or": [
|
||||
{ "target.type": "Server", "target.id": { "$in": &server_ids } },
|
||||
{ "target.type": "Stack", "target.id": { "$in": &stack_ids } },
|
||||
{ "target.type": "Deployment", "target.id": { "$in": &deployment_ids } },
|
||||
{ "target.type": "ResourceSync", "target.id": { "$in": &sync_ids } },
|
||||
]
|
||||
});
|
||||
}
|
||||
// Alerts
|
||||
let query = user_resource_target_query(user, self.query)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let alerts = find_collect(
|
||||
&db_client().alerts,
|
||||
@@ -76,13 +61,21 @@ impl Resolve<ReadArgs> for ListAlerts {
|
||||
impl Resolve<ReadArgs> for GetAlert {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetAlertResponse> {
|
||||
Ok(
|
||||
find_one_by_id(&db_client().alerts, &self.id)
|
||||
.await
|
||||
.context("failed to query db for alert")?
|
||||
.context("no alert found with given id")?,
|
||||
let alert = find_one_by_id(&db_client().alerts, &self.id)
|
||||
.await
|
||||
.context("failed to query db for alert")?
|
||||
.context("no alert found with given id")?;
|
||||
if user.admin || core_config().transparent_mode {
|
||||
return Ok(alert);
|
||||
}
|
||||
check_user_target_access(
|
||||
&alert.target,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(alert)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ use komodo_client::{
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, permission::get_check_permissions,
|
||||
resource, state::db_client,
|
||||
helpers::query::get_all_tags,
|
||||
permission::{get_check_permissions, list_resource_ids_for_user},
|
||||
resource,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -44,8 +46,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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -62,7 +69,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?,
|
||||
)
|
||||
@@ -74,9 +84,11 @@ impl Resolve<ReadArgs> for GetAlertersSummary {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetAlertersSummaryResponse> {
|
||||
let query = match resource::get_resource_object_ids_for_user::<
|
||||
Alerter,
|
||||
>(user)
|
||||
let query = match list_resource_ids_for_user::<Alerter>(
|
||||
None,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
|
||||
@@ -54,8 +54,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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +77,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?,
|
||||
)
|
||||
@@ -108,6 +116,7 @@ impl Resolve<ReadArgs> for GetBuildsSummary {
|
||||
let builds = resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -271,7 +280,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")?;
|
||||
|
||||
@@ -11,8 +11,10 @@ use komodo_client::{
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, permission::get_check_permissions,
|
||||
resource, state::db_client,
|
||||
helpers::query::get_all_tags,
|
||||
permission::{get_check_permissions, list_resource_ids_for_user},
|
||||
resource,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -44,8 +46,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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -62,7 +69,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?,
|
||||
)
|
||||
@@ -74,9 +84,11 @@ impl Resolve<ReadArgs> for GetBuildersSummary {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetBuildersSummaryResponse> {
|
||||
let query = match resource::get_resource_object_ids_for_user::<
|
||||
Builder,
|
||||
>(user)
|
||||
let query = match list_resource_ids_for_user::<Builder>(
|
||||
None,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(ids) => doc! {
|
||||
|
||||
@@ -4,23 +4,31 @@ use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
SwarmOrServer,
|
||||
deployment::{
|
||||
Deployment, DeploymentActionState, DeploymentConfig,
|
||||
DeploymentListItem, DeploymentState,
|
||||
},
|
||||
docker::container::{Container, ContainerStats},
|
||||
docker::{
|
||||
container::{Container, ContainerStats},
|
||||
service::SwarmService,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
update::Log,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::{self, container::InspectContainer};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
helpers::{
|
||||
periphery_client, query::get_all_tags, swarm::swarm_request,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
resource::{self, setup_deployment_execution},
|
||||
state::{
|
||||
action_states, deployment_status_cache, server_status_cache,
|
||||
},
|
||||
@@ -56,7 +64,10 @@ impl Resolve<ReadArgs> for ListDeployments {
|
||||
};
|
||||
let only_update_available = self.query.specific.update_available;
|
||||
let deployments = resource::list_for_user::<Deployment>(
|
||||
self.query, user, &all_tags,
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?;
|
||||
let deployments = if only_update_available {
|
||||
@@ -83,7 +94,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?,
|
||||
)
|
||||
@@ -125,30 +139,40 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = get_check_permissions::<Deployment>(
|
||||
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
return Ok(Log::default());
|
||||
}
|
||||
let server = resource::get::<Server>(&server_id).await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::GetContainerLog {
|
||||
name,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
service: deployment.name,
|
||||
tail,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed at call to periphery")?;
|
||||
Ok(res)
|
||||
.context("Failed to get service log from swarm")?,
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::GetContainerLog {
|
||||
name: deployment.name,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,32 +188,44 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = get_check_permissions::<Deployment>(
|
||||
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
return Ok(Log::default());
|
||||
}
|
||||
let server = resource::get::<Server>(&server_id).await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::GetContainerLogSearch {
|
||||
name,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
service: deployment.name,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed at call to periphery")?;
|
||||
Ok(res)
|
||||
.context("Failed to search service log from swarm")?,
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(api::container::GetContainerLogSearch {
|
||||
name: deployment.name,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to search container log from server")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,42 +235,78 @@ impl Resolve<ReadArgs> for InspectDeploymentContainer {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Container> {
|
||||
let InspectDeploymentContainer { deployment } = self;
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
..
|
||||
} = get_check_permissions::<Deployment>(
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
if server_id.is_empty() {
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot inspect deployment, not attached to any server"
|
||||
"InspectDeploymentContainer should not be called for Deployment in Swarm Mode"
|
||||
)
|
||||
.into(),
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
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 {:?}",
|
||||
"Cannot inspect container: Server is {:?}",
|
||||
cache.state
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let res = periphery_client(&server)
|
||||
|
||||
periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectContainer { name })
|
||||
.await?;
|
||||
Ok(res)
|
||||
.request(InspectContainer {
|
||||
name: deployment.name,
|
||||
})
|
||||
.await
|
||||
.context("Failed to inspect container on server")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDeploymentSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmService> {
|
||||
let InspectDeploymentSwarmService { deployment } = self;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectDeploymentSwarmService should only be called for Deployment in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmService {
|
||||
service: deployment.name,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to inspect service on swarm")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +369,7 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -342,7 +415,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")?;
|
||||
|
||||
@@ -11,6 +11,7 @@ use komodo_client::{
|
||||
build::Build,
|
||||
builder::{Builder, BuilderConfig},
|
||||
config::{DockerRegistry, GitProvider},
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
sync::ResourceSync,
|
||||
@@ -48,8 +49,10 @@ mod repo;
|
||||
mod schedule;
|
||||
mod server;
|
||||
mod stack;
|
||||
mod swarm;
|
||||
mod sync;
|
||||
mod tag;
|
||||
mod terminal;
|
||||
mod toml;
|
||||
mod update;
|
||||
mod user;
|
||||
@@ -73,36 +76,28 @@ enum ReadRequest {
|
||||
ListGitProvidersFromConfig(ListGitProvidersFromConfig),
|
||||
ListDockerRegistriesFromConfig(ListDockerRegistriesFromConfig),
|
||||
|
||||
// ==== USER ====
|
||||
GetUsername(GetUsername),
|
||||
GetPermission(GetPermission),
|
||||
FindUser(FindUser),
|
||||
ListUsers(ListUsers),
|
||||
ListApiKeys(ListApiKeys),
|
||||
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
|
||||
ListPermissions(ListPermissions),
|
||||
ListUserTargetPermissions(ListUserTargetPermissions),
|
||||
|
||||
// ==== USER GROUP ====
|
||||
GetUserGroup(GetUserGroup),
|
||||
ListUserGroups(ListUserGroups),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
GetProceduresSummary(GetProceduresSummary),
|
||||
GetProcedure(GetProcedure),
|
||||
GetProcedureActionState(GetProcedureActionState),
|
||||
ListProcedures(ListProcedures),
|
||||
ListFullProcedures(ListFullProcedures),
|
||||
|
||||
// ==== ACTION ====
|
||||
GetActionsSummary(GetActionsSummary),
|
||||
GetAction(GetAction),
|
||||
GetActionActionState(GetActionActionState),
|
||||
ListActions(ListActions),
|
||||
ListFullActions(ListFullActions),
|
||||
|
||||
// ==== SCHEDULE ====
|
||||
ListSchedules(ListSchedules),
|
||||
// ==== SWARM ====
|
||||
GetSwarmsSummary(GetSwarmsSummary),
|
||||
GetSwarm(GetSwarm),
|
||||
GetSwarmActionState(GetSwarmActionState),
|
||||
ListSwarms(ListSwarms),
|
||||
InspectSwarm(InspectSwarm),
|
||||
ListFullSwarms(ListFullSwarms),
|
||||
ListSwarmNodes(ListSwarmNodes),
|
||||
InspectSwarmNode(InspectSwarmNode),
|
||||
ListSwarmConfigs(ListSwarmConfigs),
|
||||
InspectSwarmConfig(InspectSwarmConfig),
|
||||
ListSwarmSecrets(ListSwarmSecrets),
|
||||
InspectSwarmSecret(InspectSwarmSecret),
|
||||
ListSwarmStacks(ListSwarmStacks),
|
||||
InspectSwarmStack(InspectSwarmStack),
|
||||
ListSwarmTasks(ListSwarmTasks),
|
||||
InspectSwarmTask(InspectSwarmTask),
|
||||
ListSwarmServices(ListSwarmServices),
|
||||
InspectSwarmService(InspectSwarmService),
|
||||
GetSwarmServiceLog(GetSwarmServiceLog),
|
||||
SearchSwarmServiceLog(SearchSwarmServiceLog),
|
||||
ListSwarmNetworks(ListSwarmNetworks),
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary(GetServersSummary),
|
||||
@@ -110,11 +105,11 @@ enum ReadRequest {
|
||||
GetServerState(GetServerState),
|
||||
GetPeripheryInformation(GetPeripheryInformation),
|
||||
GetServerActionState(GetServerActionState),
|
||||
GetHistoricalServerStats(GetHistoricalServerStats),
|
||||
ListServers(ListServers),
|
||||
ListFullServers(ListFullServers),
|
||||
|
||||
// ==== TERMINAL ====
|
||||
ListTerminals(ListTerminals),
|
||||
ListAllTerminals(ListAllTerminals),
|
||||
|
||||
// ==== DOCKER ====
|
||||
GetDockerContainersSummary(GetDockerContainersSummary),
|
||||
@@ -136,6 +131,7 @@ enum ReadRequest {
|
||||
// ==== SERVER STATS ====
|
||||
GetSystemInformation(GetSystemInformation),
|
||||
GetSystemStats(GetSystemStats),
|
||||
GetHistoricalServerStats(GetHistoricalServerStats),
|
||||
ListSystemProcesses(ListSystemProcesses),
|
||||
|
||||
// ==== STACK ====
|
||||
@@ -145,6 +141,7 @@ enum ReadRequest {
|
||||
GetStackLog(GetStackLog),
|
||||
SearchStackLog(SearchStackLog),
|
||||
InspectStackContainer(InspectStackContainer),
|
||||
InspectStackSwarmService(InspectStackSwarmService),
|
||||
ListStacks(ListStacks),
|
||||
ListFullStacks(ListFullStacks),
|
||||
ListStackServices(ListStackServices),
|
||||
@@ -160,6 +157,7 @@ enum ReadRequest {
|
||||
GetDeploymentLog(GetDeploymentLog),
|
||||
SearchDeploymentLog(SearchDeploymentLog),
|
||||
InspectDeploymentContainer(InspectDeploymentContainer),
|
||||
InspectDeploymentSwarmService(InspectDeploymentSwarmService),
|
||||
ListDeployments(ListDeployments),
|
||||
ListFullDeployments(ListFullDeployments),
|
||||
ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs),
|
||||
@@ -181,6 +179,23 @@ enum ReadRequest {
|
||||
ListRepos(ListRepos),
|
||||
ListFullRepos(ListFullRepos),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
GetProceduresSummary(GetProceduresSummary),
|
||||
GetProcedure(GetProcedure),
|
||||
GetProcedureActionState(GetProcedureActionState),
|
||||
ListProcedures(ListProcedures),
|
||||
ListFullProcedures(ListFullProcedures),
|
||||
|
||||
// ==== ACTION ====
|
||||
GetActionsSummary(GetActionsSummary),
|
||||
GetAction(GetAction),
|
||||
GetActionActionState(GetActionActionState),
|
||||
ListActions(ListActions),
|
||||
ListFullActions(ListFullActions),
|
||||
|
||||
// ==== SCHEDULE ====
|
||||
ListSchedules(ListSchedules),
|
||||
|
||||
// ==== SYNC ====
|
||||
GetResourceSyncsSummary(GetResourceSyncsSummary),
|
||||
GetResourceSync(GetResourceSync),
|
||||
@@ -208,6 +223,20 @@ enum ReadRequest {
|
||||
GetTag(GetTag),
|
||||
ListTags(ListTags),
|
||||
|
||||
// ==== USER ====
|
||||
GetUsername(GetUsername),
|
||||
GetPermission(GetPermission),
|
||||
FindUser(FindUser),
|
||||
ListUsers(ListUsers),
|
||||
ListApiKeys(ListApiKeys),
|
||||
ListApiKeysForServiceUser(ListApiKeysForServiceUser),
|
||||
ListPermissions(ListPermissions),
|
||||
ListUserTargetPermissions(ListUserTargetPermissions),
|
||||
|
||||
// ==== USER GROUP ====
|
||||
GetUserGroup(GetUserGroup),
|
||||
ListUserGroups(ListUserGroups),
|
||||
|
||||
// ==== UPDATE ====
|
||||
GetUpdate(GetUpdate),
|
||||
ListUpdates(ListUpdates),
|
||||
@@ -398,16 +427,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(),
|
||||
&[]
|
||||
),
|
||||
)?;
|
||||
|
||||
@@ -45,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?,
|
||||
)
|
||||
@@ -64,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?,
|
||||
)
|
||||
@@ -79,6 +85,7 @@ impl Resolve<ReadArgs> for GetProceduresSummary {
|
||||
let procedures = resource::list_full_for_user::<Procedure>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -44,8 +44,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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -62,7 +67,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?,
|
||||
)
|
||||
@@ -98,6 +106,7 @@ impl Resolve<ReadArgs> for GetReposSummary {
|
||||
let repos = resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -4,6 +4,7 @@ use komodo_client::{
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
action::Action,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
resource::{ResourceQuery, TemplatesQueryBehavior},
|
||||
schedule::Schedule,
|
||||
@@ -35,6 +36,7 @@ impl Resolve<ReadArgs> for ListSchedules {
|
||||
specific: Default::default(),
|
||||
},
|
||||
&args.user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
),
|
||||
list_full_for_user::<Procedure>(
|
||||
@@ -46,6 +48,7 @@ impl Resolve<ReadArgs> for ListSchedules {
|
||||
specific: Default::default(),
|
||||
},
|
||||
&args.user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
)?;
|
||||
|
||||
+90
-158
@@ -12,7 +12,6 @@ use database::mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{bson::doc, options::FindOptions},
|
||||
};
|
||||
use futures_util::{StreamExt as _, stream::FuturesUnordered};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
@@ -26,11 +25,10 @@ use komodo_client::{
|
||||
network::Network,
|
||||
volume::Volume,
|
||||
},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::{
|
||||
Server, ServerActionState, ServerListItem, ServerState,
|
||||
TerminalInfo, TerminalInfoWithServer,
|
||||
Server, ServerActionState, ServerListItem, ServerQuery,
|
||||
ServerState,
|
||||
},
|
||||
stack::{Stack, StackServiceNames},
|
||||
stats::{SystemInformation, SystemProcess},
|
||||
@@ -51,7 +49,7 @@ use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
permission::get_check_permissions,
|
||||
permission::{get_check_permissions, list_resources_for_user},
|
||||
resource,
|
||||
stack::compose_container_match_regex,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
@@ -67,6 +65,7 @@ impl Resolve<ReadArgs> for GetServersSummary {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
@@ -127,8 +126,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?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -145,7 +149,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?,
|
||||
)
|
||||
@@ -376,8 +383,8 @@ impl Resolve<ReadArgs> for ListDockerContainers {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(containers) = &cache.containers {
|
||||
Ok(containers.clone())
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.containers.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@@ -390,17 +397,12 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListAllDockerContainersResponse> {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
ServerQuery::builder().names(self.servers.clone()).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|server| {
|
||||
self.servers.is_empty()
|
||||
|| self.servers.contains(&server.id)
|
||||
|| self.servers.contains(&server.name)
|
||||
});
|
||||
.await?;
|
||||
|
||||
let mut containers = Vec::<ContainerListItem>::new();
|
||||
|
||||
@@ -408,9 +410,18 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(more_containers) = &cache.containers {
|
||||
containers.extend(more_containers.clone());
|
||||
}
|
||||
let Some(docker) = &cache.docker else {
|
||||
continue;
|
||||
};
|
||||
let more = docker
|
||||
.containers
|
||||
.iter()
|
||||
.filter(|container| {
|
||||
self.containers.is_empty()
|
||||
|| self.containers.contains(&container.name)
|
||||
})
|
||||
.cloned();
|
||||
containers.extend(more);
|
||||
}
|
||||
|
||||
Ok(containers)
|
||||
@@ -425,6 +436,7 @@ impl Resolve<ReadArgs> for GetDockerContainersSummary {
|
||||
let servers = resource::list_full_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
@@ -437,8 +449,8 @@ impl Resolve<ReadArgs> for GetDockerContainersSummary {
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
if let Some(containers) = &cache.containers {
|
||||
for container in containers {
|
||||
if let Some(docker) = &cache.docker {
|
||||
for container in &docker.containers {
|
||||
res.total += 1;
|
||||
match container.state {
|
||||
ContainerStateStatusEnum::Created
|
||||
@@ -576,12 +588,12 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
|
||||
}
|
||||
|
||||
// then check stacks
|
||||
let stacks =
|
||||
resource::list_full_for_user_using_document::<Stack>(
|
||||
doc! { "config.server_id": &server.id },
|
||||
user,
|
||||
)
|
||||
.await?;
|
||||
let stacks = list_resources_for_user::<Stack>(
|
||||
doc! { "config.server_id": &server.id },
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// check matching stack
|
||||
for stack in stacks {
|
||||
@@ -630,8 +642,8 @@ impl Resolve<ReadArgs> for ListDockerNetworks {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(networks) = &cache.networks {
|
||||
Ok(networks.clone())
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.networks.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@@ -683,8 +695,8 @@ impl Resolve<ReadArgs> for ListDockerImages {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(images) = &cache.images {
|
||||
Ok(images.clone())
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.images.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@@ -764,8 +776,8 @@ impl Resolve<ReadArgs> for ListDockerVolumes {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(volumes) = &cache.volumes {
|
||||
Ok(volumes.clone())
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.volumes.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@@ -814,134 +826,54 @@ impl Resolve<ReadArgs> for ListComposeProjects {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(projects) = &cache.projects {
|
||||
Ok(projects.clone())
|
||||
if let Some(docker) = &cache.docker {
|
||||
Ok(docker.projects.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TerminalCacheItem {
|
||||
list: Vec<TerminalInfo>,
|
||||
ttl: i64,
|
||||
}
|
||||
// impl Resolve<ReadArgs> for ListAllTerminals {
|
||||
// async fn resolve(
|
||||
// self,
|
||||
// args: &ReadArgs,
|
||||
// ) -> Result<Self::Response, Self::Error> {
|
||||
// // match self.tar
|
||||
// let mut terminals = resource::list_full_for_user::<Server>(
|
||||
// self.query, &args.user, &all_tags,
|
||||
// )
|
||||
// .await?
|
||||
// .into_iter()
|
||||
// .map(|server| async move {
|
||||
// (
|
||||
// list_terminals_inner(&server, self.fresh).await,
|
||||
// (server.id, server.name),
|
||||
// )
|
||||
// })
|
||||
// .collect::<FuturesUnordered<_>>()
|
||||
// .collect::<Vec<_>>()
|
||||
// .await
|
||||
// .into_iter()
|
||||
// .flat_map(|(terminals, server)| {
|
||||
// let terminals = terminals.ok()?;
|
||||
// Some((terminals, server))
|
||||
// })
|
||||
// .flat_map(|(terminals, (server_id, server_name))| {
|
||||
// terminals.into_iter().map(move |info| {
|
||||
// TerminalInfoWithServer::from_terminal_info(
|
||||
// &server_id,
|
||||
// &server_name,
|
||||
// info,
|
||||
// )
|
||||
// })
|
||||
// })
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
const TERMINAL_CACHE_TIMEOUT: i64 = 30_000;
|
||||
// terminals.sort_by(|a, b| {
|
||||
// a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name))
|
||||
// });
|
||||
|
||||
#[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?;
|
||||
list_terminals_inner(&server, self.fresh)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListAllTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
|
||||
let mut terminals = resource::list_full_for_user::<Server>(
|
||||
self.query, &args.user, &all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|server| async move {
|
||||
(
|
||||
list_terminals_inner(&server, self.fresh).await,
|
||||
(server.id, server.name),
|
||||
)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(|(terminals, server)| {
|
||||
let terminals = terminals.ok()?;
|
||||
Some((terminals, server))
|
||||
})
|
||||
.flat_map(|(terminals, (server_id, server_name))| {
|
||||
terminals.into_iter().map(move |info| {
|
||||
TerminalInfoWithServer::from_terminal_info(
|
||||
&server_id,
|
||||
&server_name,
|
||||
info,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
terminals.sort_by(|a, b| {
|
||||
a.server_name.cmp(&b.server_name).then(a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(terminals)
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_terminals_inner(
|
||||
server: &Server,
|
||||
fresh: bool,
|
||||
) -> anyhow::Result<Vec<TerminalInfo>> {
|
||||
let cache = terminals_cache().get_or_insert(server.id.clone());
|
||||
let mut cache = cache.lock().await;
|
||||
if fresh || komodo_timestamp() > cache.ttl {
|
||||
cache.list = periphery_client(server)
|
||||
.await?
|
||||
.request(periphery_client::api::terminal::ListTerminals {
|
||||
container: None,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get fresh terminal list")?;
|
||||
cache.ttl = komodo_timestamp() + TERMINAL_CACHE_TIMEOUT;
|
||||
Ok(cache.list.clone())
|
||||
} else {
|
||||
Ok(cache.list.clone())
|
||||
}
|
||||
}
|
||||
// Ok(terminals)
|
||||
// }
|
||||
// }
|
||||
|
||||
+208
-62
@@ -4,9 +4,11 @@ use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
docker::container::Container,
|
||||
SwarmOrServer,
|
||||
docker::{
|
||||
container::Container, service::SwarmService, stack::SwarmStack,
|
||||
},
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
stack::{Stack, StackActionState, StackListItem, StackState},
|
||||
},
|
||||
};
|
||||
@@ -14,14 +16,18 @@ use periphery_client::api::{
|
||||
compose::{GetComposeLog, GetComposeLogSearch},
|
||||
container::InspectContainer,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, query::get_all_tags},
|
||||
helpers::{
|
||||
periphery_client, query::get_all_tags, swarm::swarm_request,
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
stack::get_stack_and_server,
|
||||
state::{action_states, server_status_cache, stack_status_cache},
|
||||
stack::setup_stack_execution,
|
||||
state::{action_states, stack_status_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -73,28 +79,53 @@ impl Resolve<ReadArgs> for GetStackLog {
|
||||
) -> serror::Result<GetStackLogResponse> {
|
||||
let GetStackLog {
|
||||
stack,
|
||||
services,
|
||||
mut services,
|
||||
tail,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLog {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
tail,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get stack log from periphery")?;
|
||||
Ok(res)
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
)?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
// The actual service name on swarm will be stackname_servicename
|
||||
service: format!(
|
||||
"{}_{service}",
|
||||
stack.project_name(false)
|
||||
),
|
||||
tail,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to get stack service log from swarm")?
|
||||
}
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLog {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
tail,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to get stack log from periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,32 +136,55 @@ impl Resolve<ReadArgs> for SearchStackLog {
|
||||
) -> serror::Result<SearchStackLogResponse> {
|
||||
let SearchStackLog {
|
||||
stack,
|
||||
services,
|
||||
mut services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
} = self;
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLogSearch {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to search stack log from periphery")?;
|
||||
Ok(res)
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
)?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
service,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
no_task_ids: false,
|
||||
no_resolve: false,
|
||||
details: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to get stack service log from swarm")?
|
||||
}
|
||||
SwarmOrServer::Server(server) => periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeLogSearch {
|
||||
project: stack.project_name(false),
|
||||
services,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("Failed to search stack log from periphery")?,
|
||||
};
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,38 +194,29 @@ impl Resolve<ReadArgs> for InspectStackContainer {
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Container> {
|
||||
let InspectStackContainer { stack, service } = self;
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&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 {
|
||||
|
||||
let SwarmOrServer::Server(server) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot inspect container: server is {:?}",
|
||||
cache.state
|
||||
"InspectStackContainer should not be called for Stack in Swarm Mode"
|
||||
)
|
||||
.into(),
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let services = &stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
.services;
|
||||
|
||||
let Some(name) = services
|
||||
.iter()
|
||||
.find(|s| s.service == service)
|
||||
@@ -181,14 +226,101 @@ impl Resolve<ReadArgs> for InspectStackContainer {
|
||||
"No service found matching '{service}'. Was the stack last deployed manually?"
|
||||
).into());
|
||||
};
|
||||
|
||||
let res = periphery_client(&server)
|
||||
.await?
|
||||
.request(InspectContainer { name })
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to inspect container on server")?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectStackSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmService> {
|
||||
let InspectStackSwarmService { stack, service } = self;
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectStackSwarmService should only be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
let services = &stack_status_cache()
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.curr
|
||||
.services;
|
||||
|
||||
let Some(service) = services
|
||||
.iter()
|
||||
.find(|s| s.service == service)
|
||||
.and_then(|s| {
|
||||
s.swarm_service.as_ref().and_then(|c| c.name.clone())
|
||||
})
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"No service found matching '{service}'. Was the stack last deployed manually?"
|
||||
).into());
|
||||
};
|
||||
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmService { service },
|
||||
)
|
||||
.await
|
||||
.context("Failed to inspect service on swarm")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectStackSwarmInfo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmStack> {
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let SwarmOrServer::Swarm(swarm) = swarm_or_server else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"InspectStackSwarmInfo should only be called for Stack in Swarm Mode"
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
};
|
||||
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmStack {
|
||||
stack: stack.project_name(false),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("Failed to inspect stack info on swarm")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
@@ -200,10 +332,13 @@ 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")?;
|
||||
.context("Failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
@@ -231,10 +366,13 @@ 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")?;
|
||||
.context("Failed to get resources matching query")?;
|
||||
|
||||
// first collect with guaranteed uniqueness
|
||||
let mut res = HashSet::<String>::new();
|
||||
@@ -262,9 +400,13 @@ impl Resolve<ReadArgs> for ListStacks {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
let only_update_available = self.query.specific.update_available;
|
||||
let stacks =
|
||||
resource::list_for_user::<Stack>(self.query, user, &all_tags)
|
||||
.await?;
|
||||
let stacks = resource::list_for_user::<Stack>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?;
|
||||
let stacks = if only_update_available {
|
||||
stacks
|
||||
.into_iter()
|
||||
@@ -295,7 +437,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?,
|
||||
)
|
||||
@@ -331,10 +476,11 @@ impl Resolve<ReadArgs> for GetStacksSummary {
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get stacks from db")?;
|
||||
.context("Failed to get stacks from database")?;
|
||||
|
||||
let mut res = GetStacksSummaryResponse::default();
|
||||
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::read::*,
|
||||
entities::{
|
||||
permission::PermissionLevel,
|
||||
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{query::get_all_tags, swarm::swarm_request},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, server_status_cache, swarm_status_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
Ok(
|
||||
get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarms {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<SwarmListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_for_user::<Swarm>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListFullSwarms {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullSwarmsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
Ok(
|
||||
resource::list_full_for_user::<Swarm>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarmActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmActionState> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let action_state = action_states()
|
||||
.swarm
|
||||
.get(&swarm.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.get()?;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarmsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetSwarmsSummaryResponse> {
|
||||
let swarms = resource::list_full_for_user::<Swarm>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get swarms from db")?;
|
||||
|
||||
let mut res = GetSwarmsSummaryResponse::default();
|
||||
|
||||
let cache = swarm_status_cache();
|
||||
|
||||
for swarm in swarms {
|
||||
res.total += 1;
|
||||
|
||||
match cache
|
||||
.get(&swarm.id)
|
||||
.await
|
||||
.map(|status| status.state)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
SwarmState::Unknown => {
|
||||
res.unknown += 1;
|
||||
}
|
||||
SwarmState::Healthy => {
|
||||
res.healthy += 1;
|
||||
}
|
||||
SwarmState::Unhealthy => {
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
let inspect = cache
|
||||
.inspect
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.context("SwarmInspectInfo not available")?;
|
||||
Ok(inspect)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmNodes {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmNodesResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.nodes.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmNode {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmNodeResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmNode {
|
||||
node: self.node,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmServices {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmServicesResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.services.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmServiceResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmService {
|
||||
service: self.service,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetSwarmServiceLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetSwarmServiceLogResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
service: self.service,
|
||||
tail: self.tail,
|
||||
timestamps: self.timestamps,
|
||||
no_task_ids: self.no_task_ids,
|
||||
no_resolve: self.no_resolve,
|
||||
details: self.details,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for SearchSwarmServiceLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SearchSwarmServiceLogResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.logs(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
service: self.service,
|
||||
terms: self.terms,
|
||||
combinator: self.combinator,
|
||||
invert: self.invert,
|
||||
timestamps: self.timestamps,
|
||||
no_task_ids: self.no_task_ids,
|
||||
no_resolve: self.no_resolve,
|
||||
details: self.details,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmTasks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmTasksResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.tasks.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmTask {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmTaskResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmTask {
|
||||
task: self.task,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmSecrets {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmSecretsResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.secrets.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmSecret {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmSecretResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmSecret {
|
||||
secret: self.secret,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmConfigs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmConfigsResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.configs.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmConfigResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmConfig {
|
||||
config: self.config,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmStacks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmStacksResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
let cache =
|
||||
swarm_status_cache().get_or_insert_default(&swarm.id).await;
|
||||
if let Some(lists) = &cache.lists {
|
||||
Ok(lists.stacks.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarmStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmStackResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.inspect(),
|
||||
)
|
||||
.await?;
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::InspectSwarmStack {
|
||||
stack: self.stack,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmNetworks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cache = server_status_cache();
|
||||
|
||||
for server_id in swarm.config.server_ids {
|
||||
let Some(status) = cache.get(&server_id).await else {
|
||||
continue;
|
||||
};
|
||||
let Some(docker) = &status.docker else {
|
||||
continue;
|
||||
};
|
||||
let networks = docker
|
||||
.networks
|
||||
.iter()
|
||||
.filter(|network| {
|
||||
network.driver.as_deref() == Some("overlay")
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
return Ok(networks);
|
||||
}
|
||||
|
||||
Err(
|
||||
anyhow!(
|
||||
"Failed to retrieve swarm networks from any manager node."
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,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?,
|
||||
)
|
||||
@@ -64,7 +67,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?,
|
||||
)
|
||||
@@ -101,6 +107,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
use anyhow::Context as _;
|
||||
use futures_util::{
|
||||
FutureExt, StreamExt as _, stream::FuturesUnordered,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::read::{ListTerminals, ListTerminalsResponse},
|
||||
entities::{
|
||||
deployment::Deployment,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
terminal::{Terminal, TerminalTarget},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, permission::get_check_permissions,
|
||||
resource,
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListTerminalsResponse> {
|
||||
let Some(target) = self.target else {
|
||||
return list_all_terminals_for_user(user, self.use_names).await;
|
||||
};
|
||||
match &target {
|
||||
TerminalTarget::Server { server } => {
|
||||
let server = server
|
||||
.as_ref()
|
||||
.context("Must provide 'target.params.server'")
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
let server = get_check_permissions::<Server>(
|
||||
server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
TerminalTarget::Container { server, .. } => {
|
||||
let server = get_check_permissions::<Server>(
|
||||
server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
TerminalTarget::Stack { stack, .. } => {
|
||||
let server = get_check_permissions::<Stack>(
|
||||
stack,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
.config
|
||||
.server_id;
|
||||
let server = resource::get::<Server>(&server).await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
TerminalTarget::Deployment { deployment } => {
|
||||
let server = get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
.config
|
||||
.server_id;
|
||||
let server = resource::get::<Server>(&server).await?;
|
||||
list_terminals_on_server(&server, Some(target)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_all_terminals_for_user(
|
||||
user: &User,
|
||||
use_names: bool,
|
||||
) -> serror::Result<Vec<Terminal>> {
|
||||
let (mut servers, stacks, deployments) = tokio::try_join!(
|
||||
resource::list_full_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
&[]
|
||||
)
|
||||
.map(|res| res.map(|servers| servers
|
||||
.into_iter()
|
||||
// true denotes user actually has permission on this Server.
|
||||
.map(|server| (server, true))
|
||||
.collect::<Vec<_>>())),
|
||||
resource::list_full_for_user::<Stack>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
&[]
|
||||
),
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
Default::default(),
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
&[]
|
||||
),
|
||||
)?;
|
||||
|
||||
// Ensure any missing servers are present to query
|
||||
for stack in &stacks {
|
||||
if !stack.config.server_id.is_empty()
|
||||
&& !servers
|
||||
.iter()
|
||||
.any(|(server, _)| server.id == stack.config.server_id)
|
||||
{
|
||||
let server =
|
||||
resource::get::<Server>(&stack.config.server_id).await?;
|
||||
servers.push((server, false));
|
||||
}
|
||||
}
|
||||
for deployment in &deployments {
|
||||
if !deployment.config.server_id.is_empty()
|
||||
&& !servers
|
||||
.iter()
|
||||
.any(|(server, _)| server.id == deployment.config.server_id)
|
||||
{
|
||||
let server =
|
||||
resource::get::<Server>(&deployment.config.server_id).await?;
|
||||
servers.push((server, false));
|
||||
}
|
||||
}
|
||||
|
||||
let mut terminals = servers
|
||||
.into_iter()
|
||||
.map(|(server, server_permission)| async move {
|
||||
(
|
||||
list_terminals_on_server(&server, None).await,
|
||||
(server.id, server.name, server_permission),
|
||||
)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(
|
||||
|(terminals, (server_id, server_name, server_permission))| {
|
||||
let terminals = terminals
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.filter_map(|mut terminal| {
|
||||
// Only keep terminals with appropriate perms.
|
||||
match terminal.target.clone() {
|
||||
TerminalTarget::Server { .. } => server_permission
|
||||
.then(|| {
|
||||
terminal.target = TerminalTarget::Server {
|
||||
server: Some(if use_names {
|
||||
server_name.clone()
|
||||
} else {
|
||||
server_id.clone()
|
||||
}),
|
||||
};
|
||||
terminal
|
||||
}),
|
||||
TerminalTarget::Container { container, .. } => {
|
||||
server_permission.then(|| {
|
||||
terminal.target = TerminalTarget::Container {
|
||||
server: if use_names {
|
||||
server_name.clone()
|
||||
} else {
|
||||
server_id.clone()
|
||||
},
|
||||
container,
|
||||
};
|
||||
terminal
|
||||
})
|
||||
}
|
||||
TerminalTarget::Stack { stack, service } => {
|
||||
stacks.iter().find(|s| s.id == stack).map(|s| {
|
||||
terminal.target = TerminalTarget::Stack {
|
||||
stack: if use_names {
|
||||
s.name.clone()
|
||||
} else {
|
||||
s.id.clone()
|
||||
},
|
||||
service,
|
||||
};
|
||||
terminal
|
||||
})
|
||||
}
|
||||
TerminalTarget::Deployment { deployment } => {
|
||||
deployments.iter().find(|d| d.id == deployment).map(
|
||||
|d| {
|
||||
terminal.target = TerminalTarget::Deployment {
|
||||
deployment: if use_names {
|
||||
d.name.clone()
|
||||
} else {
|
||||
d.id.clone()
|
||||
},
|
||||
};
|
||||
terminal
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(terminals)
|
||||
},
|
||||
)
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
terminals.sort_by(|a, b| {
|
||||
a.target.cmp(&b.target).then(a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(terminals)
|
||||
}
|
||||
|
||||
async fn list_terminals_on_server(
|
||||
server: &Server,
|
||||
target: Option<TerminalTarget>,
|
||||
) -> serror::Result<Vec<Terminal>> {
|
||||
periphery_client(server)
|
||||
.await?
|
||||
.request(periphery_client::api::terminal::ListTerminals {
|
||||
target,
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to get Terminal list from Server {} ({})",
|
||||
server.name, server.id
|
||||
)
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
@@ -11,7 +11,8 @@ use komodo_client::{
|
||||
builder::Builder, deployment::Deployment,
|
||||
permission::PermissionLevel, procedure::Procedure, repo::Repo,
|
||||
resource::ResourceQuery, server::Server, stack::Stack,
|
||||
sync::ResourceSync, toml::ResourcesToml, user::User,
|
||||
swarm::Swarm, sync::ResourceSync, toml::ResourcesToml,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
@@ -46,6 +47,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Alerter>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -56,6 +58,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Builder>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -66,6 +69,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Server>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -76,6 +80,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Stack>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -86,6 +91,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Deployment>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -96,6 +102,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Build>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -106,6 +113,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Repo>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -116,6 +124,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Procedure>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -126,6 +135,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<Action>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -136,6 +146,7 @@ async fn get_all_targets(
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
ResourceQuery::builder().tags(tags).build(),
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
@@ -197,42 +208,21 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
let ReadArgs { user } = args;
|
||||
for target in targets {
|
||||
match target {
|
||||
ResourceTarget::Alerter(id) => {
|
||||
let mut alerter = get_check_permissions::<Alerter>(
|
||||
ResourceTarget::Swarm(id) => {
|
||||
let mut swarm = get_check_permissions::<Swarm>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Alerter::replace_ids(&mut alerter);
|
||||
res.alerters.push(convert_resource::<Alerter>(
|
||||
alerter,
|
||||
Swarm::replace_ids(&mut swarm);
|
||||
res.swarms.push(convert_resource::<Swarm>(
|
||||
swarm,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
let mut sync = get_check_permissions::<ResourceSync>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
if sync.config.file_contents.is_empty()
|
||||
&& (sync.config.files_on_host
|
||||
|| !sync.config.repo.is_empty()
|
||||
|| !sync.config.linked_repo.is_empty())
|
||||
{
|
||||
ResourceSync::replace_ids(&mut sync);
|
||||
res.resource_syncs.push(convert_resource::<ResourceSync>(
|
||||
sync,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
let mut server = get_check_permissions::<Server>(
|
||||
&id,
|
||||
@@ -248,31 +238,16 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
let mut builder = get_check_permissions::<Builder>(
|
||||
ResourceTarget::Stack(id) => {
|
||||
let mut stack = get_check_permissions::<Stack>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Builder::replace_ids(&mut builder);
|
||||
res.builders.push(convert_resource::<Builder>(
|
||||
builder,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
let mut build = get_check_permissions::<Build>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Build::replace_ids(&mut build);
|
||||
res.builds.push(convert_resource::<Build>(
|
||||
build,
|
||||
Stack::replace_ids(&mut stack);
|
||||
res.stacks.push(convert_resource::<Stack>(
|
||||
stack,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
@@ -293,6 +268,21 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
let mut build = get_check_permissions::<Build>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Build::replace_ids(&mut build);
|
||||
res.builds.push(convert_resource::<Build>(
|
||||
build,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&id,
|
||||
@@ -308,21 +298,6 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
let mut stack = get_check_permissions::<Stack>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Stack::replace_ids(&mut stack);
|
||||
res.stacks.push(convert_resource::<Stack>(
|
||||
stack,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
let mut procedure = get_check_permissions::<Procedure>(
|
||||
&id,
|
||||
@@ -353,6 +328,57 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
&id_to_tags,
|
||||
));
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
let mut sync = get_check_permissions::<ResourceSync>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
if sync.config.file_contents.is_empty()
|
||||
&& (sync.config.files_on_host
|
||||
|| !sync.config.repo.is_empty()
|
||||
|| !sync.config.linked_repo.is_empty())
|
||||
{
|
||||
ResourceSync::replace_ids(&mut sync);
|
||||
res.resource_syncs.push(convert_resource::<ResourceSync>(
|
||||
sync,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
let mut builder = get_check_permissions::<Builder>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Builder::replace_ids(&mut builder);
|
||||
res.builders.push(convert_resource::<Builder>(
|
||||
builder,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
let mut alerter = get_check_permissions::<Alerter>(
|
||||
&id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Alerter::replace_ids(&mut alerter);
|
||||
res.alerters.push(convert_resource::<Alerter>(
|
||||
alerter,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::System(_) => continue,
|
||||
};
|
||||
}
|
||||
|
||||
+11
-214
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use anyhow::Context;
|
||||
use database::mungos::{
|
||||
by_id::find_one_by_id,
|
||||
find::find_collect,
|
||||
@@ -9,18 +9,7 @@ use database::mungos::{
|
||||
use komodo_client::{
|
||||
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
|
||||
entities::{
|
||||
ResourceTarget,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
deployment::Deployment,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
sync::ResourceSync,
|
||||
update::{Update, UpdateListItem},
|
||||
user::User,
|
||||
},
|
||||
@@ -29,7 +18,9 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
permission::{get_check_permissions, get_resource_ids_for_user},
|
||||
permission::{
|
||||
check_user_target_access, user_resource_target_query,
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
@@ -42,120 +33,7 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListUpdatesResponse> {
|
||||
let query = if user.admin || core_config().transparent_mode {
|
||||
self.query
|
||||
} else {
|
||||
let server_query = get_resource_ids_for_user::<Server>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Server", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Server" });
|
||||
|
||||
let deployment_query =
|
||||
get_resource_ids_for_user::<Deployment>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Deployment", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
|
||||
|
||||
let stack_query = get_resource_ids_for_user::<Stack>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Stack", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Stack" });
|
||||
|
||||
let build_query = get_resource_ids_for_user::<Build>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Build", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Build" });
|
||||
|
||||
let repo_query = get_resource_ids_for_user::<Repo>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Repo", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Repo" });
|
||||
|
||||
let procedure_query =
|
||||
get_resource_ids_for_user::<Procedure>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Procedure", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
|
||||
|
||||
let action_query = get_resource_ids_for_user::<Action>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Action", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Action" });
|
||||
|
||||
let builder_query = get_resource_ids_for_user::<Builder>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Builder", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Builder" });
|
||||
|
||||
let alerter_query = get_resource_ids_for_user::<Alerter>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "Alerter", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
|
||||
|
||||
let resource_sync_query = get_resource_ids_for_user::<
|
||||
ResourceSync,
|
||||
>(user)
|
||||
.await?
|
||||
.map(|ids| {
|
||||
doc! {
|
||||
"target.type": "ResourceSync", "target.id": { "$in": ids }
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
|
||||
|
||||
let mut query = self.query.unwrap_or_default();
|
||||
query.extend(doc! {
|
||||
"$or": [
|
||||
server_query,
|
||||
deployment_query,
|
||||
stack_query,
|
||||
build_query,
|
||||
repo_query,
|
||||
procedure_query,
|
||||
action_query,
|
||||
alerter_query,
|
||||
builder_query,
|
||||
resource_sync_query,
|
||||
]
|
||||
});
|
||||
query.into()
|
||||
};
|
||||
let query = user_resource_target_query(user, self.query).await?;
|
||||
|
||||
let usernames = find_collect(&db_client().users, None, None)
|
||||
.await
|
||||
@@ -222,93 +100,12 @@ impl Resolve<ReadArgs> for GetUpdate {
|
||||
if user.admin || core_config().transparent_mode {
|
||||
return Ok(update);
|
||||
}
|
||||
match &update.target {
|
||||
ResourceTarget::System(_) => {
|
||||
return Err(
|
||||
anyhow!("user must be admin to view system updates").into(),
|
||||
);
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
get_check_permissions::<Server>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
get_check_permissions::<Deployment>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
get_check_permissions::<Build>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
get_check_permissions::<Repo>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
get_check_permissions::<Builder>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
get_check_permissions::<Alerter>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
get_check_permissions::<Procedure>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Action(id) => {
|
||||
get_check_permissions::<Action>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
get_check_permissions::<ResourceSync>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
get_check_permissions::<Stack>(
|
||||
id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
check_user_target_access(
|
||||
&update.target,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use komodo_client::{
|
||||
api::{terminal::*, write::TerminalRecreateMode},
|
||||
entities::{
|
||||
deployment::Deployment, permission::PermissionLevel,
|
||||
server::Server, stack::Stack, user::User,
|
||||
},
|
||||
};
|
||||
use periphery_client::api::terminal::CreateTerminal;
|
||||
use komodo_client::{api::terminal::*, entities::user::User};
|
||||
use serror::Json;
|
||||
|
||||
use crate::{
|
||||
auth::auth_request, helpers::periphery_client,
|
||||
permission::get_check_permissions, resource::get,
|
||||
state::stack_status_cache,
|
||||
auth::auth_request, helpers::terminal::setup_target_for_user,
|
||||
};
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -34,14 +22,15 @@ pub fn router() -> Router {
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
server,
|
||||
target,
|
||||
terminal,
|
||||
init = format!("{init:?}")
|
||||
)
|
||||
)]
|
||||
async fn execute_terminal(
|
||||
Extension(user): Extension<User>,
|
||||
Json(ExecuteTerminalBody {
|
||||
server,
|
||||
target,
|
||||
terminal,
|
||||
command,
|
||||
init,
|
||||
@@ -49,200 +38,13 @@ async fn execute_terminal(
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("/terminal/execute request | user: {}", user.username);
|
||||
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
// Maybe init terminal.
|
||||
if let Some(InitTerminal { command, recreate }) = init {
|
||||
periphery
|
||||
.request(CreateTerminal {
|
||||
name: terminal.clone(),
|
||||
command,
|
||||
recreate,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
let (target, terminal, periphery) =
|
||||
setup_target_for_user(target, terminal, init, &user).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_terminal(terminal, command)
|
||||
.execute_terminal(target, terminal, command)
|
||||
.await
|
||||
.context("Failed to execute command on periphery")?;
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream))
|
||||
}
|
||||
|
||||
// ======================
|
||||
// ExecuteContainerExec
|
||||
// ======================
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteContainerExec",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
server,
|
||||
container,
|
||||
shell,
|
||||
recreate = format!("{recreate:?}"),
|
||||
)
|
||||
)]
|
||||
async fn execute_container_exec(
|
||||
Extension(user): Extension<User>,
|
||||
Json(ExecuteContainerExecBody {
|
||||
server,
|
||||
container,
|
||||
shell,
|
||||
command,
|
||||
recreate,
|
||||
}): Json<ExecuteContainerExecBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("ExecuteContainerExec request | user: {}", user.username);
|
||||
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
&user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(
|
||||
container,
|
||||
shell,
|
||||
command,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
)?;
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream))
|
||||
}
|
||||
|
||||
// =======================
|
||||
// ExecuteDeploymentExec
|
||||
// =======================
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteDeploymentExec",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
deployment,
|
||||
shell,
|
||||
recreate = format!("{recreate:?}"),
|
||||
)
|
||||
)]
|
||||
async fn execute_deployment_exec(
|
||||
Extension(user): Extension<User>,
|
||||
Json(ExecuteDeploymentExecBody {
|
||||
deployment,
|
||||
shell,
|
||||
command,
|
||||
recreate,
|
||||
}): Json<ExecuteDeploymentExecBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("ExecuteDeploymentExec request | user: {}", user.username);
|
||||
|
||||
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).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(
|
||||
deployment.name,
|
||||
shell,
|
||||
command,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
)?;
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream))
|
||||
}
|
||||
|
||||
// ==================
|
||||
// ExecuteStackExec
|
||||
// ==================
|
||||
|
||||
#[instrument(
|
||||
name = "ExecuteStackExec",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
stack,
|
||||
service,
|
||||
shell,
|
||||
recreate = format!("{recreate:?}"),
|
||||
)
|
||||
)]
|
||||
async fn execute_stack_exec(
|
||||
Extension(user): Extension<User>,
|
||||
Json(ExecuteStackExecBody {
|
||||
stack,
|
||||
service,
|
||||
shell,
|
||||
command,
|
||||
recreate,
|
||||
}): Json<ExecuteStackExecBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("ExecuteStackExec request | user: {}", user.username);
|
||||
|
||||
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).await?;
|
||||
|
||||
let stream = periphery
|
||||
.execute_container_exec(
|
||||
container,
|
||||
shell,
|
||||
command,
|
||||
recreate.unwrap_or(TerminalRecreateMode::DifferentCommand),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to execute container exec command on periphery",
|
||||
)?;
|
||||
.context("Failed to execute command on Terminal")?;
|
||||
|
||||
Ok(axum::body::Body::from_stream(stream))
|
||||
}
|
||||
|
||||
+333
-19
@@ -4,23 +4,37 @@ use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Extension, Json, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use database::hash_password;
|
||||
use database::mongo_indexed::doc;
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id, mongodb::bson::to_bson,
|
||||
};
|
||||
use derive_variants::EnumVariants;
|
||||
use komodo_client::entities::random_string;
|
||||
use komodo_client::entities::{random_bytes, random_string};
|
||||
use komodo_client::{
|
||||
api::user::*,
|
||||
entities::{api_key::ApiKey, komodo_timestamp, user::User},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::{AddStatusCode, AddStatusCodeError};
|
||||
use tower_sessions::Session;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::PasskeyRegistration;
|
||||
|
||||
use crate::api::{
|
||||
SESSION_KEY_PASSKEY_ENROLLMENT, SESSION_KEY_TOTP_ENROLLMENT,
|
||||
memory_session_layer,
|
||||
};
|
||||
use crate::auth::totp::make_totp;
|
||||
use crate::config::core_config;
|
||||
use crate::helpers::validations::validate_api_key_name;
|
||||
use crate::state::webauthn;
|
||||
use crate::{
|
||||
auth::auth_request, helpers::query::get_user, state::db_client,
|
||||
};
|
||||
@@ -29,6 +43,8 @@ use super::Variant;
|
||||
|
||||
pub struct UserArgs {
|
||||
pub user: User,
|
||||
/// Per-client session state
|
||||
pub session: Option<Session>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -44,16 +60,24 @@ enum UserRequest {
|
||||
SetLastSeenUpdate(SetLastSeenUpdate),
|
||||
CreateApiKey(CreateApiKey),
|
||||
DeleteApiKey(DeleteApiKey),
|
||||
BeginTotpEnrollment(BeginTotpEnrollment),
|
||||
ConfirmTotpEnrollment(ConfirmTotpEnrollment),
|
||||
UnenrollTotp(UnenrollTotp),
|
||||
BeginPasskeyEnrollment(BeginPasskeyEnrollment),
|
||||
ConfirmPasskeyEnrollment(ConfirmPasskeyEnrollment),
|
||||
UnenrollPasskey(UnenrollPasskey),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(memory_session_layer(60))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
session: Session,
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
@@ -62,10 +86,11 @@ async fn variant_handler(
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(user, Json(req)).await
|
||||
handler(session, user, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
session: Session,
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<UserRequest>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
@@ -75,7 +100,12 @@ async fn handler(
|
||||
"/user request {req_id} | user: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
let res = request.resolve(&UserArgs { user }).await;
|
||||
let res = request
|
||||
.resolve(&UserArgs {
|
||||
user,
|
||||
session: Some(session),
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = &res {
|
||||
warn!("/user request {req_id} error: {:#}", e.error);
|
||||
}
|
||||
@@ -89,11 +119,14 @@ const RECENTLY_VIEWED_MAX: usize = 10;
|
||||
impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user }: &UserArgs,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<PushRecentlyViewedResponse> {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
let (resource_type, id) = self.resource.extract_variant_id();
|
||||
|
||||
let field = format!("recents.{resource_type}");
|
||||
|
||||
let update = match user.recents.get(&resource_type) {
|
||||
Some(recents) => {
|
||||
let mut recents = recents
|
||||
@@ -101,13 +134,16 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
.filter(|_id| !id.eq(*_id))
|
||||
.take(RECENTLY_VIEWED_MAX - 1)
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
recents.push_front(id);
|
||||
doc! { format!("recents.{resource_type}"): to_bson(&recents)? }
|
||||
|
||||
doc! { &field: to_bson(&recents)? }
|
||||
}
|
||||
None => {
|
||||
doc! { format!("recents.{resource_type}"): [id] }
|
||||
doc! { &field: [id] }
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
@@ -115,9 +151,7 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to update recents.{resource_type}")
|
||||
})?;
|
||||
.with_context(|| format!("Failed to update user '{field}'"))?;
|
||||
|
||||
Ok(PushRecentlyViewedResponse {})
|
||||
}
|
||||
@@ -126,7 +160,7 @@ impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
impl Resolve<UserArgs> for SetLastSeenUpdate {
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user }: &UserArgs,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<SetLastSeenUpdateResponse> {
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
@@ -137,7 +171,8 @@ impl Resolve<UserArgs> for SetLastSeenUpdate {
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to update user last_update_view")?;
|
||||
.context("Failed to update user 'last_update_view'")?;
|
||||
|
||||
Ok(SetLastSeenUpdateResponse {})
|
||||
}
|
||||
}
|
||||
@@ -153,14 +188,17 @@ impl Resolve<UserArgs> for CreateApiKey {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user }: &UserArgs,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<CreateApiKeyResponse> {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
validate_api_key_name(&self.name)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let key = format!("K-{}", random_string(SECRET_LENGTH));
|
||||
let secret = format!("S-{}", random_string(SECRET_LENGTH));
|
||||
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
|
||||
.context("failed at hashing secret string")?;
|
||||
.context("Failed at hashing secret string")?;
|
||||
|
||||
let api_key = ApiKey {
|
||||
name: self.name,
|
||||
@@ -170,11 +208,13 @@ impl Resolve<UserArgs> for CreateApiKey {
|
||||
created_at: komodo_timestamp(),
|
||||
expires: self.expires,
|
||||
};
|
||||
|
||||
db_client()
|
||||
.api_keys
|
||||
.insert_one(api_key)
|
||||
.await
|
||||
.context("failed to create api key on db")?;
|
||||
.context("Failed to create api key on database")?;
|
||||
|
||||
Ok(CreateApiKeyResponse { key, secret })
|
||||
}
|
||||
}
|
||||
@@ -187,23 +227,297 @@ impl Resolve<UserArgs> for DeleteApiKey {
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user }: &UserArgs,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<DeleteApiKeyResponse> {
|
||||
let client = db_client();
|
||||
|
||||
let key = client
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &self.key })
|
||||
.await
|
||||
.context("failed at db query")?
|
||||
.context("no api key with key found")?;
|
||||
.context("Failed at database query")?
|
||||
.context("No api key with key found")?;
|
||||
|
||||
if user.id != key.user_id {
|
||||
return Err(anyhow!("api key does not belong to user").into());
|
||||
return Err(
|
||||
anyhow!("Api key does not belong to user")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.api_keys
|
||||
.delete_one(doc! { "key": key.key })
|
||||
.await
|
||||
.context("failed to delete api key from db")?;
|
||||
.context("Failed to delete api key from database")?;
|
||||
|
||||
Ok(DeleteApiKeyResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
const TOTP_ENROLLMENT_SECRET_LENGTH: usize = 20;
|
||||
|
||||
impl Resolve<UserArgs> for BeginTotpEnrollment {
|
||||
#[instrument(
|
||||
"BeginTotpEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<BeginTotpEnrollmentResponse> {
|
||||
for locked_username in &core_config().lock_login_credentials_for {
|
||||
if *locked_username == user.username {
|
||||
return Err(
|
||||
anyhow!("User not allowed to enroll in TOTP 2FA.").into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let secret_bytes = random_bytes(TOTP_ENROLLMENT_SECRET_LENGTH);
|
||||
let totp = make_totp(secret_bytes.clone(), user.id.clone())?;
|
||||
let png = totp
|
||||
.get_qr_base64()
|
||||
.map_err(|e| anyhow::Error::msg(e))
|
||||
.context("Failed to generate QR code png")?;
|
||||
session
|
||||
.insert(SESSION_KEY_TOTP_ENROLLMENT, secret_bytes)
|
||||
.await?;
|
||||
|
||||
Ok(BeginTotpEnrollmentResponse {
|
||||
uri: totp.get_url(),
|
||||
png,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for ConfirmTotpEnrollment {
|
||||
#[instrument(
|
||||
"ConfirmTotpEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<ConfirmTotpEnrollmentResponse> {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let secret_bytes = session
|
||||
.remove::<Vec<u8>>(SESSION_KEY_TOTP_ENROLLMENT)
|
||||
.await
|
||||
.context("Totp enrollment was not initiated correctly")?
|
||||
.context(
|
||||
"Totp enrollment was not initiated correctly or timed out",
|
||||
)?;
|
||||
|
||||
let encoded_secret = BASE32_NOPAD.encode(&secret_bytes);
|
||||
|
||||
let totp = make_totp(secret_bytes, None)?;
|
||||
|
||||
let valid = totp
|
||||
.check_current(&self.code)
|
||||
.context("Failed to check code validity")?;
|
||||
|
||||
if !valid {
|
||||
return Err(anyhow!(
|
||||
"The provided code was not valid. Please try BeginTotpEnrollment flow again."
|
||||
).status_code(StatusCode::BAD_REQUEST));
|
||||
}
|
||||
|
||||
let recovery_codes =
|
||||
(0..10).map(|_| random_string(20)).collect::<Vec<_>>();
|
||||
let hashed_recovery_codes = recovery_codes
|
||||
.iter()
|
||||
.map(|code| hash_password(code))
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.context("Failed to generate valid recovery codes")?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
doc! {
|
||||
"$set": {
|
||||
"totp.secret": encoded_secret,
|
||||
"totp.confirmed_at": komodo_timestamp(),
|
||||
"totp.recovery_codes": hashed_recovery_codes,
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user totp fields on database")?;
|
||||
|
||||
Ok(ConfirmTotpEnrollmentResponse { recovery_codes })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for UnenrollTotp {
|
||||
#[instrument(
|
||||
"UnenrollTotp",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UnenrollTotpResponse> {
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
doc! {
|
||||
"$set": {
|
||||
"totp.secret": "",
|
||||
"totp.confirmed_at": 0,
|
||||
"totp.recovery_codes": [],
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to clear user totp fields on database")?;
|
||||
Ok(UnenrollTotpResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UserArgs> for BeginPasskeyEnrollment {
|
||||
#[instrument(
|
||||
"BeginPasskeyEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<BeginPasskeyEnrollmentResponse> {
|
||||
for locked_username in &core_config().lock_login_credentials_for {
|
||||
if *locked_username == user.username {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"User not allowed to enroll in Passkey authentication."
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
|
||||
// Get two parts from this, the first is returned to the client.
|
||||
// The second must stay server side and is used in confirmation flow.
|
||||
let (challenge, server_state) = webauthn
|
||||
.start_passkey_registration(
|
||||
Uuid::new_v4(),
|
||||
&user.username,
|
||||
&user.username,
|
||||
None,
|
||||
)?;
|
||||
|
||||
session
|
||||
.insert(
|
||||
SESSION_KEY_PASSKEY_ENROLLMENT,
|
||||
(&user.id, server_state),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to store passkey enrollment state in server side client session",
|
||||
)?;
|
||||
|
||||
Ok(challenge.into())
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UserArgs> for ConfirmPasskeyEnrollment {
|
||||
#[instrument(
|
||||
"ConfirmPasskeyEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<ConfirmPasskeyEnrollmentResponse> {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
|
||||
let (user_id, server_state) = session
|
||||
.remove::<(String, PasskeyRegistration)>(
|
||||
SESSION_KEY_PASSKEY_ENROLLMENT,
|
||||
)
|
||||
.await
|
||||
.context("Passkey enrollment was not initiated correctly")?
|
||||
.context(
|
||||
"Passkey enrollment was not initiated correctly or timed out",
|
||||
)?;
|
||||
|
||||
let passkey = webauthn
|
||||
.finish_passkey_registration(
|
||||
&self.credential.into(),
|
||||
&server_state,
|
||||
)
|
||||
.context("Failed to finish passkey registration")?;
|
||||
|
||||
let passkey = to_bson(&passkey)
|
||||
.context("Failed to serialize passkey to BSON")?;
|
||||
|
||||
let update = doc! {
|
||||
"$set": {
|
||||
"passkey.passkey": passkey,
|
||||
"passkey.created_at": komodo_timestamp()
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(&db_client().users, &user_id, update, None)
|
||||
.await
|
||||
.context("Failed to update user passkey options on database")?;
|
||||
|
||||
Ok(ConfirmPasskeyEnrollmentResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UserArgs> for UnenrollPasskey {
|
||||
#[instrument(
|
||||
"UnenrollPasskey",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UnenrollPasskeyResponse> {
|
||||
let update = doc! {
|
||||
"$set": {
|
||||
"passkey.passkey": null,
|
||||
"passkey.created_at": 0
|
||||
}
|
||||
};
|
||||
update_one_by_id(&db_client().users, &user.id, update, None)
|
||||
.await
|
||||
.context("Failed to update user passkey options on database")?;
|
||||
Ok(UnenrollPasskeyResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ mod resource;
|
||||
mod server;
|
||||
mod service_user;
|
||||
mod stack;
|
||||
mod swarm;
|
||||
mod sync;
|
||||
mod tag;
|
||||
mod terminal;
|
||||
mod user;
|
||||
mod user_group;
|
||||
mod variable;
|
||||
@@ -52,36 +54,16 @@ pub struct WriteArgs {
|
||||
#[error(serror::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum WriteRequest {
|
||||
// ==== USER ====
|
||||
CreateLocalUser(CreateLocalUser),
|
||||
UpdateUserUsername(UpdateUserUsername),
|
||||
UpdateUserPassword(UpdateUserPassword),
|
||||
DeleteUser(DeleteUser),
|
||||
|
||||
// ==== SERVICE USER ====
|
||||
CreateServiceUser(CreateServiceUser),
|
||||
UpdateServiceUserDescription(UpdateServiceUserDescription),
|
||||
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
|
||||
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
|
||||
|
||||
// ==== USER GROUP ====
|
||||
CreateUserGroup(CreateUserGroup),
|
||||
RenameUserGroup(RenameUserGroup),
|
||||
DeleteUserGroup(DeleteUserGroup),
|
||||
AddUserToUserGroup(AddUserToUserGroup),
|
||||
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
|
||||
SetUsersInUserGroup(SetUsersInUserGroup),
|
||||
SetEveryoneUserGroup(SetEveryoneUserGroup),
|
||||
|
||||
// ==== PERMISSIONS ====
|
||||
UpdateUserAdmin(UpdateUserAdmin),
|
||||
UpdateUserBasePermissions(UpdateUserBasePermissions),
|
||||
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
|
||||
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
|
||||
|
||||
// ==== RESOURCE ====
|
||||
UpdateResourceMeta(UpdateResourceMeta),
|
||||
|
||||
// ==== SWARM ====
|
||||
CreateSwarm(CreateSwarm),
|
||||
CopySwarm(CopySwarm),
|
||||
DeleteSwarm(DeleteSwarm),
|
||||
UpdateSwarm(UpdateSwarm),
|
||||
RenameSwarm(RenameSwarm),
|
||||
|
||||
// ==== SERVER ====
|
||||
CreateServer(CreateServer),
|
||||
CopyServer(CopyServer),
|
||||
@@ -89,12 +71,14 @@ pub enum WriteRequest {
|
||||
UpdateServer(UpdateServer),
|
||||
RenameServer(RenameServer),
|
||||
CreateNetwork(CreateNetwork),
|
||||
UpdateServerPublicKey(UpdateServerPublicKey),
|
||||
RotateServerKeys(RotateServerKeys),
|
||||
|
||||
// ==== TERMINAL ====
|
||||
CreateTerminal(CreateTerminal),
|
||||
DeleteTerminal(DeleteTerminal),
|
||||
DeleteAllTerminals(DeleteAllTerminals),
|
||||
BatchDeleteAllTerminals(BatchDeleteAllTerminals),
|
||||
UpdateServerPublicKey(UpdateServerPublicKey),
|
||||
RotateServerKeys(RotateServerKeys),
|
||||
|
||||
// ==== STACK ====
|
||||
CreateStack(CreateStack),
|
||||
@@ -122,13 +106,6 @@ pub enum WriteRequest {
|
||||
WriteBuildFileContents(WriteBuildFileContents),
|
||||
RefreshBuildCache(RefreshBuildCache),
|
||||
|
||||
// ==== BUILDER ====
|
||||
CreateBuilder(CreateBuilder),
|
||||
CopyBuilder(CopyBuilder),
|
||||
DeleteBuilder(DeleteBuilder),
|
||||
UpdateBuilder(UpdateBuilder),
|
||||
RenameBuilder(RenameBuilder),
|
||||
|
||||
// ==== REPO ====
|
||||
CreateRepo(CreateRepo),
|
||||
CopyRepo(CopyRepo),
|
||||
@@ -137,13 +114,6 @@ pub enum WriteRequest {
|
||||
RenameRepo(RenameRepo),
|
||||
RefreshRepoCache(RefreshRepoCache),
|
||||
|
||||
// ==== ALERTER ====
|
||||
CreateAlerter(CreateAlerter),
|
||||
CopyAlerter(CopyAlerter),
|
||||
DeleteAlerter(DeleteAlerter),
|
||||
UpdateAlerter(UpdateAlerter),
|
||||
RenameAlerter(RenameAlerter),
|
||||
|
||||
// ==== PROCEDURE ====
|
||||
CreateProcedure(CreateProcedure),
|
||||
CopyProcedure(CopyProcedure),
|
||||
@@ -168,6 +138,52 @@ pub enum WriteRequest {
|
||||
CommitSync(CommitSync),
|
||||
RefreshResourceSyncPending(RefreshResourceSyncPending),
|
||||
|
||||
// ==== BUILDER ====
|
||||
CreateBuilder(CreateBuilder),
|
||||
CopyBuilder(CopyBuilder),
|
||||
DeleteBuilder(DeleteBuilder),
|
||||
UpdateBuilder(UpdateBuilder),
|
||||
RenameBuilder(RenameBuilder),
|
||||
|
||||
// ==== ALERTER ====
|
||||
CreateAlerter(CreateAlerter),
|
||||
CopyAlerter(CopyAlerter),
|
||||
DeleteAlerter(DeleteAlerter),
|
||||
UpdateAlerter(UpdateAlerter),
|
||||
RenameAlerter(RenameAlerter),
|
||||
|
||||
// ==== ONBOARDING KEY ====
|
||||
CreateOnboardingKey(CreateOnboardingKey),
|
||||
UpdateOnboardingKey(UpdateOnboardingKey),
|
||||
DeleteOnboardingKey(DeleteOnboardingKey),
|
||||
|
||||
// ==== USER ====
|
||||
CreateLocalUser(CreateLocalUser),
|
||||
UpdateUserUsername(UpdateUserUsername),
|
||||
UpdateUserPassword(UpdateUserPassword),
|
||||
DeleteUser(DeleteUser),
|
||||
|
||||
// ==== SERVICE USER ====
|
||||
CreateServiceUser(CreateServiceUser),
|
||||
UpdateServiceUserDescription(UpdateServiceUserDescription),
|
||||
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
|
||||
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
|
||||
|
||||
// ==== USER GROUP ====
|
||||
CreateUserGroup(CreateUserGroup),
|
||||
RenameUserGroup(RenameUserGroup),
|
||||
DeleteUserGroup(DeleteUserGroup),
|
||||
AddUserToUserGroup(AddUserToUserGroup),
|
||||
RemoveUserFromUserGroup(RemoveUserFromUserGroup),
|
||||
SetUsersInUserGroup(SetUsersInUserGroup),
|
||||
SetEveryoneUserGroup(SetEveryoneUserGroup),
|
||||
|
||||
// ==== PERMISSIONS ====
|
||||
UpdateUserAdmin(UpdateUserAdmin),
|
||||
UpdateUserBasePermissions(UpdateUserBasePermissions),
|
||||
UpdatePermissionOnResourceType(UpdatePermissionOnResourceType),
|
||||
UpdatePermissionOnTarget(UpdatePermissionOnTarget),
|
||||
|
||||
// ==== TAG ====
|
||||
CreateTag(CreateTag),
|
||||
DeleteTag(DeleteTag),
|
||||
@@ -189,11 +205,6 @@ pub enum WriteRequest {
|
||||
UpdateDockerRegistryAccount(UpdateDockerRegistryAccount),
|
||||
DeleteDockerRegistryAccount(DeleteDockerRegistryAccount),
|
||||
|
||||
// ==== ONBOARDING KEY ====
|
||||
CreateOnboardingKey(CreateOnboardingKey),
|
||||
UpdateOnboardingKey(UpdateOnboardingKey),
|
||||
DeleteOnboardingKey(DeleteOnboardingKey),
|
||||
|
||||
// ==== ALERT ====
|
||||
CloseAlert(CloseAlert),
|
||||
}
|
||||
|
||||
@@ -307,8 +307,8 @@ async fn extract_user_target_with_validation(
|
||||
.users
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for users")?
|
||||
.context("no matching user found")?
|
||||
.context("Failed to query db for users")?
|
||||
.context("No matching user found")?
|
||||
.id;
|
||||
Ok((UserTargetVariant::User, id))
|
||||
}
|
||||
@@ -321,8 +321,8 @@ async fn extract_user_target_with_validation(
|
||||
.user_groups
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for user_groups")?
|
||||
.context("no matching user_group found")?
|
||||
.context("Failed to query db for user_groups")?
|
||||
.context("No matching user_group found")?
|
||||
.id;
|
||||
Ok((UserTargetVariant::UserGroup, id))
|
||||
}
|
||||
@@ -338,47 +338,19 @@ async fn extract_resource_target_with_validation(
|
||||
let res = resource_target.extract_variant_id();
|
||||
Ok((res.0, res.1.clone()))
|
||||
}
|
||||
ResourceTarget::Build(ident) => {
|
||||
ResourceTarget::Swarm(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.builds
|
||||
.swarms
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for builds")?
|
||||
.context("no matching build found")?
|
||||
.context("Failed to query db for swarms")?
|
||||
.context("No matching server found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Build, id))
|
||||
}
|
||||
ResourceTarget::Builder(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.builders
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for builders")?
|
||||
.context("no matching builder found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Builder, id))
|
||||
}
|
||||
ResourceTarget::Deployment(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.deployments
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for deployments")?
|
||||
.context("no matching deployment found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Deployment, id))
|
||||
Ok((ResourceTargetVariant::Server, id))
|
||||
}
|
||||
ResourceTarget::Server(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
@@ -389,11 +361,53 @@ async fn extract_resource_target_with_validation(
|
||||
.servers
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for servers")?
|
||||
.context("no matching server found")?
|
||||
.context("Failed to query db for servers")?
|
||||
.context("No matching server found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Server, id))
|
||||
}
|
||||
ResourceTarget::Stack(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.stacks
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("Failed to query db for stacks")?
|
||||
.context("No matching stack found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Stack, id))
|
||||
}
|
||||
ResourceTarget::Deployment(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.deployments
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("Failed to query db for deployments")?
|
||||
.context("No matching deployment found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Deployment, id))
|
||||
}
|
||||
ResourceTarget::Build(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.builds
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("Failed to query db for builds")?
|
||||
.context("No matching build found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Build, id))
|
||||
}
|
||||
ResourceTarget::Repo(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
@@ -403,25 +417,11 @@ async fn extract_resource_target_with_validation(
|
||||
.repos
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for repos")?
|
||||
.context("no matching repo found")?
|
||||
.context("Failed to query db for repos")?
|
||||
.context("No matching repo found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Repo, id))
|
||||
}
|
||||
ResourceTarget::Alerter(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.alerters
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for alerters")?
|
||||
.context("no matching alerter found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Alerter, id))
|
||||
}
|
||||
ResourceTarget::Procedure(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
@@ -431,8 +431,8 @@ async fn extract_resource_target_with_validation(
|
||||
.procedures
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for procedures")?
|
||||
.context("no matching procedure found")?
|
||||
.context("Failed to query db for procedures")?
|
||||
.context("No matching procedure found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Procedure, id))
|
||||
}
|
||||
@@ -445,8 +445,8 @@ async fn extract_resource_target_with_validation(
|
||||
.actions
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for actions")?
|
||||
.context("no matching action found")?
|
||||
.context("Failed to query db for actions")?
|
||||
.context("No matching action found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Action, id))
|
||||
}
|
||||
@@ -459,24 +459,38 @@ async fn extract_resource_target_with_validation(
|
||||
.resource_syncs
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for resource syncs")?
|
||||
.context("no matching resource sync found")?
|
||||
.context("Failed to query db for resource syncs")?
|
||||
.context("No matching resource sync found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::ResourceSync, id))
|
||||
}
|
||||
ResourceTarget::Stack(ident) => {
|
||||
ResourceTarget::Builder(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.stacks
|
||||
.builders
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("failed to query db for stacks")?
|
||||
.context("no matching stack found")?
|
||||
.context("Failed to query db for builders")?
|
||||
.context("No matching builder found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Stack, id))
|
||||
Ok((ResourceTargetVariant::Builder, id))
|
||||
}
|
||||
ResourceTarget::Alerter(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": ident },
|
||||
};
|
||||
let id = db_client()
|
||||
.alerters
|
||||
.find_one(filter)
|
||||
.await
|
||||
.context("Failed to query db for alerters")?
|
||||
.context("No matching alerter found")?
|
||||
.id;
|
||||
Ok((ResourceTargetVariant::Alerter, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ use komodo_client::{
|
||||
entities::{
|
||||
ResourceTarget, action::Action, alerter::Alerter, build::Build,
|
||||
builder::Builder, deployment::Deployment, procedure::Procedure,
|
||||
repo::Repo, server::Server, stack::Stack, sync::ResourceSync,
|
||||
repo::Repo, server::Server, stack::Stack, swarm::Swarm,
|
||||
sync::ResourceSync,
|
||||
},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
@@ -45,9 +46,15 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
ResourceTarget::Swarm(id) => {
|
||||
resource::update_meta::<Swarm>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
resource::update_meta::<Server>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
resource::update_meta::<Stack>(&id, meta, args).await?;
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
resource::update_meta::<Deployment>(&id, meta, args).await?;
|
||||
}
|
||||
@@ -57,12 +64,6 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
|
||||
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?;
|
||||
}
|
||||
@@ -73,8 +74,11 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
|
||||
resource::update_meta::<ResourceSync>(&id, meta, args)
|
||||
.await?;
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
resource::update_meta::<Stack>(&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?;
|
||||
}
|
||||
}
|
||||
Ok(UpdateResourceMetaResponse {})
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use anyhow::Context;
|
||||
use formatting::{bold, format_serror};
|
||||
use futures_util::{StreamExt, stream::FuturesUnordered};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
NoData, Operation,
|
||||
Operation,
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerInfo},
|
||||
to_docker_compatible_name,
|
||||
@@ -17,7 +16,6 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_all_tags,
|
||||
update::{add_update, make_update, update_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
@@ -189,162 +187,6 @@ impl Resolve<WriteArgs> for CreateNetwork {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CreateTerminal {
|
||||
#[instrument(
|
||||
"CreateTerminal",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
server = self.server,
|
||||
terminal = self.name,
|
||||
command = self.command,
|
||||
recreate = format!("{:?}", self.recreate),
|
||||
)
|
||||
)]
|
||||
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).await?;
|
||||
|
||||
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(
|
||||
"DeleteTerminal",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
server = self.server,
|
||||
terminal = self.terminal,
|
||||
)
|
||||
)]
|
||||
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).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteTerminal {
|
||||
terminal: self.terminal,
|
||||
})
|
||||
.await
|
||||
.context("Failed to delete terminal on Periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteAllTerminals {
|
||||
#[instrument(
|
||||
"DeleteAllTerminals",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
server = self.server,
|
||||
)
|
||||
)]
|
||||
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).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteAllTerminals {})
|
||||
.await
|
||||
.context("Failed to delete all terminals on Periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for BatchDeleteAllTerminals {
|
||||
#[instrument(
|
||||
"BatchDeleteAllTerminals",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
query = format!("{:?}", self.query),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
|
||||
resource::list_full_for_user::<Server>(
|
||||
self.query, user, &all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|server| async move {
|
||||
let res = async {
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteAllTerminals {})
|
||||
.await
|
||||
.context("Failed to delete all terminals on Periphery")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed to delete all terminals on {} ({}) | {e:#}",
|
||||
server.name, server.id
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateServerPublicKey {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{
|
||||
by_id::find_one_by_id,
|
||||
mongodb::bson::{doc, oid::ObjectId},
|
||||
};
|
||||
use database::mungos::{by_id::find_one_by_id, mongodb::bson::doc};
|
||||
use komodo_client::{
|
||||
api::{user::CreateApiKey, write::*},
|
||||
entities::{
|
||||
@@ -12,9 +7,15 @@ use komodo_client::{
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError as _};
|
||||
|
||||
use crate::{api::user::UserArgs, state::db_client};
|
||||
use crate::{
|
||||
api::user::UserArgs,
|
||||
helpers::validations::{validate_api_key_name, validate_username},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -33,20 +34,25 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
}
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("username cannot be valid ObjectId").into(),
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
validate_username(&self.username)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let config = UserConfig::Service {
|
||||
description: self.description,
|
||||
};
|
||||
|
||||
let mut user = User {
|
||||
id: Default::default(),
|
||||
username: self.username,
|
||||
config,
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
enabled: true,
|
||||
admin: false,
|
||||
super_admin: false,
|
||||
@@ -57,6 +63,7 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
all: Default::default(),
|
||||
updated_at: komodo_timestamp(),
|
||||
};
|
||||
|
||||
user.id = db_client()
|
||||
.users
|
||||
.insert_one(&user)
|
||||
@@ -66,6 +73,7 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
.as_object_id()
|
||||
.context("inserted id is not object id")?
|
||||
.to_string();
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
@@ -85,18 +93,28 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateServiceUserDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let service_user = db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user with given username")?;
|
||||
.context("Failed to query db for user")?
|
||||
.context("No user with given username")?;
|
||||
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user").into());
|
||||
return Err(
|
||||
anyhow!("Target user is not Service User")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
};
|
||||
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "username": &self.username },
|
||||
@@ -104,13 +122,15 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
|
||||
)
|
||||
.await
|
||||
.context("failed to update user on db")?;
|
||||
let res = db
|
||||
|
||||
let service_user = db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("user with username not found")?;
|
||||
Ok(res)
|
||||
|
||||
Ok(service_user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,21 +150,36 @@ impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
validate_api_key_name(&self.name)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().users, &self.user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
.context("Failed to query db for user")?
|
||||
.context("No user found with id")?;
|
||||
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user").into());
|
||||
return Err(
|
||||
anyhow!("Target user is not Service User")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
};
|
||||
|
||||
CreateApiKey {
|
||||
name: self.name,
|
||||
expires: self.expires,
|
||||
}
|
||||
.resolve(&UserArgs { user: service_user })
|
||||
.resolve(&UserArgs {
|
||||
user: service_user,
|
||||
session: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -163,23 +198,34 @@ impl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("user not admin").into());
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let api_key = db
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &self.key })
|
||||
.await
|
||||
.context("failed to query db for api key")?
|
||||
.context("did not find matching api key")?;
|
||||
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().users, &api_key.user_id)
|
||||
.await
|
||||
.context("failed to query db for user")?
|
||||
.context("no user found with id")?;
|
||||
|
||||
let UserConfig::Service { .. } = &service_user.config else {
|
||||
return Err(anyhow!("user is not service user").into());
|
||||
return Err(
|
||||
anyhow!("Target user is not Service User")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
};
|
||||
|
||||
db.api_keys
|
||||
.delete_one(doc! { "key": self.key })
|
||||
.await
|
||||
|
||||
@@ -10,7 +10,6 @@ use komodo_client::{
|
||||
all_logs_success,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
server::ServerState,
|
||||
stack::{Stack, StackInfo},
|
||||
update::Update,
|
||||
user::stack_user,
|
||||
@@ -25,9 +24,8 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_server_with_state,
|
||||
stack_git_token,
|
||||
query::get_swarm_or_server,
|
||||
stack_git_token, swarm_or_server_request,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
@@ -204,32 +202,24 @@ async fn write_stack_file_contents_on_host(
|
||||
contents: String,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
if stack.config.server_id.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Cannot write file, Files on host Stack has not configured a Server"
|
||||
).into());
|
||||
}
|
||||
let (server, state) =
|
||||
get_server_with_state(&stack.config.server_id).await?;
|
||||
if state != ServerState::Ok {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Cannot write file when server is unreachable or disabled"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(WriteComposeContentsToHost {
|
||||
let swarm_or_server = get_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&stack.config.server_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let res = swarm_or_server_request(
|
||||
&swarm_or_server,
|
||||
WriteComposeContentsToHost {
|
||||
name: stack.name,
|
||||
run_directory: stack.config.run_directory,
|
||||
file_path,
|
||||
contents,
|
||||
})
|
||||
.await
|
||||
.context("Failed to write contents to host")
|
||||
{
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
}
|
||||
@@ -239,7 +229,7 @@ async fn write_stack_file_contents_on_host(
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if !all_logs_success(&update.logs) {
|
||||
update.finalize();
|
||||
@@ -459,26 +449,22 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
let (server, state) = if stack.config.server_id.is_empty() {
|
||||
(None, ServerState::Disabled)
|
||||
} else {
|
||||
let (server, state) =
|
||||
get_server_with_state(&stack.config.server_id).await?;
|
||||
(Some(server), state)
|
||||
};
|
||||
if state != ServerState::Ok {
|
||||
(vec![], None, None, None, None)
|
||||
} else if let Some(server) = server {
|
||||
if let Ok(swarm_or_server) = get_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&stack.config.server_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let GetComposeContentsOnHostResponse { contents, errors } =
|
||||
match periphery_client(&server)
|
||||
.await?
|
||||
.request(GetComposeContentsOnHost {
|
||||
match swarm_or_server_request(
|
||||
&swarm_or_server,
|
||||
GetComposeContentsOnHost {
|
||||
file_paths: stack.all_file_dependencies(),
|
||||
name: stack.name.clone(),
|
||||
run_directory: stack.config.run_directory.clone(),
|
||||
})
|
||||
.await
|
||||
.context("failed to get compose file contents from host")
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) => GetComposeContentsOnHostResponse {
|
||||
@@ -489,7 +475,6 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
let mut services = Vec::new();
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
permission::PermissionLevel, swarm::Swarm, update::Update,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
impl Resolve<WriteArgs> for CreateSwarm {
|
||||
#[instrument(
|
||||
"CreateSwarm",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
swarm = self.name,
|
||||
config = serde_json::to_string(&self.config).unwrap(),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
resource::create::<Swarm>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CopySwarm {
|
||||
#[instrument(
|
||||
"CopySwarm",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
swarm = self.name,
|
||||
copy_swarm = self.id,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
let Swarm { config, .. } = get_check_permissions::<Swarm>(
|
||||
&self.id,
|
||||
user,
|
||||
PermissionLevel::Read.into(),
|
||||
)
|
||||
.await?;
|
||||
resource::create::<Swarm>(&self.name, config.into(), None, user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteSwarm {
|
||||
#[instrument(
|
||||
"DeleteSwarm",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
swarm = self.id,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
Ok(resource::delete::<Swarm>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for UpdateSwarm {
|
||||
#[instrument(
|
||||
"UpdateSwarm",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
swarm = self.id,
|
||||
update = serde_json::to_string(&self.config).unwrap()
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
Ok(resource::update::<Swarm>(&self.id, self.config, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for RenameSwarm {
|
||||
#[instrument(
|
||||
"RenameSwarm",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
swarm = self.id,
|
||||
new_name = self.name
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
Ok(resource::rename::<Swarm>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
use anyhow::Context as _;
|
||||
use futures_util::{StreamExt as _, stream::FuturesUnordered};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
NoData, deployment::Deployment, permission::PermissionLevel,
|
||||
server::Server, stack::Stack, terminal::TerminalTarget,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_all_tags,
|
||||
terminal::{
|
||||
create_container_terminal_inner,
|
||||
get_deployment_periphery_container,
|
||||
get_stack_service_periphery_container,
|
||||
},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for CreateTerminal {
|
||||
#[instrument(
|
||||
"CreateTerminal",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
terminal = self.name,
|
||||
target = format!("{:?}", self.target),
|
||||
command = self.command,
|
||||
mode = format!("{:?}", self.mode),
|
||||
recreate = format!("{:?}", self.recreate),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
match self.target.clone() {
|
||||
TerminalTarget::Server { server } => {
|
||||
let server = server
|
||||
.context("Must provide 'target.params.server'")
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
create_server_terminal(self, server, user).await?;
|
||||
}
|
||||
TerminalTarget::Container { server, container } => {
|
||||
create_container_terminal(self, server, container, user)
|
||||
.await?;
|
||||
}
|
||||
TerminalTarget::Stack { stack, service } => {
|
||||
let service = service
|
||||
.context("Must provide 'target.params.service'")
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
create_stack_service_terminal(self, stack, service, user)
|
||||
.await?;
|
||||
}
|
||||
TerminalTarget::Deployment { deployment } => {
|
||||
create_deployment_terminal(self, deployment, user).await?;
|
||||
}
|
||||
};
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_server_terminal(
|
||||
CreateTerminal {
|
||||
name,
|
||||
command,
|
||||
recreate,
|
||||
target: _,
|
||||
mode: _,
|
||||
}: CreateTerminal,
|
||||
server: String,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::CreateServerTerminal {
|
||||
name,
|
||||
command,
|
||||
recreate,
|
||||
})
|
||||
.await
|
||||
.context("Failed to create Server Terminal on Periphery")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_container_terminal(
|
||||
req: CreateTerminal,
|
||||
server: String,
|
||||
container: String,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
let periphery = periphery_client(&server).await?;
|
||||
create_container_terminal_inner(req, &periphery, container).await
|
||||
}
|
||||
|
||||
async fn create_stack_service_terminal(
|
||||
req: CreateTerminal,
|
||||
stack: String,
|
||||
service: String,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
let (_, periphery, container) =
|
||||
get_stack_service_periphery_container(&stack, &service, user)
|
||||
.await?;
|
||||
create_container_terminal_inner(req, &periphery, container).await
|
||||
}
|
||||
|
||||
async fn create_deployment_terminal(
|
||||
req: CreateTerminal,
|
||||
deployment: String,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
let (_, periphery, container) =
|
||||
get_deployment_periphery_container(&deployment, user).await?;
|
||||
create_container_terminal_inner(req, &periphery, container).await
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteTerminal {
|
||||
#[instrument(
|
||||
"DeleteTerminal",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
target = format!("{:?}", self.target),
|
||||
terminal = self.terminal,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = match &self.target {
|
||||
TerminalTarget::Server { server } => {
|
||||
let server = server
|
||||
.as_ref()
|
||||
.context("Must provide 'target.params.server'")
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
get_check_permissions::<Server>(
|
||||
server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
TerminalTarget::Container { server, .. } => {
|
||||
get_check_permissions::<Server>(
|
||||
server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
TerminalTarget::Stack { stack, .. } => {
|
||||
let server = get_check_permissions::<Stack>(
|
||||
stack,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
.config
|
||||
.server_id;
|
||||
resource::get::<Server>(&server).await?
|
||||
}
|
||||
TerminalTarget::Deployment { deployment } => {
|
||||
let server = get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?
|
||||
.config
|
||||
.server_id;
|
||||
resource::get::<Server>(&server).await?
|
||||
}
|
||||
};
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteTerminal {
|
||||
target: self.target,
|
||||
terminal: self.terminal,
|
||||
})
|
||||
.await
|
||||
.context("Failed to delete terminal on Periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for DeleteAllTerminals {
|
||||
#[instrument(
|
||||
"DeleteAllTerminals",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
server = self.server,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteAllTerminals {})
|
||||
.await
|
||||
.context("Failed to delete all terminals on Periphery")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for BatchDeleteAllTerminals {
|
||||
#[instrument(
|
||||
"BatchDeleteAllTerminals",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
query = format!("{:?}", self.query),
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
get_all_tags(None).await?
|
||||
};
|
||||
|
||||
resource::list_full_for_user::<Server>(
|
||||
self.query,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
&all_tags,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|server| async move {
|
||||
let res = async {
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
periphery
|
||||
.request(api::terminal::DeleteAllTerminals {})
|
||||
.await
|
||||
.context("Failed to delete all terminals on Periphery")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"Failed to delete all terminals on {} ({}) | {e:#}",
|
||||
server.name, server.id
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,13 @@ use komodo_client::{
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::validations::{validate_password, validate_username},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::WriteArgs;
|
||||
|
||||
@@ -38,24 +42,15 @@ impl Resolve<WriteArgs> for CreateLocalUser {
|
||||
) -> serror::Result<CreateLocalUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin-only.")
|
||||
anyhow!("This method is Admin Only.")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
if self.username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty.").into());
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("Username cannot be valid ObjectId").into(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.password.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty.").into());
|
||||
}
|
||||
validate_username(&self.username)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
validate_password(&self.password)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
@@ -87,6 +82,8 @@ impl Resolve<WriteArgs> for CreateLocalUser {
|
||||
config: UserConfig::Local {
|
||||
password: hashed_password,
|
||||
},
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
};
|
||||
|
||||
user.id = db_client()
|
||||
@@ -130,17 +127,11 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty.").into());
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("Username cannot be valid ObjectId").into(),
|
||||
);
|
||||
}
|
||||
validate_username(&self.username)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
@@ -150,8 +141,10 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
|
||||
{
|
||||
return Err(anyhow!("Username already taken.").into());
|
||||
}
|
||||
|
||||
let id = ObjectId::from_str(&user.id)
|
||||
.context("User id not valid ObjectId.")?;
|
||||
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "_id": id },
|
||||
@@ -159,6 +152,7 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user username on database.")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
@@ -185,7 +179,12 @@ impl Resolve<WriteArgs> for UpdateUserPassword {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validate_password(&self.password)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
db_client().set_user_password(user, &self.password).await?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
@@ -211,15 +210,19 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
if admin.username == self.user || admin.id == self.user {
|
||||
return Err(anyhow!("User cannot delete themselves.").into());
|
||||
}
|
||||
|
||||
let query = if let Ok(id) = ObjectId::from_str(&self.user) {
|
||||
doc! { "_id": id }
|
||||
} else {
|
||||
doc! { "username": self.user }
|
||||
};
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let Some(user) = db
|
||||
.users
|
||||
.find_one(query.clone())
|
||||
@@ -230,21 +233,25 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
anyhow!("No user found with given id / username").into(),
|
||||
);
|
||||
};
|
||||
|
||||
if user.super_admin {
|
||||
return Err(
|
||||
anyhow!("Cannot delete a super admin user.").into(),
|
||||
);
|
||||
}
|
||||
|
||||
if user.admin && !admin.super_admin {
|
||||
return Err(
|
||||
anyhow!("Only a Super Admin can delete an admin user.")
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
db.users
|
||||
.delete_one(query)
|
||||
.await
|
||||
.context("Failed to delete user from database")?;
|
||||
|
||||
// Also remove user id from all user groups
|
||||
if let Err(e) = db
|
||||
.user_groups
|
||||
@@ -253,6 +260,7 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
{
|
||||
warn!("Failed to remove deleted user from user groups | {e:?}");
|
||||
};
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ use komodo_client::{
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
query::get_variable,
|
||||
update::{add_update, make_update},
|
||||
validations::{validate_variable_name, validate_variable_value},
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
@@ -35,7 +36,7 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
) -> serror::Result<CreateVariableResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can create variables")
|
||||
anyhow!("Only Admins can create Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
@@ -47,6 +48,11 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
is_secret,
|
||||
} = self;
|
||||
|
||||
validate_variable_name(&name)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
validate_variable_value(&value)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let variable = Variable {
|
||||
name,
|
||||
value,
|
||||
@@ -58,7 +64,7 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
.variables
|
||||
.insert_one(&variable)
|
||||
.await
|
||||
.context("Failed to create variable on db")?;
|
||||
.context("Failed to create Variable on db")?;
|
||||
|
||||
let mut update = make_update(
|
||||
ResourceTarget::system(),
|
||||
@@ -67,7 +73,8 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
);
|
||||
|
||||
update
|
||||
.push_simple_log("create variable", format!("{variable:#?}"));
|
||||
.push_simple_log("Create Variable", format!("{variable:#?}"));
|
||||
|
||||
update.finalize();
|
||||
|
||||
add_update(update).await?;
|
||||
@@ -91,13 +98,18 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
|
||||
) -> serror::Result<UpdateVariableValueResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
anyhow!("Only Admins can update Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let UpdateVariableValue { name, value } = self;
|
||||
|
||||
validate_variable_name(&name)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
validate_variable_value(&value)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let variable = get_variable(&name).await?;
|
||||
|
||||
if value == variable.value {
|
||||
@@ -156,10 +168,11 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
|
||||
) -> serror::Result<UpdateVariableDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
anyhow!("Only Admins can update Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
db_client()
|
||||
.variables
|
||||
.update_one(
|
||||
@@ -168,6 +181,7 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
|
||||
)
|
||||
.await
|
||||
.context("Failed to update variable description on db")?;
|
||||
|
||||
Ok(get_variable(&self.name).await?)
|
||||
}
|
||||
}
|
||||
@@ -188,10 +202,11 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
|
||||
) -> serror::Result<UpdateVariableIsSecretResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update variables")
|
||||
anyhow!("Only Admins can update Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
db_client()
|
||||
.variables
|
||||
.update_one(
|
||||
@@ -199,7 +214,8 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
|
||||
doc! { "$set": { "is_secret": self.is_secret } },
|
||||
)
|
||||
.await
|
||||
.context("Failed to update variable is secret on db")?;
|
||||
.context("Failed to update Variable 'is_secret' on db")?;
|
||||
|
||||
Ok(get_variable(&self.name).await?)
|
||||
}
|
||||
}
|
||||
@@ -219,16 +235,18 @@ impl Resolve<WriteArgs> for DeleteVariable {
|
||||
) -> serror::Result<DeleteVariableResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can delete variables")
|
||||
anyhow!("Only Admins can delete Variables")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
let variable = get_variable(&self.name).await?;
|
||||
|
||||
db_client()
|
||||
.variables
|
||||
.delete_one(doc! { "name": &self.name })
|
||||
.await
|
||||
.context("Failed to delete variable on db")?;
|
||||
.context("Failed to delete Variable on db")?;
|
||||
|
||||
let mut update = make_update(
|
||||
ResourceTarget::system(),
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
helpers::query::get_user,
|
||||
state::auth_rate_limiter,
|
||||
};
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::ws::{self, WebSocket},
|
||||
http::HeaderMap,
|
||||
routing::get,
|
||||
};
|
||||
use komodo_client::{entities::user::User, ws::WsLoginMessage};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serror::{AddStatusCode, AddStatusCodeError};
|
||||
|
||||
mod terminal;
|
||||
mod update;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
// Periphery facing
|
||||
.route("/periphery", get(crate::connection::server::handler))
|
||||
// User facing
|
||||
.route("/update", get(update::handler))
|
||||
.route("/terminal", get(terminal::handler))
|
||||
}
|
||||
|
||||
async fn user_ws_login(
|
||||
mut socket: WebSocket,
|
||||
headers: &HeaderMap,
|
||||
fallback_ip: IpAddr,
|
||||
) -> Option<(WebSocket, User)> {
|
||||
let res = async {
|
||||
let message = match socket
|
||||
.recv()
|
||||
.await
|
||||
.context("Failed to receive message over socket: Closed")
|
||||
.status_code(StatusCode::BAD_REQUEST)?
|
||||
.context("Failed to recieve message over socket: Error")
|
||||
.status_code(StatusCode::BAD_REQUEST)?
|
||||
{
|
||||
ws::Message::Text(utf8_bytes) => utf8_bytes.to_string(),
|
||||
ws::Message::Binary(bytes) => String::from_utf8(bytes.into())
|
||||
.context("Received invalid message bytes: Not UTF-8")
|
||||
.status_code(StatusCode::BAD_REQUEST)?,
|
||||
message => {
|
||||
return Err(
|
||||
anyhow!("Received invalid message: {message:?}")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match WsLoginMessage::from_json_str(&message)
|
||||
.context("Invalid login message")
|
||||
.status_code(StatusCode::BAD_REQUEST)?
|
||||
{
|
||||
WsLoginMessage::Jwt { jwt } => auth_jwt_check_enabled(&jwt)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
WsLoginMessage::ApiKeys { key, secret } => {
|
||||
auth_api_key_check_enabled(&key, &secret)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(fallback_ip),
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Ok(user) => {
|
||||
let _ = socket.send(ws::Message::text("LOGGED_IN")).await;
|
||||
Some((socket, user))
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = socket
|
||||
.send(ws::Message::text(format!(
|
||||
"[{}]: {:#}",
|
||||
e.status, e.error
|
||||
)))
|
||||
.await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
|
||||
let user = get_user(user_id).await?;
|
||||
if !user.enabled {
|
||||
return Err(anyhow!("User not enabled"));
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{ConnectInfo, FromRequestParts, WebSocketUpgrade, ws},
|
||||
http::{HeaderMap, request},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_util::{SinkExt, StreamExt as _};
|
||||
use komodo_client::{
|
||||
api::terminal::ConnectTerminalQuery, entities::user::User,
|
||||
};
|
||||
use periphery_client::api::terminal::DisconnectTerminal;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
helpers::terminal::setup_target_for_user,
|
||||
periphery::{PeripheryClient, terminal::ConnectTerminalResponse},
|
||||
state::periphery_connections,
|
||||
};
|
||||
|
||||
#[instrument("ConnectTerminal", skip(ws))]
|
||||
pub async fn handler(
|
||||
Qs(query): Qs<ConnectTerminalQuery>,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
let ip = info.ip();
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
let Some((mut client_socket, user)) =
|
||||
super::user_ws_login(socket, &headers, ip).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (periphery, response) =
|
||||
match setup_forwarding(query, &user).await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
let _ = client_socket
|
||||
.send(ws::Message::text(format!("ERROR: {e:#}")))
|
||||
.await;
|
||||
let _ = client_socket.close().await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
forward_ws_channel(periphery, client_socket, response).await
|
||||
})
|
||||
}
|
||||
|
||||
async fn setup_forwarding(
|
||||
ConnectTerminalQuery {
|
||||
target,
|
||||
terminal,
|
||||
init,
|
||||
}: ConnectTerminalQuery,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(PeripheryClient, ConnectTerminalResponse)> {
|
||||
let (target, terminal, periphery) =
|
||||
setup_target_for_user(target, terminal, init, user).await?;
|
||||
let response = periphery.connect_terminal(terminal, target).await?;
|
||||
Ok((periphery, response))
|
||||
}
|
||||
|
||||
async fn forward_ws_channel(
|
||||
periphery: PeripheryClient,
|
||||
client_socket: axum::extract::ws::WebSocket,
|
||||
ConnectTerminalResponse {
|
||||
channel,
|
||||
sender: periphery_sender,
|
||||
receiver: mut periphery_receiver,
|
||||
}: ConnectTerminalResponse,
|
||||
) {
|
||||
let (mut client_send, mut client_receive) = client_socket.split();
|
||||
let cancel = CancellationToken::new();
|
||||
|
||||
periphery_receiver.set_cancel(cancel.clone());
|
||||
|
||||
trace!("starting ws exchange");
|
||||
|
||||
let core_to_periphery = async {
|
||||
loop {
|
||||
let client_recv_res = tokio::select! {
|
||||
res = client_receive.next() => res,
|
||||
_ = cancel.cancelled() => break,
|
||||
};
|
||||
let bytes = match client_recv_res {
|
||||
Some(Ok(ws::Message::Binary(bytes))) => bytes.into(),
|
||||
Some(Ok(ws::Message::Text(text))) => {
|
||||
let bytes: Bytes = text.into();
|
||||
bytes.into()
|
||||
}
|
||||
Some(Ok(ws::Message::Close(_frame))) => {
|
||||
break;
|
||||
}
|
||||
Some(Err(_e)) => {
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
// Ignore
|
||||
Some(Ok(_)) => continue,
|
||||
};
|
||||
if let Err(_e) =
|
||||
periphery_sender.send_terminal(channel, Ok(bytes)).await
|
||||
{
|
||||
break;
|
||||
};
|
||||
}
|
||||
cancel.cancel();
|
||||
let _ = periphery_sender
|
||||
.send_terminal(channel, Err(anyhow!("Client disconnected")))
|
||||
.await;
|
||||
};
|
||||
|
||||
let periphery_to_core = async {
|
||||
loop {
|
||||
// Already adheres to cancellation token
|
||||
match periphery_receiver.recv().await {
|
||||
Ok(Ok(bytes)) => {
|
||||
if let Err(e) =
|
||||
client_send.send(ws::Message::Binary(bytes.into())).await
|
||||
{
|
||||
debug!("{e:?}");
|
||||
break;
|
||||
};
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let _ = client_send
|
||||
.send(ws::Message::text(format!("{e:#}")))
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ =
|
||||
client_send.send(ws::Message::text("STREAM EOF")).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = client_send.close().await;
|
||||
cancel.cancel();
|
||||
};
|
||||
|
||||
tokio::join!(core_to_periphery, periphery_to_core);
|
||||
|
||||
// Cleanup
|
||||
if let Err(e) =
|
||||
periphery.request(DisconnectTerminal { channel }).await
|
||||
{
|
||||
warn!(
|
||||
"Failed to disconnect Periphery terminal forwarding | {e:#}",
|
||||
)
|
||||
}
|
||||
if let Some(connection) =
|
||||
periphery_connections().get(&periphery.id).await
|
||||
{
|
||||
connection.terminals.remove(&channel).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Qs<T>(pub T);
|
||||
|
||||
impl<S, T> FromRequestParts<S> for Qs<T>
|
||||
where
|
||||
S: Send + Sync,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
type Rejection = axum::response::Response;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let raw = parts.uri.query().unwrap_or_default();
|
||||
serde_qs::from_str::<T>(raw).map(Qs).map_err(|e| {
|
||||
axum::response::IntoResponse::into_response((
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
format!("Failed to parse request query: {e}"),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{WebSocketUpgrade, ws::Message},
|
||||
extract::{ConnectInfo, WebSocketUpgrade, ws::Message},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
@@ -16,17 +19,24 @@ use crate::helpers::{
|
||||
channel::update_channel, query::get_user_permission_on_target,
|
||||
};
|
||||
|
||||
pub async fn handler(ws: WebSocketUpgrade) -> impl IntoResponse {
|
||||
pub async fn handler(
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
// get a reveiver for internal update messages.
|
||||
let mut receiver = update_channel().receiver.resubscribe();
|
||||
let ip = info.ip();
|
||||
|
||||
// handle http -> ws updgrade
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let Some((socket, user)) = super::user_ws_login(socket).await else {
|
||||
return
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
let Some((client_socket, user)) =
|
||||
super::user_ws_login(socket, &headers, ip).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (mut ws_sender, mut ws_reciever) = socket.split();
|
||||
let (mut ws_sender, mut ws_reciever) = client_socket.split();
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
+102
-34
@@ -1,20 +1,37 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router, extract::Query, response::Redirect, routing::get,
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use database::mongo_indexed::Document;
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use komodo_client::entities::{
|
||||
komodo_timestamp, random_string,
|
||||
user::{User, UserConfig},
|
||||
use futures_util::TryFutureExt;
|
||||
use komodo_client::{
|
||||
api::auth::UserIdOrTwoFactor,
|
||||
entities::{
|
||||
komodo_timestamp, random_string,
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
use serror::{AddStatusCode, AddStatusCodeError as _};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
SESSION_KEY_PASSKEY_LOGIN, SESSION_KEY_TOTP_LOGIN,
|
||||
SESSION_KEY_USER_ID,
|
||||
},
|
||||
auth::format_redirect,
|
||||
config::core_config,
|
||||
state::{db_client, jwt_client},
|
||||
state::{auth_rate_limiter, db_client, webauthn},
|
||||
};
|
||||
|
||||
use self::client::github_oauth_client;
|
||||
@@ -28,21 +45,32 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/login",
|
||||
get(|Query(query): Query<RedirectQuery>| async {
|
||||
Redirect::to(
|
||||
&github_oauth_client()
|
||||
.as_ref()
|
||||
// OK: the router is only mounted in case that the client is populated
|
||||
.unwrap()
|
||||
.get_login_redirect_url(query.redirect)
|
||||
.await,
|
||||
)
|
||||
let uri = github_oauth_client()
|
||||
.as_ref()
|
||||
.context("Github Oauth not configured")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?
|
||||
.get_login_redirect_url(query.redirect)
|
||||
.await;
|
||||
serror::Result::Ok(Redirect::to(&uri))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query| async {
|
||||
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
|
||||
}),
|
||||
get(
|
||||
|query,
|
||||
session: Session,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(query, session)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,6 +82,7 @@ struct CallbackQuery {
|
||||
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
session: Session,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client = github_oauth_client().as_ref().unwrap();
|
||||
if !client.check_state(&query.state).await {
|
||||
@@ -69,10 +98,39 @@ async fn callback(
|
||||
.find_one(doc! { "config.data.github_id": &github_id })
|
||||
.await
|
||||
.context("failed at find user query from database")?;
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.encode(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
let user_id_or_two_factor = match user {
|
||||
Some(user) => {
|
||||
match (user.passkey.passkey, user.totp.enrolled()) {
|
||||
// WebAuthn Passkey 2FA
|
||||
(Some(passkey), _) => {
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
let (response, server_state) = webauthn
|
||||
.start_passkey_authentication(&[passkey])
|
||||
.context("Failed to start passkey authentication flow")?;
|
||||
session
|
||||
.insert(
|
||||
SESSION_KEY_PASSKEY_LOGIN,
|
||||
(user.id, server_state),
|
||||
)
|
||||
.await?;
|
||||
UserIdOrTwoFactor::Passkey(response)
|
||||
}
|
||||
// TOTP 2FA
|
||||
(None, true) => {
|
||||
session
|
||||
.insert(SESSION_KEY_TOTP_LOGIN, user.id)
|
||||
.await
|
||||
.context(
|
||||
"Failed to store totp login state in for user session",
|
||||
)?;
|
||||
UserIdOrTwoFactor::Totp {}
|
||||
}
|
||||
// No 2FA
|
||||
(None, false) => UserIdOrTwoFactor::UserId(user.id),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let ts = komodo_timestamp();
|
||||
let no_users_exist =
|
||||
@@ -111,28 +169,38 @@ async fn callback(
|
||||
github_id,
|
||||
avatar: github_user.avatar_url,
|
||||
},
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
};
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("failed to create user on mongo")?
|
||||
.context("Failed to create user on mongo")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
jwt_client()
|
||||
.encode(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
UserIdOrTwoFactor::UserId(user_id)
|
||||
}
|
||||
};
|
||||
let exchange_token = jwt_client().create_exchange_token(jwt).await;
|
||||
let redirect = &query.state[STATE_PREFIX_LENGTH..];
|
||||
let redirect_url = if redirect.is_empty() {
|
||||
format!("{}?token={exchange_token}", core_config().host)
|
||||
} else {
|
||||
let splitter = if redirect.contains('?') { '&' } else { '?' };
|
||||
format!("{redirect}{splitter}token={exchange_token}")
|
||||
};
|
||||
Ok(Redirect::to(&redirect_url))
|
||||
let redirect = Some(&query.state[STATE_PREFIX_LENGTH..]);
|
||||
match user_id_or_two_factor {
|
||||
UserIdOrTwoFactor::UserId(user_id) => {
|
||||
session
|
||||
.insert(SESSION_KEY_USER_ID, user_id)
|
||||
.await
|
||||
.context("Failed to store user id for client session")?;
|
||||
Ok(format_redirect(redirect, "redeem_ready=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Totp {} => {
|
||||
Ok(format_redirect(redirect, "totp=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Passkey(passkey) => {
|
||||
let passkey = serde_json::to_string(&passkey)
|
||||
.context("Failed to serialize passkey response")?;
|
||||
let passkey = urlencoding::encode(&passkey);
|
||||
Ok(format_redirect(redirect, &format!("passkey={passkey}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use jsonwebtoken::{DecodingKey, Validation, decode};
|
||||
use jsonwebtoken::dangerous::insecure_decode;
|
||||
use komodo_client::entities::{
|
||||
config::core::{CoreConfig, OauthCredentials},
|
||||
random_string,
|
||||
@@ -138,15 +138,8 @@ impl GoogleOauthClient {
|
||||
&self,
|
||||
id_token: &str,
|
||||
) -> anyhow::Result<GoogleUser> {
|
||||
let mut v = Validation::new(Default::default());
|
||||
v.insecure_disable_signature_validation();
|
||||
v.validate_aud = false;
|
||||
let res = decode::<GoogleUser>(
|
||||
id_token,
|
||||
&DecodingKey::from_secret(b""),
|
||||
&v,
|
||||
)
|
||||
.context("failed to decode google id token")?;
|
||||
let res = insecure_decode::<GoogleUser>(id_token)
|
||||
.context("failed to decode google id token")?;
|
||||
Ok(res.claims)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
Router, extract::Query, response::Redirect, routing::get,
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use database::mongo_indexed::Document;
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use komodo_client::entities::{
|
||||
random_string,
|
||||
user::{User, UserConfig},
|
||||
use futures_util::TryFutureExt;
|
||||
use komodo_client::{
|
||||
api::auth::UserIdOrTwoFactor,
|
||||
entities::{
|
||||
random_string,
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
use serror::{AddStatusCode, AddStatusCodeError as _};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
api::{SESSION_KEY_PASSKEY_LOGIN, SESSION_KEY_TOTP_LOGIN, SESSION_KEY_USER_ID},
|
||||
auth::format_redirect,
|
||||
config::core_config,
|
||||
state::{db_client, jwt_client},
|
||||
state::{auth_rate_limiter, db_client, webauthn},
|
||||
};
|
||||
|
||||
use self::client::google_oauth_client;
|
||||
@@ -29,21 +43,32 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/login",
|
||||
get(|Query(query): Query<RedirectQuery>| async move {
|
||||
Redirect::to(
|
||||
&google_oauth_client()
|
||||
.as_ref()
|
||||
// OK: its not mounted unless the client is populated
|
||||
.unwrap()
|
||||
.get_login_redirect_url(query.redirect)
|
||||
.await,
|
||||
)
|
||||
let uri = google_oauth_client()
|
||||
.as_ref()
|
||||
.context("Google Oauth not configured")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?
|
||||
.get_login_redirect_url(query.redirect)
|
||||
.await;
|
||||
serror::Result::Ok(Redirect::to(&uri))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query| async {
|
||||
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
|
||||
}),
|
||||
get(
|
||||
|query,
|
||||
session: Session,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(query, session)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,6 +81,7 @@ struct CallbackQuery {
|
||||
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
session: Session,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
// Safe: the method is only called after the client is_some
|
||||
let client = google_oauth_client().as_ref().unwrap();
|
||||
@@ -81,10 +107,39 @@ async fn callback(
|
||||
.find_one(doc! { "config.data.google_id": &google_id })
|
||||
.await
|
||||
.context("failed at find user query from mongo")?;
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.encode(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
let user_id_or_two_factor = match user {
|
||||
Some(user) => {
|
||||
match (user.passkey.passkey, user.totp.enrolled()) {
|
||||
// WebAuthn Passkey 2FA
|
||||
(Some(passkey), _) => {
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
let (response, server_state) = webauthn
|
||||
.start_passkey_authentication(&[passkey])
|
||||
.context("Failed to start passkey authentication flow")?;
|
||||
session
|
||||
.insert(
|
||||
SESSION_KEY_PASSKEY_LOGIN,
|
||||
(user.id, server_state),
|
||||
)
|
||||
.await?;
|
||||
UserIdOrTwoFactor::Passkey(response)
|
||||
}
|
||||
// TOTP 2FA
|
||||
(None, true) => {
|
||||
session
|
||||
.insert(SESSION_KEY_TOTP_LOGIN, user.id)
|
||||
.await
|
||||
.context(
|
||||
"Failed to store totp login state in for user session",
|
||||
)?;
|
||||
UserIdOrTwoFactor::Totp {}
|
||||
}
|
||||
// No 2FA
|
||||
(None, false) => UserIdOrTwoFactor::UserId(user.id),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let no_users_exist =
|
||||
@@ -128,28 +183,38 @@ async fn callback(
|
||||
google_id,
|
||||
avatar: google_user.picture,
|
||||
},
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
};
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("failed to create user on mongo")?
|
||||
.context("Failed to create user on mongo")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
jwt_client()
|
||||
.encode(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
UserIdOrTwoFactor::UserId(user_id)
|
||||
}
|
||||
};
|
||||
let exchange_token = jwt_client().create_exchange_token(jwt).await;
|
||||
let redirect = &state[STATE_PREFIX_LENGTH..];
|
||||
let redirect_url = if redirect.is_empty() {
|
||||
format!("{}?token={exchange_token}", core_config().host)
|
||||
} else {
|
||||
let splitter = if redirect.contains('?') { '&' } else { '?' };
|
||||
format!("{redirect}{splitter}token={exchange_token}")
|
||||
};
|
||||
Ok(Redirect::to(&redirect_url))
|
||||
let redirect = Some(&state[STATE_PREFIX_LENGTH..]);
|
||||
match user_id_or_two_factor {
|
||||
UserIdOrTwoFactor::UserId(user_id) => {
|
||||
session
|
||||
.insert(SESSION_KEY_USER_ID, user_id)
|
||||
.await
|
||||
.context("Failed to store user id for client session")?;
|
||||
Ok(format_redirect(redirect, "redeem_ready=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Totp {} => {
|
||||
Ok(format_redirect(redirect, "totp=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Passkey(passkey) => {
|
||||
let passkey = serde_json::to_string(&passkey)
|
||||
.context("Failed to serialize passkey response")?;
|
||||
let passkey = urlencoding::encode(&passkey);
|
||||
Ok(format_redirect(redirect, &format!("passkey={passkey}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::{
|
||||
Timelength, get_timelength_in_ms, unix_timestamp_ms,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use async_timing_util::{get_timelength_in_ms, unix_timestamp_ms};
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use jsonwebtoken::{
|
||||
DecodingKey, EncodingKey, Header, Validation, decode, encode,
|
||||
@@ -13,9 +9,6 @@ use komodo_client::{
|
||||
entities::{config::core::CoreConfig, random_string},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
type ExchangeTokenMap = Mutex<HashMap<String, (JwtResponse, u128)>>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct JwtClaims {
|
||||
@@ -30,7 +23,6 @@ pub struct JwtClient {
|
||||
encoding_key: EncodingKey,
|
||||
decoding_key: DecodingKey,
|
||||
ttl_ms: u128,
|
||||
exchange_tokens: ExchangeTokenMap,
|
||||
}
|
||||
|
||||
impl JwtClient {
|
||||
@@ -48,7 +40,6 @@ impl JwtClient {
|
||||
ttl_ms: get_timelength_in_ms(
|
||||
config.jwt_ttl.to_string().parse()?,
|
||||
),
|
||||
exchange_tokens: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,46 +55,13 @@ impl JwtClient {
|
||||
exp,
|
||||
};
|
||||
let jwt = encode(&self.header, &claims, &self.encoding_key)
|
||||
.context("failed at signing claim")?;
|
||||
.context("Failed at signing claim")?;
|
||||
Ok(JwtResponse { user_id, jwt })
|
||||
}
|
||||
|
||||
pub fn decode(&self, jwt: &str) -> anyhow::Result<JwtClaims> {
|
||||
decode::<JwtClaims>(jwt, &self.decoding_key, &self.validation)
|
||||
.map(|res| res.claims)
|
||||
.context("failed to decode token claims")
|
||||
}
|
||||
|
||||
pub async fn create_exchange_token(
|
||||
&self,
|
||||
jwt: JwtResponse,
|
||||
) -> String {
|
||||
let exchange_token = random_string(40);
|
||||
self.exchange_tokens.lock().await.insert(
|
||||
exchange_token.clone(),
|
||||
(
|
||||
jwt,
|
||||
unix_timestamp_ms()
|
||||
+ get_timelength_in_ms(Timelength::OneMinute),
|
||||
),
|
||||
);
|
||||
exchange_token
|
||||
}
|
||||
|
||||
pub async fn redeem_exchange_token(
|
||||
&self,
|
||||
exchange_token: &str,
|
||||
) -> anyhow::Result<JwtResponse> {
|
||||
let (jwt, valid_until) = self
|
||||
.exchange_tokens
|
||||
.lock()
|
||||
.await
|
||||
.remove(exchange_token)
|
||||
.context("invalid exchange token: unrecognized")?;
|
||||
if unix_timestamp_ms() < valid_until {
|
||||
Ok(jwt)
|
||||
} else {
|
||||
Err(anyhow!("invalid exchange token: expired"))
|
||||
}
|
||||
.context("Failed to decode token claims")
|
||||
}
|
||||
}
|
||||
|
||||
+206
-109
@@ -1,10 +1,10 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use database::{
|
||||
hash_password,
|
||||
mungos::mongodb::bson::{Document, doc, oid::ObjectId},
|
||||
mungos::mongodb::bson::{Document, doc},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::auth::{
|
||||
@@ -13,136 +13,233 @@ use komodo_client::{
|
||||
},
|
||||
entities::user::{User, UserConfig},
|
||||
};
|
||||
use rate_limit::{RateLimiter, WithFailureRateLimit};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
api::auth::AuthArgs,
|
||||
api::{
|
||||
SESSION_KEY_PASSKEY_LOGIN, SESSION_KEY_TOTP_LOGIN, auth::AuthArgs,
|
||||
},
|
||||
config::core_config,
|
||||
state::{db_client, jwt_client},
|
||||
helpers::validations::{validate_password, validate_username},
|
||||
state::{auth_rate_limiter, db_client, jwt_client, webauthn},
|
||||
};
|
||||
|
||||
impl Resolve<AuthArgs> for SignUpLocalUser {
|
||||
#[instrument("SignUpLocalUser", skip(self))]
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &AuthArgs,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<SignUpLocalUserResponse> {
|
||||
let core_config = core_config();
|
||||
|
||||
if !core_config.local_auth {
|
||||
return Err(anyhow!("Local auth is not enabled").into());
|
||||
}
|
||||
|
||||
if self.username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty string").into());
|
||||
}
|
||||
|
||||
if ObjectId::from_str(&self.username).is_ok() {
|
||||
return Err(
|
||||
anyhow!("Username cannot be valid ObjectId").into(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.password.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty string").into());
|
||||
}
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let no_users_exist =
|
||||
db.users.find_one(Document::new()).await?.is_none();
|
||||
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled").into());
|
||||
}
|
||||
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
sign_up_local_user(self)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
.context("Failed to query for existing users")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!("Username already taken.").into());
|
||||
}
|
||||
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let hashed_password = hash_password(self.password)?;
|
||||
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username: self.username,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Local {
|
||||
password: hashed_password,
|
||||
},
|
||||
};
|
||||
|
||||
let user_id = db_client()
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("failed to create user")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
jwt_client()
|
||||
.encode(user_id.clone())
|
||||
.context("failed to generate jwt for user")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
async fn sign_up_local_user(
|
||||
req: SignUpLocalUser,
|
||||
) -> serror::Result<SignUpLocalUserResponse> {
|
||||
let config = core_config();
|
||||
|
||||
if !config.local_auth {
|
||||
return Err(anyhow!("Local auth is not enabled").into());
|
||||
}
|
||||
|
||||
validate_username(&req.username)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
validate_password(&req.password)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let no_users_exist =
|
||||
db.users.find_one(Document::new()).await?.is_none();
|
||||
|
||||
if !no_users_exist && config.disable_user_registration {
|
||||
return Err(
|
||||
anyhow!("User registration is disabled")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &req.username })
|
||||
.await
|
||||
.context("Failed to query for existing users")?
|
||||
.is_some()
|
||||
{
|
||||
// When user registration is enabled, there is no way around allowing
|
||||
// potential attackers to gain some insight about which usernames exist
|
||||
// if they are allowed to register accounts. Since this can be easily inferred,
|
||||
// might as well be clear. The auth rate limiter is critical here.
|
||||
return Err(
|
||||
anyhow!("Username already taken.")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let hashed_password = hash_password(req.password)?;
|
||||
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username: req.username,
|
||||
enabled: no_users_exist || config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Local {
|
||||
password: hashed_password,
|
||||
},
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
};
|
||||
|
||||
let user_id = db_client()
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("Failed to create user on database")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("The 'inserted_id' is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
jwt_client()
|
||||
.encode(user_id)
|
||||
.context("Failed to generate JWT for user")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Local login method has a dedicated rate limiter
|
||||
/// so the UI background calls using existing JWT do
|
||||
/// not influence the number of attempts user has
|
||||
/// to log in.
|
||||
fn login_local_user_rate_limiter() -> &'static RateLimiter {
|
||||
static LOGIN_LOCAL_USER_RATE_LIMITER: OnceLock<Arc<RateLimiter>> =
|
||||
OnceLock::new();
|
||||
LOGIN_LOCAL_USER_RATE_LIMITER.get_or_init(|| {
|
||||
let config = core_config();
|
||||
RateLimiter::new(
|
||||
config.auth_rate_limit_disabled,
|
||||
config.auth_rate_limit_max_attempts as usize,
|
||||
config.auth_rate_limit_window_seconds,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for LoginLocalUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &AuthArgs,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<LoginLocalUserResponse> {
|
||||
if !core_config().local_auth {
|
||||
return Err(anyhow!("local auth is not enabled").into());
|
||||
}
|
||||
login_local_user(
|
||||
self,
|
||||
session
|
||||
.as_ref()
|
||||
.context("Method called in context without session")?,
|
||||
)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
login_local_user_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_local_user(
|
||||
req: LoginLocalUser,
|
||||
session: &Session,
|
||||
) -> serror::Result<LoginLocalUserResponse> {
|
||||
if !core_config().local_auth {
|
||||
return Err(
|
||||
anyhow!("Local auth is not enabled")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
validate_username(&req.username)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let user = db_client()
|
||||
.users
|
||||
.find_one(doc! { "username": &req.username })
|
||||
.await
|
||||
.context("Failed at db query for users")?
|
||||
.context("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let user = db_client()
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
.await
|
||||
.context("failed at db query for users")?
|
||||
.with_context(|| {
|
||||
format!("did not find user with username {}", self.username)
|
||||
})?;
|
||||
let UserConfig::Local {
|
||||
password: user_pw_hash,
|
||||
} = user.config
|
||||
else {
|
||||
return Err(
|
||||
anyhow!("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
};
|
||||
|
||||
let UserConfig::Local {
|
||||
password: user_pw_hash,
|
||||
} = user.config
|
||||
else {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"non-local auth users can not log in with a password"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
};
|
||||
let verified = bcrypt::verify(req.password, &user_pw_hash)
|
||||
.context("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let verified = bcrypt::verify(self.password, &user_pw_hash)
|
||||
.context("failed at verify password")?;
|
||||
if !verified {
|
||||
return Err(
|
||||
anyhow!("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
if !verified {
|
||||
return Err(anyhow!("invalid credentials").into());
|
||||
match (user.passkey.passkey, user.totp.enrolled()) {
|
||||
// WebAuthn 2FA
|
||||
(Some(passkey), _) => {
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
let (response, server_state) = webauthn
|
||||
.start_passkey_authentication(&[passkey])
|
||||
.context("Failed to start passkey authentication flow")?;
|
||||
session
|
||||
.insert(SESSION_KEY_PASSKEY_LOGIN, (user.id, server_state))
|
||||
.await?;
|
||||
Ok(LoginLocalUserResponse::Passkey(response))
|
||||
}
|
||||
|
||||
jwt_client()
|
||||
.encode(user.id.clone())
|
||||
.context("failed at generating jwt for user")
|
||||
.map_err(Into::into)
|
||||
// TOTP 2FA
|
||||
(None, true) => {
|
||||
session.insert(SESSION_KEY_TOTP_LOGIN, user.id).await?;
|
||||
Ok(LoginLocalUserResponse::Totp {})
|
||||
}
|
||||
// No 2FA, can return JWT immediately
|
||||
(None, false) => {
|
||||
jwt_client()
|
||||
.encode(user.id)
|
||||
// This is in internal error (500), not auth error
|
||||
.context("Failed to generate JWT for user")
|
||||
.map(|jwt| LoginLocalUserResponse::Jwt(jwt))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+78
-29
@@ -1,18 +1,25 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::Request, http::HeaderMap, middleware::Next,
|
||||
response::Response,
|
||||
extract::{ConnectInfo, Request},
|
||||
http::HeaderMap,
|
||||
middleware::Next,
|
||||
response::{Redirect, Response},
|
||||
};
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use futures_util::TryFutureExt;
|
||||
use komodo_client::entities::{komodo_timestamp, user::User};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
use serror::AddStatusCodeError as _;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::query::get_user,
|
||||
state::{db_client, jwt_client},
|
||||
state::{auth_rate_limiter, db_client, jwt_client},
|
||||
};
|
||||
|
||||
use self::jwt::JwtClaims;
|
||||
@@ -21,10 +28,16 @@ pub mod github;
|
||||
pub mod google;
|
||||
pub mod jwt;
|
||||
pub mod oidc;
|
||||
pub mod totp;
|
||||
|
||||
mod local;
|
||||
|
||||
/// Length of random token in Oauth / OIDC 'state'
|
||||
const STATE_PREFIX_LENGTH: usize = 20;
|
||||
/// JWT Clock skew tolerance in milliseconds (10 seconds for JWTs)
|
||||
const JWT_CLOCK_SKEW_TOLERANCE_MS: u128 = 10 * 1000;
|
||||
/// Api Key Clock skew tolerance in milliseconds (5 minutes for Api Keys)
|
||||
const API_KEY_CLOCK_SKEW_TOLERANCE_MS: i64 = 5 * 60 * 1000;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RedirectQuery {
|
||||
@@ -36,9 +49,21 @@ pub async fn auth_request(
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> serror::Result<Response> {
|
||||
let user = authenticate_check_enabled(&headers)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
let fallback = req
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|addr| addr.ip());
|
||||
let mut user = authenticate_check_enabled(&headers)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
fallback,
|
||||
)
|
||||
.await?;
|
||||
// Sanitize the user for safety before
|
||||
// attaching to the request handlers.
|
||||
user.sanitize();
|
||||
req.extensions_mut().insert(user);
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
@@ -53,23 +78,21 @@ pub async fn get_user_id_from_headers(
|
||||
) {
|
||||
(Some(jwt), _, _) => {
|
||||
// USE JWT
|
||||
let jwt = jwt.to_str().context("jwt is not str")?;
|
||||
auth_jwt_get_user_id(jwt)
|
||||
.await
|
||||
.context("failed to authenticate jwt")
|
||||
let jwt = jwt.to_str().context("JWT is not valid UTF-8")?;
|
||||
auth_jwt_get_user_id(jwt).await
|
||||
}
|
||||
(None, Some(key), Some(secret)) => {
|
||||
// USE API KEY / SECRET
|
||||
let key = key.to_str().context("key is not str")?;
|
||||
let secret = secret.to_str().context("secret is not str")?;
|
||||
auth_api_key_get_user_id(key, secret)
|
||||
.await
|
||||
.context("failed to authenticate api key")
|
||||
let key =
|
||||
key.to_str().context("X-API-KEY is not valid UTF-8")?;
|
||||
let secret =
|
||||
secret.to_str().context("X-API-SECRET is not valid UTF-8")?;
|
||||
auth_api_key_get_user_id(key, secret).await
|
||||
}
|
||||
_ => {
|
||||
// AUTH FAIL
|
||||
Err(anyhow!(
|
||||
"must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"
|
||||
"Must attach either AUTHORIZATION header with jwt OR pass X-API-KEY and X-API-SECRET"
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -79,22 +102,30 @@ pub async fn authenticate_check_enabled(
|
||||
headers: &HeaderMap,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = get_user_id_from_headers(headers).await?;
|
||||
let user = get_user(&user_id).await?;
|
||||
let user = get_user(&user_id)
|
||||
.await
|
||||
.map_err(|_| anyhow!("Invalid user credentials"))?;
|
||||
if user.enabled {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(anyhow!("user not enabled"))
|
||||
Err(anyhow!("Invalid user credentials"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auth_jwt_get_user_id(
|
||||
jwt: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let claims: JwtClaims = jwt_client().decode(jwt)?;
|
||||
if claims.exp > unix_timestamp_ms() {
|
||||
let claims: JwtClaims = jwt_client()
|
||||
.decode(jwt)
|
||||
.map_err(|_| anyhow!("Invalid user credentials"))?;
|
||||
// Apply clock skew tolerance.
|
||||
// Token is valid if expiration is greater than (now - tolerance)
|
||||
if claims.exp
|
||||
> unix_timestamp_ms().saturating_sub(JWT_CLOCK_SKEW_TOLERANCE_MS)
|
||||
{
|
||||
Ok(claims.id)
|
||||
} else {
|
||||
Err(anyhow!("token has expired"))
|
||||
Err(anyhow!("Invalid user credentials"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,19 +144,25 @@ pub async fn auth_api_key_get_user_id(
|
||||
.api_keys
|
||||
.find_one(doc! { "key": key })
|
||||
.await
|
||||
.context("failed to query db")?
|
||||
.context("no api key matching key")?;
|
||||
if key.expires != 0 && key.expires < komodo_timestamp() {
|
||||
return Err(anyhow!("api key expired"));
|
||||
.context("Failed to query db")?
|
||||
.context("Invalid user credentials")?;
|
||||
// Apply clock skew tolerance.
|
||||
// Token is invalid if expiration is less than (now - tolerance)
|
||||
if key.expires != 0
|
||||
&& key.expires
|
||||
< komodo_timestamp()
|
||||
.saturating_sub(API_KEY_CLOCK_SKEW_TOLERANCE_MS)
|
||||
{
|
||||
return Err(anyhow!("Invalid user credentials"));
|
||||
}
|
||||
if bcrypt::verify(secret, &key.secret)
|
||||
.context("failed to verify secret hash")?
|
||||
.map_err(|_| anyhow!("Invalid user credentials"))?
|
||||
{
|
||||
// secret matches
|
||||
Ok(key.user_id)
|
||||
} else {
|
||||
// secret mismatch
|
||||
Err(anyhow!("invalid api secret"))
|
||||
Err(anyhow!("Invalid user credentials"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +179,18 @@ async fn check_enabled(user_id: String) -> anyhow::Result<User> {
|
||||
if user.enabled {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(anyhow!("user not enabled"))
|
||||
Err(anyhow!("Invalid user credentials"))
|
||||
}
|
||||
}
|
||||
|
||||
fn format_redirect(redirect: Option<&str>, extra: &str) -> Redirect {
|
||||
let redirect_url = if let Some(redirect) = redirect
|
||||
&& !redirect.is_empty()
|
||||
{
|
||||
let splitter = if redirect.contains('?') { '&' } else { '?' };
|
||||
format!("{redirect}{splitter}{extra}")
|
||||
} else {
|
||||
format!("{}?{extra}", core_config().host)
|
||||
};
|
||||
Redirect::to(&redirect_url)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::{net::SocketAddr, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router, extract::Query, response::Redirect, routing::get,
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use client::oidc_client;
|
||||
use dashmap::DashMap;
|
||||
use database::mungos::mongodb::bson::{Document, doc};
|
||||
use komodo_client::entities::{
|
||||
komodo_timestamp, random_string,
|
||||
user::{User, UserConfig},
|
||||
use futures_util::TryFutureExt;
|
||||
use komodo_client::{
|
||||
api::auth::UserIdOrTwoFactor,
|
||||
entities::{
|
||||
komodo_timestamp, random_string,
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use openidconnect::{
|
||||
AccessTokenHash, AuthorizationCode, CsrfToken,
|
||||
@@ -17,13 +25,20 @@ use openidconnect::{
|
||||
PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
|
||||
core::{CoreAuthenticationFlow, CoreGenderClaim},
|
||||
};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
SESSION_KEY_PASSKEY_LOGIN, SESSION_KEY_TOTP_LOGIN,
|
||||
SESSION_KEY_USER_ID,
|
||||
},
|
||||
auth::format_redirect,
|
||||
config::core_config,
|
||||
state::{db_client, jwt_client},
|
||||
state::{auth_rate_limiter, db_client, webauthn},
|
||||
};
|
||||
|
||||
use super::RedirectQuery;
|
||||
@@ -68,9 +83,21 @@ pub fn router() -> Router {
|
||||
)
|
||||
.route(
|
||||
"/callback",
|
||||
get(|query| async {
|
||||
callback(query).await.status_code(StatusCode::UNAUTHORIZED)
|
||||
}),
|
||||
get(
|
||||
|query,
|
||||
session: Session,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(query, session)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,6 +165,7 @@ struct CallbackQuery {
|
||||
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
session: Session,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client = oidc_client().load();
|
||||
let client =
|
||||
@@ -217,12 +245,41 @@ async fn callback(
|
||||
"config.data.user_id": user_id
|
||||
})
|
||||
.await
|
||||
.context("failed at find user query from database")?;
|
||||
.context("Failed at find user query from database")?;
|
||||
|
||||
let jwt = match user {
|
||||
Some(user) => jwt_client()
|
||||
.encode(user.id)
|
||||
.context("failed to generate jwt")?,
|
||||
let user_id_or_two_factor = match user {
|
||||
Some(user) => {
|
||||
match (user.passkey.passkey, user.totp.enrolled()) {
|
||||
// WebAuthn Passkey 2FA
|
||||
(Some(passkey), _) => {
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
let (response, server_state) = webauthn
|
||||
.start_passkey_authentication(&[passkey])
|
||||
.context("Failed to start passkey authentication flow")?;
|
||||
session
|
||||
.insert(
|
||||
SESSION_KEY_PASSKEY_LOGIN,
|
||||
(user.id, server_state),
|
||||
)
|
||||
.await?;
|
||||
UserIdOrTwoFactor::Passkey(response)
|
||||
}
|
||||
// TOTP 2FA
|
||||
(None, true) => {
|
||||
session
|
||||
.insert(SESSION_KEY_TOTP_LOGIN, user.id)
|
||||
.await
|
||||
.context(
|
||||
"Failed to store totp login state in for user session",
|
||||
)?;
|
||||
UserIdOrTwoFactor::Totp {}
|
||||
}
|
||||
// No 2FA
|
||||
(None, false) => UserIdOrTwoFactor::UserId(user.id),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let ts = komodo_timestamp();
|
||||
let no_users_exist =
|
||||
@@ -293,6 +350,8 @@ async fn callback(
|
||||
provider: core_config.oidc_provider.clone(),
|
||||
user_id: user_id.to_string(),
|
||||
},
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
};
|
||||
|
||||
let user_id = db_client
|
||||
@@ -305,17 +364,29 @@ async fn callback(
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
jwt_client()
|
||||
.encode(user_id)
|
||||
.context("failed to generate jwt")?
|
||||
UserIdOrTwoFactor::UserId(user_id)
|
||||
}
|
||||
};
|
||||
let exchange_token = jwt_client().create_exchange_token(jwt).await;
|
||||
let redirect_url = if let Some(redirect) = redirect {
|
||||
let splitter = if redirect.contains('?') { '&' } else { '?' };
|
||||
format!("{redirect}{splitter}token={exchange_token}")
|
||||
} else {
|
||||
format!("{}?token={exchange_token}", core_config().host)
|
||||
};
|
||||
Ok(Redirect::to(&redirect_url))
|
||||
|
||||
match user_id_or_two_factor {
|
||||
UserIdOrTwoFactor::UserId(user_id) => {
|
||||
session
|
||||
.insert(SESSION_KEY_USER_ID, user_id)
|
||||
.await
|
||||
.context("Failed to store user id for client session")?;
|
||||
Ok(format_redirect(redirect.as_deref(), "redeem_ready=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Totp {} => {
|
||||
Ok(format_redirect(redirect.as_deref(), "totp=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Passkey(passkey) => {
|
||||
let passkey = serde_json::to_string(&passkey)
|
||||
.context("Failed to serialize passkey response")?;
|
||||
let passkey = urlencoding::encode(&passkey);
|
||||
Ok(format_redirect(
|
||||
redirect.as_deref(),
|
||||
&format!("passkey={passkey}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use anyhow::Context as _;
|
||||
|
||||
pub fn make_totp(
|
||||
secret_bytes: Vec<u8>,
|
||||
account_name: impl Into<Option<String>>,
|
||||
) -> anyhow::Result<totp_rs::TOTP> {
|
||||
totp_rs::TOTP::new(
|
||||
totp_rs::Algorithm::SHA1,
|
||||
6,
|
||||
1,
|
||||
30,
|
||||
secret_bytes,
|
||||
Some(String::from("Komodo")),
|
||||
account_name.into().unwrap_or_default(),
|
||||
)
|
||||
.context("Failed to construct TOTP")
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use aws_sdk_ec2::{
|
||||
Tag, TagSpecification,
|
||||
},
|
||||
};
|
||||
use base64::Engine;
|
||||
use data_encoding::BASE64_NOPAD;
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
@@ -127,10 +127,7 @@ pub async fn launch_ec2_instance(
|
||||
)
|
||||
.min_count(1)
|
||||
.max_count(1)
|
||||
.user_data(
|
||||
base64::engine::general_purpose::STANDARD_NO_PAD
|
||||
.encode(user_data),
|
||||
);
|
||||
.user_data(BASE64_NOPAD.encode(user_data.as_bytes()));
|
||||
|
||||
let res = req
|
||||
.send()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::http::HeaderValue;
|
||||
use colored::Colorize;
|
||||
use config::ConfigLoader;
|
||||
use environment_file::{
|
||||
@@ -14,6 +15,7 @@ use komodo_client::entities::{
|
||||
logger::LogConfig,
|
||||
};
|
||||
use noise::key::{RotatableKeyPair, SpkiPublicKey};
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
/// Should call in startup to ensure Core errors without valid private key.
|
||||
pub fn core_keys() -> &'static RotatableKeyPair {
|
||||
@@ -89,6 +91,70 @@ pub fn periphery_public_keys() -> Option<&'static [SpkiPublicKey]> {
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
/// Creates a CORS layer based on the Core configuration.
|
||||
///
|
||||
/// - If `cors_allowed_origins` is empty: Allows all origins (backward compatibility)
|
||||
/// - If `cors_allowed_origins` is set: Only allows the specified origins
|
||||
/// - Methods and headers are always allowed (Any)
|
||||
/// - Credentials are only allowed if `cors_allow_credentials` is true
|
||||
pub fn cors_layer() -> CorsLayer {
|
||||
let config = core_config();
|
||||
let mut cors = CorsLayer::new()
|
||||
.allow_methods(tower_http::cors::AllowMethods::mirror_request())
|
||||
.allow_headers(tower_http::cors::AllowHeaders::mirror_request())
|
||||
.allow_credentials(config.cors_allow_credentials);
|
||||
if config.cors_allowed_origins.is_empty() {
|
||||
warn!(
|
||||
"CORS using allowed origin 'Any' (*). Use KOMODO_CORS_ALLOWED_ORIGINS to configure specific origins."
|
||||
);
|
||||
cors = cors.allow_origin(tower_http::cors::Any)
|
||||
} else {
|
||||
let allowed_origins = config
|
||||
.cors_allowed_origins
|
||||
.iter()
|
||||
.filter_map(|origin| {
|
||||
HeaderValue::from_str(origin)
|
||||
.inspect_err(|e| {
|
||||
warn!("Invalid CORS allowed origin: {origin} | {e:?}")
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
info!("CORS using allowed origin/s: {allowed_origins:?}");
|
||||
cors = cors.allow_origin(allowed_origins);
|
||||
};
|
||||
cors
|
||||
}
|
||||
|
||||
pub fn monitoring_interval() -> async_timing_util::Timelength {
|
||||
static MONITORING_INTERVAL: OnceLock<
|
||||
async_timing_util::Timelength,
|
||||
> = OnceLock::new();
|
||||
*MONITORING_INTERVAL.get_or_init(|| {
|
||||
core_config().monitoring_interval.try_into().unwrap_or_else(
|
||||
|_| {
|
||||
error!("Invalid 'monitoring_interval', using default 15-sec");
|
||||
async_timing_util::Timelength::FifteenSeconds
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn core_host() -> Option<&'static url::Url> {
|
||||
static CORE_URL: OnceLock<Option<url::Url>> = OnceLock::new();
|
||||
CORE_URL
|
||||
.get_or_init(|| {
|
||||
url::Url::parse(&core_config().host)
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Invalid KOMODO_HOST: not URL. Passkeys won't work. | {e:?}"
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn core_config() -> &'static CoreConfig {
|
||||
static CORE_CONFIG: OnceLock<CoreConfig> = OnceLock::new();
|
||||
CORE_CONFIG.get_or_init(|| {
|
||||
@@ -281,6 +347,21 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.komodo_frontend_path
|
||||
.unwrap_or(config.frontend_path),
|
||||
jwt_ttl: env.komodo_jwt_ttl.unwrap_or(config.jwt_ttl),
|
||||
auth_rate_limit_disabled: env
|
||||
.komodo_auth_rate_limit_disabled
|
||||
.unwrap_or(config.auth_rate_limit_disabled),
|
||||
auth_rate_limit_max_attempts: env
|
||||
.komodo_auth_rate_limit_max_attempts
|
||||
.unwrap_or(config.auth_rate_limit_max_attempts),
|
||||
auth_rate_limit_window_seconds: env
|
||||
.komodo_auth_rate_limit_window_seconds
|
||||
.unwrap_or(config.auth_rate_limit_window_seconds),
|
||||
cors_allowed_origins: env
|
||||
.komodo_cors_allowed_origins
|
||||
.unwrap_or(config.cors_allowed_origins),
|
||||
cors_allow_credentials: env
|
||||
.komodo_cors_allow_credentials
|
||||
.unwrap_or(config.cors_allow_credentials),
|
||||
sync_directory: env
|
||||
.komodo_sync_directory
|
||||
.unwrap_or(config.sync_directory),
|
||||
@@ -336,6 +417,9 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.komodo_lock_login_credentials_for
|
||||
.unwrap_or(config.lock_login_credentials_for),
|
||||
local_auth: env.komodo_local_auth.unwrap_or(config.local_auth),
|
||||
min_password_length: env
|
||||
.komodo_min_password_length
|
||||
.unwrap_or(config.min_password_length),
|
||||
logging: LogConfig {
|
||||
level: env
|
||||
.komodo_logging_level
|
||||
|
||||
@@ -45,7 +45,6 @@ impl PeripheryConnectionArgs<'_> {
|
||||
periphery_connections().insert(id.clone(), self).await;
|
||||
|
||||
let responses = connection.responses.clone();
|
||||
let terminals = connection.terminals.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -91,11 +90,7 @@ impl PeripheryConnectionArgs<'_> {
|
||||
}
|
||||
});
|
||||
|
||||
Ok(PeripheryClient {
|
||||
id,
|
||||
responses,
|
||||
terminals,
|
||||
})
|
||||
Ok(PeripheryClient { id, responses })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use transport::{
|
||||
},
|
||||
channel::{BufferedReceiver, Sender, buffered_channel},
|
||||
websocket::{
|
||||
Websocket, WebsocketMessage, WebsocketReceiver as _,
|
||||
Websocket, WebsocketReceiver as _, WebsocketReceiverExt,
|
||||
WebsocketSender as _,
|
||||
},
|
||||
};
|
||||
@@ -367,8 +367,22 @@ impl PeripheryConnection {
|
||||
|
||||
let forward_writes = async {
|
||||
loop {
|
||||
let Ok(message) = receiver.recv().await else {
|
||||
break;
|
||||
let message = match tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
receiver.recv(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(message)) => message,
|
||||
Ok(Err(_)) => break,
|
||||
// Handle sending Ping
|
||||
Err(_) => {
|
||||
if let Err(e) = ws_write.ping().await {
|
||||
self.set_error(e).await;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match ws_write.send(message.into_bytes()).await {
|
||||
Ok(_) => receiver.clear_buffer(),
|
||||
@@ -385,19 +399,13 @@ impl PeripheryConnection {
|
||||
|
||||
let handle_reads = async {
|
||||
loop {
|
||||
match ws_read.recv().await {
|
||||
Ok(WebsocketMessage::Message(message)) => {
|
||||
self.handle_incoming_message(message).await
|
||||
}
|
||||
Ok(WebsocketMessage::Close(_))
|
||||
| Ok(WebsocketMessage::Closed) => {
|
||||
self.set_error(anyhow!("Connection closed")).await;
|
||||
break;
|
||||
}
|
||||
match ws_read.recv_message().await {
|
||||
Ok(message) => self.handle_incoming_message(message).await,
|
||||
Err(e) => {
|
||||
self.set_error(e).await;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// Cancel again if not already
|
||||
cancel.cancel();
|
||||
@@ -410,15 +418,8 @@ impl PeripheryConnection {
|
||||
|
||||
pub async fn handle_incoming_message(
|
||||
&self,
|
||||
message: EncodedTransportMessage,
|
||||
message: TransportMessage,
|
||||
) {
|
||||
let message: TransportMessage = match message.decode() {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse Message bytes | {e:#}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match message {
|
||||
TransportMessage::Response(data) => {
|
||||
match data.decode().map(ResponseMessage::into_inner) {
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
//! # Action State Management
|
||||
//!
|
||||
//! This module provides thread-safe state management for resource exections.
|
||||
//! It prevents concurrent execution of exections on the same resource using
|
||||
//! a Mutex-based locking mechanism with RAII guards.
|
||||
//!
|
||||
//! ## Safety
|
||||
//!
|
||||
//! - Uses RAII pattern to ensure locks are always released
|
||||
//! - Handles lock poisoning gracefully
|
||||
//! - Prevents race conditions through per-resource locks
|
||||
//! - No deadlock risk: each resource has independent locks
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::anyhow;
|
||||
@@ -9,12 +22,13 @@ use komodo_client::{
|
||||
deployment::DeploymentActionState,
|
||||
procedure::ProcedureActionState, repo::RepoActionState,
|
||||
server::ServerActionState, stack::StackActionState,
|
||||
sync::ResourceSyncActionState,
|
||||
swarm::SwarmActionState, sync::ResourceSyncActionState,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ActionStates {
|
||||
pub swarm: CloneCache<String, Arc<ActionState<SwarmActionState>>>,
|
||||
pub server: CloneCache<String, Arc<ActionState<ServerActionState>>>,
|
||||
pub stack: CloneCache<String, Arc<ActionState<StackActionState>>>,
|
||||
pub deployment:
|
||||
@@ -28,7 +42,16 @@ pub struct ActionStates {
|
||||
CloneCache<String, Arc<ActionState<ResourceSyncActionState>>>,
|
||||
}
|
||||
|
||||
/// Need to be able to check "busy" with write lock acquired.
|
||||
/// Thread-safe state container for resource executions.
|
||||
///
|
||||
/// Uses a Mutex to prevent concurrent executions and provides
|
||||
/// RAII-based locking through [UpdateGuard].
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - Each resource has its own ActionState instance
|
||||
/// - State is reset to default when [UpdateGuard] is dropped
|
||||
/// - Lock poisoning error handling is handled gracefully with anyhow::Error
|
||||
#[derive(Default)]
|
||||
pub struct ActionState<States: Default + Send + 'static>(
|
||||
Mutex<States>,
|
||||
@@ -42,7 +65,7 @@ impl<States: Default + Busy + Copy + Send + 'static>
|
||||
*self
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?,
|
||||
.map_err(|e| anyhow!("Action state lock poisoned | {e:?}"))?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,14 +74,33 @@ impl<States: Default + Busy + Copy + Send + 'static>
|
||||
self
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|e| anyhow!("action state lock poisoned | {e:?}"))?
|
||||
.map_err(|e| anyhow!("Action state lock poisoned | {e:?}"))?
|
||||
.busy(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Will acquire lock, check busy, and if not will
|
||||
/// run the provided update function on the states.
|
||||
/// Returns a guard that returns the states to default (not busy) when dropped.
|
||||
/// Acquires lock, checks if resource is busy, and if not,
|
||||
/// runs the provided update function on the states.
|
||||
///
|
||||
/// Returns an `UpdateGuard` that automatically resets the state
|
||||
/// to default (not busy) when dropped.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The lock is poisoned
|
||||
/// - The resource is currently busy
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// let guard = action_state.update(|state| {
|
||||
/// *state = SomeNewState;
|
||||
/// })?;
|
||||
/// // State is locked and marked as busy
|
||||
/// // ... perform work ...
|
||||
/// drop(guard) // Guard is dropped, state returns to default
|
||||
/// ```
|
||||
pub fn update(
|
||||
&self,
|
||||
update_fn: impl Fn(&mut States),
|
||||
@@ -91,10 +133,22 @@ impl<States: Default + Busy + Copy + Send + 'static>
|
||||
}
|
||||
}
|
||||
|
||||
/// When dropped will return the inner state to default.
|
||||
/// The inner mutex guard must already be dropped BEFORE this is dropped,
|
||||
/// which is guaranteed as the inner guard is dropped by all public methods before
|
||||
/// user could drop UpdateGuard.
|
||||
/// RAII guard that automatically resets the action state when dropped.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The inner mutex guard is guaranteed to be dropped before this guard
|
||||
/// is dropped, preventing deadlocks. This is ensured by all public methods
|
||||
/// that create UpdateGuard instances.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// When dropped, this guard will:
|
||||
/// 1. Re-acquire the lock
|
||||
/// 2. Call the provided return function (typically resetting to default)
|
||||
/// 3. Release the lock
|
||||
///
|
||||
/// If the lock is poisoned, an error is logged but the drop continues.
|
||||
pub struct UpdateGuard<'a, States: Default + Send + 'static>(
|
||||
&'a Mutex<States>,
|
||||
Box<dyn Fn(&mut States) + Send>,
|
||||
|
||||
@@ -3,11 +3,12 @@ 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,
|
||||
server::Server, stack::Stack, swarm::Swarm, sync::ResourceSync,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AllResourcesById {
|
||||
pub swarms: HashMap<String, Swarm>,
|
||||
pub servers: HashMap<String, Server>,
|
||||
pub deployments: HashMap<String, Deployment>,
|
||||
pub stacks: HashMap<String, Stack>,
|
||||
@@ -27,6 +28,10 @@ impl AllResourcesById {
|
||||
let id_to_tags = ↦
|
||||
let match_tags = &[];
|
||||
Ok(Self {
|
||||
swarms: crate::resource::get_id_to_resource_map::<Swarm>(
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
.await?,
|
||||
servers: crate::resource::get_id_to_resource_map::<Server>(
|
||||
id_to_tags, match_tags,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::{Context, anyhow};
|
||||
use database::mongo_indexed::Document;
|
||||
use database::mungos::mongodb::bson::{Bson, doc};
|
||||
use indexmap::IndexSet;
|
||||
use komodo_client::entities::SwarmOrServer;
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget,
|
||||
build::Build,
|
||||
@@ -15,7 +16,11 @@ use komodo_client::entities::{
|
||||
stack::Stack,
|
||||
user::User,
|
||||
};
|
||||
use resolver_api::HasResponse;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::helpers::swarm::swarm_request;
|
||||
use crate::{
|
||||
config::core_config, connection::PeripheryConnectionArgs,
|
||||
periphery::PeripheryClient, state::db_client,
|
||||
@@ -30,9 +35,10 @@ pub mod matcher;
|
||||
pub mod procedure;
|
||||
pub mod prune;
|
||||
pub mod query;
|
||||
pub mod swarm;
|
||||
pub mod terminal;
|
||||
pub mod update;
|
||||
|
||||
// pub mod resource;
|
||||
pub mod validations;
|
||||
|
||||
pub fn empty_or_only_spaces(word: &str) -> bool {
|
||||
if word.is_empty() {
|
||||
@@ -263,3 +269,21 @@ pub fn repo_link(
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn swarm_or_server_request<T>(
|
||||
swarm_or_server: &SwarmOrServer,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response>
|
||||
where
|
||||
T: std::fmt::Debug + Clone + Serialize + HasResponse,
|
||||
T::Response: DeserializeOwned,
|
||||
{
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
swarm_request(&swarm.config.server_ids, request).await
|
||||
}
|
||||
SwarmOrServer::Server(server) => {
|
||||
periphery_client(server).await?.request(request).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use komodo_client::{
|
||||
action::Action,
|
||||
build::Build,
|
||||
deployment::Deployment,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
stack::Stack,
|
||||
@@ -456,7 +457,7 @@ async fn execute_execution(
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveDeployment"),
|
||||
.context("Failed at DestroyDeployment"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
@@ -1161,6 +1162,91 @@ async fn execute_execution(
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmNodes(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmNodes(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmNodes(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmNodes"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmStacks(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmStacks(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmStacks(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmStacks"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmServices(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmServices(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmServices(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmServices"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmConfigs(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmConfigs(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmConfigs(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmConfigs"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::RemoveSwarmSecrets(req) => {
|
||||
let req = ExecuteRequest::RemoveSwarmSecrets(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RemoveSwarmSecrets(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
req
|
||||
.resolve(&ExecuteArgs { user, update, id })
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed at RemoveSwarmSecrets"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::ClearRepoCache(req) => {
|
||||
let req = ExecuteRequest::ClearRepoCache(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
@@ -1311,6 +1397,7 @@ async fn extend_batch_exection<E: ExtendBatch>(
|
||||
pattern,
|
||||
Default::default(),
|
||||
procedure_user(),
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -11,7 +11,7 @@ use database::mungos::{
|
||||
use komodo_client::{
|
||||
busy::Busy,
|
||||
entities::{
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant, SwarmOrServer,
|
||||
action::{Action, ActionState},
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
@@ -25,6 +25,7 @@ use komodo_client::{
|
||||
repo::Repo,
|
||||
server::{Server, ServerState},
|
||||
stack::{Stack, StackServiceNames, StackState},
|
||||
swarm::Swarm,
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
update::Update,
|
||||
@@ -36,6 +37,7 @@ use komodo_client::{
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::swarm::swarm_request,
|
||||
permission::get_user_permission_on_resource,
|
||||
resource::{self, KomodoResource},
|
||||
stack::compose_container_match_regex,
|
||||
@@ -55,8 +57,19 @@ pub async fn get_user(user: &str) -> anyhow::Result<User> {
|
||||
.users
|
||||
.find_one(id_or_username_filter(user))
|
||||
.await
|
||||
.context("failed to query mongo for user")?
|
||||
.with_context(|| format!("no user found with {user}"))
|
||||
.context("Failed to query mongo for user")?
|
||||
.with_context(|| format!("No user found matching '{user}'"))
|
||||
}
|
||||
|
||||
pub async fn get_swarm_reachability(
|
||||
swarm: &Swarm,
|
||||
) -> anyhow::Result<()> {
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::GetVersion {},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn get_server_with_state(
|
||||
@@ -181,7 +194,9 @@ pub fn get_stack_state_from_containers(
|
||||
pub async fn get_stack_state(
|
||||
stack: &Stack,
|
||||
) -> anyhow::Result<StackState> {
|
||||
if stack.config.server_id.is_empty() {
|
||||
if stack.config.swarm_id.is_empty()
|
||||
&& stack.config.server_id.is_empty()
|
||||
{
|
||||
return Ok(StackState::Down);
|
||||
}
|
||||
let state = stack_status_cache()
|
||||
@@ -287,24 +302,24 @@ pub async fn get_user_permission_on_target(
|
||||
) -> anyhow::Result<PermissionLevelAndSpecifics> {
|
||||
match target {
|
||||
ResourceTarget::System(_) => Ok(PermissionLevel::None.into()),
|
||||
ResourceTarget::Build(id) => {
|
||||
get_user_permission_on_resource::<Build>(user, id).await
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
get_user_permission_on_resource::<Builder>(user, id).await
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
get_user_permission_on_resource::<Deployment>(user, id).await
|
||||
ResourceTarget::Swarm(id) => {
|
||||
get_user_permission_on_resource::<Swarm>(user, id).await
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
get_user_permission_on_resource::<Server>(user, id).await
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
get_user_permission_on_resource::<Stack>(user, id).await
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
get_user_permission_on_resource::<Deployment>(user, id).await
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
get_user_permission_on_resource::<Build>(user, id).await
|
||||
}
|
||||
ResourceTarget::Repo(id) => {
|
||||
get_user_permission_on_resource::<Repo>(user, id).await
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
get_user_permission_on_resource::<Alerter>(user, id).await
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
get_user_permission_on_resource::<Procedure>(user, id).await
|
||||
}
|
||||
@@ -314,8 +329,11 @@ pub async fn get_user_permission_on_target(
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
get_user_permission_on_resource::<ResourceSync>(user, id).await
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
get_user_permission_on_resource::<Stack>(user, id).await
|
||||
ResourceTarget::Builder(id) => {
|
||||
get_user_permission_on_resource::<Builder>(user, id).await
|
||||
}
|
||||
ResourceTarget::Alerter(id) => {
|
||||
get_user_permission_on_resource::<Alerter>(user, id).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,3 +465,35 @@ pub async fn get_procedure_state(id: &String) -> ProcedureState {
|
||||
}
|
||||
procedure_state_cache().get(id).await.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get's a resource's assigned swarm or server, with swarm taking precedence.
|
||||
/// Makes sure the target is reachable before passing along for commands.
|
||||
pub async fn get_swarm_or_server(
|
||||
swarm_id: &str,
|
||||
server_id: &str,
|
||||
) -> anyhow::Result<SwarmOrServer> {
|
||||
if !swarm_id.is_empty() {
|
||||
let swarm = resource::get::<Swarm>(swarm_id).await?;
|
||||
|
||||
// Errors if not reachable, and returns the error
|
||||
get_swarm_reachability(&swarm).await?;
|
||||
|
||||
return Ok(SwarmOrServer::Swarm(swarm));
|
||||
}
|
||||
|
||||
if server_id.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Neither Swarm nor Server has been configured in this resource."
|
||||
));
|
||||
}
|
||||
|
||||
let (server, state) = get_server_with_state(server_id).await?;
|
||||
|
||||
if state != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"Cannot send command when Server is unreachable or disabled"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(SwarmOrServer::Server(server))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use komodo_client::entities::server::{Server, ServerState};
|
||||
use resolver_api::HasResponse;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, resource, state::server_status_cache,
|
||||
};
|
||||
|
||||
pub async fn swarm_request<T>(
|
||||
server_ids: &[String],
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response>
|
||||
where
|
||||
T: std::fmt::Debug + Clone + Serialize + HasResponse,
|
||||
T::Response: DeserializeOwned,
|
||||
{
|
||||
swarm_request_custom_timeout(
|
||||
server_ids,
|
||||
request,
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn swarm_request_custom_timeout<T>(
|
||||
server_ids: &[String],
|
||||
request: T,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<T::Response>
|
||||
where
|
||||
T: std::fmt::Debug + Clone + Serialize + HasResponse,
|
||||
T::Response: DeserializeOwned,
|
||||
{
|
||||
let status_cache = server_status_cache();
|
||||
let mut err = Option::<anyhow::Error>::None;
|
||||
for server_id in server_ids {
|
||||
let Some(ServerState::Ok) =
|
||||
status_cache.get(server_id).await.map(|status| status.state)
|
||||
else {
|
||||
err = Some(anyhow!("No managers connected"));
|
||||
continue;
|
||||
};
|
||||
let Ok(periphery) =
|
||||
(match resource::get::<Server>(server_id).await {
|
||||
Ok(server) => periphery_client(&server).await,
|
||||
Err(e) => {
|
||||
err = Some(e);
|
||||
continue;
|
||||
}
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
match periphery
|
||||
.request_custom_timeout(request.clone(), timeout)
|
||||
.await
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(e) => err = Some(e),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err
|
||||
.unwrap_or_else(|| anyhow!("Unknown error"))
|
||||
.context("Failed to request from Swarm"),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use komodo_client::{
|
||||
api::{terminal::InitTerminal, write::CreateTerminal},
|
||||
entities::{
|
||||
deployment::Deployment,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
terminal::{ContainerTerminalMode, TerminalTarget},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use periphery_client::api;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, periphery::PeripheryClient,
|
||||
permission::get_check_permissions, resource,
|
||||
state::stack_status_cache,
|
||||
};
|
||||
|
||||
pub async fn setup_target_for_user(
|
||||
target: TerminalTarget,
|
||||
terminal: Option<String>,
|
||||
init: Option<InitTerminal>,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(TerminalTarget, String, PeripheryClient)> {
|
||||
match target {
|
||||
TerminalTarget::Server { server } => {
|
||||
setup_server_target_for_user(
|
||||
server.context("Missing 'target.params.server'")?,
|
||||
terminal,
|
||||
init,
|
||||
user,
|
||||
)
|
||||
.await
|
||||
}
|
||||
TerminalTarget::Container { server, container } => {
|
||||
setup_container_target_for_user(
|
||||
server, container, terminal, init, user,
|
||||
)
|
||||
.await
|
||||
}
|
||||
TerminalTarget::Stack { stack, service } => {
|
||||
setup_stack_service_target_for_user(
|
||||
stack,
|
||||
service.context("Missing 'target.params.service'")?,
|
||||
terminal,
|
||||
init,
|
||||
user,
|
||||
)
|
||||
.await
|
||||
}
|
||||
TerminalTarget::Deployment { deployment } => {
|
||||
setup_deployment_target_for_user(
|
||||
deployment, terminal, init, user,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_server_target_for_user(
|
||||
server: String,
|
||||
terminal: Option<String>,
|
||||
init: Option<InitTerminal>,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(TerminalTarget, String, PeripheryClient)> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let terminal = terminal.unwrap_or_else(|| {
|
||||
init
|
||||
.as_ref()
|
||||
.and_then(|init| init.command.clone())
|
||||
.unwrap_or_else(|| String::from("term"))
|
||||
});
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
if let Some(init) = init {
|
||||
periphery
|
||||
.request(api::terminal::CreateServerTerminal {
|
||||
name: terminal.clone(),
|
||||
command: init.command,
|
||||
recreate: init.recreate,
|
||||
})
|
||||
.await
|
||||
.context("Failed to create Server Terminal on Periphery")?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
TerminalTarget::Server {
|
||||
server: Some(server.id),
|
||||
},
|
||||
terminal,
|
||||
periphery,
|
||||
))
|
||||
}
|
||||
|
||||
async fn setup_container_target_for_user(
|
||||
server: String,
|
||||
container: String,
|
||||
terminal: Option<String>,
|
||||
init: Option<InitTerminal>,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(TerminalTarget, String, PeripheryClient)> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&server,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let terminal = default_container_terminal_name(
|
||||
terminal,
|
||||
&container,
|
||||
init.as_ref(),
|
||||
);
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
let target = TerminalTarget::Container {
|
||||
server: server.id,
|
||||
container: container.clone(),
|
||||
};
|
||||
|
||||
if let Some(init) = init {
|
||||
create_container_terminal_inner(
|
||||
CreateTerminal {
|
||||
name: terminal.clone(),
|
||||
target: target.clone(),
|
||||
command: init.command,
|
||||
mode: init.mode,
|
||||
recreate: init.recreate,
|
||||
},
|
||||
&periphery,
|
||||
container,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((target, terminal, periphery))
|
||||
}
|
||||
|
||||
async fn setup_stack_service_target_for_user(
|
||||
stack: String,
|
||||
service: String,
|
||||
terminal: Option<String>,
|
||||
init: Option<InitTerminal>,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(TerminalTarget, String, PeripheryClient)> {
|
||||
let (target, periphery, container) =
|
||||
get_stack_service_periphery_container(&stack, &service, user)
|
||||
.await?;
|
||||
|
||||
let terminal = default_container_terminal_name(
|
||||
terminal,
|
||||
&container,
|
||||
init.as_ref(),
|
||||
);
|
||||
|
||||
if let Some(init) = init {
|
||||
create_container_terminal_inner(
|
||||
CreateTerminal {
|
||||
name: terminal.clone(),
|
||||
target: target.clone(),
|
||||
command: init.command,
|
||||
mode: init.mode,
|
||||
recreate: init.recreate,
|
||||
},
|
||||
&periphery,
|
||||
container,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((target, terminal, periphery))
|
||||
}
|
||||
|
||||
async fn setup_deployment_target_for_user(
|
||||
deployment: String,
|
||||
terminal: Option<String>,
|
||||
init: Option<InitTerminal>,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(TerminalTarget, String, PeripheryClient)> {
|
||||
let (target, periphery, container) =
|
||||
get_deployment_periphery_container(&deployment, user).await?;
|
||||
|
||||
let terminal = default_container_terminal_name(
|
||||
terminal,
|
||||
&container,
|
||||
init.as_ref(),
|
||||
);
|
||||
|
||||
if let Some(init) = init {
|
||||
create_container_terminal_inner(
|
||||
CreateTerminal {
|
||||
name: terminal.clone(),
|
||||
target: target.clone(),
|
||||
command: init.command,
|
||||
mode: init.mode,
|
||||
recreate: init.recreate,
|
||||
},
|
||||
&periphery,
|
||||
container,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((target, terminal, periphery))
|
||||
}
|
||||
|
||||
fn default_container_terminal_name(
|
||||
terminal: Option<String>,
|
||||
container: &str,
|
||||
init: Option<&InitTerminal>,
|
||||
) -> String {
|
||||
terminal.unwrap_or_else(|| {
|
||||
init
|
||||
.as_ref()
|
||||
.map(|init| {
|
||||
init.command.clone().unwrap_or_else(|| {
|
||||
init.mode.unwrap_or_default().as_ref().to_string()
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| container.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_container_terminal_inner(
|
||||
CreateTerminal {
|
||||
name,
|
||||
target,
|
||||
command,
|
||||
mode,
|
||||
recreate,
|
||||
}: CreateTerminal,
|
||||
periphery: &PeripheryClient,
|
||||
container: String,
|
||||
) -> anyhow::Result<()> {
|
||||
match mode.unwrap_or_default() {
|
||||
ContainerTerminalMode::Exec => periphery
|
||||
.request(periphery_client::api::terminal::CreateContainerExecTerminal {
|
||||
name,
|
||||
target,
|
||||
container,
|
||||
command,
|
||||
recreate,
|
||||
})
|
||||
.await
|
||||
.context(
|
||||
"Failed to create Container Exec Terminal on Periphery",
|
||||
)?,
|
||||
ContainerTerminalMode::Attach => periphery
|
||||
.request(periphery_client::api::terminal::CreateContainerAttachTerminal {
|
||||
name,
|
||||
target,
|
||||
container,
|
||||
recreate,
|
||||
})
|
||||
.await
|
||||
.context(
|
||||
"Failed to create Container Attach Terminal on Periphery",
|
||||
)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_stack_service_periphery_container(
|
||||
stack: &str,
|
||||
service: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(TerminalTarget, PeripheryClient, String)> {
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
stack,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&stack.config.server_id).await?;
|
||||
|
||||
let Some(status) = stack_status_cache().get(&stack.id).await else {
|
||||
return Err(anyhow!("Could not get Stack status"));
|
||||
};
|
||||
|
||||
let container = status
|
||||
.curr
|
||||
.services
|
||||
.iter()
|
||||
.find(|s| s.service.as_str() == service)
|
||||
.with_context(|| {
|
||||
format!("Did not find Stack service matching {service}")
|
||||
})?
|
||||
.container
|
||||
.as_ref()
|
||||
.with_context(|| {
|
||||
format!("Did not find container for Stack service {service}")
|
||||
})?
|
||||
.name
|
||||
.clone();
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
Ok((
|
||||
TerminalTarget::Stack {
|
||||
stack: stack.id,
|
||||
service: Some(service.to_string()),
|
||||
},
|
||||
periphery,
|
||||
container,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_deployment_periphery_container(
|
||||
deployment: &str,
|
||||
user: &User,
|
||||
) -> anyhow::Result<(TerminalTarget, PeripheryClient, String)> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
user,
|
||||
PermissionLevel::Read.terminal(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server =
|
||||
resource::get::<Server>(&deployment.config.server_id).await?;
|
||||
|
||||
let periphery = periphery_client(&server).await?;
|
||||
|
||||
let container = deployment.name.clone();
|
||||
|
||||
Ok((
|
||||
TerminalTarget::Deployment {
|
||||
deployment: deployment.id,
|
||||
},
|
||||
periphery,
|
||||
container,
|
||||
))
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use komodo_client::entities::{
|
||||
repo::Repo,
|
||||
server::Server,
|
||||
stack::Stack,
|
||||
swarm::Swarm,
|
||||
sync::ResourceSync,
|
||||
update::{Update, UpdateListItem},
|
||||
user::User,
|
||||
@@ -121,6 +122,38 @@ pub async fn init_execution_update(
|
||||
user: &User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let (operation, target) = match &request {
|
||||
// Swarm
|
||||
ExecuteRequest::RemoveSwarmNodes(data) => (
|
||||
Operation::RemoveSwarmNodes,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmStacks(data) => (
|
||||
Operation::RemoveSwarmStacks,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmServices(data) => (
|
||||
Operation::RemoveSwarmServices,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmConfigs(data) => (
|
||||
Operation::RemoveSwarmConfigs,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::RemoveSwarmSecrets(data) => (
|
||||
Operation::RemoveSwarmSecrets,
|
||||
ResourceTarget::Swarm(
|
||||
resource::get::<Swarm>(&data.swarm).await?.id,
|
||||
),
|
||||
),
|
||||
|
||||
// Server
|
||||
ExecuteRequest::StartContainer(data) => (
|
||||
Operation::StartContainer,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! # Input Validation Module
|
||||
//!
|
||||
//! This module provides validation functions for user inputs to prevent
|
||||
//! invalid data from entering the system and improve security.
|
||||
|
||||
use anyhow::Context;
|
||||
use validations::{StringValidator, StringValidatorMatches};
|
||||
|
||||
use crate::config::core_config;
|
||||
|
||||
/// Minimum length for usernames
|
||||
pub const MIN_USERNAME_LENGTH: usize = 1;
|
||||
/// Maximum length for usernames
|
||||
pub const MAX_USERNAME_LENGTH: usize = 100;
|
||||
|
||||
/// Validate usernames
|
||||
///
|
||||
/// - Between [MIN_USERNAME_LENGTH] and [MAX_USERNAME_LENGTH] characters
|
||||
/// - Matches `^[a-zA-Z0-9._@-]+$`
|
||||
pub fn validate_username(username: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.min_length(MIN_USERNAME_LENGTH)
|
||||
.max_length(MAX_USERNAME_LENGTH)
|
||||
.matches(StringValidatorMatches::Username)
|
||||
.validate(username)
|
||||
.context("Failed to validate username")
|
||||
}
|
||||
|
||||
/// Maximum length for passwords
|
||||
pub const MAX_PASSWORD_LENGTH: usize = 1000;
|
||||
|
||||
/// Validate passwords
|
||||
///
|
||||
/// - Between [CoreConfig::min_password_length][komodo_client::entities::config::core::CoreConfig::min_password_length] and [MAX_PASSWORD_LENGTH] characters
|
||||
pub fn validate_password(password: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.min_length(core_config().min_password_length as usize)
|
||||
.max_length(MAX_PASSWORD_LENGTH)
|
||||
.validate(password)
|
||||
.context("Failed to validate password")
|
||||
}
|
||||
|
||||
/// Maximum length for API key names
|
||||
pub const MAX_API_KEY_NAME_LENGTH: usize = 200;
|
||||
|
||||
/// Validate api key names
|
||||
///
|
||||
/// - Greater than [MAX_API_KEY_NAME_LENGTH] characters
|
||||
pub fn validate_api_key_name(name: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.max_length(MAX_API_KEY_NAME_LENGTH)
|
||||
.validate(name)
|
||||
.context("Failed to validate api key name")
|
||||
}
|
||||
|
||||
/// Minimum length for variable names
|
||||
pub const MIN_VARIABLE_NAME_LENGTH: usize = 1;
|
||||
/// Maximum length for variable names
|
||||
pub const MAX_VARIABLE_NAME_LENGTH: usize = 500;
|
||||
|
||||
/// Validate variable names
|
||||
///
|
||||
/// - Between [MIN_VARIABLE_NAME_LENGTH] and [MAX_VARIABLE_NAME_LENGTH] characters
|
||||
/// - Matches `^[a-zA-Z_][a-zA-Z0-9_]*$`
|
||||
pub fn validate_variable_name(name: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.min_length(MIN_VARIABLE_NAME_LENGTH)
|
||||
.max_length(MAX_VARIABLE_NAME_LENGTH)
|
||||
.matches(StringValidatorMatches::VariableName)
|
||||
.validate(name)
|
||||
.context("Failed to validate variable name")
|
||||
}
|
||||
|
||||
/// Maximum length for variable values
|
||||
pub const MAX_VARIABLE_VALUE_LENGTH: usize = 10000;
|
||||
|
||||
/// Validate variable values
|
||||
///
|
||||
/// - Less than [MAX_VARIABLE_VALUE_LENGTH] characters
|
||||
pub fn validate_variable_value(value: &str) -> anyhow::Result<()> {
|
||||
StringValidator::default()
|
||||
.max_length(MAX_VARIABLE_VALUE_LENGTH)
|
||||
.validate(value)
|
||||
.context("Failed to validate variable value")
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
+4
-34
@@ -6,12 +6,7 @@ extern crate tracing;
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{Router, routing::get};
|
||||
use axum_server::{Handle, tls_rustls::RustlsConfig};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
services::{ServeDir, ServeFile},
|
||||
};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::config::{core_config, core_keys};
|
||||
@@ -23,7 +18,6 @@ mod cloud;
|
||||
mod config;
|
||||
mod connection;
|
||||
mod helpers;
|
||||
mod listener;
|
||||
mod monitor;
|
||||
mod network;
|
||||
mod periphery;
|
||||
@@ -35,7 +29,6 @@ mod startup;
|
||||
mod state;
|
||||
mod sync;
|
||||
mod ts_client;
|
||||
mod ws;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
@@ -43,6 +36,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
logger::init(&config.logging)?;
|
||||
|
||||
let startup_span = info_span!("CoreStartup");
|
||||
|
||||
async {
|
||||
info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
@@ -75,7 +69,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
startup::on_startup().await;
|
||||
|
||||
// Spawn background tasks
|
||||
monitor::spawn_monitor_loop();
|
||||
monitor::spawn_monitoring_loops();
|
||||
resource::spawn_resource_refresh_loop();
|
||||
resource::spawn_all_resources_cache_refresh_loop();
|
||||
resource::spawn_build_state_refresh_loop();
|
||||
@@ -88,32 +82,8 @@ async fn app() -> anyhow::Result<()> {
|
||||
.instrument(startup_span)
|
||||
.await;
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = &config.frontend_path;
|
||||
let frontend_index =
|
||||
ServeFile::new(format!("{frontend_path}/index.html"));
|
||||
let serve_frontend = ServeDir::new(frontend_path)
|
||||
.not_found_service(frontend_index.clone());
|
||||
|
||||
let app = Router::new()
|
||||
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
|
||||
.nest("/auth", api::auth::router())
|
||||
.nest("/user", api::user::router())
|
||||
.nest("/read", api::read::router())
|
||||
.nest("/write", api::write::router())
|
||||
.nest("/execute", api::execute::router())
|
||||
.nest("/terminal", api::terminal::router())
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
.fallback_service(serve_frontend)
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
)
|
||||
.into_make_service();
|
||||
let app =
|
||||
api::app().into_make_service_with_connect_info::<SocketAddr>();
|
||||
|
||||
let addr =
|
||||
format!("{}:{}", core_config().bind_ip, core_config().port);
|
||||
|
||||
@@ -2,7 +2,8 @@ use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{
|
||||
resource::ResourceQuery, server::Server, user::User,
|
||||
permission::PermissionLevel, resource::ResourceQuery,
|
||||
server::Server, user::system_user,
|
||||
};
|
||||
|
||||
use crate::resource;
|
||||
@@ -33,10 +34,8 @@ async fn get_all_servers_map()
|
||||
{
|
||||
let servers = resource::list_full_for_user::<Server>(
|
||||
ResourceQuery::default(),
|
||||
&User {
|
||||
admin: true,
|
||||
..Default::default()
|
||||
},
|
||||
system_user(),
|
||||
PermissionLevel::Read.into(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,69 +1,50 @@
|
||||
use komodo_client::entities::{
|
||||
alert::SeverityLevel,
|
||||
deployment::{Deployment, DeploymentState},
|
||||
docker::{
|
||||
container::ContainerListItem, image::ImageListItem,
|
||||
network::NetworkListItem, volume::VolumeListItem,
|
||||
},
|
||||
docker::DockerLists,
|
||||
repo::Repo,
|
||||
server::{
|
||||
PeripheryInformation, Server, ServerConfig, ServerHealth,
|
||||
ServerHealthState, ServerState,
|
||||
},
|
||||
stack::{ComposeProject, Stack, StackState},
|
||||
stack::{Stack, StackState},
|
||||
stats::{SingleDiskUsage, SystemInformation, SystemStats},
|
||||
};
|
||||
use serror::Serror;
|
||||
|
||||
use crate::state::{
|
||||
deployment_status_cache, repo_status_cache, server_status_cache,
|
||||
stack_status_cache,
|
||||
};
|
||||
|
||||
use super::{
|
||||
CachedDeploymentStatus, CachedRepoStatus, CachedServerStatus,
|
||||
CachedStackStatus, History,
|
||||
CachedStackStatus, History, deployment_status_cache,
|
||||
repo_status_cache, server_status_cache, stack_status_cache,
|
||||
};
|
||||
|
||||
pub async fn insert_deployments_status_unknown(
|
||||
deployments: Vec<Deployment>,
|
||||
pub async fn insert_server_status(
|
||||
server: &Server,
|
||||
state: ServerState,
|
||||
periphery_info: Option<PeripheryInformation>,
|
||||
system_info: Option<SystemInformation>,
|
||||
system_stats: Option<SystemStats>,
|
||||
docker: Option<DockerLists>,
|
||||
err: impl Into<Option<Serror>>,
|
||||
) {
|
||||
let status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let prev =
|
||||
status_cache.get(&deployment.id).await.map(|s| s.curr.state);
|
||||
status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state: DeploymentState::Unknown,
|
||||
container: None,
|
||||
update_available: false,
|
||||
},
|
||||
prev,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert_repos_status_unknown(repos: Vec<Repo>) {
|
||||
let status_cache = repo_status_cache();
|
||||
for repo in repos {
|
||||
status_cache
|
||||
.insert(
|
||||
repo.id.clone(),
|
||||
CachedRepoStatus {
|
||||
latest_hash: None,
|
||||
latest_message: None,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let health =
|
||||
system_stats.as_ref().map(|s| get_server_health(server, s));
|
||||
server_status_cache()
|
||||
.insert(
|
||||
server.id.clone(),
|
||||
CachedServerStatus {
|
||||
id: server.id.clone(),
|
||||
state,
|
||||
periphery_info,
|
||||
system_info,
|
||||
system_stats,
|
||||
health,
|
||||
docker,
|
||||
err: err.into(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn insert_stacks_status_unknown(stacks: Vec<Stack>) {
|
||||
@@ -88,45 +69,46 @@ pub async fn insert_stacks_status_unknown(stacks: Vec<Stack>) {
|
||||
}
|
||||
}
|
||||
|
||||
type DockerLists = (
|
||||
Option<Vec<ContainerListItem>>,
|
||||
Option<Vec<NetworkListItem>>,
|
||||
Option<Vec<ImageListItem>>,
|
||||
Option<Vec<VolumeListItem>>,
|
||||
Option<Vec<ComposeProject>>,
|
||||
);
|
||||
|
||||
pub async fn insert_server_status(
|
||||
server: &Server,
|
||||
state: ServerState,
|
||||
periphery_info: Option<PeripheryInformation>,
|
||||
system_info: Option<SystemInformation>,
|
||||
system_stats: Option<SystemStats>,
|
||||
(containers, networks, images, volumes, projects): DockerLists,
|
||||
err: impl Into<Option<Serror>>,
|
||||
pub async fn insert_deployments_status_unknown(
|
||||
deployments: Vec<Deployment>,
|
||||
) {
|
||||
let health =
|
||||
system_stats.as_ref().map(|s| get_server_health(server, s));
|
||||
server_status_cache()
|
||||
.insert(
|
||||
server.id.clone(),
|
||||
CachedServerStatus {
|
||||
id: server.id.clone(),
|
||||
state,
|
||||
periphery_info,
|
||||
system_info,
|
||||
system_stats,
|
||||
health,
|
||||
containers,
|
||||
networks,
|
||||
images,
|
||||
volumes,
|
||||
projects,
|
||||
err: err.into(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let prev =
|
||||
status_cache.get(&deployment.id).await.map(|s| s.curr.state);
|
||||
status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state: DeploymentState::Unknown,
|
||||
container: None,
|
||||
service: None,
|
||||
update_available: false,
|
||||
},
|
||||
prev,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert_repos_status_unknown(repos: Vec<Repo>) {
|
||||
let status_cache = repo_status_cache();
|
||||
for repo in repos {
|
||||
status_cache
|
||||
.insert(
|
||||
repo.id.clone(),
|
||||
CachedRepoStatus {
|
||||
latest_hash: None,
|
||||
latest_message: None,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
const ALERT_PERCENTAGE_THRESHOLD: f32 = 5.0;
|
||||
|
||||
+85
-100
@@ -7,30 +7,27 @@ use futures_util::future::join_all;
|
||||
use helpers::insert_stacks_status_unknown;
|
||||
use komodo_client::entities::{
|
||||
build::Build,
|
||||
deployment::{Deployment, DeploymentState},
|
||||
docker::{
|
||||
container::ContainerListItem, image::ImageListItem,
|
||||
network::NetworkListItem, volume::VolumeListItem,
|
||||
},
|
||||
deployment::Deployment,
|
||||
komodo_timestamp, optional_string,
|
||||
repo::Repo,
|
||||
server::{PeripheryInformation, Server, ServerHealth, ServerState},
|
||||
stack::{ComposeProject, Stack, StackService, StackState},
|
||||
stats::{SystemInformation, SystemStats},
|
||||
server::{Server, ServerState},
|
||||
stack::Stack,
|
||||
stats::SystemStats,
|
||||
swarm::Swarm,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
self, PollStatusResponse, git::GetLatestCommit,
|
||||
self, git::GetLatestCommit, poll::PollStatusResponse,
|
||||
};
|
||||
use serror::Serror;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
config::monitoring_interval,
|
||||
helpers::periphery_client,
|
||||
monitor::{alert::check_alerts, record::record_server_stats},
|
||||
state::{
|
||||
db_client, deployment_status_cache, periphery_connections,
|
||||
repo_status_cache,
|
||||
CachedRepoStatus, db_client, deployment_status_cache,
|
||||
periphery_connections, repo_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,64 +40,21 @@ mod alert;
|
||||
mod helpers;
|
||||
mod record;
|
||||
mod resources;
|
||||
mod swarm;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct History<Curr: Default, Prev> {
|
||||
pub curr: Curr,
|
||||
pub prev: Option<Prev>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedServerStatus {
|
||||
pub id: String,
|
||||
pub state: ServerState,
|
||||
pub health: Option<ServerHealth>,
|
||||
pub periphery_info: Option<PeripheryInformation>,
|
||||
pub system_info: Option<SystemInformation>,
|
||||
pub system_stats: Option<SystemStats>,
|
||||
pub containers: Option<Vec<ContainerListItem>>,
|
||||
pub networks: Option<Vec<NetworkListItem>>,
|
||||
pub images: Option<Vec<ImageListItem>>,
|
||||
pub volumes: Option<Vec<VolumeListItem>>,
|
||||
pub projects: Option<Vec<ComposeProject>>,
|
||||
/// Store the error in reaching periphery
|
||||
pub err: Option<serror::Serror>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedDeploymentStatus {
|
||||
/// The deployment id
|
||||
pub id: String,
|
||||
pub state: DeploymentState,
|
||||
pub container: Option<ContainerListItem>,
|
||||
pub update_available: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedRepoStatus {
|
||||
pub latest_hash: Option<String>,
|
||||
pub latest_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct CachedStackStatus {
|
||||
/// The stack id
|
||||
pub id: String,
|
||||
/// The stack state
|
||||
pub state: StackState,
|
||||
/// The services connected to the stack
|
||||
pub services: Vec<StackService>,
|
||||
}
|
||||
pub use swarm::update_cache_for_swarm;
|
||||
|
||||
const ADDITIONAL_MS: u128 = 500;
|
||||
|
||||
pub fn spawn_monitor_loop() {
|
||||
let interval: async_timing_util::Timelength = core_config()
|
||||
.monitoring_interval
|
||||
.try_into()
|
||||
.expect("Invalid monitoring interval");
|
||||
pub fn spawn_monitoring_loops() {
|
||||
spawn_server_monitoring_loop();
|
||||
swarm::spawn_swarm_monitoring_loop();
|
||||
}
|
||||
|
||||
fn spawn_server_monitoring_loop() {
|
||||
tokio::spawn(async move {
|
||||
refresh_server_cache(komodo_timestamp()).await;
|
||||
let interval = monitoring_interval();
|
||||
loop {
|
||||
let ts = (wait_until_timelength(interval, ADDITIONAL_MS).await
|
||||
- ADDITIONAL_MS) as i64;
|
||||
@@ -115,7 +69,7 @@ async fn refresh_server_cache(ts: i64) {
|
||||
Ok(servers) => servers,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"failed to get server list (manage status cache) | {e:#}"
|
||||
"Failed to get server list (refresh server cache) | {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +115,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
|
||||
|
||||
*lock = now;
|
||||
|
||||
let resources = UpdateCacheResources::load(server).await;
|
||||
let resources = UpdateCacheResources::load_server(server).await;
|
||||
|
||||
// Handle server disabled
|
||||
if !server.config.enabled {
|
||||
@@ -172,7 +126,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
(None, None, None, None, None),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
@@ -190,7 +144,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
(None, None, None, None, None),
|
||||
None,
|
||||
Serror::from(&e),
|
||||
)
|
||||
.await;
|
||||
@@ -202,14 +156,11 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
|
||||
periphery_info,
|
||||
system_info,
|
||||
system_stats,
|
||||
mut containers,
|
||||
networks,
|
||||
images,
|
||||
volumes,
|
||||
projects,
|
||||
mut docker,
|
||||
} = match periphery
|
||||
.request(api::PollStatus {
|
||||
.request(api::poll::PollStatus {
|
||||
include_stats: server.config.stats_monitoring,
|
||||
include_docker: true,
|
||||
})
|
||||
.await
|
||||
{
|
||||
@@ -222,7 +173,7 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
(None, None, None, None, None),
|
||||
None,
|
||||
Serror::from(&e),
|
||||
)
|
||||
.await;
|
||||
@@ -230,37 +181,44 @@ pub async fn update_cache_for_server(server: &Server, force: bool) {
|
||||
}
|
||||
};
|
||||
|
||||
containers.iter_mut().for_each(|container| {
|
||||
container.server_id = Some(server.id.clone())
|
||||
});
|
||||
if let Some(docker) = &mut docker {
|
||||
docker.containers.iter_mut().for_each(|container| {
|
||||
container.server_id = Some(server.id.clone())
|
||||
});
|
||||
}
|
||||
|
||||
let containers = docker
|
||||
.as_ref()
|
||||
.map(|docker| docker.containers.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let images = docker
|
||||
.as_ref()
|
||||
.map(|docker| docker.images.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
|
||||
tokio::join!(
|
||||
resources::update_deployment_cache(
|
||||
server.name.clone(),
|
||||
resources.deployments,
|
||||
&containers,
|
||||
&images,
|
||||
&resources.builds,
|
||||
),
|
||||
resources::update_stack_cache(
|
||||
resources::update_server_stack_cache(
|
||||
server.name.clone(),
|
||||
resources.stacks,
|
||||
&containers,
|
||||
&images
|
||||
containers,
|
||||
images
|
||||
),
|
||||
resources::update_server_deployment_cache(
|
||||
server.name.clone(),
|
||||
resources.deployments,
|
||||
containers,
|
||||
images,
|
||||
&resources.builds,
|
||||
),
|
||||
);
|
||||
|
||||
insert_server_status(
|
||||
server,
|
||||
ServerState::Ok,
|
||||
Some(periphery_info),
|
||||
Some(system_info),
|
||||
system_stats.map(|stats| filter_volumes(server, stats)),
|
||||
(
|
||||
Some(containers.clone()),
|
||||
Some(networks),
|
||||
Some(images),
|
||||
Some(volumes),
|
||||
Some(projects),
|
||||
),
|
||||
docker,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
@@ -298,7 +256,34 @@ struct UpdateCacheResources {
|
||||
}
|
||||
|
||||
impl UpdateCacheResources {
|
||||
pub async fn load(server: &Server) -> Self {
|
||||
pub async fn load_swarm(swarm: &Swarm) -> Self {
|
||||
let (stacks, deployments, builds) = tokio::join!(
|
||||
find_collect(
|
||||
&db_client().stacks,
|
||||
doc! { "config.swarm_id": &swarm.id },
|
||||
None,
|
||||
),
|
||||
find_collect(
|
||||
&db_client().deployments,
|
||||
doc! { "config.swarm_id": &swarm.id },
|
||||
None,
|
||||
),
|
||||
find_collect(&db_client().builds, doc! {}, None,),
|
||||
);
|
||||
|
||||
let stacks = stacks.inspect_err(|e| error!("Failed to get stacks list from db (update swarm status cache) | swarm: {} | {e:#}", swarm.name)).unwrap_or_default();
|
||||
let deployments = deployments.inspect_err(|e| error!("Failed to get deployments list from db (update swarm status cache) | swarm : {} | {e:#}", swarm.name)).unwrap_or_default();
|
||||
let builds = builds.inspect_err(|e| error!("Failed to get builds list from db (update swarm status cache) | swarm : {} | {e:#}", swarm.name)).unwrap_or_default();
|
||||
|
||||
Self {
|
||||
stacks,
|
||||
deployments,
|
||||
builds,
|
||||
repos: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_server(server: &Server) -> Self {
|
||||
let (stacks, deployments, builds, repos) = tokio::join!(
|
||||
find_collect(
|
||||
&db_client().stacks,
|
||||
@@ -318,10 +303,10 @@ impl UpdateCacheResources {
|
||||
),
|
||||
);
|
||||
|
||||
let stacks = stacks.inspect_err(|e| error!("failed to get stacks list from db (update status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let deployments = deployments.inspect_err(|e| error!("failed to get deployments list from db (update status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let builds = builds.inspect_err(|e| error!("failed to get builds list from db (update status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let repos = repos.inspect_err(|e| error!("failed to get repos list from db (update status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let stacks = stacks.inspect_err(|e| error!("Failed to get stacks list from db (update server status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let deployments = deployments.inspect_err(|e| error!("Failed to get deployments list from db (update server status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let builds = builds.inspect_err(|e| error!("Failed to get builds list from db (update server status cache) | server : {} | {e:#}", server.name)).unwrap_or_default();
|
||||
let repos = repos.inspect_err(|e| error!("Failed to get repos list from db (update server status cache) | server: {} | {e:#}", server.name)).unwrap_or_default();
|
||||
|
||||
Self {
|
||||
stacks,
|
||||
|
||||
+390
-197
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
hash::Hash,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
@@ -14,9 +15,13 @@ use komodo_client::{
|
||||
docker::{
|
||||
container::{ContainerListItem, ContainerStateStatusEnum},
|
||||
image::ImageListItem,
|
||||
service::SwarmServiceListItem,
|
||||
stack::SwarmStackListItem,
|
||||
task::SwarmTaskListItem,
|
||||
},
|
||||
komodo_timestamp,
|
||||
stack::{Stack, StackService, StackServiceNames, StackState},
|
||||
swarm::SwarmState,
|
||||
user::auto_redeploy_user,
|
||||
},
|
||||
};
|
||||
@@ -30,193 +35,97 @@ use crate::{
|
||||
services::extract_services_from_stack,
|
||||
},
|
||||
state::{
|
||||
CachedDeploymentStatus, CachedStackStatus, History,
|
||||
action_states, db_client, deployment_status_cache,
|
||||
stack_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{CachedDeploymentStatus, CachedStackStatus, History};
|
||||
|
||||
fn deployment_alert_sent_cache() -> &'static Mutex<HashSet<String>> {
|
||||
static CACHE: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||
/// (StackId, Service)
|
||||
fn stack_alert_sent_cache() -> &'static AlertCache<(String, String)> {
|
||||
static CACHE: OnceLock<AlertCache<(String, String)>> =
|
||||
OnceLock::new();
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn update_deployment_cache(
|
||||
server_name: String,
|
||||
deployments: Vec<Deployment>,
|
||||
containers: &[ContainerListItem],
|
||||
images: &[ImageListItem],
|
||||
builds: &[Build],
|
||||
pub async fn update_swarm_stack_cache(
|
||||
stacks: Vec<Stack>,
|
||||
swarm_stacks: &[SwarmStackListItem],
|
||||
swarm_services: &[SwarmServiceListItem],
|
||||
) {
|
||||
let deployment_status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let container = containers
|
||||
let stack_status_cache = stack_status_cache();
|
||||
for stack in stacks {
|
||||
let project_name = stack.project_name(false);
|
||||
let current_state = swarm_stacks
|
||||
.iter()
|
||||
.find(|container| container.name == deployment.name)
|
||||
.cloned();
|
||||
let prev = deployment_status_cache
|
||||
.get(&deployment.id)
|
||||
.find(|stack| {
|
||||
stack
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|name| name == &project_name)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.map(|stack| match stack.state {
|
||||
Some(SwarmState::Healthy) => StackState::Running,
|
||||
Some(SwarmState::Unhealthy) => StackState::Unhealthy,
|
||||
Some(SwarmState::Unknown) | None => StackState::Unknown,
|
||||
})
|
||||
.unwrap_or(StackState::Down);
|
||||
let services = extract_services_from_stack(&stack);
|
||||
let service_prefix = format!("{project_name}_");
|
||||
let mut services_with_swarm_services = services
|
||||
.iter()
|
||||
.map(
|
||||
|StackServiceNames {
|
||||
service_name,
|
||||
image,
|
||||
..
|
||||
}| {
|
||||
let swarm_service = swarm_services
|
||||
.iter()
|
||||
.find(|service| {
|
||||
service
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|name| {
|
||||
// The services are named like {stackname_servicename}.
|
||||
// If they don't match this pattern, they aren't part of stack.
|
||||
let Some(name) = name.strip_prefix(&service_prefix)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
name == service_name
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.cloned();
|
||||
StackService {
|
||||
service: service_name.clone(),
|
||||
image: image.clone(),
|
||||
container: None,
|
||||
swarm_service,
|
||||
update_available: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
services_with_swarm_services
|
||||
.sort_by(|a, b| a.service.cmp(&b.service));
|
||||
let prev_state = stack_status_cache
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.map(|s| s.curr.state);
|
||||
let state = container
|
||||
.as_ref()
|
||||
.map(|c| c.state.into())
|
||||
.unwrap_or(DeploymentState::NotDeployed);
|
||||
let image = match deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let (build_name, build_version) = builds
|
||||
.iter()
|
||||
.find(|build| build.id == build_id)
|
||||
.map(|b| (b.name.as_ref(), b.config.version))
|
||||
.unwrap_or(("Unknown", Default::default()));
|
||||
let version = if version.is_none() {
|
||||
build_version.to_string()
|
||||
} else {
|
||||
version.to_string()
|
||||
};
|
||||
format!("{build_name}:{version}")
|
||||
}
|
||||
DeploymentImage::Image { image } => {
|
||||
// If image already has tag, leave it,
|
||||
// otherwise default the tag to latest
|
||||
if image.contains(':') {
|
||||
image.to_string()
|
||||
} else {
|
||||
format!("{image}:latest")
|
||||
}
|
||||
}
|
||||
let status = CachedStackStatus {
|
||||
id: stack.id.clone(),
|
||||
state: current_state,
|
||||
services: services_with_swarm_services,
|
||||
};
|
||||
let update_available = if let Some(ContainerListItem {
|
||||
image_id: Some(curr_image_id),
|
||||
..
|
||||
}) = &container
|
||||
{
|
||||
// Docker will automatically strip `docker.io` from incoming image names re #468.
|
||||
// Need to strip it in order to match by image name and find available updates.
|
||||
let image = image.strip_prefix("docker.io/").unwrap_or(&image);
|
||||
images
|
||||
.iter()
|
||||
.find(|i| i.name == image)
|
||||
.map(|i| &i.id != curr_image_id)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if update_available {
|
||||
if deployment.config.auto_update {
|
||||
if state == DeploymentState::Running
|
||||
&& !action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await
|
||||
.busy()
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let id = deployment.id.clone();
|
||||
let server_name = server_name.clone();
|
||||
tokio::spawn(async move {
|
||||
match execute::inner_handler(
|
||||
ExecuteRequest::Deploy(Deploy {
|
||||
deployment: deployment.name.clone(),
|
||||
stop_time: None,
|
||||
stop_signal: None,
|
||||
}),
|
||||
auto_redeploy_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(id.clone()),
|
||||
data: AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name: deployment.name,
|
||||
server_name,
|
||||
server_id: deployment.config.server_id,
|
||||
image,
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentAutoUpdated to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to auto update Deployment {} | {e:#}",
|
||||
deployment.name
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if state == DeploymentState::Running
|
||||
&& deployment.config.send_alerts
|
||||
&& !deployment_alert_sent_cache()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&deployment.id)
|
||||
{
|
||||
// Add that it is already sent to the cache, so another alert won't be sent.
|
||||
deployment_alert_sent_cache()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(deployment.id.clone());
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
data: AlertData::DeploymentImageUpdateAvailable {
|
||||
id: deployment.id.clone(),
|
||||
name: deployment.name,
|
||||
server_name: server_name.clone(),
|
||||
server_id: deployment.config.server_id,
|
||||
image,
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentImageUpdateAvailable to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
} else {
|
||||
// If it sees there is no longer update available, remove
|
||||
// from the sent cache, so on next `update_available = true`
|
||||
// the cache is empty and a fresh alert will be sent.
|
||||
deployment_alert_sent_cache()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&deployment.id);
|
||||
}
|
||||
deployment_status_cache
|
||||
stack_status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
stack.id,
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state,
|
||||
container,
|
||||
update_available,
|
||||
},
|
||||
prev,
|
||||
curr: status,
|
||||
prev: prev_state,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -224,21 +133,14 @@ pub async fn update_deployment_cache(
|
||||
}
|
||||
}
|
||||
|
||||
/// (StackId, Service)
|
||||
fn stack_alert_sent_cache()
|
||||
-> &'static Mutex<HashSet<(String, String)>> {
|
||||
static CACHE: OnceLock<Mutex<HashSet<(String, String)>>> =
|
||||
OnceLock::new();
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn update_stack_cache(
|
||||
pub async fn update_server_stack_cache(
|
||||
server_name: String,
|
||||
stacks: Vec<Stack>,
|
||||
containers: &[ContainerListItem],
|
||||
images: &[ImageListItem],
|
||||
) {
|
||||
let stack_status_cache = stack_status_cache();
|
||||
let stack_alert_sent_cache = stack_alert_sent_cache();
|
||||
for stack in stacks {
|
||||
let services = extract_services_from_stack(&stack);
|
||||
let mut services_with_containers = services.iter().map(|StackServiceNames { service_name, container_name, image }| {
|
||||
@@ -274,16 +176,12 @@ pub async fn update_stack_cache(
|
||||
if update_available {
|
||||
if !stack.config.auto_update
|
||||
&& stack.config.send_alerts
|
||||
&& container.is_some()
|
||||
&& container.as_ref().unwrap().state == ContainerStateStatusEnum::Running
|
||||
&& !stack_alert_sent_cache()
|
||||
.lock()
|
||||
.unwrap()
|
||||
&& let Some(container) = &container
|
||||
&& container.state == ContainerStateStatusEnum::Running
|
||||
&& !stack_alert_sent_cache
|
||||
.contains(&(stack.id.clone(), service_name.clone()))
|
||||
{
|
||||
stack_alert_sent_cache()
|
||||
.lock()
|
||||
.unwrap()
|
||||
stack_alert_sent_cache
|
||||
.insert((stack.id.clone(), service_name.clone()));
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
@@ -313,15 +211,17 @@ pub async fn update_stack_cache(
|
||||
});
|
||||
}
|
||||
} else {
|
||||
stack_alert_sent_cache()
|
||||
.lock()
|
||||
.unwrap()
|
||||
// If it sees there is no longer update available, remove
|
||||
// from the sent cache, so on next `update_available = true`
|
||||
// the cache is empty and a fresh alert will be sent.
|
||||
stack_alert_sent_cache
|
||||
.remove(&(stack.id.clone(), service_name.clone()));
|
||||
}
|
||||
StackService {
|
||||
service: service_name.clone(),
|
||||
image: image.clone(),
|
||||
container,
|
||||
swarm_service: None,
|
||||
update_available,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
@@ -345,14 +245,14 @@ pub async fn update_stack_cache(
|
||||
}
|
||||
}
|
||||
|
||||
let state = get_stack_state_from_containers(
|
||||
let current_state = get_stack_state_from_containers(
|
||||
&stack.config.ignore_services,
|
||||
&services,
|
||||
containers,
|
||||
);
|
||||
if !services_to_update.is_empty()
|
||||
&& stack.config.auto_update
|
||||
&& state == StackState::Running
|
||||
&& current_state == StackState::Running
|
||||
&& !action_states()
|
||||
.stack
|
||||
.get_or_insert_default(&stack.id)
|
||||
@@ -411,17 +311,310 @@ pub async fn update_stack_cache(
|
||||
}
|
||||
services_with_containers
|
||||
.sort_by(|a, b| a.service.cmp(&b.service));
|
||||
let prev = stack_status_cache
|
||||
let prev_state = stack_status_cache
|
||||
.get(&stack.id)
|
||||
.await
|
||||
.map(|s| s.curr.state);
|
||||
let status = CachedStackStatus {
|
||||
id: stack.id.clone(),
|
||||
state,
|
||||
state: current_state,
|
||||
services: services_with_containers,
|
||||
};
|
||||
stack_status_cache
|
||||
.insert(stack.id, History { curr: status, prev }.into())
|
||||
.insert(
|
||||
stack.id,
|
||||
History {
|
||||
curr: status,
|
||||
prev: prev_state,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn deployment_alert_sent_cache() -> &'static AlertCache<String> {
|
||||
static CACHE: OnceLock<AlertCache<String>> = OnceLock::new();
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub async fn update_swarm_deployment_cache(
|
||||
deployments: Vec<Deployment>,
|
||||
swarm_services: &[SwarmServiceListItem],
|
||||
swarm_tasks: &[SwarmTaskListItem],
|
||||
) {
|
||||
let deployment_status_cache = deployment_status_cache();
|
||||
for deployment in deployments {
|
||||
let service = swarm_services
|
||||
.iter()
|
||||
.find(|service| {
|
||||
service
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|name| name == &deployment.name)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.cloned();
|
||||
let prev_state = deployment_status_cache
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.map(|s| s.curr.state);
|
||||
let current_state = service
|
||||
.as_ref()
|
||||
.map(|service| {
|
||||
let Some(service_id) = &service.id else {
|
||||
return DeploymentState::Unknown;
|
||||
};
|
||||
let tasks = swarm_tasks
|
||||
.iter()
|
||||
.filter(|task| {
|
||||
task
|
||||
.service_id
|
||||
.as_ref()
|
||||
.map(|id| id == service_id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// If service exists but no tasks, it is unhealthy
|
||||
if tasks.is_empty() {
|
||||
return DeploymentState::Unhealthy;
|
||||
}
|
||||
for task in tasks {
|
||||
if task.desired_state != task.state {
|
||||
return DeploymentState::Unhealthy;
|
||||
}
|
||||
}
|
||||
DeploymentState::Running
|
||||
})
|
||||
.unwrap_or(DeploymentState::NotDeployed);
|
||||
deployment_status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state: current_state,
|
||||
service,
|
||||
container: None,
|
||||
update_available: false,
|
||||
},
|
||||
prev: prev_state,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_server_deployment_cache(
|
||||
server_name: String,
|
||||
deployments: Vec<Deployment>,
|
||||
containers: &[ContainerListItem],
|
||||
images: &[ImageListItem],
|
||||
builds: &[Build],
|
||||
) {
|
||||
let deployment_status_cache = deployment_status_cache();
|
||||
let deployment_alert_sent_cache = deployment_alert_sent_cache();
|
||||
|
||||
for deployment in deployments {
|
||||
let container = containers
|
||||
.iter()
|
||||
.find(|container| container.name == deployment.name)
|
||||
.cloned();
|
||||
let prev_state = deployment_status_cache
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.map(|s| s.curr.state);
|
||||
let current_state = container
|
||||
.as_ref()
|
||||
.map(|c| c.state.into())
|
||||
.unwrap_or(DeploymentState::NotDeployed);
|
||||
let image = match deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let (build_name, build_version) = builds
|
||||
.iter()
|
||||
.find(|build| build.id == build_id)
|
||||
.map(|b| (b.name.as_ref(), b.config.version))
|
||||
.unwrap_or(("Unknown", Default::default()));
|
||||
let version = if version.is_none() {
|
||||
build_version.to_string()
|
||||
} else {
|
||||
version.to_string()
|
||||
};
|
||||
format!("{build_name}:{version}")
|
||||
}
|
||||
DeploymentImage::Image { image } => {
|
||||
// If image already has tag, leave it,
|
||||
// otherwise default the tag to latest
|
||||
if image.contains(':') {
|
||||
image.to_string()
|
||||
} else {
|
||||
format!("{image}:latest")
|
||||
}
|
||||
}
|
||||
};
|
||||
let update_available = if let Some(ContainerListItem {
|
||||
image_id: Some(curr_image_id),
|
||||
..
|
||||
}) = &container
|
||||
{
|
||||
// Docker will automatically strip `docker.io` from incoming image names re #468.
|
||||
// Need to strip it in order to match by image name and find available updates.
|
||||
let image = image.strip_prefix("docker.io/").unwrap_or(&image);
|
||||
images
|
||||
.iter()
|
||||
.find(|i| i.name == image)
|
||||
.map(|i| &i.id != curr_image_id)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if update_available {
|
||||
if deployment.config.auto_update {
|
||||
if current_state == DeploymentState::Running
|
||||
&& !action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await
|
||||
.busy()
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let id = deployment.id.clone();
|
||||
let server_name = server_name.clone();
|
||||
tokio::spawn(async move {
|
||||
match execute::inner_handler(
|
||||
ExecuteRequest::Deploy(Deploy {
|
||||
deployment: deployment.name.clone(),
|
||||
stop_time: None,
|
||||
stop_signal: None,
|
||||
}),
|
||||
auto_redeploy_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(id.clone()),
|
||||
data: AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name: deployment.name,
|
||||
server_name,
|
||||
server_id: deployment.config.server_id,
|
||||
image,
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentAutoUpdated to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to auto update Deployment {} | {e:#}",
|
||||
deployment.name
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if current_state == DeploymentState::Running
|
||||
&& deployment.config.send_alerts
|
||||
&& deployment_alert_sent_cache.contains(&deployment.id)
|
||||
{
|
||||
// Add that it is already sent to the cache, so another alert won't be sent.
|
||||
deployment_alert_sent_cache.insert(deployment.id.clone());
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
data: AlertData::DeploymentImageUpdateAvailable {
|
||||
id: deployment.id.clone(),
|
||||
name: deployment.name,
|
||||
server_name: server_name.clone(),
|
||||
server_id: deployment.config.server_id,
|
||||
image,
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentImageUpdateAvailable to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
} else {
|
||||
// If it sees there is no longer update available, remove
|
||||
// from the sent cache, so on next `update_available = true`
|
||||
// the cache is empty and a fresh alert will be sent.
|
||||
deployment_alert_sent_cache.remove(&deployment.id);
|
||||
}
|
||||
deployment_status_cache
|
||||
.insert(
|
||||
deployment.id.clone(),
|
||||
History {
|
||||
curr: CachedDeploymentStatus {
|
||||
id: deployment.id,
|
||||
state: current_state,
|
||||
container,
|
||||
service: None,
|
||||
update_available,
|
||||
},
|
||||
prev: prev_state,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
struct AlertCache<K>(Mutex<HashSet<K>>);
|
||||
|
||||
impl<K> Default for AlertCache<K> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash> AlertCache<K> {
|
||||
/// Checks the cache for an existing key entry.
|
||||
/// If the cache is poisoned, will always return 'true' and include error log.
|
||||
fn contains(&self, key: &K) -> bool {
|
||||
self
|
||||
.0
|
||||
.lock()
|
||||
.map(|cache| cache.contains(key))
|
||||
.map_err(|_| error!("Alert Cache poisoned, this blocks container state change alerts, please restart Komodo Core."))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn insert(&self, key: K) {
|
||||
let _ = self
|
||||
.0
|
||||
.lock()
|
||||
.map(|mut cache| cache.insert(key))
|
||||
.map_err(|_| error!("Alert Cache poisoned, this blocks container state change alerts, please restart Komodo Core."));
|
||||
}
|
||||
|
||||
fn remove(&self, key: &K) {
|
||||
let _ = self
|
||||
.0
|
||||
.lock()
|
||||
.map(|mut cache| cache.remove(key))
|
||||
.map_err(|_| error!("Alert Cache poisoned, this blocks container state change alerts, please restart Komodo Core."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
use std::{
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_timing_util::wait_until_timelength;
|
||||
use cache::CloneCache;
|
||||
use database::mungos::find::find_collect;
|
||||
use formatting::format_serror;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::entities::{
|
||||
docker::node::NodeState,
|
||||
komodo_timestamp,
|
||||
swarm::{Swarm, SwarmState},
|
||||
};
|
||||
use periphery_client::api::swarm::{
|
||||
PollSwarmStatus, PollSwarmStatusResponse,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
config::monitoring_interval,
|
||||
helpers::swarm::swarm_request_custom_timeout,
|
||||
monitor::{
|
||||
UpdateCacheResources,
|
||||
resources::{
|
||||
update_swarm_deployment_cache, update_swarm_stack_cache,
|
||||
},
|
||||
},
|
||||
state::{CachedSwarmStatus, db_client, swarm_status_cache},
|
||||
};
|
||||
|
||||
const ADDITIONAL_MS: u128 = 1000;
|
||||
|
||||
pub fn spawn_swarm_monitoring_loop() {
|
||||
tokio::spawn(async move {
|
||||
refresh_swarm_cache().await;
|
||||
let interval = monitoring_interval();
|
||||
loop {
|
||||
wait_until_timelength(interval, ADDITIONAL_MS).await;
|
||||
refresh_swarm_cache().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn refresh_swarm_cache() {
|
||||
let swarms =
|
||||
match find_collect(&db_client().swarms, None, None).await {
|
||||
Ok(swarms) => swarms,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to get swarm list (refresh swarm cache) | {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let futures = swarms.into_iter().map(|swarm| async move {
|
||||
update_cache_for_swarm(&swarm, false).await;
|
||||
});
|
||||
join_all(futures).await;
|
||||
// tokio::join!(check_alerts(ts), record_swarm_stats(ts));
|
||||
}
|
||||
|
||||
/// Makes sure cache for swarm doesn't update too frequently / simultaneously.
|
||||
/// If forced, will still block against simultaneous update.
|
||||
fn update_cache_for_swarm_controller()
|
||||
-> &'static CloneCache<String, Arc<Mutex<i64>>> {
|
||||
static CACHE: OnceLock<CloneCache<String, Arc<Mutex<i64>>>> =
|
||||
OnceLock::new();
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// The background loop will call this with force: false,
|
||||
/// which exits early if the lock is busy or it was completed too recently.
|
||||
/// If force is true, it will wait on simultaneous calls, and will
|
||||
/// ignore the restriction on being completed too recently.
|
||||
pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) {
|
||||
// Concurrency controller to ensure it isn't done too often
|
||||
// when it happens in other contexts.
|
||||
let controller = update_cache_for_swarm_controller()
|
||||
.get_or_insert_default(&swarm.id)
|
||||
.await;
|
||||
let mut lock = match controller.try_lock() {
|
||||
Ok(lock) => lock,
|
||||
Err(_) if force => controller.lock().await,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let now = komodo_timestamp();
|
||||
|
||||
// early return if called again sooner than 1s.
|
||||
if !force && *lock > now - 1_000 {
|
||||
return;
|
||||
}
|
||||
|
||||
*lock = now;
|
||||
|
||||
let resources = UpdateCacheResources::load_swarm(swarm).await;
|
||||
|
||||
if swarm.config.server_ids.is_empty() {
|
||||
resources.insert_status_unknown().await;
|
||||
swarm_status_cache()
|
||||
.insert(
|
||||
swarm.id.clone(),
|
||||
CachedSwarmStatus {
|
||||
state: SwarmState::Unknown,
|
||||
inspect: None,
|
||||
lists: None,
|
||||
err: Some(format_serror(
|
||||
&anyhow!("No Servers configured as manager nodes").into(),
|
||||
)),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let PollSwarmStatusResponse { inspect, lists } =
|
||||
match swarm_request_custom_timeout(
|
||||
&swarm.config.server_ids,
|
||||
PollSwarmStatus {},
|
||||
Duration::from_secs(1),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
resources.insert_status_unknown().await;
|
||||
swarm_status_cache()
|
||||
.insert(
|
||||
swarm.id.clone(),
|
||||
CachedSwarmStatus {
|
||||
state: SwarmState::Unknown,
|
||||
inspect: None,
|
||||
lists: None,
|
||||
err: Some(format_serror(&e.into())),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut state = SwarmState::Healthy;
|
||||
|
||||
for node in &lists.nodes {
|
||||
if !matches!(node.state, Some(NodeState::READY)) {
|
||||
state = SwarmState::Unhealthy;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::join!(
|
||||
update_swarm_stack_cache(
|
||||
resources.stacks,
|
||||
&lists.stacks,
|
||||
&lists.services,
|
||||
),
|
||||
update_swarm_deployment_cache(
|
||||
resources.deployments,
|
||||
&lists.services,
|
||||
&lists.tasks,
|
||||
)
|
||||
);
|
||||
|
||||
swarm_status_cache()
|
||||
.insert(
|
||||
swarm.id.clone(),
|
||||
CachedSwarmStatus {
|
||||
state,
|
||||
inspect,
|
||||
lists: Some(lists),
|
||||
err: None,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
connection::{
|
||||
PeripheryConnection, PeripheryConnectionArgs, ResponseChannels,
|
||||
TerminalChannels,
|
||||
},
|
||||
state::periphery_connections,
|
||||
};
|
||||
@@ -21,9 +20,9 @@ pub mod terminal;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PeripheryClient {
|
||||
/// Usually the server id
|
||||
pub id: String,
|
||||
pub responses: Arc<ResponseChannels>,
|
||||
pub terminals: Arc<TerminalChannels>,
|
||||
}
|
||||
|
||||
impl PeripheryClient {
|
||||
@@ -50,7 +49,6 @@ impl PeripheryClient {
|
||||
return Ok(PeripheryClient {
|
||||
id,
|
||||
responses: connection.responses.clone(),
|
||||
terminals: connection.terminals.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +65,6 @@ impl PeripheryClient {
|
||||
Ok(PeripheryClient {
|
||||
id,
|
||||
responses: connection.responses.clone(),
|
||||
terminals: connection.terminals.clone(),
|
||||
})
|
||||
} else {
|
||||
// Core -> Periphery connection
|
||||
@@ -88,6 +85,20 @@ impl PeripheryClient {
|
||||
&self,
|
||||
request: T,
|
||||
) -> anyhow::Result<T::Response>
|
||||
where
|
||||
T: std::fmt::Debug + Serialize + HasResponse,
|
||||
T::Response: DeserializeOwned,
|
||||
{
|
||||
self
|
||||
.request_custom_timeout(request, Duration::from_secs(10))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn request_custom_timeout<T>(
|
||||
&self,
|
||||
request: T,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<T::Response>
|
||||
where
|
||||
T: std::fmt::Debug + Serialize + HasResponse,
|
||||
T::Response: DeserializeOwned,
|
||||
@@ -116,24 +127,28 @@ impl PeripheryClient {
|
||||
.await
|
||||
.context("Failed to send request over channel")
|
||||
{
|
||||
// cleanup
|
||||
self.terminals.remove(&channel_id).await;
|
||||
self.responses.remove(&channel_id).await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Poll for the associated response
|
||||
loop {
|
||||
let message = response_receiever
|
||||
.recv()
|
||||
.with_timeout(Duration::from_secs(10))
|
||||
.await?;
|
||||
let res = async {
|
||||
// Poll for the associated response
|
||||
loop {
|
||||
let message =
|
||||
response_receiever.recv().with_timeout(timeout).await?;
|
||||
|
||||
// Still in progress, sent to avoid timeout.
|
||||
let Some(message) = message.decode()? else {
|
||||
continue;
|
||||
};
|
||||
// Still in progress, sent to avoid timeout.
|
||||
let Some(message) = message.decode()? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
return message.decode();
|
||||
return message.decode();
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
self.responses.remove(&channel_id).await;
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ use std::{
|
||||
use anyhow::Context;
|
||||
use cache::CloneCache;
|
||||
use futures_util::Stream;
|
||||
use komodo_client::api::write::TerminalRecreateMode;
|
||||
use komodo_client::entities::terminal::{
|
||||
TerminalStdinMessageVariant, TerminalTarget,
|
||||
};
|
||||
use periphery_client::{
|
||||
api::terminal::{
|
||||
ConnectContainerAttach, ConnectContainerExec, ConnectTerminal,
|
||||
END_OF_OUTPUT, ExecuteContainerExec, ExecuteTerminal,
|
||||
},
|
||||
api::terminal::{ConnectTerminal, END_OF_OUTPUT, ExecuteTerminal},
|
||||
transport::EncodedTransportMessage,
|
||||
};
|
||||
use transport::channel::{Receiver, Sender};
|
||||
@@ -33,9 +32,10 @@ impl PeripheryClient {
|
||||
pub async fn connect_terminal(
|
||||
&self,
|
||||
terminal: String,
|
||||
target: TerminalTarget,
|
||||
) -> anyhow::Result<ConnectTerminalResponse> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectTerminal | terminal name: {terminal}",
|
||||
"request | type: ConnectTerminal | Terminal: {terminal} | Target: {target:?}",
|
||||
);
|
||||
|
||||
let connection =
|
||||
@@ -44,7 +44,7 @@ impl PeripheryClient {
|
||||
)?;
|
||||
|
||||
let channel = self
|
||||
.request(ConnectTerminal { terminal })
|
||||
.request(ConnectTerminal { terminal, target })
|
||||
.await
|
||||
.context("Failed to create terminal connection")?;
|
||||
|
||||
@@ -53,94 +53,13 @@ impl PeripheryClient {
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17))) // 16 bytes uuid + 1 EncodedResponse
|
||||
.send_terminal(
|
||||
channel,
|
||||
Ok(vec![TerminalStdinMessageVariant::Begin.as_byte()]),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok(ConnectTerminalResponse {
|
||||
channel,
|
||||
sender: connection.sender.clone(),
|
||||
receiver,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument("ConnectContainerExec", skip(self), fields(server_id = self.id))]
|
||||
pub async fn connect_container_exec(
|
||||
&self,
|
||||
container: String,
|
||||
shell: String,
|
||||
recreate: TerminalRecreateMode,
|
||||
) -> anyhow::Result<ConnectTerminalResponse> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectContainerExec | container name: {container} | shell: {shell}",
|
||||
);
|
||||
|
||||
let connection =
|
||||
periphery_connections().get(&self.id).await.with_context(
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let channel = self
|
||||
.request(ConnectContainerExec {
|
||||
container,
|
||||
shell,
|
||||
recreate,
|
||||
})
|
||||
.await
|
||||
.context("Failed to create container exec connection")?;
|
||||
|
||||
let (sender, receiver) = transport::channel::channel();
|
||||
connection.terminals.insert(channel, sender).await;
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok(ConnectTerminalResponse {
|
||||
channel,
|
||||
sender: connection.sender.clone(),
|
||||
receiver,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument("ConnectContainerAttach", skip(self), fields(server_id = self.id))]
|
||||
pub async fn connect_container_attach(
|
||||
&self,
|
||||
container: String,
|
||||
recreate: TerminalRecreateMode,
|
||||
) -> anyhow::Result<ConnectTerminalResponse> {
|
||||
tracing::trace!(
|
||||
"request | type: ConnectContainerAttach | container name: {container}",
|
||||
);
|
||||
|
||||
let connection =
|
||||
periphery_connections().get(&self.id).await.with_context(
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let channel = self
|
||||
.request(ConnectContainerAttach {
|
||||
container,
|
||||
recreate,
|
||||
})
|
||||
.await
|
||||
.context("Failed to create container attach connection")?;
|
||||
|
||||
let (sender, receiver) = transport::channel::channel();
|
||||
connection.terminals.insert(channel, sender).await;
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
"Failed to send TerminalMessage Begin byte to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok(ConnectTerminalResponse {
|
||||
@@ -166,13 +85,14 @@ impl PeripheryClient {
|
||||
#[instrument("ExecuteTerminal", skip(self), fields(server_id = self.id))]
|
||||
pub async fn execute_terminal(
|
||||
&self,
|
||||
target: TerminalTarget,
|
||||
terminal: String,
|
||||
command: String,
|
||||
) -> anyhow::Result<
|
||||
impl Stream<Item = anyhow::Result<Vec<u8>>> + 'static,
|
||||
> {
|
||||
trace!(
|
||||
"sending request | type: ExecuteTerminal | terminal name: {terminal} | command: {command}",
|
||||
"sending request | type: ExecuteTerminal | {target:?} | terminal name: {terminal} | command: {command}",
|
||||
);
|
||||
|
||||
let connection =
|
||||
@@ -181,65 +101,10 @@ impl PeripheryClient {
|
||||
)?;
|
||||
|
||||
let channel = self
|
||||
.request(ExecuteTerminal { terminal, command })
|
||||
.await
|
||||
.context("Failed to create execute terminal connection")?;
|
||||
|
||||
let (terminal_sender, terminal_receiver) =
|
||||
transport::channel::channel();
|
||||
connection.terminals.insert(channel, terminal_sender).await;
|
||||
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok(ReceiverStream {
|
||||
channel,
|
||||
receiver: terminal_receiver,
|
||||
channels: connection.terminals.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Executes command on specified container,
|
||||
/// and streams the response ending in [KOMODO_EXIT_CODE][komodo_client::entities::KOMODO_EXIT_CODE]
|
||||
/// sentinal value as the expected final line of the stream.
|
||||
///
|
||||
/// Example final line:
|
||||
/// ```text
|
||||
/// __KOMODO_EXIT_CODE:0
|
||||
/// ```
|
||||
///
|
||||
/// This means the command exited with code 0 (success).
|
||||
///
|
||||
/// If this value is NOT the final item before stream closes, it means
|
||||
/// the container shell exited mid command, before giving status. Example: running `exit`.
|
||||
#[instrument("ExecuteContainerExec", skip(self), fields(server_id = self.id))]
|
||||
pub async fn execute_container_exec(
|
||||
&self,
|
||||
container: String,
|
||||
shell: String,
|
||||
command: String,
|
||||
recreate: TerminalRecreateMode,
|
||||
) -> anyhow::Result<ReceiverStream> {
|
||||
tracing::trace!(
|
||||
"sending request | type: ExecuteContainerExec | container: {container} | shell: {shell} | command: {command}",
|
||||
);
|
||||
|
||||
let connection =
|
||||
periphery_connections().get(&self.id).await.with_context(
|
||||
|| format!("No connection found for server {}", self.id),
|
||||
)?;
|
||||
|
||||
let channel = self
|
||||
.request(ExecuteContainerExec {
|
||||
container,
|
||||
shell,
|
||||
.request(ExecuteTerminal {
|
||||
terminal,
|
||||
target,
|
||||
command,
|
||||
recreate,
|
||||
})
|
||||
.await
|
||||
.context("Failed to create execute terminal connection")?;
|
||||
@@ -248,12 +113,16 @@ impl PeripheryClient {
|
||||
transport::channel::channel();
|
||||
connection.terminals.insert(channel, terminal_sender).await;
|
||||
|
||||
// Trigger forwarding to begin now that forwarding channel is ready.
|
||||
// This is required to not miss messages.
|
||||
connection
|
||||
.sender
|
||||
.send_terminal(channel, Ok(Vec::with_capacity(17)))
|
||||
.await?;
|
||||
.send_terminal(
|
||||
channel,
|
||||
Ok(vec![TerminalStdinMessageVariant::Begin.as_byte()]),
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to send TerminalTrigger to begin forwarding.",
|
||||
)?;
|
||||
|
||||
Ok(ReceiverStream {
|
||||
channel,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user