mirror of
https://github.com/moghtech/komodo.git
synced 2026-11-02 00:02:23 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d71e9dca11 | ||
|
|
165131bdf8 | ||
|
|
0a81d2a0d0 | ||
|
|
44ab5eb804 | ||
|
|
e3d8e603ec | ||
|
|
8b5c179473 | ||
|
|
8582bc92da | ||
|
|
8ee270d045 | ||
|
|
2cfae525e9 | ||
|
|
80e5d2a972 | ||
|
|
6f22c011a6 | ||
|
|
401cccee79 | ||
|
|
654b923f98 | ||
|
|
61261be70f | ||
|
|
46418125e3 | ||
|
|
e029e94f0d |
+7
-1
@@ -5,4 +5,10 @@ LICENSE
|
||||
*.code-workspace
|
||||
|
||||
*/node_modules
|
||||
*/dist
|
||||
*/dist
|
||||
|
||||
creds.toml
|
||||
.core-repos
|
||||
.repos
|
||||
.stacks
|
||||
.ssl
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
target
|
||||
/frontend/build
|
||||
node_modules
|
||||
/lib/ts_client/build
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.development
|
||||
.DS_Store
|
||||
|
||||
creds.toml
|
||||
.syncs
|
||||
.stacks
|
||||
.DS_Store
|
||||
.komodo
|
||||
Generated
+54
-53
@@ -41,7 +41,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -201,9 +201,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.5.7"
|
||||
version = "1.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8191fb3091fa0561d1379ef80333c3c7191c6f0435d986e85821bcf7acbd1126"
|
||||
checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -268,9 +268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ec2"
|
||||
version = "1.75.0"
|
||||
version = "1.77.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6787d920877cca6a4ee3953093f6a47cefe26de95a4f7b3681c5850bfe657b4"
|
||||
checksum = "4bb6f841697b994ec3a020c560b52693bc9fcb7b9c69210088ab56e03df23b5e"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -292,9 +292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.44.0"
|
||||
version = "1.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b90cfe6504115e13c41d3ea90286ede5aa14da294f3fe077027a6e83850843c"
|
||||
checksum = "e33ae899566f3d395cbf42858e433930682cc9c1889fa89318896082fef45efb"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -314,9 +314,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.45.0"
|
||||
version = "1.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167c0fad1f212952084137308359e8e4c4724d1c643038ce163f06de9662c1d0"
|
||||
checksum = "f39c09e199ebd96b9f860b0fce4b6625f211e064ad7c8693b72ecf7ef03881e0"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -336,9 +336,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.44.0"
|
||||
version = "1.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cb5f98188ec1435b68097daa2a37d74b9d17c9caa799466338a8d1544e71b9d"
|
||||
checksum = "3d95f93a98130389eb6233b9d615249e543f6c24a68ca1f109af9ca5164a8765"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -887,9 +887,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.19"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -897,9 +897,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.19"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -943,7 +943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"run_command",
|
||||
@@ -1149,18 +1149,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.1"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.1"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -1170,9 +1170,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.1"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.77",
|
||||
@@ -1355,7 +1355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1439,7 +1439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -1452,9 +1452,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1467,9 +1467,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1477,15 +1477,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -1494,15 +1494,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1511,21 +1511,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1571,7 +1571,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2192,7 +2192,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2208,7 +2208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2239,7 +2239,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2296,7 +2296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2306,6 +2306,7 @@ dependencies = [
|
||||
"bollard",
|
||||
"clap",
|
||||
"command",
|
||||
"derive_variants",
|
||||
"dotenvy",
|
||||
"environment_file",
|
||||
"envy",
|
||||
@@ -2382,7 +2383,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2446,7 +2447,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "migrator"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenvy",
|
||||
@@ -3101,7 +3102,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -4249,9 +4250,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.31.4"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be"
|
||||
checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -4879,7 +4880,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
|
||||
+9
-9
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.15.1"
|
||||
version = "1.15.7"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -15,7 +15,7 @@ homepage = "https://komo.do"
|
||||
|
||||
[workspace.dependencies]
|
||||
# LOCAL
|
||||
# komodo_client = "1.14.3"
|
||||
# komodo_client = "1.15.6"
|
||||
komodo_client = { path = "client/core/rs" }
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
environment_file = { path = "lib/environment_file" }
|
||||
@@ -44,8 +44,8 @@ svi = "1.0.1"
|
||||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
tokio = { version = "1.38.1", features = ["full"] }
|
||||
tokio-util = "0.7.12"
|
||||
futures = "0.3.30"
|
||||
futures-util = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
|
||||
# SERVER
|
||||
axum-extra = { version = "0.9.4", features = ["typed-header"] }
|
||||
@@ -76,7 +76,7 @@ opentelemetry = "0.25.0"
|
||||
tracing = "0.1.40"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.19", features = ["derive"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
@@ -95,14 +95,14 @@ hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
bollard = "0.17.1"
|
||||
sysinfo = "0.31.4"
|
||||
sysinfo = "0.32.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.5.7"
|
||||
aws-sdk-ec2 = "1.75.0"
|
||||
aws-config = "1.5.8"
|
||||
aws-sdk-ec2 = "1.77.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.1"
|
||||
derive_builder = "0.20.2"
|
||||
typeshare = "1.0.3"
|
||||
octorust = "0.7.0"
|
||||
dashmap = "6.1.0"
|
||||
|
||||
@@ -132,6 +132,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::DeployStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::DeployStackIfChanged(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
Execution::StartStack(data) => {
|
||||
println!("{}: {data:?}", "Data".dimmed())
|
||||
}
|
||||
@@ -273,6 +276,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
|
||||
Execution::DeployStack(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
Execution::DeployStackIfChanged(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
Execution::StartStack(request) => {
|
||||
komodo_client().execute(request).await
|
||||
}
|
||||
|
||||
+21
-15
@@ -10,36 +10,42 @@ use komodo_client::entities::{
|
||||
ResourceTargetVariant,
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
mod discord;
|
||||
mod slack;
|
||||
|
||||
#[instrument]
|
||||
pub async fn send_alerts(alerts: &[Alert]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(alerters) = find_collect(
|
||||
&db_client().alerters,
|
||||
doc! { "config.enabled": true },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
let span =
|
||||
info_span!("send_alerts", alerts = format!("{alerts:?}"));
|
||||
async {
|
||||
let Ok(alerters) = find_collect(
|
||||
&db_client().alerters,
|
||||
doc! { "config.enabled": true },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
"ERROR sending alerts | failed to get alerters from db | {e:#}"
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let handles =
|
||||
alerts.iter().map(|alert| send_alert(&alerters, alert));
|
||||
let handles =
|
||||
alerts.iter().map(|alert| send_alert(&alerters, alert));
|
||||
|
||||
join_all(handles).await;
|
||||
join_all(handles).await;
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
|
||||
@@ -64,7 +64,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let mut vars_and_secrets = get_variables_and_secrets().await?;
|
||||
|
||||
if build.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("Must attach builder to RunBuild"));
|
||||
@@ -85,6 +85,14 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
// Add the $VERSION to variables. Use with [[$VERSION]]
|
||||
if !vars_and_secrets.variables.contains_key("$VERSION") {
|
||||
vars_and_secrets.variables.insert(
|
||||
String::from("$VERSION"),
|
||||
build.config.version.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let git_token = git_token(
|
||||
&build.config.git_provider,
|
||||
&build.config.git_account,
|
||||
@@ -171,7 +179,6 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
};
|
||||
|
||||
// CLONE REPO
|
||||
|
||||
let secret_replacers = if !build.config.skip_secret_interp {
|
||||
// Interpolate variables / secrets into pre build command
|
||||
let mut global_replacers = HashSet::new();
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
@@ -33,7 +34,10 @@ mod stack;
|
||||
mod sync;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
|
||||
)]
|
||||
#[variant_derive(Debug)]
|
||||
#[resolver_target(State)]
|
||||
#[resolver_args((User, Update))]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
@@ -72,6 +76,7 @@ pub enum ExecuteRequest {
|
||||
|
||||
// ==== STACK ====
|
||||
DeployStack(DeployStack),
|
||||
DeployStackIfChanged(DeployStackIfChanged),
|
||||
StartStack(StartStack),
|
||||
RestartStack(RestartStack),
|
||||
StopStack(StopStack),
|
||||
@@ -154,7 +159,15 @@ async fn handler(
|
||||
Ok(Json(update))
|
||||
}
|
||||
|
||||
#[instrument(name = "ExecuteRequest", skip(user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
#[instrument(
|
||||
name = "ExecuteRequest",
|
||||
skip(user, update),
|
||||
fields(
|
||||
user_id = user.id,
|
||||
update_id = update.id,
|
||||
request = format!("{:?}", request.extract_variant()))
|
||||
)
|
||||
]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
request: ExecuteRequest,
|
||||
|
||||
@@ -3,9 +3,11 @@ use std::collections::HashSet;
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::*,
|
||||
api::{execute::*, write::RefreshStackCache},
|
||||
entities::{
|
||||
permission::PermissionLevel, stack::StackInfo, update::Update,
|
||||
permission::PermissionLevel,
|
||||
stack::{Stack, StackInfo},
|
||||
update::Update,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
@@ -19,12 +21,14 @@ use crate::{
|
||||
add_interp_update_log,
|
||||
interpolate_variables_secrets_into_extra_args,
|
||||
interpolate_variables_secrets_into_string,
|
||||
interpolate_variables_secrets_into_system_command,
|
||||
},
|
||||
periphery_client,
|
||||
query::get_variables_and_secrets,
|
||||
update::update_update,
|
||||
update::{add_update_without_send, update_update},
|
||||
},
|
||||
monitor::update_cache_for_server,
|
||||
resource,
|
||||
stack::{
|
||||
execute::execute_compose, get_stack_and_server,
|
||||
services::extract_services_into_res,
|
||||
@@ -102,6 +106,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_system_command(
|
||||
&vars_and_secrets,
|
||||
&mut stack.config.pre_deploy,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
add_interp_update_log(
|
||||
&mut update,
|
||||
&global_replacers,
|
||||
@@ -235,6 +246,77 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<DeployStackIfChanged, (User, Update)> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeployStackIfChanged { stack, stop_time }: DeployStackIfChanged,
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let stack = resource::get_check_permissions::<Stack>(
|
||||
&stack,
|
||||
&user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
State
|
||||
.resolve(
|
||||
RefreshStackCache {
|
||||
stack: stack.id.clone(),
|
||||
},
|
||||
user.clone(),
|
||||
)
|
||||
.await?;
|
||||
let stack = resource::get::<Stack>(&stack.id).await?;
|
||||
let changed = match (
|
||||
&stack.info.deployed_contents,
|
||||
&stack.info.remote_contents,
|
||||
) {
|
||||
(Some(deployed_contents), Some(latest_contents)) => {
|
||||
let changed = || {
|
||||
for latest in latest_contents {
|
||||
let Some(deployed) = deployed_contents
|
||||
.iter()
|
||||
.find(|c| c.path == latest.path)
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
if latest.contents != deployed.contents {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
};
|
||||
changed()
|
||||
}
|
||||
(None, _) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if !changed {
|
||||
update.push_simple_log(
|
||||
"Diff compose files",
|
||||
String::from("Deploy cancelled after no changes detected."),
|
||||
);
|
||||
update.finalize();
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
// Don't actually send it here, let the handler send it after it can set action state.
|
||||
// This is usually done in crate::helpers::update::init_execution_update.
|
||||
update.id = add_update_without_send(&update).await?;
|
||||
|
||||
State
|
||||
.resolve(
|
||||
DeployStack {
|
||||
stack: stack.name,
|
||||
stop_time,
|
||||
},
|
||||
(user, update),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<StartStack, (User, Update)> for State {
|
||||
#[instrument(name = "StartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
async fn resolve(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{colored, format_serror, Color};
|
||||
@@ -20,23 +20,27 @@ use komodo_client::{
|
||||
sync::ResourceSync,
|
||||
update::{Log, Update},
|
||||
user::{sync_user, User},
|
||||
ResourceTargetVariant,
|
||||
},
|
||||
};
|
||||
use mongo_indexed::doc;
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
use mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::bson::{oid::ObjectId, to_document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{query::get_id_to_tags, update::update_update},
|
||||
resource::{self, refresh_resource_sync_state_cache},
|
||||
state::{db_client, State},
|
||||
state::{action_states, db_client, State},
|
||||
sync::{
|
||||
deploy::{
|
||||
build_deploy_cache, deploy_from_cache, SyncDeployParams,
|
||||
},
|
||||
execute::{get_updates_for_execution, ExecuteResourceSync},
|
||||
remote::RemoteResources,
|
||||
AllResourcesById,
|
||||
AllResourcesById, ResourceSyncTrait,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,7 +48,11 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
#[instrument(name = "RunSync", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
RunSync { sync }: RunSync,
|
||||
RunSync {
|
||||
sync,
|
||||
resource_type: match_resource_type,
|
||||
resources: match_resources,
|
||||
}: RunSync,
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let sync = resource::get_check_permissions::<
|
||||
@@ -52,6 +60,17 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
>(&sync, &user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
|
||||
// get the action state for the sync (or insert default).
|
||||
let action_state = action_states()
|
||||
.resource_sync
|
||||
.get_or_insert_default(&sync.id)
|
||||
.await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure sync not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.syncing = true)?;
|
||||
|
||||
// Send update here for FE to recheck action state
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -70,22 +89,101 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if !file_errors.is_empty() {
|
||||
return Err(anyhow!("Found file errors. Cannot execute sync."))
|
||||
return Err(anyhow!("Found file errors. Cannot execute sync."));
|
||||
}
|
||||
|
||||
let resources = resources?;
|
||||
|
||||
let id_to_tags = get_id_to_tags(None).await?;
|
||||
let all_resources = AllResourcesById::load().await?;
|
||||
// Convert all match_resources to names
|
||||
let match_resources = match_resources.map(|resources| {
|
||||
resources
|
||||
.into_iter()
|
||||
.filter_map(|name_or_id| {
|
||||
let Some(resource_type) = match_resource_type else {
|
||||
return Some(name_or_id);
|
||||
};
|
||||
match ObjectId::from_str(&name_or_id) {
|
||||
Ok(_) => match resource_type {
|
||||
ResourceTargetVariant::Alerter => all_resources
|
||||
.alerters
|
||||
.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::Repo => all_resources
|
||||
.repos
|
||||
.get(&name_or_id)
|
||||
.map(|r| r.name.clone()),
|
||||
ResourceTargetVariant::Server => all_resources
|
||||
.servers
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::ServerTemplate => all_resources
|
||||
.templates
|
||||
.get(&name_or_id)
|
||||
.map(|t| t.name.clone()),
|
||||
ResourceTargetVariant::Stack => all_resources
|
||||
.stacks
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::ResourceSync => all_resources
|
||||
.syncs
|
||||
.get(&name_or_id)
|
||||
.map(|s| s.name.clone()),
|
||||
ResourceTargetVariant::System => None,
|
||||
},
|
||||
Err(_) => Some(name_or_id),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let deployments_by_name = all_resources
|
||||
.deployments
|
||||
.values()
|
||||
.filter(|deployment| {
|
||||
Deployment::include_resource(
|
||||
&deployment.name,
|
||||
&deployment.config,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&deployment.tags,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
})
|
||||
.map(|deployment| (deployment.name.clone(), deployment.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let stacks_by_name = all_resources
|
||||
.stacks
|
||||
.values()
|
||||
.filter(|stack| {
|
||||
Stack::include_resource(
|
||||
&stack.name,
|
||||
&stack.config,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&stack.tags,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
})
|
||||
.map(|stack| (stack.name.clone(), stack.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
@@ -105,6 +203,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.servers,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -117,6 +217,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.deployments,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -126,6 +228,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.stacks,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -135,6 +239,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.builds,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -144,6 +250,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.repos,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -156,6 +264,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.procedures,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -165,6 +275,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.builders,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -174,6 +286,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.alerters,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -186,6 +300,8 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.server_templates,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
@@ -198,31 +314,48 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
resources.resource_syncs,
|
||||
delete,
|
||||
&all_resources,
|
||||
match_resource_type,
|
||||
match_resources.as_deref(),
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (
|
||||
variables_to_create,
|
||||
variables_to_update,
|
||||
variables_to_delete,
|
||||
) = crate::sync::variables::get_updates_for_execution(
|
||||
resources.variables,
|
||||
// Delete doesn't work with variables when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
)
|
||||
.await?;
|
||||
) = if match_resource_type.is_none()
|
||||
&& match_resources.is_none()
|
||||
&& sync.config.match_tags.is_empty()
|
||||
{
|
||||
crate::sync::variables::get_updates_for_execution(
|
||||
resources.variables,
|
||||
// Delete doesn't work with variables when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let (
|
||||
user_groups_to_create,
|
||||
user_groups_to_update,
|
||||
user_groups_to_delete,
|
||||
) = crate::sync::user_groups::get_updates_for_execution(
|
||||
resources.user_groups,
|
||||
// Delete doesn't work with user groups when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
&all_resources,
|
||||
)
|
||||
.await?;
|
||||
) = if match_resource_type.is_none()
|
||||
&& match_resources.is_none()
|
||||
&& sync.config.match_tags.is_empty()
|
||||
{
|
||||
crate::sync::user_groups::get_updates_for_execution(
|
||||
resources.user_groups,
|
||||
// Delete doesn't work with user groups when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
&all_resources,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
if deploy_cache.is_empty()
|
||||
&& resource_syncs_to_create.is_empty()
|
||||
|
||||
@@ -88,7 +88,11 @@ const MAX_LOG_LENGTH: u64 = 5000;
|
||||
impl Resolve<GetDeploymentLog, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDeploymentLog { deployment, tail }: GetDeploymentLog,
|
||||
GetDeploymentLog {
|
||||
deployment,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetDeploymentLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
let Deployment {
|
||||
@@ -109,6 +113,7 @@ impl Resolve<GetDeploymentLog, User> for State {
|
||||
.request(api::container::GetContainerLog {
|
||||
name,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
@@ -123,6 +128,7 @@ impl Resolve<SearchDeploymentLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: SearchDeploymentLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -146,6 +152,7 @@ impl Resolve<SearchDeploymentLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
|
||||
@@ -111,6 +111,7 @@ enum ReadRequest {
|
||||
InspectDockerImage(InspectDockerImage),
|
||||
ListDockerImageHistory(ListDockerImageHistory),
|
||||
InspectDockerVolume(InspectDockerVolume),
|
||||
ListAllDockerContainers(ListAllDockerContainers),
|
||||
#[to_string_resolver]
|
||||
ListDockerContainers(ListDockerContainers),
|
||||
#[to_string_resolver]
|
||||
|
||||
@@ -13,7 +13,7 @@ use komodo_client::{
|
||||
entities::{
|
||||
deployment::Deployment,
|
||||
docker::{
|
||||
container::Container,
|
||||
container::{Container, ContainerListItem},
|
||||
image::{Image, ImageHistoryResponseItem},
|
||||
network::Network,
|
||||
volume::Volume,
|
||||
@@ -289,7 +289,7 @@ impl ResolveToString<ListSystemProcesses, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
const STATS_PER_PAGE: i64 = 500;
|
||||
const STATS_PER_PAGE: i64 = 200;
|
||||
|
||||
impl Resolve<GetHistoricalServerStats, User> for State {
|
||||
async fn resolve(
|
||||
@@ -368,6 +368,37 @@ impl ResolveToString<ListDockerContainers, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ListAllDockerContainers, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ListAllDockerContainers { servers }: ListAllDockerContainers,
|
||||
user: User,
|
||||
) -> anyhow::Result<Vec<ContainerListItem>> {
|
||||
let servers =
|
||||
resource::list_for_user::<Server>(Default::default(), &user)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|server| {
|
||||
servers.is_empty()
|
||||
|| servers.contains(&server.id)
|
||||
|| servers.contains(&server.name)
|
||||
});
|
||||
|
||||
let mut containers = Vec::<ContainerListItem>::new();
|
||||
|
||||
for server in servers {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
if let Some(more_containers) = &cache.containers {
|
||||
containers.extend(more_containers.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(containers)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<InspectDockerContainer, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
@@ -404,6 +435,7 @@ impl Resolve<GetContainerLog, User> for State {
|
||||
server,
|
||||
container,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetContainerLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -417,6 +449,7 @@ impl Resolve<GetContainerLog, User> for State {
|
||||
.request(periphery::container::GetContainerLog {
|
||||
name: container,
|
||||
tail: cmp::min(tail, MAX_LOG_LENGTH),
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
@@ -432,6 +465,7 @@ impl Resolve<SearchContainerLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: SearchContainerLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -447,6 +481,7 @@ impl Resolve<SearchContainerLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed at call to periphery")
|
||||
|
||||
@@ -70,6 +70,7 @@ impl Resolve<GetStackServiceLog, User> for State {
|
||||
stack,
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetStackServiceLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<GetStackServiceLogResponse> {
|
||||
@@ -85,6 +86,7 @@ impl Resolve<GetStackServiceLog, User> for State {
|
||||
project: stack.project_name(false),
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed to get stack service log from periphery")
|
||||
@@ -100,6 +102,7 @@ impl Resolve<SearchStackServiceLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: SearchStackServiceLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<SearchStackServiceLogResponse> {
|
||||
@@ -117,6 +120,7 @@ impl Resolve<SearchStackServiceLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
})
|
||||
.await
|
||||
.context("failed to get stack service log from periphery")
|
||||
|
||||
@@ -184,9 +184,12 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
res
|
||||
.alerters
|
||||
.push(convert_resource::<Alerter>(alerter, &id_to_tags))
|
||||
res.alerters.push(convert_resource::<Alerter>(
|
||||
alerter,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::ResourceSync(id) => {
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
@@ -201,6 +204,8 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
{
|
||||
res.resource_syncs.push(convert_resource::<ResourceSync>(
|
||||
sync,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
@@ -213,7 +218,12 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
)
|
||||
.await?;
|
||||
res.server_templates.push(
|
||||
convert_resource::<ServerTemplate>(template, &id_to_tags),
|
||||
convert_resource::<ServerTemplate>(
|
||||
template,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
),
|
||||
)
|
||||
}
|
||||
ResourceTarget::Server(id) => {
|
||||
@@ -223,9 +233,12 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await?;
|
||||
res
|
||||
.servers
|
||||
.push(convert_resource::<Server>(server, &id_to_tags))
|
||||
res.servers.push(convert_resource::<Server>(
|
||||
server,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Builder(id) => {
|
||||
let mut builder =
|
||||
@@ -236,9 +249,12 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
)
|
||||
.await?;
|
||||
Builder::replace_ids(&mut builder, &all);
|
||||
res
|
||||
.builders
|
||||
.push(convert_resource::<Builder>(builder, &id_to_tags))
|
||||
res.builders.push(convert_resource::<Builder>(
|
||||
builder,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Build(id) => {
|
||||
let mut build = resource::get_check_permissions::<Build>(
|
||||
@@ -248,9 +264,12 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
)
|
||||
.await?;
|
||||
Build::replace_ids(&mut build, &all);
|
||||
res
|
||||
.builds
|
||||
.push(convert_resource::<Build>(build, &id_to_tags))
|
||||
res.builds.push(convert_resource::<Build>(
|
||||
build,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Deployment(id) => {
|
||||
let mut deployment = resource::get_check_permissions::<
|
||||
@@ -262,6 +281,8 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
Deployment::replace_ids(&mut deployment, &all);
|
||||
res.deployments.push(convert_resource::<Deployment>(
|
||||
deployment,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
@@ -273,7 +294,12 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
)
|
||||
.await?;
|
||||
Repo::replace_ids(&mut repo, &all);
|
||||
res.repos.push(convert_resource::<Repo>(repo, &id_to_tags))
|
||||
res.repos.push(convert_resource::<Repo>(
|
||||
repo,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
let mut stack = resource::get_check_permissions::<Stack>(
|
||||
@@ -283,9 +309,12 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
)
|
||||
.await?;
|
||||
Stack::replace_ids(&mut stack, &all);
|
||||
res
|
||||
.stacks
|
||||
.push(convert_resource::<Stack>(stack, &id_to_tags))
|
||||
res.stacks.push(convert_resource::<Stack>(
|
||||
stack,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
))
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
let mut procedure = resource::get_check_permissions::<
|
||||
@@ -297,6 +326,8 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
Procedure::replace_ids(&mut procedure, &all);
|
||||
res.procedures.push(convert_resource::<Procedure>(
|
||||
procedure,
|
||||
false,
|
||||
vec![],
|
||||
&id_to_tags,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ mod service_user;
|
||||
mod stack;
|
||||
mod sync;
|
||||
mod tag;
|
||||
mod user;
|
||||
mod user_group;
|
||||
mod variable;
|
||||
|
||||
@@ -40,6 +41,11 @@ mod variable;
|
||||
#[resolver_args(User)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum WriteRequest {
|
||||
// ==== USER ====
|
||||
UpdateUserUsername(UpdateUserUsername),
|
||||
UpdateUserPassword(UpdateUserPassword),
|
||||
DeleteUser(DeleteUser),
|
||||
|
||||
// ==== SERVICE USER ====
|
||||
CreateServiceUser(CreateServiceUser),
|
||||
UpdateServiceUserDescription(UpdateServiceUserDescription),
|
||||
@@ -124,8 +130,9 @@ pub enum WriteRequest {
|
||||
CopyResourceSync(CopyResourceSync),
|
||||
DeleteResourceSync(DeleteResourceSync),
|
||||
UpdateResourceSync(UpdateResourceSync),
|
||||
RefreshResourceSyncPending(RefreshResourceSyncPending),
|
||||
WriteSyncFileContents(WriteSyncFileContents),
|
||||
CommitSync(CommitSync),
|
||||
RefreshResourceSyncPending(RefreshResourceSyncPending),
|
||||
CreateSyncWebhook(CreateSyncWebhook),
|
||||
DeleteSyncWebhook(DeleteSyncWebhook),
|
||||
|
||||
@@ -188,7 +195,10 @@ async fn handler(
|
||||
#[instrument(
|
||||
name = "WriteRequest",
|
||||
skip(user, request),
|
||||
fields(user_id = user.id, request = format!("{:?}", request.extract_variant()))
|
||||
fields(
|
||||
user_id = user.id,
|
||||
request = format!("{:?}", request.extract_variant())
|
||||
)
|
||||
)]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
|
||||
@@ -22,14 +22,14 @@ use octorust::types::{
|
||||
};
|
||||
use periphery_client::api::compose::{
|
||||
GetComposeContentsOnHost, GetComposeContentsOnHostResponse,
|
||||
WriteComposeContentsToHost,
|
||||
WriteCommitComposeContents, WriteComposeContentsToHost,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
periphery_client,
|
||||
git_token, periphery_client,
|
||||
query::get_server_with_state,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
@@ -143,7 +143,7 @@ impl Resolve<WriteStackFileContents, User> for State {
|
||||
}: WriteStackFileContents,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let (stack, server) = get_stack_and_server(
|
||||
let (mut stack, server) = get_stack_and_server(
|
||||
&stack,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
@@ -151,9 +151,9 @@ impl Resolve<WriteStackFileContents, User> for State {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !stack.config.files_on_host {
|
||||
if !stack.config.files_on_host && stack.config.repo.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Stack is not configured to use files on host, can't write file contents"
|
||||
"Stack is not configured to use Files on Host or Git Repo, can't write file contents"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -162,30 +162,72 @@ impl Resolve<WriteStackFileContents, User> for State {
|
||||
|
||||
update.push_simple_log("File contents to write", &contents);
|
||||
|
||||
match periphery_client(&server)?
|
||||
.request(WriteComposeContentsToHost {
|
||||
name: stack.name,
|
||||
run_directory: stack.config.run_directory,
|
||||
file_path,
|
||||
contents,
|
||||
})
|
||||
.await
|
||||
.context("Failed to write contents to host")
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Write file contents",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
let stack_id = stack.id.clone();
|
||||
|
||||
if stack.config.files_on_host {
|
||||
match periphery_client(&server)?
|
||||
.request(WriteComposeContentsToHost {
|
||||
name: stack.name,
|
||||
run_directory: stack.config.run_directory,
|
||||
file_path,
|
||||
contents,
|
||||
})
|
||||
.await
|
||||
.context("Failed to write contents to host")
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Write file contents",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let git_token = if !stack.config.git_account.is_empty() {
|
||||
git_token(
|
||||
&stack.config.git_provider,
|
||||
&stack.config.git_account,
|
||||
|https| stack.config.git_https = https,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to get git token. | {} | {}",
|
||||
stack.config.git_account, stack.config.git_provider
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match periphery_client(&server)?
|
||||
.request(WriteCommitComposeContents {
|
||||
stack,
|
||||
username: Some(user.username),
|
||||
file_path,
|
||||
contents,
|
||||
git_token,
|
||||
})
|
||||
.await
|
||||
.context("Failed to write contents to host")
|
||||
{
|
||||
Ok(res) => {
|
||||
update.logs.extend(res.logs);
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Write file contents",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(e) = State
|
||||
.resolve(
|
||||
RefreshStackCache { stack: stack.id },
|
||||
RefreshStackCache { stack: stack_id },
|
||||
stack_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
@@ -227,10 +269,11 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
.await?;
|
||||
|
||||
let file_contents_empty = stack.config.file_contents.is_empty();
|
||||
let repo_empty = stack.config.repo.is_empty();
|
||||
|
||||
if !stack.config.files_on_host
|
||||
&& file_contents_empty
|
||||
&& stack.config.repo.is_empty()
|
||||
&& repo_empty
|
||||
{
|
||||
// Nothing to do without one of these
|
||||
return Ok(NoData {});
|
||||
@@ -297,7 +340,7 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
(services, Some(contents), Some(errors), None, None)
|
||||
}
|
||||
}
|
||||
} else if file_contents_empty {
|
||||
} else if !repo_empty {
|
||||
// ================
|
||||
// REPO BASED STACK
|
||||
// ================
|
||||
|
||||
+300
-135
@@ -8,6 +8,7 @@ use komodo_client::{
|
||||
self,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
alerter::Alerter,
|
||||
all_logs_success,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
config::core::CoreConfig,
|
||||
@@ -22,9 +23,10 @@ use komodo_client::{
|
||||
sync::{
|
||||
PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,
|
||||
},
|
||||
update::Log,
|
||||
to_komodo_name,
|
||||
update::{Log, Update},
|
||||
user::{sync_user, User},
|
||||
NoData, Operation, ResourceTarget,
|
||||
CloneArgs, NoData, Operation, ResourceTarget,
|
||||
},
|
||||
};
|
||||
use mungos::{
|
||||
@@ -35,6 +37,7 @@ use octorust::types::{
|
||||
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
@@ -103,6 +106,271 @@ impl Resolve<UpdateResourceSync, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteSyncFileContents, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
WriteSyncFileContents {
|
||||
sync,
|
||||
resource_path,
|
||||
file_path,
|
||||
contents,
|
||||
}: WriteSyncFileContents,
|
||||
user: User,
|
||||
) -> anyhow::Result<Update> {
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
&sync,
|
||||
&user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !sync.config.files_on_host && sync.config.repo.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"This method is only for files on host, or repo based syncs."
|
||||
));
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&sync, Operation::WriteSyncContents, &user);
|
||||
|
||||
update.push_simple_log("File contents", &contents);
|
||||
|
||||
let root = if sync.config.files_on_host {
|
||||
core_config()
|
||||
.sync_directory
|
||||
.join(to_komodo_name(&sync.name))
|
||||
} else {
|
||||
let clone_args: CloneArgs = (&sync).into();
|
||||
clone_args.unique_path(&core_config().repo_directory)?
|
||||
};
|
||||
let file_path =
|
||||
file_path.parse::<PathBuf>().context("Invalid file path")?;
|
||||
let resource_path = resource_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
let full_path = root.join(&resource_path).join(&file_path);
|
||||
|
||||
if let Some(parent) = full_path.parent() {
|
||||
let _ = fs::create_dir_all(parent).await;
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
fs::write(&full_path, &contents).await.with_context(|| {
|
||||
format!("Failed to write file contents to {full_path:?}")
|
||||
})
|
||||
{
|
||||
update.push_error_log("Write file", format_serror(&e.into()));
|
||||
} else {
|
||||
update.push_simple_log(
|
||||
"Write file",
|
||||
format!("File written to {full_path:?}"),
|
||||
);
|
||||
};
|
||||
|
||||
if !all_logs_success(&update.logs) {
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
if sync.config.files_on_host {
|
||||
if let Err(e) = State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
|
||||
.await
|
||||
{
|
||||
update
|
||||
.push_error_log("Refresh failed", format_serror(&e.into()));
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
let commit_res = git::commit_file(
|
||||
&format!("{}: Commit Resource File", user.username),
|
||||
&root,
|
||||
&resource_path.join(&file_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
update.logs.extend(commit_res.logs);
|
||||
|
||||
if let Err(e) = State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
|
||||
.await
|
||||
{
|
||||
update
|
||||
.push_error_log("Refresh failed", format_serror(&e.into()));
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<CommitSync, User> for State {
|
||||
#[instrument(name = "CommitSync", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CommitSync { sync }: CommitSync,
|
||||
user: User,
|
||||
) -> anyhow::Result<ResourceSync> {
|
||||
let sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&sync, &user, PermissionLevel::Write)
|
||||
.await?;
|
||||
|
||||
let fresh_sync = !sync.config.files_on_host
|
||||
&& sync.config.file_contents.is_empty()
|
||||
&& sync.config.repo.is_empty();
|
||||
|
||||
if !sync.config.managed && !fresh_sync {
|
||||
return Err(anyhow!(
|
||||
"Cannot commit to sync. Enabled 'managed' mode."
|
||||
));
|
||||
}
|
||||
|
||||
let resource_path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.first()
|
||||
.context("Sync does not have resource path configured.")?
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
|
||||
if resource_path
|
||||
.extension()
|
||||
.context("Resource path missing '.toml' extension")?
|
||||
!= "toml"
|
||||
{
|
||||
return Err(anyhow!("Resource path missing '.toml' extension"));
|
||||
}
|
||||
|
||||
let res = State
|
||||
.resolve(
|
||||
ExportAllResourcesToToml {
|
||||
tags: sync.config.match_tags.clone(),
|
||||
},
|
||||
sync_user().to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update = make_update(&sync, Operation::CommitSync, &user);
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
update.logs.push(Log::simple("Resources", res.toml.clone()));
|
||||
|
||||
if sync.config.files_on_host {
|
||||
let file_path = core_config()
|
||||
.sync_directory
|
||||
.join(to_komodo_name(&sync.name))
|
||||
.join(&resource_path);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
let _ = tokio::fs::create_dir_all(&parent).await;
|
||||
};
|
||||
if let Err(e) = tokio::fs::write(&file_path, &res.toml)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write resource file to {file_path:?}",)
|
||||
})
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
} else {
|
||||
update.push_simple_log(
|
||||
"Write contents",
|
||||
format!("File contents written to {file_path:?}"),
|
||||
);
|
||||
}
|
||||
} else if !sync.config.repo.is_empty() {
|
||||
// GIT REPO
|
||||
let args: CloneArgs = (&sync).into();
|
||||
let root = args.unique_path(&core_config().repo_directory)?;
|
||||
match git::write_commit_file(
|
||||
"Commit Sync",
|
||||
&root,
|
||||
&resource_path,
|
||||
&res.toml,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => update.logs.extend(res.logs),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
}
|
||||
}
|
||||
// ===========
|
||||
// UI DEFINED
|
||||
} else if let Err(e) = db_client()
|
||||
.resource_syncs
|
||||
.update_one(
|
||||
doc! { "name": &sync.name },
|
||||
doc! { "$set": { "config.file_contents": res.toml } },
|
||||
)
|
||||
.await
|
||||
.context("failed to update file_contents on db")
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource to database",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
}
|
||||
|
||||
let res = match State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
|
||||
.await
|
||||
{
|
||||
Ok(sync) => Ok(sync),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Refresh sync pending",
|
||||
format_serror(&(&e).into()),
|
||||
);
|
||||
Err(e)
|
||||
}
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_resource_sync_state_cache().await;
|
||||
}
|
||||
update_update(update).await?;
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
#[instrument(
|
||||
name = "RefreshResourceSyncPending",
|
||||
@@ -190,6 +458,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.servers,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -199,6 +469,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.stacks,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -208,6 +480,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.deployments,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -217,6 +491,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.builds,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -226,6 +502,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.repos,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -235,6 +513,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.procedures,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -244,6 +524,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.builders,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -253,6 +535,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.alerters,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -262,6 +546,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.server_templates,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -271,6 +557,8 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
resources.resource_syncs,
|
||||
delete,
|
||||
&all_resources,
|
||||
None,
|
||||
None,
|
||||
&id_to_tags,
|
||||
&sync.config.match_tags,
|
||||
&mut diffs,
|
||||
@@ -278,22 +566,28 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let variable_updates =
|
||||
let variable_updates = if sync.config.match_tags.is_empty() {
|
||||
crate::sync::variables::get_updates_for_view(
|
||||
&resources.variables,
|
||||
// Delete doesn't work with variables when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let user_group_updates =
|
||||
let user_group_updates = if sync.config.match_tags.is_empty() {
|
||||
crate::sync::user_groups::get_updates_for_view(
|
||||
resources.user_groups,
|
||||
// Delete doesn't work with user groups when match tags are set
|
||||
sync.config.match_tags.is_empty() && delete,
|
||||
&all_resources,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
anyhow::Ok((
|
||||
diffs,
|
||||
@@ -418,135 +712,6 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<CommitSync, User> for State {
|
||||
#[instrument(name = "CommitSync", skip(self, user))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
CommitSync { sync }: CommitSync,
|
||||
user: User,
|
||||
) -> anyhow::Result<ResourceSync> {
|
||||
let sync = resource::get_check_permissions::<
|
||||
entities::sync::ResourceSync,
|
||||
>(&sync, &user, PermissionLevel::Write)
|
||||
.await?;
|
||||
|
||||
let fresh_sync = !sync.config.files_on_host
|
||||
&& sync.config.file_contents.is_empty()
|
||||
&& sync.config.repo.is_empty();
|
||||
|
||||
if !sync.config.managed && !fresh_sync {
|
||||
return Err(anyhow!(
|
||||
"Cannot commit to sync. Enabled 'managed' mode."
|
||||
));
|
||||
}
|
||||
|
||||
let res = State
|
||||
.resolve(
|
||||
ExportAllResourcesToToml {
|
||||
tags: sync.config.match_tags,
|
||||
},
|
||||
sync_user().to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut update = make_update(
|
||||
ResourceTarget::ResourceSync(sync.id),
|
||||
Operation::CommitSync,
|
||||
&user,
|
||||
);
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
if sync.config.files_on_host {
|
||||
let path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Resource path is not valid file path")?;
|
||||
let extension = path
|
||||
.extension()
|
||||
.context("Resource path missing '.toml' extension")?;
|
||||
if extension != "toml" {
|
||||
return Err(anyhow!("Wrong file extension. Expected '.toml', got '.{extension:?}'"));
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = tokio::fs::create_dir_all(&parent).await;
|
||||
};
|
||||
if let Err(e) =
|
||||
tokio::fs::write(&sync.config.resource_path, &res.toml)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to write resource file to {}",
|
||||
sync.config.resource_path
|
||||
)
|
||||
})
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
}
|
||||
} else if let Err(e) = db_client()
|
||||
.resource_syncs
|
||||
.update_one(
|
||||
doc! { "name": &sync.name },
|
||||
doc! { "$set": { "config.file_contents": &res.toml } },
|
||||
)
|
||||
.await
|
||||
.context("failed to update file_contents on db")
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource to database",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
update.finalize();
|
||||
add_update(update).await?;
|
||||
return resource::get::<ResourceSync>(&sync.name).await;
|
||||
}
|
||||
|
||||
update
|
||||
.logs
|
||||
.push(Log::simple("Committed resources", res.toml));
|
||||
|
||||
let res = match State
|
||||
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
|
||||
.await
|
||||
{
|
||||
Ok(sync) => Ok(sync),
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Refresh sync pending",
|
||||
format_serror(&(&e).into()),
|
||||
);
|
||||
Err(e)
|
||||
}
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
// but will fail to update cache in that case.
|
||||
if let Ok(update_doc) = to_document(&update) {
|
||||
let _ = update_one_by_id(
|
||||
&db_client().updates,
|
||||
&update.id,
|
||||
mungos::update::Update::Set(update_doc),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
refresh_resource_sync_state_cache().await;
|
||||
}
|
||||
update_update(update).await?;
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<CreateSyncWebhook, User> for State {
|
||||
#[instrument(name = "CreateSyncWebhook", skip(self, user))]
|
||||
async fn resolve(
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use komodo_client::{
|
||||
api::write::{
|
||||
DeleteUser, DeleteUserResponse, UpdateUserPassword,
|
||||
UpdateUserPasswordResponse, UpdateUserUsername,
|
||||
UpdateUserUsernameResponse,
|
||||
},
|
||||
entities::{
|
||||
user::{User, UserConfig},
|
||||
NoData,
|
||||
},
|
||||
};
|
||||
use mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::hash_password,
|
||||
state::{db_client, State},
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UpdateUserUsername, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateUserUsername { username }: UpdateUserUsername,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateUserUsernameResponse> {
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username cannot be empty."));
|
||||
}
|
||||
let db = db_client();
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &username })
|
||||
.await
|
||||
.context("Failed to query for existing users")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!("Username already taken."));
|
||||
}
|
||||
let id = ObjectId::from_str(&user.id)
|
||||
.context("User id not valid ObjectId.")?;
|
||||
db.users
|
||||
.update_one(
|
||||
doc! { "_id": id },
|
||||
doc! { "$set": { "username": username } },
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user username on database.")?;
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UpdateUserPassword, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
UpdateUserPassword { password }: UpdateUserPassword,
|
||||
user: User,
|
||||
) -> anyhow::Result<UpdateUserPasswordResponse> {
|
||||
let UserConfig::Local { .. } = user.config else {
|
||||
return Err(anyhow!("User is not local user"));
|
||||
};
|
||||
if password.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty."));
|
||||
}
|
||||
let id = ObjectId::from_str(&user.id)
|
||||
.context("User id not valid ObjectId.")?;
|
||||
let hashed_password = hash_password(password)?;
|
||||
db_client()
|
||||
.users
|
||||
.update_one(
|
||||
doc! { "_id": id },
|
||||
doc! { "$set": {
|
||||
"config.data.password": hashed_password
|
||||
} },
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user password on database.")?;
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<DeleteUser, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
DeleteUser { user }: DeleteUser,
|
||||
admin: User,
|
||||
) -> anyhow::Result<DeleteUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("Calling user is not admin."));
|
||||
}
|
||||
if admin.username == user || admin.id == user {
|
||||
return Err(anyhow!("User cannot delete themselves."));
|
||||
}
|
||||
let query = if let Ok(id) = ObjectId::from_str(&user) {
|
||||
doc! { "_id": id }
|
||||
} else {
|
||||
doc! { "username": user }
|
||||
};
|
||||
let db = db_client();
|
||||
let Some(user) = db
|
||||
.users
|
||||
.find_one(query.clone())
|
||||
.await
|
||||
.context("Failed to query database for users.")?
|
||||
else {
|
||||
return Err(anyhow!("No user found with given id / username"));
|
||||
};
|
||||
if user.super_admin {
|
||||
return Err(anyhow!("Cannot delete a super admin user."));
|
||||
}
|
||||
if user.admin && !admin.super_admin {
|
||||
return Err(anyhow!(
|
||||
"Only a Super Admin can delete an admin user."
|
||||
));
|
||||
}
|
||||
db.users
|
||||
.delete_one(query)
|
||||
.await
|
||||
.context("Failed to delete user from database")?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,10 @@ use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
state::State,
|
||||
state::{db_client, jwt_client},
|
||||
helpers::hash_password,
|
||||
state::{db_client, jwt_client, State},
|
||||
};
|
||||
|
||||
const BCRYPT_COST: u32 = 10;
|
||||
|
||||
impl Resolve<CreateLocalUser, HeaderMap> for State {
|
||||
#[instrument(name = "CreateLocalUser", skip(self))]
|
||||
async fn resolve(
|
||||
@@ -47,8 +45,7 @@ impl Resolve<CreateLocalUser, HeaderMap> for State {
|
||||
return Err(anyhow!("Password cannot be empty string"));
|
||||
}
|
||||
|
||||
let password = bcrypt::hash(password, BCRYPT_COST)
|
||||
.context("failed to hash password")?;
|
||||
let hashed_password = hash_password(password)?;
|
||||
|
||||
let no_users_exist =
|
||||
db_client().users.find_one(Document::new()).await?.is_none();
|
||||
@@ -71,7 +68,9 @@ impl Resolve<CreateLocalUser, HeaderMap> for State {
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Local { password },
|
||||
config: UserConfig::Local {
|
||||
password: hashed_password,
|
||||
},
|
||||
};
|
||||
|
||||
let user_id = db_client()
|
||||
|
||||
@@ -152,8 +152,21 @@ async fn callback(
|
||||
let id_token = token_response
|
||||
.id_token()
|
||||
.context("OIDC Server did not return an ID token")?;
|
||||
|
||||
// Some providers attach additional audiences, they must be added here
|
||||
// so token verification succeeds.
|
||||
let verifier = client.id_token_verifier();
|
||||
let additional_audiences = &core_config().oidc_additional_audiences;
|
||||
let verifier = if additional_audiences.is_empty() {
|
||||
verifier
|
||||
} else {
|
||||
verifier.set_other_audience_verifier_fn(|aud| {
|
||||
additional_audiences.contains(aud)
|
||||
})
|
||||
};
|
||||
|
||||
let claims = id_token
|
||||
.claims(&client.id_token_verifier(), &nonce)
|
||||
.claims(&verifier, &nonce)
|
||||
.context("Failed to verify token claims")?;
|
||||
|
||||
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||
|
||||
@@ -87,6 +87,9 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.unwrap_or(config.oidc_client_secret),
|
||||
oidc_use_full_email: env.komodo_oidc_use_full_email
|
||||
.unwrap_or(config.oidc_use_full_email),
|
||||
oidc_additional_audiences: maybe_read_list_from_file(env.komodo_oidc_additional_audiences_file,env
|
||||
.komodo_oidc_additional_audiences)
|
||||
.unwrap_or(config.oidc_additional_audiences),
|
||||
google_oauth: OauthCredentials {
|
||||
enabled: env
|
||||
.komodo_google_oauth_enabled
|
||||
@@ -141,6 +144,9 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
jwt_ttl: env
|
||||
.komodo_jwt_ttl
|
||||
.unwrap_or(config.jwt_ttl),
|
||||
sync_directory: env
|
||||
.komodo_sync_directory
|
||||
.unwrap_or(config.sync_directory),
|
||||
repo_directory: env
|
||||
.komodo_repo_directory
|
||||
.unwrap_or(config.repo_directory),
|
||||
|
||||
+50
-14
@@ -3,8 +3,9 @@ use std::{str::FromStr, time::Duration};
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::future::join_all;
|
||||
use komodo_client::{
|
||||
api::write::CreateServer,
|
||||
api::write::{CreateBuilder, CreateServer},
|
||||
entities::{
|
||||
builder::{PartialBuilderConfig, PartialServerBuilderConfig},
|
||||
komodo_timestamp,
|
||||
permission::{Permission, PermissionLevel, UserTarget},
|
||||
server::{PartialServerConfig, Server},
|
||||
@@ -65,6 +66,15 @@ pub fn random_string(length: usize) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
const BCRYPT_COST: u32 = 10;
|
||||
pub fn hash_password<P>(password: P) -> anyhow::Result<String>
|
||||
where
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
bcrypt::hash(password, BCRYPT_COST)
|
||||
.context("failed to hash password")
|
||||
}
|
||||
|
||||
/// First checks db for token, then checks core config.
|
||||
/// Only errors if db call errors.
|
||||
/// Returns (token, use_https)
|
||||
@@ -280,8 +290,8 @@ async fn startup_open_alert_cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures a default server exists with the defined address
|
||||
pub async fn ensure_first_server() {
|
||||
/// Ensures a default server / builder exists with the defined address
|
||||
pub async fn ensure_first_server_and_builder() {
|
||||
let first_server = &core_config().first_server;
|
||||
if first_server.is_empty() {
|
||||
return;
|
||||
@@ -295,23 +305,49 @@ pub async fn ensure_first_server() {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if server.is_some() {
|
||||
return;
|
||||
}
|
||||
let server = if let Some(server) = server {
|
||||
server
|
||||
} else {
|
||||
match State
|
||||
.resolve(
|
||||
CreateServer {
|
||||
name: format!("server-{}", random_string(5)),
|
||||
config: PartialServerConfig {
|
||||
address: Some(first_server.to_string()),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
system_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(server) => server,
|
||||
Err(e) => {
|
||||
error!("Failed to initialize 'first_server'. Failed to CreateServer. {e:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
let Ok(None) = db.builders
|
||||
.find_one(Document::new()).await
|
||||
.inspect_err(|e| error!("Failed to initialize 'first_builder'. Failed to query db. {e:?}")) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = State
|
||||
.resolve(
|
||||
CreateServer {
|
||||
name: format!("server-{}", random_string(5)),
|
||||
config: PartialServerConfig {
|
||||
address: Some(first_server.to_string()),
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
CreateBuilder {
|
||||
name: String::from("local"),
|
||||
config: PartialBuilderConfig::Server(
|
||||
PartialServerBuilderConfig {
|
||||
server_id: Some(server.id),
|
||||
},
|
||||
),
|
||||
},
|
||||
system_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to initialize 'first_server'. Failed to CreateServer. {e:?}");
|
||||
error!("Failed to initialize 'first_builder'. Failed to CreateBuilder. {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,6 +722,22 @@ async fn execute_execution(
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::DeployStackIfChanged(req) => {
|
||||
let req = ExecuteRequest::DeployStackIfChanged(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStackIfChanged(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
let update_id = update.id.clone();
|
||||
handle_resolve_result(
|
||||
State
|
||||
.resolve(req, (user, update))
|
||||
.await
|
||||
.context("Failed at DeployStackIfChanged"),
|
||||
&update_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Execution::StartStack(req) => {
|
||||
let req = ExecuteRequest::StartStack(req);
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
|
||||
@@ -370,6 +370,12 @@ pub async fn init_execution_update(
|
||||
resource::get::<Stack>(&data.stack).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::DeployStackIfChanged(data) => (
|
||||
Operation::DeployStack,
|
||||
ResourceTarget::Stack(
|
||||
resource::get::<Stack>(&data.stack).await?.id,
|
||||
),
|
||||
),
|
||||
ExecuteRequest::StartStack(data) => (
|
||||
if data.service.is_some() {
|
||||
Operation::StartStackService
|
||||
@@ -429,7 +435,10 @@ pub async fn init_execution_update(
|
||||
};
|
||||
let mut update = make_update(target, operation, user);
|
||||
update.in_progress();
|
||||
// Don't actually send it here, let the handlers send it after they can set action state.
|
||||
update.id = add_update_without_send(&update).await?;
|
||||
// Hold off on even adding update for DeployStackIfChanged
|
||||
if !matches!(&request, ExecuteRequest::DeployStackIfChanged(_)) {
|
||||
// Don't actually send it here, let the handlers send it after they can set action state.
|
||||
update.id = add_update_without_send(&update).await?;
|
||||
}
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::sync::OnceLock;
|
||||
use anyhow::anyhow;
|
||||
use axum::http::HeaderMap;
|
||||
use komodo_client::{
|
||||
api::{execute::DeployStack, write::RefreshStackCache},
|
||||
api::{
|
||||
execute::{DeployStack, DeployStackIfChanged},
|
||||
write::RefreshStackCache,
|
||||
},
|
||||
entities::{stack::Stack, user::git_webhook_user},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
@@ -78,14 +81,28 @@ pub async fn handle_stack_deploy_webhook(
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::DeployStack(DeployStack {
|
||||
stack: stack_id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStack(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
if stack.config.webhook_force_deploy {
|
||||
let req = ExecuteRequest::DeployStack(DeployStack {
|
||||
stack: stack_id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStack(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
} else {
|
||||
let req =
|
||||
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
|
||||
stack: stack_id,
|
||||
stop_time: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::DeployStackIfChanged(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -78,7 +78,11 @@ pub async fn handle_sync_execute_webhook(
|
||||
}
|
||||
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = ExecuteRequest::RunSync(RunSync { sync: sync_id });
|
||||
let req = ExecuteRequest::RunSync(RunSync {
|
||||
sync: sync_id,
|
||||
resource_type: None,
|
||||
resources: None,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let ExecuteRequest::RunSync(req) = req else {
|
||||
unreachable!()
|
||||
|
||||
@@ -44,7 +44,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
);
|
||||
tokio::join!(
|
||||
// Maybe initialize first server
|
||||
helpers::ensure_first_server(),
|
||||
helpers::ensure_first_server_and_builder(),
|
||||
// Cleanup open updates / invalid alerts
|
||||
helpers::startup_cleanup(),
|
||||
);
|
||||
|
||||
@@ -206,7 +206,10 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
};
|
||||
|
||||
match lists::get_docker_lists(&periphery).await {
|
||||
Ok((containers, networks, images, volumes, projects)) => {
|
||||
Ok((mut containers, networks, images, volumes, projects)) => {
|
||||
containers.iter_mut().for_each(|container| {
|
||||
container.server_id = Some(server.id.clone())
|
||||
});
|
||||
tokio::join!(
|
||||
resources::update_deployment_cache(deployments, &containers),
|
||||
resources::update_stack_cache(stacks, &containers),
|
||||
|
||||
@@ -57,6 +57,7 @@ impl super::KomodoResource for Build {
|
||||
version: build.config.version,
|
||||
builder_id: build.config.builder_id,
|
||||
git_provider: build.config.git_provider,
|
||||
image_registry_domain: build.config.image_registry.domain,
|
||||
repo: build.config.repo,
|
||||
branch: build.config.branch,
|
||||
built_hash: build.info.built_hash,
|
||||
@@ -179,9 +180,13 @@ async fn validate_config(
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(builder_id) = &config.builder_id {
|
||||
if !builder_id.is_empty() {
|
||||
let builder = super::get_check_permissions::<Builder>(builder_id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create build using this builder. user must have at least read permissions on the builder.")?;
|
||||
let builder = super::get_check_permissions::<Builder>(
|
||||
builder_id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
.context("Cannot attach Build to this Builder")?;
|
||||
config.builder_id = Some(builder.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,9 +262,13 @@ async fn validate_config(
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(server_id) = &config.server_id {
|
||||
if !server_id.is_empty() {
|
||||
let server = get_check_permissions::<Server>(server_id, user, PermissionLevel::Write)
|
||||
.await
|
||||
.context("cannot create deployment on this server. user must have update permissions on the server to perform this action.")?;
|
||||
let server = get_check_permissions::<Server>(
|
||||
server_id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
.context("Cannot attach Deployment to this Server")?;
|
||||
config.server_id = Some(server.id);
|
||||
}
|
||||
}
|
||||
@@ -272,9 +276,15 @@ async fn validate_config(
|
||||
&config.image
|
||||
{
|
||||
if !build_id.is_empty() {
|
||||
let build = get_check_permissions::<Build>(build_id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create deployment with this build attached. user must have at least read permissions on the build to perform this action.")?;
|
||||
let build = get_check_permissions::<Build>(
|
||||
build_id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Cannot update deployment with this build attached.",
|
||||
)?;
|
||||
config.image = Some(DeploymentImage::Build {
|
||||
build_id: build.id,
|
||||
version: *version,
|
||||
|
||||
@@ -15,7 +15,7 @@ use komodo_client::{
|
||||
tag::Tag,
|
||||
to_komodo_name,
|
||||
update::Update,
|
||||
user::User,
|
||||
user::{system_user, User},
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
},
|
||||
};
|
||||
@@ -228,7 +228,7 @@ pub async fn get_check_permissions<T: KomodoResource>(
|
||||
Ok(resource)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"user does not have required permissions on this {}",
|
||||
"User does not have required permissions on this {}. Must have at least {permission_level} permissions",
|
||||
T::resource_type()
|
||||
))
|
||||
}
|
||||
@@ -508,6 +508,17 @@ pub async fn create<T: KomodoResource>(
|
||||
return Err(anyhow!("valid ObjectIds cannot be used as names."));
|
||||
}
|
||||
|
||||
// Ensure an existing resource with same name doesn't already exist
|
||||
// The database indexing also ensures this but doesn't give a good error message.
|
||||
if list_full_for_user::<T>(Default::default(), system_user())
|
||||
.await
|
||||
.context("Failed to list all resources for duplicate name check")?
|
||||
.into_iter()
|
||||
.any(|r| r.name == name)
|
||||
{
|
||||
return Err(anyhow!("Must provide unique name for resource."));
|
||||
}
|
||||
|
||||
let start_ts = komodo_timestamp();
|
||||
|
||||
T::validate_create_config(&mut config, user).await?;
|
||||
|
||||
@@ -503,6 +503,15 @@ async fn validate_config(
|
||||
.await?;
|
||||
params.stack = stack.id;
|
||||
}
|
||||
Execution::DeployStackIfChanged(params) => {
|
||||
let stack = super::get_check_permissions::<Stack>(
|
||||
¶ms.stack,
|
||||
user,
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
params.stack = stack.id;
|
||||
}
|
||||
Execution::StartStack(params) => {
|
||||
let stack = super::get_check_permissions::<Stack>(
|
||||
¶ms.stack,
|
||||
|
||||
@@ -213,15 +213,19 @@ async fn validate_config(
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
.context("Cannot attach repo to this server. User must have write permissions on the server.")?;
|
||||
.context("Cannot attach Repo to this Server")?;
|
||||
config.server_id = Some(server.id);
|
||||
}
|
||||
}
|
||||
if let Some(builder_id) = &config.builder_id {
|
||||
if !builder_id.is_empty() {
|
||||
let builder = super::get_check_permissions::<Builder>(builder_id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("Cannot attach repo to this builder. User must have at least read permissions on the builder.")?;
|
||||
let builder = super::get_check_permissions::<Builder>(
|
||||
builder_id,
|
||||
user,
|
||||
PermissionLevel::Read,
|
||||
)
|
||||
.await
|
||||
.context("Cannot attach Repo to this Builder")?;
|
||||
config.builder_id = Some(builder.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +301,13 @@ async fn validate_config(
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(server_id) = &config.server_id {
|
||||
if !server_id.is_empty() {
|
||||
let server = get_check_permissions::<Server>(server_id, user, PermissionLevel::Write)
|
||||
.await
|
||||
.context("cannot create stack on this server. user must have update permissions on the server to perform this action.")?;
|
||||
let server = get_check_permissions::<Server>(
|
||||
server_id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
.context("Cannot attach stack to this Server")?;
|
||||
// in case it comes in as name
|
||||
config.server_id = Some(server.id);
|
||||
}
|
||||
|
||||
+40
-37
@@ -18,7 +18,7 @@ use komodo_client::{
|
||||
toml::ResourceToml,
|
||||
update::Log,
|
||||
user::sync_user,
|
||||
ResourceTarget,
|
||||
FileContents, ResourceTarget,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
@@ -26,7 +26,6 @@ use resolver_api::Resolve;
|
||||
use crate::{
|
||||
api::execute::ExecuteRequest,
|
||||
helpers::update::init_execution_update,
|
||||
stack::remote::ensure_remote_repo,
|
||||
state::{deployment_status_cache, stack_status_cache, State},
|
||||
};
|
||||
|
||||
@@ -541,7 +540,45 @@ fn build_cache_for_stack<'a>(
|
||||
StackState::Running => {
|
||||
// Here can diff the changes, to see if they merit a redeploy.
|
||||
|
||||
// First merge toml resource config (partial) onto default resource config.
|
||||
// See if any remote contents don't match deployed contents
|
||||
match (
|
||||
&original.info.deployed_contents,
|
||||
&original.info.remote_contents,
|
||||
) {
|
||||
(Some(deployed_contents), Some(remote_contents)) => {
|
||||
for FileContents { path, contents } in remote_contents {
|
||||
if let Some(deployed) =
|
||||
deployed_contents.iter().find(|c| &c.path == path)
|
||||
{
|
||||
if &deployed.contents != contents {
|
||||
cache.insert(
|
||||
target,
|
||||
Some((
|
||||
format!(
|
||||
"File contents for {path} have changed"
|
||||
),
|
||||
after,
|
||||
)),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
cache.insert(
|
||||
target,
|
||||
Some((
|
||||
format!("New file contents at {path}"),
|
||||
after,
|
||||
)),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Maybe should handle other cases
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Merge toml resource config (partial) onto default resource config.
|
||||
// Makes sure things that aren't defined in toml (come through as None) actually get removed.
|
||||
let config: StackConfig = stack.config.clone().into();
|
||||
let mut config: PartialStackConfig = config.into();
|
||||
@@ -589,40 +626,6 @@ fn build_cache_for_stack<'a>(
|
||||
}
|
||||
};
|
||||
|
||||
// We know the config hasn't changed at this point, but still need
|
||||
// to check if its a repo based stack, and the hash has updated.
|
||||
// Can use 'original' for this (config hasn't changed)
|
||||
if stack.latest_hash {
|
||||
if let Some(deployed_hash) = &original.info.deployed_hash {
|
||||
let (_, _, hash, _) = ensure_remote_repo(original.into())
|
||||
.await
|
||||
.context("failed to get latest hash for repo based stack")
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Stack {} {}",
|
||||
bold(&stack.name),
|
||||
colored("has errors", Color::Red)
|
||||
)
|
||||
})?;
|
||||
if let Some(hash) = hash {
|
||||
if &hash != deployed_hash {
|
||||
cache.insert(
|
||||
target,
|
||||
Some((
|
||||
format!(
|
||||
"outdated hash. deployed: {} -> latest: {}",
|
||||
colored(deployed_hash, Color::Red),
|
||||
colored(hash, Color::Green)
|
||||
),
|
||||
after,
|
||||
)),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 'after' to see if they deploy.
|
||||
insert_target_using_after_list(
|
||||
target,
|
||||
|
||||
@@ -6,6 +6,7 @@ use komodo_client::{
|
||||
api::write::{UpdateDescription, UpdateTagsOnResource},
|
||||
entities::{
|
||||
tag::Tag, toml::ResourceToml, update::Log, user::sync_user,
|
||||
ResourceTargetVariant,
|
||||
},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
@@ -26,6 +27,8 @@ pub async fn get_updates_for_execution<
|
||||
resources: Vec<ResourceToml<Resource::PartialConfig>>,
|
||||
delete: bool,
|
||||
all_resources: &AllResourcesById,
|
||||
match_resource_type: Option<ResourceTargetVariant>,
|
||||
match_resources: Option<&[String]>,
|
||||
id_to_tags: &HashMap<String, Tag>,
|
||||
match_tags: &[String],
|
||||
) -> anyhow::Result<UpdatesResult<Resource::PartialConfig>> {
|
||||
@@ -35,11 +38,31 @@ pub async fn get_updates_for_execution<
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
Resource::include_resource(
|
||||
&r.config, &r.tags, id_to_tags, match_tags,
|
||||
&r.name,
|
||||
&r.config,
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
&r.tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
)
|
||||
})
|
||||
.map(|r| (r.name.clone(), r))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let resources = resources
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
Resource::include_resource_partial(
|
||||
&r.name,
|
||||
&r.config,
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
&r.tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut to_create = ToCreate::<Resource::PartialConfig>::new();
|
||||
let mut to_update = ToUpdate::<Resource::PartialConfig>::new();
|
||||
@@ -54,15 +77,6 @@ pub async fn get_updates_for_execution<
|
||||
}
|
||||
|
||||
for mut resource in resources {
|
||||
// only resource that might not be included is resource sync
|
||||
if !Resource::include_resource_partial(
|
||||
&resource.config,
|
||||
&resource.tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
match map.get(&resource.name) {
|
||||
Some(original) => {
|
||||
// First merge toml resource config (partial) onto default resource config.
|
||||
|
||||
+182
-73
@@ -1,91 +1,201 @@
|
||||
use std::{fs, path::Path};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{colored, format_serror, muted, Color};
|
||||
use formatting::{bold, colored, format_serror, muted, Color};
|
||||
use komodo_client::entities::{
|
||||
sync::SyncFileContents,
|
||||
toml::{ResourceToml, ResourcesToml},
|
||||
update::Log,
|
||||
FileContents,
|
||||
};
|
||||
|
||||
pub fn read_resources(
|
||||
path: &Path,
|
||||
root_path: &Path,
|
||||
resource_path: &[String],
|
||||
match_tags: &[String],
|
||||
logs: &mut Vec<Log>,
|
||||
files: &mut Vec<FileContents>,
|
||||
file_errors: &mut Vec<FileContents>,
|
||||
files: &mut Vec<SyncFileContents>,
|
||||
file_errors: &mut Vec<SyncFileContents>,
|
||||
) -> anyhow::Result<ResourcesToml> {
|
||||
let mut res = ResourcesToml::default();
|
||||
let mut log =
|
||||
format!("{}: reading resources from {path:?}", muted("INFO"));
|
||||
if let Err(e) = read_resources_recursive(
|
||||
path,
|
||||
match_tags,
|
||||
&mut res,
|
||||
&mut log,
|
||||
files,
|
||||
file_errors,
|
||||
)
|
||||
.with_context(|| format!("failed to read resources from {path:?}"))
|
||||
{
|
||||
file_errors.push(FileContents {
|
||||
path: path.display().to_string(),
|
||||
contents: format_serror(&e.into()),
|
||||
});
|
||||
logs.push(Log::error("read remote resources", log));
|
||||
} else {
|
||||
logs.push(Log::simple("read remote resources", log));
|
||||
};
|
||||
Ok(res)
|
||||
let mut resources = ResourcesToml::default();
|
||||
|
||||
for resource_path in resource_path {
|
||||
let resource_path = resource_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid resource path")?;
|
||||
let full_path = root_path
|
||||
.join(&resource_path)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
|
||||
let mut log = format!(
|
||||
"{}: reading resources from {full_path:?}",
|
||||
muted("INFO")
|
||||
);
|
||||
|
||||
if full_path.is_file() {
|
||||
if let Err(e) = read_resource_file(
|
||||
root_path,
|
||||
None,
|
||||
&resource_path,
|
||||
match_tags,
|
||||
&mut resources,
|
||||
&mut log,
|
||||
files,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("failed to read resources from {full_path:?}")
|
||||
}) {
|
||||
file_errors.push(SyncFileContents {
|
||||
resource_path: String::new(),
|
||||
path: resource_path.display().to_string(),
|
||||
contents: format_serror(&e.into()),
|
||||
});
|
||||
logs.push(Log::error("Read remote resources", log));
|
||||
} else {
|
||||
logs.push(Log::simple("Read remote resources", log));
|
||||
};
|
||||
} else if full_path.is_dir() {
|
||||
if let Err(e) = read_resources_directory(
|
||||
root_path,
|
||||
&resource_path,
|
||||
&PathBuf::new(),
|
||||
match_tags,
|
||||
&mut resources,
|
||||
&mut log,
|
||||
files,
|
||||
file_errors,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("Failed to read resources from {full_path:?}")
|
||||
}) {
|
||||
file_errors.push(SyncFileContents {
|
||||
resource_path: String::new(),
|
||||
path: resource_path.display().to_string(),
|
||||
contents: format_serror(&e.into()),
|
||||
});
|
||||
logs.push(Log::error("Read remote resources", log));
|
||||
} else {
|
||||
logs.push(Log::simple("Read remote resources", log));
|
||||
};
|
||||
} else {
|
||||
log.push_str(&format!(
|
||||
"{}: Resoure path {} is neither a file nor a directory.",
|
||||
colored("WARN", Color::Red),
|
||||
bold(resource_path.display())
|
||||
));
|
||||
logs.push(Log::error("Read remote resources", log));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resources)
|
||||
}
|
||||
|
||||
fn read_resources_recursive(
|
||||
path: &Path,
|
||||
/// Use when incoming resource path is a file.
|
||||
fn read_resource_file(
|
||||
root_path: &Path,
|
||||
// relative to root path.
|
||||
resource_path: Option<&Path>,
|
||||
// relative to resource path if provided, or root path.
|
||||
file_path: &Path,
|
||||
match_tags: &[String],
|
||||
resources: &mut ResourcesToml,
|
||||
log: &mut String,
|
||||
files: &mut Vec<FileContents>,
|
||||
file_errors: &mut Vec<FileContents>,
|
||||
files: &mut Vec<SyncFileContents>,
|
||||
) -> anyhow::Result<()> {
|
||||
let res =
|
||||
fs::metadata(path).context("failed to get path metadata")?;
|
||||
if res.is_file() {
|
||||
if !path
|
||||
.extension()
|
||||
.map(|ext| ext == "toml")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.context("failed to read file contents")?;
|
||||
files.push(FileContents {
|
||||
path: path.display().to_string(),
|
||||
contents: contents.clone(),
|
||||
});
|
||||
let more = toml::from_str::<ResourcesToml>(&contents)
|
||||
// the error without this comes through with multiple lines (\n) and looks bad
|
||||
.map_err(|e| anyhow!("{e:#}"))
|
||||
.context("failed to parse resource file contents")?;
|
||||
let full_path = if let Some(resource_path) = resource_path {
|
||||
root_path.join(resource_path).join(file_path)
|
||||
} else {
|
||||
root_path.join(file_path)
|
||||
};
|
||||
if !full_path
|
||||
.extension()
|
||||
.map(|ext| ext == "toml")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let contents = std::fs::read_to_string(&full_path)
|
||||
.context("failed to read file contents")?;
|
||||
|
||||
log.push('\n');
|
||||
log.push_str(&format!(
|
||||
"{}: {} from {}",
|
||||
muted("INFO"),
|
||||
colored("adding resources", Color::Green),
|
||||
colored(path.display(), Color::Blue)
|
||||
));
|
||||
files.push(SyncFileContents {
|
||||
resource_path: resource_path
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_default(),
|
||||
path: file_path.display().to_string(),
|
||||
contents: contents.clone(),
|
||||
});
|
||||
let more = toml::from_str::<ResourcesToml>(&contents)
|
||||
// the error without this comes through with multiple lines (\n) and looks bad
|
||||
.map_err(|e| anyhow!("{e:#}"))
|
||||
.context("failed to parse resource file contents")?;
|
||||
log.push('\n');
|
||||
let path_for_view =
|
||||
if let Some(resource_path) = resource_path.as_ref() {
|
||||
resource_path.join(&file_path)
|
||||
} else {
|
||||
file_path.to_path_buf()
|
||||
};
|
||||
log.push_str(&format!(
|
||||
"{}: {} from {}",
|
||||
muted("INFO"),
|
||||
colored("adding resources", Color::Green),
|
||||
colored(path_for_view.display(), Color::Blue)
|
||||
));
|
||||
|
||||
extend_resources(resources, more, match_tags);
|
||||
extend_resources(resources, more, match_tags);
|
||||
|
||||
Ok(())
|
||||
} else if res.is_dir() {
|
||||
let directory = fs::read_dir(path)
|
||||
.context("failed to read directory contents")?;
|
||||
for entry in directory.into_iter().flatten() {
|
||||
let path = entry.path();
|
||||
if let Err(e) = read_resources_recursive(
|
||||
&path,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads down into directories.
|
||||
fn read_resources_directory(
|
||||
root_path: &Path,
|
||||
// relative to root path.
|
||||
resource_path: &Path,
|
||||
// relative to resource path. start as empty path
|
||||
curr_path: &Path,
|
||||
match_tags: &[String],
|
||||
resources: &mut ResourcesToml,
|
||||
log: &mut String,
|
||||
files: &mut Vec<SyncFileContents>,
|
||||
file_errors: &mut Vec<SyncFileContents>,
|
||||
) -> anyhow::Result<()> {
|
||||
let full_resource_path = root_path.join(resource_path);
|
||||
let full_path = full_resource_path.join(&curr_path);
|
||||
let directory = fs::read_dir(&full_path).with_context(|| {
|
||||
format!("Failed to read directory contents at {full_path:?}")
|
||||
})?;
|
||||
for entry in directory.into_iter().flatten() {
|
||||
let path = entry.path();
|
||||
let curr_path =
|
||||
path.strip_prefix(&full_resource_path).unwrap_or(&path);
|
||||
if path.is_file() {
|
||||
if let Err(e) = read_resource_file(
|
||||
root_path,
|
||||
Some(resource_path),
|
||||
curr_path,
|
||||
match_tags,
|
||||
resources,
|
||||
log,
|
||||
files,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("failed to read resources from {full_path:?}")
|
||||
}) {
|
||||
file_errors.push(SyncFileContents {
|
||||
resource_path: String::new(),
|
||||
path: resource_path.display().to_string(),
|
||||
contents: format_serror(&e.into()),
|
||||
});
|
||||
};
|
||||
} else if path.is_dir() {
|
||||
if let Err(e) = read_resources_directory(
|
||||
root_path,
|
||||
resource_path,
|
||||
curr_path,
|
||||
match_tags,
|
||||
resources,
|
||||
log,
|
||||
@@ -95,8 +205,9 @@ fn read_resources_recursive(
|
||||
.with_context(|| {
|
||||
format!("failed to read resources from {path:?}")
|
||||
}) {
|
||||
file_errors.push(FileContents {
|
||||
path: path.display().to_string(),
|
||||
file_errors.push(SyncFileContents {
|
||||
resource_path: resource_path.display().to_string(),
|
||||
path: curr_path.display().to_string(),
|
||||
contents: format_serror(&e.into()),
|
||||
});
|
||||
log.push('\n');
|
||||
@@ -108,10 +219,8 @@ fn read_resources_recursive(
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("resources path is neither file nor directory"))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extend_resources(
|
||||
|
||||
@@ -5,10 +5,13 @@ use komodo_client::entities::{
|
||||
deployment::Deployment, procedure::Procedure, repo::Repo,
|
||||
server::Server, server_template::ServerTemplate, stack::Stack,
|
||||
sync::ResourceSync, tag::Tag, toml::ResourceToml, ResourceTarget,
|
||||
ResourceTargetVariant,
|
||||
};
|
||||
use mungos::mongodb::bson::oid::ObjectId;
|
||||
use toml::ToToml;
|
||||
|
||||
use crate::resource::KomodoResource;
|
||||
|
||||
pub mod deploy;
|
||||
pub mod execute;
|
||||
pub mod file;
|
||||
@@ -38,22 +41,44 @@ pub trait ResourceSyncTrait: ToToml + Sized {
|
||||
|
||||
/// To exclude resource syncs with "file_contents" (they aren't compatible)
|
||||
fn include_resource(
|
||||
name: &String,
|
||||
_config: &Self::Config,
|
||||
match_resource_type: Option<ResourceTargetVariant>,
|
||||
match_resources: Option<&[String]>,
|
||||
resource_tags: &[String],
|
||||
id_to_tags: &HashMap<String, Tag>,
|
||||
match_tags: &[String],
|
||||
) -> bool {
|
||||
include_resource_by_tags(resource_tags, id_to_tags, match_tags)
|
||||
include_resource_by_resource_type_and_name::<Self>(
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
name,
|
||||
) && include_resource_by_tags(
|
||||
resource_tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
)
|
||||
}
|
||||
|
||||
/// To exclude resource syncs with "file_contents" (they aren't compatible)
|
||||
fn include_resource_partial(
|
||||
name: &String,
|
||||
_config: &Self::PartialConfig,
|
||||
match_resource_type: Option<ResourceTargetVariant>,
|
||||
match_resources: Option<&[String]>,
|
||||
resource_tags: &[String],
|
||||
id_to_tags: &HashMap<String, Tag>,
|
||||
match_tags: &[String],
|
||||
) -> bool {
|
||||
include_resource_by_tags(resource_tags, id_to_tags, match_tags)
|
||||
include_resource_by_resource_type_and_name::<Self>(
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
name,
|
||||
) && include_resource_by_tags(
|
||||
resource_tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
)
|
||||
}
|
||||
|
||||
/// Apply any changes to incoming toml partial config
|
||||
@@ -90,6 +115,31 @@ pub fn include_resource_by_tags(
|
||||
match_tags.iter().all(|tag| tag_names.contains(&tag))
|
||||
}
|
||||
|
||||
pub fn include_resource_by_resource_type_and_name<
|
||||
T: KomodoResource,
|
||||
>(
|
||||
resource_type: Option<ResourceTargetVariant>,
|
||||
resources: Option<&[String]>,
|
||||
name: &String,
|
||||
) -> bool {
|
||||
match (resource_type, resources) {
|
||||
(Some(resource_type), Some(resources)) => {
|
||||
if T::resource_type() != resource_type {
|
||||
return false;
|
||||
}
|
||||
resources.contains(name)
|
||||
}
|
||||
(Some(resource_type), None) => {
|
||||
if T::resource_type() != resource_type {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
(None, Some(resources)) => resources.contains(name),
|
||||
(None, None) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AllResourcesById {
|
||||
pub servers: HashMap<String, Server>,
|
||||
pub deployments: HashMap<String, Deployment>,
|
||||
|
||||
+21
-24
@@ -1,10 +1,11 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use git::GitRes;
|
||||
use komodo_client::entities::{
|
||||
sync::ResourceSync, toml::ResourcesToml, update::Log, CloneArgs,
|
||||
FileContents,
|
||||
sync::{ResourceSync, SyncFileContents},
|
||||
to_komodo_name,
|
||||
toml::ResourcesToml,
|
||||
update::Log,
|
||||
CloneArgs,
|
||||
};
|
||||
|
||||
use crate::{config::core_config, helpers::git_token};
|
||||
@@ -13,8 +14,8 @@ use super::file::extend_resources;
|
||||
|
||||
pub struct RemoteResources {
|
||||
pub resources: anyhow::Result<ResourcesToml>,
|
||||
pub files: Vec<FileContents>,
|
||||
pub file_errors: Vec<FileContents>,
|
||||
pub files: Vec<SyncFileContents>,
|
||||
pub file_errors: Vec<SyncFileContents>,
|
||||
pub logs: Vec<Log>,
|
||||
pub hash: Option<String>,
|
||||
pub message: Option<String>,
|
||||
@@ -28,15 +29,14 @@ pub async fn get_remote_resources(
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
let path = sync
|
||||
.config
|
||||
.resource_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Resource path is not valid path")?;
|
||||
let root_path = core_config()
|
||||
.sync_directory
|
||||
.join(to_komodo_name(&sync.name));
|
||||
let (mut logs, mut files, mut file_errors) =
|
||||
(Vec::new(), Vec::new(), Vec::new());
|
||||
let resources = super::file::read_resources(
|
||||
&path,
|
||||
&root_path,
|
||||
&sync.config.resource_path,
|
||||
&sync.config.match_tags,
|
||||
&mut logs,
|
||||
&mut files,
|
||||
@@ -50,9 +50,7 @@ pub async fn get_remote_resources(
|
||||
hash: None,
|
||||
message: None,
|
||||
});
|
||||
} else if sync.config.managed
|
||||
|| !sync.config.file_contents.is_empty()
|
||||
{
|
||||
} else if sync.config.repo.is_empty() {
|
||||
// ==========
|
||||
// UI DEFINED
|
||||
// ==========
|
||||
@@ -72,10 +70,10 @@ pub async fn get_remote_resources(
|
||||
Ok(resources)
|
||||
};
|
||||
|
||||
// filter_by_
|
||||
return Ok(RemoteResources {
|
||||
resources,
|
||||
files: vec![FileContents {
|
||||
files: vec![SyncFileContents {
|
||||
resource_path: String::new(),
|
||||
path: "database file".to_string(),
|
||||
contents: sync.config.file_contents.clone(),
|
||||
}],
|
||||
@@ -101,10 +99,10 @@ pub async fn get_remote_resources(
|
||||
|
||||
let access_token = if let Some(account) = &clone_args.account {
|
||||
git_token(&clone_args.provider, account, |https| clone_args.https = https)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
|
||||
)?
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -139,11 +137,10 @@ pub async fn get_remote_resources(
|
||||
let message =
|
||||
message.context("failed to get commit hash message")?;
|
||||
|
||||
let resource_path = repo_path.join(&sync.config.resource_path);
|
||||
|
||||
let (mut files, mut file_errors) = (Vec::new(), Vec::new());
|
||||
let resources = super::file::read_resources(
|
||||
&resource_path,
|
||||
&repo_path,
|
||||
&sync.config.resource_path,
|
||||
&sync.config.match_tags,
|
||||
&mut logs,
|
||||
&mut files,
|
||||
|
||||
@@ -17,7 +17,7 @@ use komodo_client::{
|
||||
tag::Tag,
|
||||
update::Log,
|
||||
user::sync_user,
|
||||
ResourceTarget,
|
||||
ResourceTarget, ResourceTargetVariant,
|
||||
},
|
||||
};
|
||||
use partial_derive2::{MaybeNone, PartialDiff};
|
||||
@@ -31,8 +31,10 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
execute::ExecuteResourceSync, include_resource_by_tags,
|
||||
AllResourcesById, ResourceSyncTrait, ToCreate, ToDelete, ToUpdate,
|
||||
execute::ExecuteResourceSync,
|
||||
include_resource_by_resource_type_and_name,
|
||||
include_resource_by_tags, AllResourcesById, ResourceSyncTrait,
|
||||
ToCreate, ToDelete, ToUpdate,
|
||||
};
|
||||
|
||||
impl ResourceSyncTrait for Server {
|
||||
@@ -237,12 +239,19 @@ impl ResourceSyncTrait for ResourceSync {
|
||||
}
|
||||
|
||||
fn include_resource(
|
||||
name: &String,
|
||||
config: &Self::Config,
|
||||
match_resource_type: Option<ResourceTargetVariant>,
|
||||
match_resources: Option<&[String]>,
|
||||
resource_tags: &[String],
|
||||
id_to_tags: &HashMap<String, Tag>,
|
||||
match_tags: &[String],
|
||||
) -> bool {
|
||||
if !include_resource_by_tags(
|
||||
if !include_resource_by_resource_type_and_name::<ResourceSync>(
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
name,
|
||||
) || !include_resource_by_tags(
|
||||
resource_tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
@@ -259,17 +268,24 @@ impl ResourceSyncTrait for ResourceSync {
|
||||
}
|
||||
// The file contents MUST be empty
|
||||
contents_empty &&
|
||||
// The sync must be files on host mode OR NOT managed
|
||||
(config.files_on_host || !config.managed)
|
||||
// The sync must be files on host mode OR git repo mode
|
||||
(config.files_on_host || !config.repo.is_empty())
|
||||
}
|
||||
|
||||
fn include_resource_partial(
|
||||
name: &String,
|
||||
config: &Self::PartialConfig,
|
||||
match_resource_type: Option<ResourceTargetVariant>,
|
||||
match_resources: Option<&[String]>,
|
||||
resource_tags: &[String],
|
||||
id_to_tags: &HashMap<String, Tag>,
|
||||
match_tags: &[String],
|
||||
) -> bool {
|
||||
if !include_resource_by_tags(
|
||||
if !include_resource_by_resource_type_and_name::<ResourceSync>(
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
name,
|
||||
) || !include_resource_by_tags(
|
||||
resource_tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
@@ -291,8 +307,8 @@ impl ResourceSyncTrait for ResourceSync {
|
||||
}
|
||||
// The file contents MUST be empty
|
||||
contents_empty &&
|
||||
// The sync must be files on host mode OR NOT managed
|
||||
(files_on_host || !config.managed.unwrap_or_default())
|
||||
// The sync must be files on host mode OR git repo mode
|
||||
(files_on_host || !config.repo.as_deref().unwrap_or_default().is_empty())
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
@@ -579,6 +595,13 @@ impl ResourceSyncTrait for Procedure {
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::DeployStackIfChanged(config) => {
|
||||
config.stack = resources
|
||||
.stacks
|
||||
.get(&config.stack)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Execution::StartStack(config) => {
|
||||
config.stack = resources
|
||||
.stacks
|
||||
|
||||
@@ -105,6 +105,8 @@ pub fn resource_toml_to_toml_string<R: ToToml>(
|
||||
|
||||
pub fn resource_push_to_toml<R: ToToml>(
|
||||
mut resource: Resource<R::Config, R::Info>,
|
||||
deploy: bool,
|
||||
after: Vec<String>,
|
||||
toml: &mut String,
|
||||
all: &AllResourcesById,
|
||||
all_tags: &HashMap<String, Tag>,
|
||||
@@ -116,7 +118,7 @@ pub fn resource_push_to_toml<R: ToToml>(
|
||||
toml
|
||||
.push_str(&format!("[[{}]]\n", R::resource_type().toml_header()));
|
||||
R::push_to_toml_string(
|
||||
convert_resource::<R>(resource, all_tags),
|
||||
convert_resource::<R>(resource, deploy, after, all_tags),
|
||||
toml,
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -124,16 +126,22 @@ pub fn resource_push_to_toml<R: ToToml>(
|
||||
|
||||
pub fn resource_to_toml<R: ToToml>(
|
||||
resource: Resource<R::Config, R::Info>,
|
||||
deploy: bool,
|
||||
after: Vec<String>,
|
||||
all: &AllResourcesById,
|
||||
all_tags: &HashMap<String, Tag>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut toml = String::new();
|
||||
resource_push_to_toml::<R>(resource, &mut toml, all, all_tags)?;
|
||||
resource_push_to_toml::<R>(
|
||||
resource, deploy, after, &mut toml, all, all_tags,
|
||||
)?;
|
||||
Ok(toml)
|
||||
}
|
||||
|
||||
pub fn convert_resource<R: KomodoResource>(
|
||||
resource: Resource<R::Config, R::Info>,
|
||||
deploy: bool,
|
||||
after: Vec<String>,
|
||||
all_tags: &HashMap<String, Tag>,
|
||||
) -> ResourceToml<R::PartialConfig> {
|
||||
ResourceToml {
|
||||
@@ -144,9 +152,8 @@ pub fn convert_resource<R: KomodoResource>(
|
||||
.filter_map(|t| all_tags.get(t).map(|t| t.name.clone()))
|
||||
.collect(),
|
||||
description: resource.description,
|
||||
deploy: false,
|
||||
after: Default::default(),
|
||||
latest_hash: false,
|
||||
deploy,
|
||||
after,
|
||||
// The config still needs to be minimized.
|
||||
// This happens in ToToml::push_to_toml
|
||||
config: resource.config.into(),
|
||||
@@ -274,7 +281,7 @@ impl ToToml for Build {
|
||||
config
|
||||
.into_iter()
|
||||
.map(|(key, value)| match key.as_str() {
|
||||
"builder_id" => return Ok((String::from("builder"), value)),
|
||||
"builder_id" => Ok((String::from("builder"), value)),
|
||||
"version" => {
|
||||
match (
|
||||
&resource.config.version,
|
||||
@@ -353,7 +360,7 @@ impl ToToml for ServerTemplate {
|
||||
if empty_params {
|
||||
// toml_pretty will remove empty map
|
||||
// but in this case its needed to deserialize the enums.
|
||||
toml.push_str("\nconfig.params = {}");
|
||||
toml.push_str("\nparams = {}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,7 +392,7 @@ impl ToToml for Builder {
|
||||
if empty_params {
|
||||
// toml_pretty will remove empty map
|
||||
// but in this case its needed to deserialize the enums.
|
||||
toml.push_str("\nconfig.params = {}");
|
||||
toml.push_str("\nparams = {}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -687,6 +694,15 @@ impl ToToml for Procedure {
|
||||
.map(|r| &r.name)
|
||||
.unwrap_or(&String::new()),
|
||||
),
|
||||
Execution::DeployStackIfChanged(exec) => {
|
||||
exec.stack.clone_from(
|
||||
all
|
||||
.stacks
|
||||
.get(&exec.stack)
|
||||
.map(|r| &r.name)
|
||||
.unwrap_or(&String::new()),
|
||||
)
|
||||
}
|
||||
Execution::StartStack(exec) => exec.stack.clone_from(
|
||||
all
|
||||
.stacks
|
||||
|
||||
@@ -94,6 +94,7 @@ pub async fn get_updates_for_view(
|
||||
Some(original) => original,
|
||||
None => {
|
||||
diffs.push(DiffData::Create {
|
||||
name: user_group.name.clone(),
|
||||
proposed: format!(
|
||||
"[[user_group]]\n{}",
|
||||
toml_pretty::to_string(&user_group, TOML_PRETTY_OPTIONS)
|
||||
|
||||
@@ -75,6 +75,7 @@ pub async fn get_updates_for_view(
|
||||
}
|
||||
None => {
|
||||
diffs.push(DiffData::Create {
|
||||
name: variable.name.clone(),
|
||||
proposed: format!(
|
||||
"[[variable]]\n{}",
|
||||
toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS)
|
||||
|
||||
+35
-15
@@ -5,6 +5,7 @@ use komodo_client::entities::{
|
||||
sync::{DiffData, ResourceDiff},
|
||||
tag::Tag,
|
||||
toml::ResourceToml,
|
||||
ResourceTargetVariant,
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use partial_derive2::MaybeNone;
|
||||
@@ -15,7 +16,9 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
|
||||
resources: Vec<ResourceToml<Resource::PartialConfig>>,
|
||||
delete: bool,
|
||||
all_resources: &AllResourcesById,
|
||||
all_tags: &HashMap<String, Tag>,
|
||||
match_resource_type: Option<ResourceTargetVariant>,
|
||||
match_resources: Option<&[String]>,
|
||||
id_to_tags: &HashMap<String, Tag>,
|
||||
match_tags: &[String],
|
||||
diffs: &mut Vec<ResourceDiff>,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -25,12 +28,33 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
Resource::include_resource(
|
||||
&r.config, &r.tags, all_tags, match_tags,
|
||||
&r.name,
|
||||
&r.config,
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
&r.tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
)
|
||||
})
|
||||
.map(|r| (r.name.clone(), r))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let resources = resources
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
Resource::include_resource_partial(
|
||||
&r.name,
|
||||
&r.config,
|
||||
match_resource_type,
|
||||
match_resources,
|
||||
&r.tags,
|
||||
id_to_tags,
|
||||
match_tags,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if delete {
|
||||
for current_resource in current_map.values() {
|
||||
if !resources.iter().any(|r| r.name == current_resource.name) {
|
||||
@@ -41,8 +65,10 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
|
||||
data: DiffData::Delete {
|
||||
current: super::toml::resource_to_toml::<Resource>(
|
||||
current_resource.clone(),
|
||||
false,
|
||||
vec![],
|
||||
all_resources,
|
||||
all_tags,
|
||||
id_to_tags,
|
||||
)?,
|
||||
},
|
||||
});
|
||||
@@ -51,15 +77,6 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
|
||||
}
|
||||
|
||||
for mut proposed_resource in resources {
|
||||
// only resource that might not be included is resource sync
|
||||
if !Resource::include_resource_partial(
|
||||
&proposed_resource.config,
|
||||
&proposed_resource.tags,
|
||||
all_tags,
|
||||
match_tags,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
match current_map.get(&proposed_resource.name) {
|
||||
Some(current_resource) => {
|
||||
// First merge toml resource config (partial) onto default resource config.
|
||||
@@ -87,7 +104,7 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
|
||||
let current_tags = current_resource
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|id| all_tags.get(id).map(|t| t.name.clone()))
|
||||
.filter_map(|id| id_to_tags.get(id).map(|t| t.name.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Only proceed if there are any fields to update,
|
||||
@@ -105,12 +122,14 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
|
||||
current_resource.id.clone(),
|
||||
),
|
||||
data: DiffData::Update {
|
||||
proposed,
|
||||
current: super::toml::resource_to_toml::<Resource>(
|
||||
current_resource.clone(),
|
||||
proposed_resource.deploy,
|
||||
proposed_resource.after,
|
||||
all_resources,
|
||||
all_tags,
|
||||
id_to_tags,
|
||||
)?,
|
||||
proposed,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -120,6 +139,7 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
|
||||
target: Resource::resource_target(String::new()),
|
||||
|
||||
data: DiffData::Create {
|
||||
name: proposed_resource.name.clone(),
|
||||
proposed: super::toml::resource_toml_to_toml_string::<
|
||||
Resource,
|
||||
>(proposed_resource)?,
|
||||
|
||||
@@ -26,6 +26,7 @@ git.workspace = true
|
||||
serror = { workspace = true, features = ["axum"] }
|
||||
merge_config_files.workspace = true
|
||||
async_timing_util.workspace = true
|
||||
derive_variants.workspace = true
|
||||
resolver_api.workspace = true
|
||||
run_command.workspace = true
|
||||
svi.workspace = true
|
||||
|
||||
@@ -87,10 +87,18 @@ impl Resolve<build::Build> for State {
|
||||
// Get command parts
|
||||
let image_name =
|
||||
get_image_name(&build).context("failed to make image name")?;
|
||||
let build_args = parse_build_args(
|
||||
&environment_vars_from_str(build_args)
|
||||
.context("Invalid build_args")?,
|
||||
);
|
||||
|
||||
// Add VERSION to build args (if not already there)
|
||||
let mut build_args = environment_vars_from_str(build_args)
|
||||
.context("Invalid build_args")?;
|
||||
if !build_args.iter().any(|a| a.variable == "VERSION") {
|
||||
build_args.push(EnvironmentVar {
|
||||
variable: String::from("VERSION"),
|
||||
value: build.config.version.to_string(),
|
||||
});
|
||||
}
|
||||
let build_args = parse_build_args(&build_args);
|
||||
|
||||
let secret_args = environment_vars_from_str(secret_args)
|
||||
.context("Invalid secret_args")?;
|
||||
let _secret_args =
|
||||
@@ -110,13 +118,16 @@ impl Resolve<build::Build> for State {
|
||||
|
||||
// Construct command
|
||||
let command = format!(
|
||||
"cd {} && docker{buildx} build{build_args}{_secret_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}",
|
||||
build_dir.display()
|
||||
"docker{buildx} build{build_args}{_secret_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}",
|
||||
);
|
||||
|
||||
if *skip_secret_interp {
|
||||
let build_log =
|
||||
run_komodo_command("docker build", command).await;
|
||||
let build_log = run_komodo_command(
|
||||
"docker build",
|
||||
build_dir.as_ref(),
|
||||
command,
|
||||
)
|
||||
.await;
|
||||
logs.push(build_log);
|
||||
} else {
|
||||
// Interpolate any missing secrets
|
||||
@@ -131,8 +142,12 @@ impl Resolve<build::Build> for State {
|
||||
)?;
|
||||
replacers.extend(core_replacers);
|
||||
|
||||
let mut build_log =
|
||||
run_komodo_command("docker build", command).await;
|
||||
let mut build_log = run_komodo_command(
|
||||
"docker build",
|
||||
build_dir.as_ref(),
|
||||
command,
|
||||
)
|
||||
.await;
|
||||
build_log.command =
|
||||
svi::replace_in_string(&build_log.command, &replacers);
|
||||
build_log.stdout =
|
||||
@@ -229,7 +244,7 @@ impl Resolve<PruneBuilders> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker builder prune -a -f");
|
||||
Ok(run_komodo_command("prune builders", command).await)
|
||||
Ok(run_komodo_command("prune builders", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +258,6 @@ impl Resolve<PruneBuildx> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker buildx prune -a -f");
|
||||
Ok(run_komodo_command("prune buildx", command).await)
|
||||
Ok(run_komodo_command("prune buildx", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ use std::path::PathBuf;
|
||||
use anyhow::{anyhow, Context};
|
||||
use command::run_komodo_command;
|
||||
use formatting::format_serror;
|
||||
use git::{write_commit_file, GitRes};
|
||||
use komodo_client::entities::{
|
||||
stack::ComposeProject, to_komodo_name, update::Log, FileContents,
|
||||
stack::ComposeProject, to_komodo_name, update::Log, CloneArgs,
|
||||
FileContents,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
compose::*,
|
||||
git::{PullOrCloneRepo, RepoActionResponse},
|
||||
};
|
||||
use periphery_client::api::compose::*;
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
@@ -28,6 +33,7 @@ impl Resolve<ListComposeProjects, ()> for State {
|
||||
let docker_compose = docker_compose();
|
||||
let res = run_komodo_command(
|
||||
"list projects",
|
||||
None,
|
||||
format!("{docker_compose} ls --all --format json"),
|
||||
)
|
||||
.await;
|
||||
@@ -76,6 +82,61 @@ pub struct DockerComposeLsItem {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<GetComposeServiceLog> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeServiceLog",
|
||||
level = "debug",
|
||||
skip(self)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetComposeServiceLog {
|
||||
project,
|
||||
service,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetComposeServiceLog,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let docker_compose = docker_compose();
|
||||
let timestamps =
|
||||
timestamps.then_some(" --timestamps").unwrap_or_default();
|
||||
let command = format!(
|
||||
"{docker_compose} -p {project} logs {service} --tail {tail}{timestamps}"
|
||||
);
|
||||
Ok(run_komodo_command("get stack log", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<GetComposeServiceLogSearch> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeServiceLogSearch",
|
||||
level = "debug",
|
||||
skip(self)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetComposeServiceLogSearch {
|
||||
project,
|
||||
service,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: GetComposeServiceLogSearch,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let docker_compose = docker_compose();
|
||||
let grep = log_grep(&terms, combinator, invert);
|
||||
let timestamps =
|
||||
timestamps.then_some(" --timestamps").unwrap_or_default();
|
||||
let command = format!("{docker_compose} -p {project} logs {service} --tail 5000{timestamps} 2>&1 | {grep}");
|
||||
Ok(run_komodo_command("get stack log grep", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎
|
||||
services:
|
||||
hello_world:
|
||||
@@ -95,6 +156,11 @@ services:
|
||||
";
|
||||
|
||||
impl Resolve<GetComposeContentsOnHost, ()> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeContentsOnHost",
|
||||
level = "debug",
|
||||
skip(self)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetComposeContentsOnHost {
|
||||
@@ -115,16 +181,11 @@ impl Resolve<GetComposeContentsOnHost, ()> for State {
|
||||
.context("Failed to initialize run directory")?;
|
||||
}
|
||||
|
||||
let file_paths = file_paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
run_directory.join(path).components().collect::<PathBuf>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut res = GetComposeContentsOnHostResponse::default();
|
||||
|
||||
for full_path in &file_paths {
|
||||
for path in file_paths {
|
||||
let full_path =
|
||||
run_directory.join(&path).components().collect::<PathBuf>();
|
||||
if !full_path.exists() {
|
||||
fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS)
|
||||
.await
|
||||
@@ -136,74 +197,25 @@ impl Resolve<GetComposeContentsOnHost, ()> for State {
|
||||
)
|
||||
}) {
|
||||
Ok(contents) => {
|
||||
res.contents.push(FileContents {
|
||||
path: full_path.display().to_string(),
|
||||
contents,
|
||||
});
|
||||
// The path we store here has to be the same as incoming file path in the array,
|
||||
// in order for WriteComposeContentsToHost to write to the correct path.
|
||||
res.contents.push(FileContents { path, contents });
|
||||
}
|
||||
Err(e) => {
|
||||
res.errors.push(FileContents {
|
||||
path: full_path.display().to_string(),
|
||||
path,
|
||||
contents: format_serror(&e.into()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<GetComposeServiceLog> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeServiceLog",
|
||||
level = "debug",
|
||||
skip(self)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetComposeServiceLog {
|
||||
project,
|
||||
service,
|
||||
tail,
|
||||
}: GetComposeServiceLog,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let docker_compose = docker_compose();
|
||||
let command = format!(
|
||||
"{docker_compose} -p {project} logs {service} --tail {tail}"
|
||||
);
|
||||
Ok(run_komodo_command("get stack log", command).await)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<GetComposeServiceLogSearch> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeServiceLogSearch",
|
||||
level = "debug",
|
||||
skip(self)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetComposeServiceLogSearch {
|
||||
project,
|
||||
service,
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
}: GetComposeServiceLogSearch,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let docker_compose = docker_compose();
|
||||
let grep = log_grep(&terms, combinator, invert);
|
||||
let command = format!("{docker_compose} -p {project} logs {service} --tail 5000 2>&1 | {grep}");
|
||||
Ok(run_komodo_command("get stack log grep", command).await)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteComposeContentsToHost> for State {
|
||||
#[instrument(name = "WriteComposeContentsToHost", skip(self))]
|
||||
async fn resolve(
|
||||
@@ -216,13 +228,10 @@ impl Resolve<WriteComposeContentsToHost> for State {
|
||||
}: WriteComposeContentsToHost,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let root =
|
||||
periphery_config().stack_dir.join(to_komodo_name(&name));
|
||||
let run_directory = root.join(&run_directory);
|
||||
let run_directory = run_directory.canonicalize().context(
|
||||
"failed to validate run directory on host (canonicalize error)",
|
||||
)?;
|
||||
let file_path = run_directory
|
||||
let file_path = periphery_config()
|
||||
.stack_dir
|
||||
.join(to_komodo_name(&name))
|
||||
.join(&run_directory)
|
||||
.join(file_path)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
@@ -244,6 +253,104 @@ impl Resolve<WriteComposeContentsToHost> for State {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteCommitComposeContents> for State {
|
||||
#[instrument(name = "WriteCommitComposeContents", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
WriteCommitComposeContents {
|
||||
stack,
|
||||
username,
|
||||
file_path,
|
||||
contents,
|
||||
git_token,
|
||||
}: WriteCommitComposeContents,
|
||||
_: (),
|
||||
) -> anyhow::Result<RepoActionResponse> {
|
||||
if stack.config.files_on_host {
|
||||
return Err(anyhow!(
|
||||
"Wrong method called for files on host stack"
|
||||
));
|
||||
}
|
||||
if stack.config.repo.is_empty() {
|
||||
return Err(anyhow!("Repo is not configured"));
|
||||
}
|
||||
|
||||
let root = periphery_config()
|
||||
.stack_dir
|
||||
.join(to_komodo_name(&stack.name));
|
||||
|
||||
let mut args: CloneArgs = (&stack).into();
|
||||
// Set the clone destination to the one created for this run
|
||||
args.destination = Some(root.display().to_string());
|
||||
|
||||
let git_token = match git_token {
|
||||
Some(token) => Some(token),
|
||||
None => {
|
||||
if !stack.config.git_account.is_empty() {
|
||||
match crate::helpers::git_token(
|
||||
&stack.config.git_provider,
|
||||
&stack.config.git_account,
|
||||
) {
|
||||
Ok(token) => Some(token.to_string()),
|
||||
Err(e) => {
|
||||
return Err(
|
||||
e.context("Failed to find required git token"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
State
|
||||
.resolve(
|
||||
PullOrCloneRepo {
|
||||
args,
|
||||
git_token,
|
||||
environment: vec![],
|
||||
env_file_path: stack.config.env_file_path.clone(),
|
||||
skip_secret_interp: stack.config.skip_secret_interp,
|
||||
// repo replacer only needed for on_clone / on_pull,
|
||||
// which aren't available for stacks
|
||||
replacers: Default::default(),
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let file_path = stack
|
||||
.config
|
||||
.run_directory
|
||||
.parse::<PathBuf>()
|
||||
.context("Run directory is not a valid path")?
|
||||
.join(&file_path);
|
||||
|
||||
let msg = if let Some(username) = username {
|
||||
format!("{}: Write Compose File", username)
|
||||
} else {
|
||||
"Write Compose File".to_string()
|
||||
};
|
||||
|
||||
let GitRes {
|
||||
logs,
|
||||
hash,
|
||||
message,
|
||||
..
|
||||
} = write_commit_file(&msg, &root, &file_path, &contents).await?;
|
||||
|
||||
Ok(RepoActionResponse {
|
||||
logs,
|
||||
commit_hash: hash,
|
||||
commit_message: message,
|
||||
env_file_path: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ComposeUp> for State {
|
||||
#[instrument(
|
||||
name = "ComposeUp",
|
||||
@@ -292,6 +399,7 @@ impl Resolve<ComposeExecution> for State {
|
||||
let docker_compose = docker_compose();
|
||||
let log = run_komodo_command(
|
||||
"compose command",
|
||||
None,
|
||||
format!("{docker_compose} -p {project} {command}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -42,11 +42,18 @@ impl Resolve<GetContainerLog> for State {
|
||||
#[instrument(name = "GetContainerLog", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetContainerLog { name, tail }: GetContainerLog,
|
||||
GetContainerLog {
|
||||
name,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetContainerLog,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = format!("docker logs {name} --tail {tail}");
|
||||
Ok(run_komodo_command("get container log", command).await)
|
||||
let timestamps =
|
||||
timestamps.then_some(" --timestamps").unwrap_or_default();
|
||||
let command =
|
||||
format!("docker logs {name} --tail {tail}{timestamps}");
|
||||
Ok(run_komodo_command("get container log", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +72,20 @@ impl Resolve<GetContainerLogSearch> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: GetContainerLogSearch,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let grep = log_grep(&terms, combinator, invert);
|
||||
let command =
|
||||
format!("docker logs {name} --tail 5000 2>&1 | {grep}");
|
||||
Ok(run_komodo_command("get container log grep", command).await)
|
||||
let timestamps =
|
||||
timestamps.then_some(" --timestamps").unwrap_or_default();
|
||||
let command = format!(
|
||||
"docker logs {name} --tail 5000{timestamps} 2>&1 | {grep}"
|
||||
);
|
||||
Ok(
|
||||
run_komodo_command("get container log grep", None, command)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +140,7 @@ impl Resolve<StartContainer> for State {
|
||||
Ok(
|
||||
run_komodo_command(
|
||||
"docker start",
|
||||
None,
|
||||
format!("docker start {name}"),
|
||||
)
|
||||
.await,
|
||||
@@ -145,6 +160,7 @@ impl Resolve<RestartContainer> for State {
|
||||
Ok(
|
||||
run_komodo_command(
|
||||
"docker restart",
|
||||
None,
|
||||
format!("docker restart {name}"),
|
||||
)
|
||||
.await,
|
||||
@@ -164,6 +180,7 @@ impl Resolve<PauseContainer> for State {
|
||||
Ok(
|
||||
run_komodo_command(
|
||||
"docker pause",
|
||||
None,
|
||||
format!("docker pause {name}"),
|
||||
)
|
||||
.await,
|
||||
@@ -181,6 +198,7 @@ impl Resolve<UnpauseContainer> for State {
|
||||
Ok(
|
||||
run_komodo_command(
|
||||
"docker unpause",
|
||||
None,
|
||||
format!("docker unpause {name}"),
|
||||
)
|
||||
.await,
|
||||
@@ -198,10 +216,11 @@ impl Resolve<StopContainer> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = stop_container_command(&name, signal, time);
|
||||
let log = run_komodo_command("docker stop", command).await;
|
||||
let log = run_komodo_command("docker stop", None, command).await;
|
||||
if log.stderr.contains("unknown flag: --signal") {
|
||||
let command = stop_container_command(&name, None, time);
|
||||
let mut log = run_komodo_command("docker stop", command).await;
|
||||
let mut log =
|
||||
run_komodo_command("docker stop", None, command).await;
|
||||
log.stderr = format!(
|
||||
"old docker version: unable to use --signal flag{}",
|
||||
if !log.stderr.is_empty() {
|
||||
@@ -230,12 +249,14 @@ impl Resolve<RemoveContainer> for State {
|
||||
let command =
|
||||
format!("{stop_command} && docker container rm {name}");
|
||||
let log =
|
||||
run_komodo_command("docker stop and remove", command).await;
|
||||
run_komodo_command("docker stop and remove", None, command)
|
||||
.await;
|
||||
if log.stderr.contains("unknown flag: --signal") {
|
||||
let stop_command = stop_container_command(&name, None, time);
|
||||
let command =
|
||||
format!("{stop_command} && docker container rm {name}");
|
||||
let mut log = run_komodo_command("docker stop", command).await;
|
||||
let mut log =
|
||||
run_komodo_command("docker stop", None, command).await;
|
||||
log.stderr = format!(
|
||||
"old docker version: unable to use --signal flag{}",
|
||||
if !log.stderr.is_empty() {
|
||||
@@ -265,7 +286,7 @@ impl Resolve<RenameContainer> for State {
|
||||
) -> anyhow::Result<Log> {
|
||||
let new = to_komodo_name(&new_name);
|
||||
let command = format!("docker rename {curr_name} {new}");
|
||||
Ok(run_komodo_command("docker rename", command).await)
|
||||
Ok(run_komodo_command("docker rename", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +300,7 @@ impl Resolve<PruneContainers> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker container prune -f");
|
||||
Ok(run_komodo_command("prune containers", command).await)
|
||||
Ok(run_komodo_command("prune containers", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,15 +317,17 @@ impl Resolve<StartAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures =
|
||||
containers.iter().map(
|
||||
|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker start {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), command).await
|
||||
}
|
||||
},
|
||||
);
|
||||
let futures = containers.iter().filter_map(
|
||||
|ContainerListItem { name, labels, .. }| {
|
||||
if labels.contains_key("komodo.skip") {
|
||||
return None;
|
||||
}
|
||||
let command = format!("docker start {name}");
|
||||
Some(async move {
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
})
|
||||
},
|
||||
);
|
||||
Ok(join_all(futures).await)
|
||||
}
|
||||
}
|
||||
@@ -322,12 +345,15 @@ impl Resolve<RestartAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures = containers.iter().map(
|
||||
|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker restart {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), command).await
|
||||
let futures = containers.iter().filter_map(
|
||||
|ContainerListItem { name, labels, .. }| {
|
||||
if labels.contains_key("komodo.skip") {
|
||||
return None;
|
||||
}
|
||||
let command = format!("docker restart {name}");
|
||||
Some(async move {
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
})
|
||||
},
|
||||
);
|
||||
Ok(join_all(futures).await)
|
||||
@@ -347,12 +373,15 @@ impl Resolve<PauseAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures = containers.iter().map(
|
||||
|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker pause {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), command).await
|
||||
let futures = containers.iter().filter_map(
|
||||
|ContainerListItem { name, labels, .. }| {
|
||||
if labels.contains_key("komodo.skip") {
|
||||
return None;
|
||||
}
|
||||
let command = format!("docker pause {name}");
|
||||
Some(async move {
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
})
|
||||
},
|
||||
);
|
||||
Ok(join_all(futures).await)
|
||||
@@ -372,12 +401,15 @@ impl Resolve<UnpauseAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures = containers.iter().map(
|
||||
|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker unpause {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), command).await
|
||||
let futures = containers.iter().filter_map(
|
||||
|ContainerListItem { name, labels, .. }| {
|
||||
if labels.contains_key("komodo.skip") {
|
||||
return None;
|
||||
}
|
||||
let command = format!("docker unpause {name}");
|
||||
Some(async move {
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
})
|
||||
},
|
||||
);
|
||||
Ok(join_all(futures).await)
|
||||
@@ -397,13 +429,19 @@ impl Resolve<StopAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures = containers.iter().map(
|
||||
|ContainerListItem { name, .. }| async move {
|
||||
run_komodo_command(
|
||||
&format!("docker stop {name}"),
|
||||
stop_container_command(name, None, None),
|
||||
)
|
||||
.await
|
||||
let futures = containers.iter().filter_map(
|
||||
|ContainerListItem { name, labels, .. }| {
|
||||
if labels.contains_key("komodo.skip") {
|
||||
return None;
|
||||
}
|
||||
Some(async move {
|
||||
run_komodo_command(
|
||||
&format!("docker stop {name}"),
|
||||
None,
|
||||
stop_container_command(name, None, None),
|
||||
)
|
||||
.await
|
||||
})
|
||||
},
|
||||
);
|
||||
Ok(join_all(futures).await)
|
||||
|
||||
@@ -87,7 +87,7 @@ impl Resolve<Deploy> for State {
|
||||
debug!("docker run command: {command}");
|
||||
|
||||
if deployment.config.skip_secret_interp {
|
||||
Ok(run_komodo_command("docker run", command).await)
|
||||
Ok(run_komodo_command("docker run", None, command).await)
|
||||
} else {
|
||||
let command = svi::interpolate_variables(
|
||||
&command,
|
||||
@@ -107,7 +107,8 @@ impl Resolve<Deploy> for State {
|
||||
};
|
||||
|
||||
replacers.extend(core_replacers);
|
||||
let mut log = run_komodo_command("docker run", command).await;
|
||||
let mut log =
|
||||
run_komodo_command("docker run", None, command).await;
|
||||
log.command = svi::replace_in_string(&log.command, &replacers);
|
||||
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
|
||||
log.stderr = svi::replace_in_string(&log.stderr, &replacers);
|
||||
|
||||
@@ -44,7 +44,7 @@ impl Resolve<DeleteImage> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = format!("docker image rm {name}");
|
||||
Ok(run_komodo_command("delete image", command).await)
|
||||
Ok(run_komodo_command("delete image", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,6 @@ impl Resolve<PruneImages> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker image prune -a -f");
|
||||
Ok(run_komodo_command("prune images", command).await)
|
||||
Ok(run_komodo_command("prune images", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_command;
|
||||
use derive_variants::EnumVariants;
|
||||
use futures::TryFutureExt;
|
||||
use komodo_client::entities::{update::Log, SystemCommand};
|
||||
use periphery_client::api::{
|
||||
@@ -30,7 +31,10 @@ mod network;
|
||||
mod stats;
|
||||
mod volume;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
|
||||
)]
|
||||
#[variant_derive(Debug)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
#[resolver_target(State)]
|
||||
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
|
||||
@@ -76,6 +80,7 @@ pub enum PeripheryRequest {
|
||||
|
||||
// Compose (Write)
|
||||
WriteComposeContentsToHost(WriteComposeContentsToHost),
|
||||
WriteCommitComposeContents(WriteCommitComposeContents),
|
||||
ComposeUp(ComposeUp),
|
||||
ComposeExecution(ComposeExecution),
|
||||
|
||||
@@ -206,7 +211,7 @@ impl ResolveToString<ListSecrets> for State {
|
||||
}
|
||||
|
||||
impl Resolve<GetDockerLists> for State {
|
||||
#[instrument(name = "GetDockerLists", skip(self))]
|
||||
#[instrument(name = "GetDockerLists", level = "debug", skip(self))]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetDockerLists {}: GetDockerLists,
|
||||
@@ -251,7 +256,7 @@ impl Resolve<RunCommand> for State {
|
||||
} else {
|
||||
format!("cd {path} && {command}")
|
||||
};
|
||||
run_komodo_command("run command", command).await
|
||||
run_komodo_command("run command", None, command).await
|
||||
})
|
||||
.await
|
||||
.context("failure in spawned task")
|
||||
@@ -266,6 +271,6 @@ impl Resolve<PruneSystem> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker system prune -a -f --volumes");
|
||||
Ok(run_komodo_command("prune system", command).await)
|
||||
Ok(run_komodo_command("prune system", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ impl Resolve<CreateNetwork> for State {
|
||||
None => String::new(),
|
||||
};
|
||||
let command = format!("docker network create{driver} {name}");
|
||||
Ok(run_komodo_command("create network", command).await)
|
||||
Ok(run_komodo_command("create network", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ impl Resolve<DeleteNetwork> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = format!("docker network rm {name}");
|
||||
Ok(run_komodo_command("delete network", command).await)
|
||||
Ok(run_komodo_command("delete network", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,6 @@ impl Resolve<PruneNetworks> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker network prune -f");
|
||||
Ok(run_komodo_command("prune networks", command).await)
|
||||
Ok(run_komodo_command("prune networks", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Resolve<DeleteVolume> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = format!("docker volume rm {name}");
|
||||
Ok(run_komodo_command("delete volume", command).await)
|
||||
Ok(run_komodo_command("delete volume", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,6 @@ impl Resolve<PruneVolumes> for State {
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
let command = String::from("docker volume prune -a -f");
|
||||
Ok(run_komodo_command("prune volumes", command).await)
|
||||
Ok(run_komodo_command("prune volumes", None, command).await)
|
||||
}
|
||||
}
|
||||
|
||||
+167
-46
@@ -10,14 +10,16 @@ use komodo_client::entities::{
|
||||
};
|
||||
use periphery_client::api::{
|
||||
compose::ComposeUpResponse,
|
||||
git::{PullOrCloneRepo, RepoActionResponse},
|
||||
git::{CloneRepo, PullOrCloneRepo, RepoActionResponse},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
config::periphery_config, docker::docker_login,
|
||||
helpers::parse_extra_args, State,
|
||||
config::periphery_config,
|
||||
docker::docker_login,
|
||||
helpers::{interpolate_variables, parse_extra_args},
|
||||
State,
|
||||
};
|
||||
|
||||
pub fn docker_compose() -> &'static str {
|
||||
@@ -71,7 +73,7 @@ pub async fn compose_up(
|
||||
return Err(anyhow!("A compose file doesn't exist after writing stack. Ensure the run_directory and file_paths are correct."));
|
||||
}
|
||||
|
||||
for (_, full_path) in &file_paths {
|
||||
for (path, full_path) in &file_paths {
|
||||
let file_contents =
|
||||
match fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!(
|
||||
@@ -86,7 +88,7 @@ pub async fn compose_up(
|
||||
.push(Log::error("read compose file", error.clone()));
|
||||
// This should only happen for repo stacks, ie remote error
|
||||
res.remote_errors.push(FileContents {
|
||||
path: full_path.display().to_string(),
|
||||
path: path.to_string(),
|
||||
contents: error,
|
||||
});
|
||||
return Err(anyhow!(
|
||||
@@ -95,13 +97,12 @@ pub async fn compose_up(
|
||||
}
|
||||
};
|
||||
res.file_contents.push(FileContents {
|
||||
path: full_path.display().to_string(),
|
||||
path: path.to_string(),
|
||||
contents: file_contents,
|
||||
});
|
||||
}
|
||||
|
||||
let docker_compose = docker_compose();
|
||||
let run_dir = run_directory.display();
|
||||
let service_arg = service
|
||||
.as_ref()
|
||||
.map(|service| format!(" {service}"))
|
||||
@@ -137,7 +138,7 @@ pub async fn compose_up(
|
||||
}
|
||||
|
||||
let env_file = env_file_path
|
||||
.map(|path| format!(" --env-file {}", path.display()))
|
||||
.map(|path| format!(" --env-file {path}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build images before destroying to minimize downtime.
|
||||
@@ -146,10 +147,15 @@ pub async fn compose_up(
|
||||
let build_extra_args =
|
||||
parse_extra_args(&stack.config.build_extra_args);
|
||||
let command = format!(
|
||||
"cd {run_dir} && {docker_compose} -p {project_name} -f {file_args}{env_file} build{build_extra_args}{service_arg}",
|
||||
"{docker_compose} -p {project_name} -f {file_args}{env_file} build{build_extra_args}{service_arg}",
|
||||
);
|
||||
if stack.config.skip_secret_interp {
|
||||
let log = run_komodo_command("compose build", command).await;
|
||||
let log = run_komodo_command(
|
||||
"compose build",
|
||||
run_directory.as_ref(),
|
||||
command,
|
||||
)
|
||||
.await;
|
||||
res.logs.push(log);
|
||||
} else {
|
||||
let (command, mut replacers) = svi::interpolate_variables(
|
||||
@@ -160,8 +166,12 @@ pub async fn compose_up(
|
||||
).context("failed to interpolate periphery secrets into stack build command")?;
|
||||
replacers.extend(core_replacers.clone());
|
||||
|
||||
let mut log =
|
||||
run_komodo_command("compose build", command).await;
|
||||
let mut log = run_komodo_command(
|
||||
"compose build",
|
||||
run_directory.as_ref(),
|
||||
command,
|
||||
)
|
||||
.await;
|
||||
|
||||
log.command = svi::replace_in_string(&log.command, &replacers);
|
||||
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
|
||||
@@ -177,13 +187,15 @@ pub async fn compose_up(
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
if stack.config.auto_pull {
|
||||
// Pull images before destroying to minimize downtime.
|
||||
// If this fails, do not continue.
|
||||
let log = run_komodo_command(
|
||||
"compose pull",
|
||||
run_directory.as_ref(),
|
||||
format!(
|
||||
"cd {run_dir} && {docker_compose} -p {project_name} -f {file_args}{env_file} pull{service_arg}",
|
||||
"{docker_compose} -p {project_name} -f {file_args}{env_file} pull{service_arg}",
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -197,20 +209,78 @@ pub async fn compose_up(
|
||||
}
|
||||
}
|
||||
|
||||
// Take down the existing containers.
|
||||
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
|
||||
compose_down(&last_project_name, service, res)
|
||||
.await
|
||||
.context("failed to destroy existing containers")?;
|
||||
if !stack.config.pre_deploy.command.is_empty() {
|
||||
let pre_deploy_path =
|
||||
run_directory.join(&stack.config.pre_deploy.path);
|
||||
if !stack.config.skip_secret_interp {
|
||||
let (full_command, mut replacers) =
|
||||
interpolate_variables(&stack.config.pre_deploy.command)
|
||||
.context(
|
||||
"failed to interpolate secrets into pre_deploy command",
|
||||
)?;
|
||||
replacers.extend(core_replacers.to_owned());
|
||||
let mut pre_deploy_log = run_komodo_command(
|
||||
"pre deploy",
|
||||
pre_deploy_path.as_ref(),
|
||||
&full_command,
|
||||
)
|
||||
.await;
|
||||
|
||||
pre_deploy_log.command =
|
||||
svi::replace_in_string(&pre_deploy_log.command, &replacers);
|
||||
pre_deploy_log.stdout =
|
||||
svi::replace_in_string(&pre_deploy_log.stdout, &replacers);
|
||||
pre_deploy_log.stderr =
|
||||
svi::replace_in_string(&pre_deploy_log.stderr, &replacers);
|
||||
|
||||
tracing::debug!(
|
||||
"run Stack pre_deploy command | command: {} | cwd: {:?}",
|
||||
pre_deploy_log.command,
|
||||
pre_deploy_path
|
||||
);
|
||||
|
||||
res.logs.push(pre_deploy_log);
|
||||
} else {
|
||||
let pre_deploy_log = run_komodo_command(
|
||||
"pre deploy",
|
||||
pre_deploy_path.as_ref(),
|
||||
&stack.config.pre_deploy.command,
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(
|
||||
"run Stack pre_deploy command | command: {} | cwd: {:?}",
|
||||
&stack.config.pre_deploy.command,
|
||||
pre_deploy_path
|
||||
);
|
||||
res.logs.push(pre_deploy_log);
|
||||
}
|
||||
if !all_logs_success(&res.logs) {
|
||||
return Err(anyhow!(
|
||||
"Failed at running pre_deploy command, stopping the run."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if stack.config.destroy_before_deploy
|
||||
// Also check if project name changed, which also requires taking down.
|
||||
|| last_project_name != project_name
|
||||
{
|
||||
// Take down the existing containers.
|
||||
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
|
||||
compose_down(&last_project_name, service, res)
|
||||
.await
|
||||
.context("failed to destroy existing containers")?;
|
||||
}
|
||||
|
||||
// Run compose up
|
||||
let extra_args = parse_extra_args(&stack.config.extra_args);
|
||||
let command = format!(
|
||||
"cd {run_dir} && {docker_compose} -p {project_name} -f {file_args}{env_file} up -d{extra_args}{service_arg}",
|
||||
"{docker_compose} -p {project_name} -f {file_args}{env_file} up -d{extra_args}{service_arg}",
|
||||
);
|
||||
|
||||
let log = if stack.config.skip_secret_interp {
|
||||
run_komodo_command("compose up", command).await
|
||||
run_komodo_command("compose up", run_directory.as_ref(), command)
|
||||
.await
|
||||
} else {
|
||||
let (command, mut replacers) = svi::interpolate_variables(
|
||||
&command,
|
||||
@@ -220,7 +290,12 @@ pub async fn compose_up(
|
||||
).context("failed to interpolate periphery secrets into stack run command")?;
|
||||
replacers.extend(core_replacers);
|
||||
|
||||
let mut log = run_komodo_command("compose up", command).await;
|
||||
let mut log = run_komodo_command(
|
||||
"compose up",
|
||||
run_directory.as_ref(),
|
||||
command,
|
||||
)
|
||||
.await;
|
||||
|
||||
log.command = svi::replace_in_string(&log.command, &replacers);
|
||||
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
|
||||
@@ -237,11 +312,11 @@ pub async fn compose_up(
|
||||
|
||||
/// Either writes the stack file_contents to a file, or clones the repo.
|
||||
/// Returns (run_directory, env_file_path)
|
||||
async fn write_stack(
|
||||
stack: &Stack,
|
||||
async fn write_stack<'a>(
|
||||
stack: &'a Stack,
|
||||
git_token: Option<String>,
|
||||
res: &mut ComposeUpResponse,
|
||||
) -> anyhow::Result<(PathBuf, Option<PathBuf>)> {
|
||||
) -> anyhow::Result<(PathBuf, Option<&'a str>)> {
|
||||
let root = periphery_config()
|
||||
.stack_dir
|
||||
.join(to_komodo_name(&stack.name));
|
||||
@@ -265,7 +340,7 @@ async fn write_stack(
|
||||
.config
|
||||
.skip_secret_interp
|
||||
.then_some(&periphery_config().secrets),
|
||||
&run_directory,
|
||||
run_directory.as_ref(),
|
||||
&mut res.logs,
|
||||
)
|
||||
.await
|
||||
@@ -275,7 +350,14 @@ async fn write_stack(
|
||||
return Err(anyhow!("failed to write environment file"));
|
||||
}
|
||||
};
|
||||
Ok((run_directory, env_file_path))
|
||||
Ok((
|
||||
run_directory,
|
||||
// Env file paths are already relative to run directory,
|
||||
// so need to pass original env_file_path here.
|
||||
env_file_path
|
||||
.is_some()
|
||||
.then_some(&stack.config.env_file_path),
|
||||
))
|
||||
} else if stack.config.repo.is_empty() {
|
||||
if stack.config.file_contents.trim().is_empty() {
|
||||
return Err(anyhow!("Must either input compose file contents directly, or use file one host / git repo options."));
|
||||
@@ -296,7 +378,7 @@ async fn write_stack(
|
||||
.config
|
||||
.skip_secret_interp
|
||||
.then_some(&periphery_config().secrets),
|
||||
&run_directory,
|
||||
run_directory.as_ref(),
|
||||
&mut res.logs,
|
||||
)
|
||||
.await
|
||||
@@ -324,7 +406,12 @@ async fn write_stack(
|
||||
format!("failed to write compose file to {file_path:?}")
|
||||
})?;
|
||||
|
||||
Ok((run_directory, env_file_path))
|
||||
Ok((
|
||||
run_directory,
|
||||
env_file_path
|
||||
.is_some()
|
||||
.then_some(&stack.config.env_file_path),
|
||||
))
|
||||
} else {
|
||||
// ================
|
||||
// REPO BASED FILES
|
||||
@@ -362,27 +449,55 @@ async fn write_stack(
|
||||
}
|
||||
};
|
||||
|
||||
let env_file_path = stack
|
||||
.config
|
||||
.run_directory
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid run_directory")?
|
||||
.join(&stack.config.env_file_path)
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
let clone_or_pull_res = if stack.config.reclone {
|
||||
State
|
||||
.resolve(
|
||||
CloneRepo {
|
||||
args,
|
||||
git_token,
|
||||
environment: env_vars,
|
||||
env_file_path,
|
||||
skip_secret_interp: stack.config.skip_secret_interp,
|
||||
// repo replacer only needed for on_clone / on_pull,
|
||||
// which aren't available for stacks
|
||||
replacers: Default::default(),
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
State
|
||||
.resolve(
|
||||
PullOrCloneRepo {
|
||||
args,
|
||||
git_token,
|
||||
environment: env_vars,
|
||||
env_file_path,
|
||||
skip_secret_interp: stack.config.skip_secret_interp,
|
||||
// repo replacer only needed for on_clone / on_pull,
|
||||
// which aren't available for stacks
|
||||
replacers: Default::default(),
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let RepoActionResponse {
|
||||
logs,
|
||||
commit_hash,
|
||||
commit_message,
|
||||
env_file_path,
|
||||
} = match State
|
||||
.resolve(
|
||||
PullOrCloneRepo {
|
||||
args,
|
||||
git_token,
|
||||
environment: env_vars,
|
||||
env_file_path: stack.config.env_file_path.clone(),
|
||||
skip_secret_interp: stack.config.skip_secret_interp,
|
||||
// repo replacer only needed for on_clone / on_pull,
|
||||
// which aren't available for stacks
|
||||
replacers: Default::default(),
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
} = match clone_or_pull_res {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
let error = format_serror(
|
||||
@@ -407,7 +522,12 @@ async fn write_stack(
|
||||
return Err(anyhow!("Stopped after repo pull failure"));
|
||||
}
|
||||
|
||||
Ok((run_directory, env_file_path))
|
||||
Ok((
|
||||
run_directory,
|
||||
env_file_path
|
||||
.is_some()
|
||||
.then_some(&stack.config.env_file_path),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +542,8 @@ async fn compose_down(
|
||||
.map(|service| format!(" {service}"))
|
||||
.unwrap_or_default();
|
||||
let log = run_komodo_command(
|
||||
"destroy container",
|
||||
"compose down",
|
||||
None,
|
||||
format!("{docker_compose} -p {project} down{service_arg}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -50,6 +50,7 @@ impl DockerClient {
|
||||
.into_iter()
|
||||
.map(|container| {
|
||||
Ok(ContainerListItem {
|
||||
server_id: None,
|
||||
name: container
|
||||
.names
|
||||
.context("no names on container")?
|
||||
@@ -88,6 +89,7 @@ impl DockerClient {
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
labels: container.labels.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -935,7 +937,7 @@ pub async fn docker_login(
|
||||
#[instrument]
|
||||
pub async fn pull_image(image: &str) -> Log {
|
||||
let command = format!("docker pull {image}");
|
||||
run_komodo_command("docker pull", command).await
|
||||
run_komodo_command("docker pull", None, command).await
|
||||
}
|
||||
|
||||
pub fn stop_container_command(
|
||||
|
||||
@@ -66,3 +66,14 @@ pub fn log_grep(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interpolate_variables(
|
||||
input: &str,
|
||||
) -> svi::Result<(String, Vec<(String, String)>)> {
|
||||
svi::interpolate_variables(
|
||||
input,
|
||||
&periphery_config().secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{net::SocketAddr, time::Instant};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
@@ -11,6 +11,7 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use axum_extra::{headers::ContentType, TypedHeader};
|
||||
use derive_variants::ExtractVariant;
|
||||
use resolver_api::Resolver;
|
||||
use serror::{AddStatusCode, AddStatusCodeError, Json};
|
||||
use uuid::Uuid;
|
||||
@@ -40,13 +41,12 @@ async fn handler(
|
||||
Ok((TypedHeader(ContentType::json()), res??))
|
||||
}
|
||||
|
||||
#[instrument(name = "PeripheryHandler")]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
request: crate::api::PeripheryRequest,
|
||||
) -> anyhow::Result<String> {
|
||||
let timer = Instant::now();
|
||||
|
||||
let variant = request.extract_variant();
|
||||
|
||||
let res =
|
||||
State
|
||||
.resolve_request(request, ())
|
||||
@@ -59,16 +59,12 @@ async fn task(
|
||||
});
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("request {req_id} error: {e:#}");
|
||||
warn!("request {req_id} | type: {variant:?} | error: {e:#}");
|
||||
}
|
||||
|
||||
let elapsed = timer.elapsed();
|
||||
debug!("request {req_id} | resolve time: {elapsed:?}");
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
@@ -100,7 +96,6 @@ async fn guard_request_by_passkey(
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn guard_request_by_ip(
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
|
||||
@@ -104,6 +104,7 @@ pub enum Execution {
|
||||
|
||||
// STACK
|
||||
DeployStack(DeployStack),
|
||||
DeployStackIfChanged(DeployStackIfChanged),
|
||||
StartStack(StartStack),
|
||||
RestartStack(RestartStack),
|
||||
PauseStack(PauseStack),
|
||||
|
||||
@@ -9,8 +9,6 @@ use crate::entities::update::Update;
|
||||
use super::KomodoExecuteRequest;
|
||||
|
||||
/// Deploys the target stack. `docker compose up`. Response: [Update]
|
||||
///
|
||||
/// Note. If the stack is already deployed, it will be destroyed first.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -32,6 +30,30 @@ pub struct DeployStack {
|
||||
pub stop_time: Option<i32>,
|
||||
}
|
||||
|
||||
/// Checks deployed contents vs latest contents,
|
||||
/// and only if any changes found
|
||||
/// will `docker compose up`. Response: [Update]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Request,
|
||||
EmptyTraits,
|
||||
Parser,
|
||||
)]
|
||||
#[empty_traits(KomodoExecuteRequest)]
|
||||
#[response(Update)]
|
||||
pub struct DeployStackIfChanged {
|
||||
/// Id or name
|
||||
pub stack: String,
|
||||
/// Override the default termination max time.
|
||||
/// Only used if the stack needs to be taken down first.
|
||||
pub stop_time: Option<i32>,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Starts the target stack. `docker compose start`. Response: [Update]
|
||||
@@ -128,7 +150,7 @@ pub struct UnpauseStack {
|
||||
|
||||
//
|
||||
|
||||
/// Starts the target stack. `docker compose stop`. Response: [Update]
|
||||
/// Stops the target stack. `docker compose stop`. Response: [Update]
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug,
|
||||
|
||||
@@ -4,7 +4,7 @@ use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::update::Update;
|
||||
use crate::entities::{update::Update, ResourceTargetVariant};
|
||||
|
||||
use super::KomodoExecuteRequest;
|
||||
|
||||
@@ -25,4 +25,11 @@ use super::KomodoExecuteRequest;
|
||||
pub struct RunSync {
|
||||
/// Id or name
|
||||
pub sync: String,
|
||||
/// Only execute sync on a specific resource type.
|
||||
/// Combine with `resource_id` to specify resource.
|
||||
pub resource_type: Option<ResourceTargetVariant>,
|
||||
/// Only execute sync on a specific resources.
|
||||
/// Combine with `resource_type` to specify resources.
|
||||
/// Supports name or id.
|
||||
pub resources: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ pub struct GetDeploymentLog {
|
||||
/// Max: 5000.
|
||||
#[serde(default = "default_tail")]
|
||||
pub tail: U64,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
fn default_tail() -> u64 {
|
||||
@@ -156,6 +159,9 @@ pub struct SearchDeploymentLog {
|
||||
/// Invert the results, ie return all lines that DON'T match the terms / combinator.
|
||||
#[serde(default)]
|
||||
pub invert: bool,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -262,6 +262,25 @@ pub type ListDockerContainersResponse = Vec<ContainerListItem>;
|
||||
|
||||
//
|
||||
|
||||
/// List all docker containers on the target server.
|
||||
/// Response: [ListDockerContainersResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListAllDockerContainersResponse)]
|
||||
pub struct ListAllDockerContainers {
|
||||
/// Filter by server id or name.
|
||||
#[serde(default)]
|
||||
pub servers: Vec<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListAllDockerContainersResponse = Vec<ContainerListItem>;
|
||||
|
||||
//
|
||||
|
||||
/// Inspect a docker container on the server. Response: [Container].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
@@ -303,6 +322,9 @@ pub struct GetContainerLog {
|
||||
/// Max: 5000.
|
||||
#[serde(default = "default_tail")]
|
||||
pub tail: U64,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
fn default_tail() -> u64 {
|
||||
@@ -341,6 +363,9 @@ pub struct SearchContainerLog {
|
||||
/// Invert the results, ie return all lines that DON'T match the terms / combinator.
|
||||
#[serde(default)]
|
||||
pub invert: bool,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -69,6 +69,9 @@ pub struct GetStackServiceLog {
|
||||
/// Max: 5000.
|
||||
#[serde(default = "default_tail")]
|
||||
pub tail: U64,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
fn default_tail() -> u64 {
|
||||
@@ -107,6 +110,9 @@ pub struct SearchStackServiceLog {
|
||||
/// Invert the results, ie return all lines that DON'T match the terms / combinator.
|
||||
#[serde(default)]
|
||||
pub invert: bool,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -103,7 +103,7 @@ pub struct RenameStack {
|
||||
|
||||
//
|
||||
|
||||
/// Rename the stack at id to the given name. Response: [Update].
|
||||
/// Update file contents in Files on Server or Git Repo mode. Response: [Update].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
@@ -111,7 +111,7 @@ pub struct RenameStack {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
pub struct WriteStackFileContents {
|
||||
/// The name or id of the Stack to write the contents to.
|
||||
/// The name or id of the target Stack.
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub stack: String,
|
||||
/// The file path relative to the stack run directory,
|
||||
|
||||
@@ -5,6 +5,7 @@ use typeshare::typeshare;
|
||||
|
||||
use crate::entities::{
|
||||
sync::{ResourceSync, _PartialResourceSyncConfig},
|
||||
update::Update,
|
||||
NoData,
|
||||
};
|
||||
|
||||
@@ -98,6 +99,28 @@ pub struct RefreshResourceSyncPending {
|
||||
|
||||
//
|
||||
|
||||
/// Rename the stack at id to the given name. Response: [Update].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
pub struct WriteSyncFileContents {
|
||||
/// The name or id of the target Sync.
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub sync: String,
|
||||
/// If this file was under a resource folder, this will be the folder.
|
||||
/// Otherwise, it should be empty string.
|
||||
pub resource_path: String,
|
||||
/// The file path relative to the resource path.
|
||||
pub file_path: String,
|
||||
/// The contents to write.
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Commits matching resources updated configuration to the target resource sync. Response: [Update]
|
||||
///
|
||||
/// Note. Will fail if the Sync is not `managed`.
|
||||
|
||||
@@ -3,12 +3,69 @@ use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::user::User;
|
||||
use crate::entities::{user::User, NoData};
|
||||
|
||||
use super::KomodoWriteRequest;
|
||||
|
||||
//
|
||||
|
||||
/// **Only for local users**. Update the calling users username.
|
||||
/// Response: [NoData].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(UpdateUserUsernameResponse)]
|
||||
pub struct UpdateUserUsername {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type UpdateUserUsernameResponse = NoData;
|
||||
|
||||
//
|
||||
|
||||
/// **Only for local users**. Update the calling users password.
|
||||
/// Response: [NoData].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(UpdateUserPasswordResponse)]
|
||||
pub struct UpdateUserPassword {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type UpdateUserPasswordResponse = NoData;
|
||||
|
||||
//
|
||||
|
||||
/// **Admin only**. Delete a user.
|
||||
/// Admins can delete any non-admin user.
|
||||
/// Only Super Admin can delete an admin.
|
||||
/// No users can delete a Super Admin user.
|
||||
/// User cannot delete themselves.
|
||||
/// Response: [NoData].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(DeleteUserResponse)]
|
||||
pub struct DeleteUser {
|
||||
/// User id or username
|
||||
#[serde(alias = "username", alias = "id")]
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type DeleteUserResponse = User;
|
||||
|
||||
//
|
||||
|
||||
/// **Admin only.** Create a service user.
|
||||
/// Response: [User].
|
||||
#[typeshare]
|
||||
|
||||
@@ -37,9 +37,8 @@ pub type _PartialAlerterConfig = PartialAlerterConfig;
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct AlerterConfig {
|
||||
/// Whether the alerter is enabled
|
||||
#[serde(default = "default_enabled")]
|
||||
#[builder(default = "default_enabled()")]
|
||||
#[partial_default(default_enabled())]
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Where to route the alert messages.
|
||||
@@ -73,14 +72,10 @@ impl AlerterConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for AlerterConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: default_enabled(),
|
||||
enabled: Default::default(),
|
||||
endpoint: Default::default(),
|
||||
alert_types: Default::default(),
|
||||
resources: Default::default(),
|
||||
|
||||
@@ -33,6 +33,8 @@ pub struct BuildListItemInfo {
|
||||
pub builder_id: String,
|
||||
/// The git provider domain
|
||||
pub git_provider: String,
|
||||
/// The image registry domain
|
||||
pub image_registry_domain: String,
|
||||
/// The repo used as the source of the build
|
||||
pub repo: String,
|
||||
/// The branch of the repo
|
||||
|
||||
@@ -58,6 +58,8 @@ pub struct Env {
|
||||
pub komodo_jwt_secret_file: Option<PathBuf>,
|
||||
/// Override `jwt_ttl`
|
||||
pub komodo_jwt_ttl: Option<Timelength>,
|
||||
/// Override `sync_directory`
|
||||
pub komodo_sync_directory: Option<PathBuf>,
|
||||
/// Override `repo_directory`
|
||||
pub komodo_repo_directory: Option<PathBuf>,
|
||||
/// Override `resource_poll_interval`
|
||||
@@ -116,6 +118,10 @@ pub struct Env {
|
||||
pub komodo_oidc_client_secret_file: Option<PathBuf>,
|
||||
/// Override `oidc_use_full_email`
|
||||
pub komodo_oidc_use_full_email: Option<bool>,
|
||||
/// Override `oidc_additional_audiences`
|
||||
pub komodo_oidc_additional_audiences: Option<Vec<String>>,
|
||||
/// Override `oidc_additional_audiences` from file
|
||||
pub komodo_oidc_additional_audiences_file: Option<PathBuf>,
|
||||
|
||||
/// Override `google_oauth.enabled`
|
||||
pub komodo_google_oauth_enabled: Option<bool>,
|
||||
@@ -344,6 +350,11 @@ pub struct CoreConfig {
|
||||
#[serde(default)]
|
||||
pub oidc_use_full_email: bool,
|
||||
|
||||
/// Your OIDC provider may set additional audiences other than `client_id`,
|
||||
/// they must be added here to make claims verification work.
|
||||
#[serde(default)]
|
||||
pub oidc_additional_audiences: Vec<String>,
|
||||
|
||||
// =========
|
||||
// = Oauth =
|
||||
// =========
|
||||
@@ -450,15 +461,9 @@ pub struct CoreConfig {
|
||||
#[serde(default)]
|
||||
pub secrets: HashMap<String, String>,
|
||||
|
||||
// =========
|
||||
// = Other =
|
||||
// =========
|
||||
/// Specify the directory used to clone stack / repo / build repos, for latest hash / contents.
|
||||
/// The default is fine when using a container.
|
||||
/// Default: `/repo-cache`
|
||||
#[serde(default = "default_repo_directory")]
|
||||
pub repo_directory: PathBuf,
|
||||
|
||||
// =======
|
||||
// = SSL =
|
||||
// =======
|
||||
/// Whether to enable ssl.
|
||||
#[serde(default)]
|
||||
pub ssl_enabled: bool,
|
||||
@@ -472,6 +477,20 @@ pub struct CoreConfig {
|
||||
/// Default: `/config/ssl/cert.pem`.
|
||||
#[serde(default = "default_ssl_cert_file")]
|
||||
pub ssl_cert_file: PathBuf,
|
||||
|
||||
// =========
|
||||
// = Other =
|
||||
// =========
|
||||
/// Configure directory to store sync files.
|
||||
/// Default: `/syncs`
|
||||
#[serde(default = "default_sync_directory")]
|
||||
pub sync_directory: PathBuf,
|
||||
|
||||
/// Specify the directory used to clone stack / repo / build repos, for latest hash / contents.
|
||||
/// The default is fine when using a container.
|
||||
/// Default: `/repo-cache`
|
||||
#[serde(default = "default_repo_directory")]
|
||||
pub repo_directory: PathBuf,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
@@ -495,6 +514,11 @@ fn default_repo_directory() -> PathBuf {
|
||||
PathBuf::from_str("/repo-cache").unwrap()
|
||||
}
|
||||
|
||||
fn default_sync_directory() -> PathBuf {
|
||||
// unwrap ok: `/syncs` will always be valid path
|
||||
PathBuf::from_str("/syncs").unwrap()
|
||||
}
|
||||
|
||||
fn default_prune_days() -> u64 {
|
||||
14
|
||||
}
|
||||
@@ -528,6 +552,7 @@ impl CoreConfig {
|
||||
jwt_secret: empty_or_redacted(&config.jwt_secret),
|
||||
jwt_ttl: config.jwt_ttl,
|
||||
repo_directory: config.repo_directory,
|
||||
sync_directory: config.sync_directory,
|
||||
resource_poll_interval: config.resource_poll_interval,
|
||||
monitoring_interval: config.monitoring_interval,
|
||||
keep_stats_for_days: config.keep_stats_for_days,
|
||||
@@ -548,6 +573,11 @@ impl CoreConfig {
|
||||
&config.oidc_client_secret,
|
||||
),
|
||||
oidc_use_full_email: config.oidc_use_full_email,
|
||||
oidc_additional_audiences: config
|
||||
.oidc_additional_audiences
|
||||
.iter()
|
||||
.map(|aud| empty_or_redacted(aud))
|
||||
.collect(),
|
||||
google_oauth: OauthCredentials {
|
||||
enabled: config.google_oauth.enabled,
|
||||
id: empty_or_redacted(&config.google_oauth.id),
|
||||
|
||||
@@ -87,7 +87,7 @@ pub struct Env {
|
||||
/// If not provided, will use Default config.
|
||||
///
|
||||
/// Note. This is overridden if the equivalent arg is passed in [CliArgs].
|
||||
#[serde(default)]
|
||||
#[serde(default, alias = "periphery_config_path")]
|
||||
pub periphery_config_paths: Vec<String>,
|
||||
/// If specifying folders, use this to narrow down which
|
||||
/// files will be matched to parse into the final [PeripheryConfig].
|
||||
@@ -95,7 +95,7 @@ pub struct Env {
|
||||
/// provided to `config_keywords` will be included.
|
||||
///
|
||||
/// Note. This is overridden if the equivalent arg is passed in [CliArgs].
|
||||
#[serde(default)]
|
||||
#[serde(default, alias = "periphery_config_keyword")]
|
||||
pub periphery_config_keywords: Vec<String>,
|
||||
|
||||
/// Will merge nested config object (eg. secrets, providers) across multiple
|
||||
|
||||
@@ -13,6 +13,8 @@ use super::{ContainerConfig, GraphDriverData, PortBinding};
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct ContainerListItem {
|
||||
/// The Server which holds the container.
|
||||
pub server_id: Option<String>,
|
||||
/// The first name in Names, not including the initial '/'
|
||||
pub name: String,
|
||||
/// The ID of this container
|
||||
@@ -37,6 +39,11 @@ pub struct ContainerListItem {
|
||||
pub networks: Vec<String>,
|
||||
/// The volume names attached to container
|
||||
pub volumes: Vec<String>,
|
||||
/// The labels attached to container.
|
||||
/// It's too big to send with container list,
|
||||
/// can get it using InspectContainer
|
||||
#[serde(default, skip_serializing)]
|
||||
pub labels: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -164,7 +164,11 @@ pub fn get_image_name(
|
||||
}
|
||||
|
||||
pub fn to_komodo_name(name: &str) -> String {
|
||||
name.to_lowercase().replace([' ', '.'], "_")
|
||||
name
|
||||
.to_lowercase()
|
||||
.replace([' ', '.'], "_")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Unix timestamp in milliseconds as i64
|
||||
@@ -193,7 +197,7 @@ pub struct __Serror {
|
||||
pub struct SystemCommand {
|
||||
#[serde(default)]
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "file_contents_deserializer")]
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
@@ -215,7 +219,7 @@ impl SystemCommand {
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.path.is_empty() || self.command.is_empty()
|
||||
self.command.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +614,7 @@ impl CloneArgs {
|
||||
pub fn path(&self, repo_dir: &Path) -> PathBuf {
|
||||
let path = match &self.destination {
|
||||
Some(destination) => PathBuf::from(&destination),
|
||||
None => repo_dir.join(&to_komodo_name(&self.name)),
|
||||
None => repo_dir.join(to_komodo_name(&self.name)),
|
||||
};
|
||||
path.components().collect::<PathBuf>()
|
||||
}
|
||||
@@ -620,7 +624,7 @@ impl CloneArgs {
|
||||
access_token: Option<&str>,
|
||||
) -> anyhow::Result<String> {
|
||||
let access_token_at = match &access_token {
|
||||
Some(token) => format!("{token}@"),
|
||||
Some(token) => format!("token:{token}@"),
|
||||
None => String::new(),
|
||||
};
|
||||
let protocol = if self.https { "https" } else { "http" };
|
||||
@@ -646,9 +650,7 @@ impl CloneArgs {
|
||||
.join(self.provider.replace('/', "-"))
|
||||
.join(repo.replace('/', "-"))
|
||||
.join(self.branch.replace('/', "-"))
|
||||
.join(
|
||||
self.commit.as_ref().map(String::as_str).unwrap_or("latest"),
|
||||
);
|
||||
.join(self.commit.as_deref().unwrap_or("latest"));
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
@@ -947,6 +949,7 @@ pub enum Operation {
|
||||
CreateResourceSync,
|
||||
UpdateResourceSync,
|
||||
DeleteResourceSync,
|
||||
WriteSyncContents,
|
||||
CommitSync,
|
||||
RunSync,
|
||||
|
||||
@@ -1202,8 +1205,8 @@ impl<'de> Visitor<'de> for FileContentsVisitor {
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let out = v.to_string();
|
||||
if out.is_empty() || out.ends_with('\n') {
|
||||
let out = v.trim_end().to_string();
|
||||
if out.is_empty() {
|
||||
Ok(out)
|
||||
} else {
|
||||
Ok(out + "\n")
|
||||
|
||||
@@ -104,7 +104,7 @@ pub struct ProcedureStage {
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
/// The executions in the stage
|
||||
#[serde(default)]
|
||||
#[serde(default, alias = "execution")]
|
||||
pub executions: Vec<EnabledExecution>,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use typeshare::typeshare;
|
||||
use super::{
|
||||
docker::container::ContainerListItem,
|
||||
resource::{Resource, ResourceListItem, ResourceQuery},
|
||||
to_komodo_name, FileContents,
|
||||
to_komodo_name, FileContents, SystemCommand,
|
||||
};
|
||||
|
||||
#[typeshare]
|
||||
@@ -213,6 +213,11 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub run_build: bool,
|
||||
|
||||
/// Whether to run `docker compose down` before `compose up`.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub destroy_before_deploy: bool,
|
||||
|
||||
/// Whether to skip secret interpolation into the stack environment variables.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
@@ -284,6 +289,12 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub commit: String,
|
||||
|
||||
/// By default, the Stack will `git pull` the repo after it is first cloned.
|
||||
/// If this option is enabled, the repo folder will be deleted and recloned instead.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub reclone: bool,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
#[builder(default = "default_webhook_enabled()")]
|
||||
@@ -296,6 +307,12 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub webhook_secret: String,
|
||||
|
||||
/// By default, the Stack will `DeployStackIfChanged`.
|
||||
/// If this option is enabled, will always run `DeployStack` without diffing.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub webhook_force_deploy: bool,
|
||||
|
||||
/// Whether to send StackStateChange alerts for this stack.
|
||||
#[serde(default = "default_send_alerts")]
|
||||
#[builder(default = "default_send_alerts()")]
|
||||
@@ -312,6 +329,11 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub registry_account: String,
|
||||
|
||||
/// The optional command to run before the Stack is deployed.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub pre_deploy: SystemCommand,
|
||||
|
||||
/// The extra arguments to pass after `docker compose up -d`.
|
||||
/// If empty, no extra arguments will be passed.
|
||||
#[serde(default)]
|
||||
@@ -410,10 +432,12 @@ impl Default for StackConfig {
|
||||
file_contents: Default::default(),
|
||||
auto_pull: default_auto_pull(),
|
||||
ignore_services: Default::default(),
|
||||
pre_deploy: Default::default(),
|
||||
extra_args: Default::default(),
|
||||
environment: Default::default(),
|
||||
env_file_path: default_env_file_path(),
|
||||
run_build: Default::default(),
|
||||
destroy_before_deploy: Default::default(),
|
||||
build_extra_args: Default::default(),
|
||||
skip_secret_interp: Default::default(),
|
||||
git_provider: default_git_provider(),
|
||||
@@ -421,9 +445,11 @@ impl Default for StackConfig {
|
||||
repo: Default::default(),
|
||||
branch: default_branch(),
|
||||
commit: Default::default(),
|
||||
reclone: Default::default(),
|
||||
git_account: Default::default(),
|
||||
webhook_enabled: default_webhook_enabled(),
|
||||
webhook_secret: Default::default(),
|
||||
webhook_force_deploy: Default::default(),
|
||||
send_alerts: default_send_alerts(),
|
||||
links: Default::default(),
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ use bson::{doc, Document};
|
||||
use derive_builder::Builder;
|
||||
use derive_default_builder::DefaultBuilder;
|
||||
use partial_derive2::Partial;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{
|
||||
de::{value::SeqAccessDeserializer, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use strum::Display;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use super::{
|
||||
resource::{Resource, ResourceListItem, ResourceQuery},
|
||||
FileContents, ResourceTarget, I64,
|
||||
ResourceTarget, I64,
|
||||
};
|
||||
|
||||
#[typeshare]
|
||||
@@ -26,8 +29,8 @@ pub struct ResourceSyncListItemInfo {
|
||||
pub file_contents: bool,
|
||||
/// Whether sync has `managed` mode enabled.
|
||||
pub managed: bool,
|
||||
/// Resource path to the files.
|
||||
pub resource_path: String,
|
||||
/// Resource paths to the files.
|
||||
pub resource_path: Vec<String>,
|
||||
/// The git provider domain.
|
||||
pub git_provider: String,
|
||||
/// The Github repo used as the source of the sync resources
|
||||
@@ -96,10 +99,10 @@ pub struct ResourceSyncInfo {
|
||||
|
||||
/// The current sync files
|
||||
#[serde(default)]
|
||||
pub remote_contents: Vec<FileContents>,
|
||||
pub remote_contents: Vec<SyncFileContents>,
|
||||
/// Any read errors in files by path
|
||||
#[serde(default)]
|
||||
pub remote_errors: Vec<FileContents>,
|
||||
pub remote_errors: Vec<SyncFileContents>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -118,6 +121,9 @@ pub struct ResourceDiff {
|
||||
pub enum DiffData {
|
||||
/// Resource will be created
|
||||
Create {
|
||||
/// The name of resource to create
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
/// The proposed resource to create in TOML
|
||||
proposed: String,
|
||||
},
|
||||
@@ -213,10 +219,13 @@ pub struct ResourceSyncConfig {
|
||||
/// - If Git Repo based, this is relative to the root of the repo.
|
||||
/// Can be a specific file, or a directory containing multiple files / folders.
|
||||
/// See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information.
|
||||
#[serde(default = "default_resource_path")]
|
||||
#[builder(default = "default_resource_path()")]
|
||||
#[partial_default(default_resource_path())]
|
||||
pub resource_path: String,
|
||||
#[serde(default, deserialize_with = "resource_path_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_resource_path_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub resource_path: Vec<String>,
|
||||
|
||||
/// Enable "pushes" to the file,
|
||||
/// which exports resources matching tags to single file.
|
||||
@@ -270,10 +279,6 @@ fn default_branch() -> String {
|
||||
String::from("main")
|
||||
}
|
||||
|
||||
fn default_resource_path() -> String {
|
||||
String::from("./resources.toml")
|
||||
}
|
||||
|
||||
fn default_webhook_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -287,7 +292,7 @@ impl Default for ResourceSyncConfig {
|
||||
branch: default_branch(),
|
||||
commit: Default::default(),
|
||||
git_account: Default::default(),
|
||||
resource_path: default_resource_path(),
|
||||
resource_path: Default::default(),
|
||||
files_on_host: Default::default(),
|
||||
file_contents: Default::default(),
|
||||
managed: Default::default(),
|
||||
@@ -299,6 +304,106 @@ impl Default for ResourceSyncConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SyncFileContents {
|
||||
/// The base resource path.
|
||||
#[serde(default)]
|
||||
pub resource_path: String,
|
||||
/// The path of the file / error path relative to the resource path.
|
||||
pub path: String,
|
||||
/// The contents of the file
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
/// Using this ensures the resource path deserialized to Vec<String>
|
||||
pub fn resource_path_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(ResourcePathVisitor)
|
||||
}
|
||||
|
||||
/// Using this ensures the resource path deserialized to Vec<String>
|
||||
pub fn option_resource_path_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<String>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(OptionResourcePathVisitor)
|
||||
}
|
||||
|
||||
struct ResourcePathVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ResourcePathVisitor {
|
||||
type Value = Vec<String>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(vec![v.to_string()])
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
Vec::deserialize(SeqAccessDeserializer::new(seq))
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionResourcePathVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for OptionResourcePathVisitor {
|
||||
type Value = Option<Vec<String>>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "null or string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
ResourcePathVisitor.visit_str(v).map(Some)
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
ResourcePathVisitor.visit_seq(seq).map(Some)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct ResourceSyncActionState {
|
||||
|
||||
@@ -129,14 +129,6 @@ pub struct ResourceToml<PartialConfig: Default> {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub after: Vec<String>,
|
||||
|
||||
/// Optional. Only relevant for stacks / repos.
|
||||
///
|
||||
/// Will ensure stacks with 'deploy = true' are running with the latest commit hash for the repo.
|
||||
/// Deploy actions to achieve this will be included in the sync.
|
||||
/// Default is false.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub latest_hash: bool,
|
||||
|
||||
/// Resource specific configuration.
|
||||
#[serde(default)]
|
||||
pub config: PartialConfig,
|
||||
|
||||
@@ -27,8 +27,8 @@ pub fn parse_key_value_list(
|
||||
.trim_start_matches('-')
|
||||
.trim();
|
||||
// Remove wrapping quotes (from yaml list)
|
||||
let line = if let Some(line) = line.strip_prefix('"') {
|
||||
line.strip_suffix('"').unwrap_or(line)
|
||||
let line = if let Some(line) = line.strip_prefix(['"', '\'']) {
|
||||
line.strip_suffix(['"', '\'']).unwrap_or(line)
|
||||
} else {
|
||||
line
|
||||
};
|
||||
@@ -43,11 +43,12 @@ pub fn parse_key_value_list(
|
||||
.map(|(key, value)| {
|
||||
let value = value.trim();
|
||||
// Remove wrapping quotes around value
|
||||
if let Some(value) = value.strip_prefix('"') {
|
||||
value.strip_suffix('"').unwrap_or(value)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let value =
|
||||
if let Some(value) = value.strip_prefix(['"', '\'']) {
|
||||
value.strip_suffix(['"', '\'']).unwrap_or(value)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
(key.trim().to_string(), value.trim().to_string())
|
||||
})?;
|
||||
anyhow::Ok((key, value))
|
||||
|
||||
@@ -58,6 +58,7 @@ export type ReadResponses = {
|
||||
GetServerState: Types.GetServerStateResponse;
|
||||
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
|
||||
ListDockerContainers: Types.ListDockerContainersResponse;
|
||||
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
|
||||
InspectDockerContainer: Types.InspectDockerContainerResponse;
|
||||
GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse;
|
||||
GetContainerLog: Types.GetContainerLogResponse;
|
||||
@@ -172,6 +173,11 @@ export type ReadResponses = {
|
||||
};
|
||||
|
||||
export type WriteResponses = {
|
||||
// ==== USER ====
|
||||
UpdateUserUsername: Types.UpdateUserUsername;
|
||||
UpdateUserPassword: Types.UpdateUserPassword;
|
||||
DeleteUser: Types.DeleteUser;
|
||||
|
||||
// ==== SERVICE USER ====
|
||||
CreateServiceUser: Types.CreateServiceUserResponse;
|
||||
UpdateServiceUserDescription: Types.UpdateServiceUserDescriptionResponse;
|
||||
@@ -257,8 +263,9 @@ export type WriteResponses = {
|
||||
CopyResourceSync: Types.ResourceSync;
|
||||
DeleteResourceSync: Types.ResourceSync;
|
||||
UpdateResourceSync: Types.ResourceSync;
|
||||
RefreshResourceSyncPending: Types.ResourceSync;
|
||||
CommitSync: Types.ResourceSync;
|
||||
WriteSyncFileContents: Types.Update;
|
||||
RefreshResourceSyncPending: Types.ResourceSync;
|
||||
CreateSyncWebhook: Types.CreateSyncWebhookResponse;
|
||||
DeleteSyncWebhook: Types.DeleteSyncWebhookResponse;
|
||||
|
||||
@@ -349,6 +356,7 @@ export type ExecuteResponses = {
|
||||
|
||||
// ==== STACK ====
|
||||
DeployStack: Types.Update;
|
||||
DeployStackIfChanged: Types.Update;
|
||||
StartStack: Types.Update;
|
||||
RestartStack: Types.Update;
|
||||
StopStack: Types.Update;
|
||||
|
||||
+155
-17
@@ -297,7 +297,7 @@ export type AlerterEndpoint =
|
||||
|
||||
export interface AlerterConfig {
|
||||
/** Whether the alerter is enabled */
|
||||
enabled: boolean;
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Where to route the alert messages.
|
||||
*
|
||||
@@ -438,8 +438,6 @@ export interface BuildConfig {
|
||||
webhook_secret?: string;
|
||||
/** The optional command run after repo clone and before docker build. */
|
||||
pre_build?: SystemCommand;
|
||||
/** Configuration for the registry to push the built image to. */
|
||||
image_registry?: ImageRegistryConfig;
|
||||
/**
|
||||
* The path of the docker build context relative to the root of the repo.
|
||||
* Default: "." (the root of the repo).
|
||||
@@ -447,6 +445,8 @@ export interface BuildConfig {
|
||||
build_path: string;
|
||||
/** The path of the dockerfile relative to the build path. */
|
||||
dockerfile_path: string;
|
||||
/** Configuration for the registry to push the built image to. */
|
||||
image_registry?: ImageRegistryConfig;
|
||||
/** Whether to skip secret interpolation in the build_args. */
|
||||
skip_secret_interp?: boolean;
|
||||
/** Whether to use buildx to build (eg `docker buildx build ...`) */
|
||||
@@ -512,6 +512,8 @@ export interface BuildListItemInfo {
|
||||
builder_id: string;
|
||||
/** The git provider domain */
|
||||
git_provider: string;
|
||||
/** The image registry domain */
|
||||
image_registry_domain: string;
|
||||
/** The repo used as the source of the build */
|
||||
repo: string;
|
||||
/** The branch of the repo */
|
||||
@@ -863,6 +865,7 @@ export type Execution =
|
||||
| { type: "PruneSystem", params: PruneSystem }
|
||||
| { type: "RunSync", params: RunSync }
|
||||
| { type: "DeployStack", params: DeployStack }
|
||||
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
|
||||
| { type: "StartStack", params: StartStack }
|
||||
| { type: "RestartStack", params: RestartStack }
|
||||
| { type: "PauseStack", params: PauseStack }
|
||||
@@ -1496,6 +1499,8 @@ export enum ContainerStateStatusEnum {
|
||||
}
|
||||
|
||||
export interface ContainerListItem {
|
||||
/** The Server which holds the container. */
|
||||
server_id?: string;
|
||||
/** The first name in Names, not including the initial '/' */
|
||||
name: string;
|
||||
/** The ID of this container */
|
||||
@@ -1520,10 +1525,18 @@ export interface ContainerListItem {
|
||||
networks: string[];
|
||||
/** The volume names attached to container */
|
||||
volumes: string[];
|
||||
/**
|
||||
* The labels attached to container.
|
||||
* It's too big to send with container list,
|
||||
* can get it using InspectContainer
|
||||
*/
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type ListDockerContainersResponse = ContainerListItem[];
|
||||
|
||||
export type ListAllDockerContainersResponse = ContainerListItem[];
|
||||
|
||||
export enum HealthStatusEnum {
|
||||
Empty = "",
|
||||
None = "none",
|
||||
@@ -2321,6 +2334,8 @@ export interface StackConfig {
|
||||
* Combine with build_extra_args for custom behaviors.
|
||||
*/
|
||||
run_build?: boolean;
|
||||
/** Whether to run `docker compose down` before `compose up`. */
|
||||
destroy_before_deploy?: boolean;
|
||||
/** Whether to skip secret interpolation into the stack environment variables. */
|
||||
skip_secret_interp?: boolean;
|
||||
/**
|
||||
@@ -2365,6 +2380,11 @@ export interface StackConfig {
|
||||
branch: string;
|
||||
/** Optionally set a specific commit hash. */
|
||||
commit?: string;
|
||||
/**
|
||||
* By default, the Stack will `git pull` the repo after it is first cloned.
|
||||
* If this option is enabled, the repo folder will be deleted and recloned instead.
|
||||
*/
|
||||
reclone?: boolean;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
@@ -2372,12 +2392,19 @@ export interface StackConfig {
|
||||
* If its an empty string, use the default secret from the config.
|
||||
*/
|
||||
webhook_secret?: string;
|
||||
/**
|
||||
* By default, the Stack will `DeployStackIfChanged`.
|
||||
* If this option is enabled, will always run `DeployStack` without diffing.
|
||||
*/
|
||||
webhook_force_deploy?: boolean;
|
||||
/** Whether to send StackStateChange alerts for this stack. */
|
||||
send_alerts: boolean;
|
||||
/** Used with `registry_account` to login to a registry before docker compose up. */
|
||||
registry_provider?: string;
|
||||
/** Used with `registry_provider` to login to a registry before docker compose up. */
|
||||
registry_account?: string;
|
||||
/** The optional command to run before the Stack is deployed. */
|
||||
pre_deploy?: SystemCommand;
|
||||
/**
|
||||
* The extra arguments to pass after `docker compose up -d`.
|
||||
* If empty, no extra arguments will be passed.
|
||||
@@ -2630,7 +2657,7 @@ export interface ResourceSyncConfig {
|
||||
* Can be a specific file, or a directory containing multiple files / folders.
|
||||
* See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information.
|
||||
*/
|
||||
resource_path: string;
|
||||
resource_path?: string[];
|
||||
/**
|
||||
* Enable "pushes" to the file,
|
||||
* which exports resources matching tags to single file.
|
||||
@@ -2656,6 +2683,8 @@ export interface ResourceSyncConfig {
|
||||
export type DiffData =
|
||||
/** Resource will be created */
|
||||
| { type: "Create", data: {
|
||||
/** The name of resource to create */
|
||||
name?: string;
|
||||
/** The proposed resource to create in TOML */
|
||||
proposed: string;
|
||||
}}
|
||||
@@ -2687,6 +2716,15 @@ export interface SyncDeployUpdate {
|
||||
log: string;
|
||||
}
|
||||
|
||||
export interface SyncFileContents {
|
||||
/** The base resource path. */
|
||||
resource_path?: string;
|
||||
/** The path of the file / error path relative to the resource path. */
|
||||
path: string;
|
||||
/** The contents of the file */
|
||||
contents: string;
|
||||
}
|
||||
|
||||
export interface ResourceSyncInfo {
|
||||
/** Unix timestamp of last applied sync */
|
||||
last_sync_ts?: I64;
|
||||
@@ -2709,9 +2747,9 @@ export interface ResourceSyncInfo {
|
||||
/** The commit message which produced these pending updates. */
|
||||
pending_message?: string;
|
||||
/** The current sync files */
|
||||
remote_contents?: FileContents[];
|
||||
remote_contents?: SyncFileContents[];
|
||||
/** Any read errors in files by path */
|
||||
remote_errors?: FileContents[];
|
||||
remote_errors?: SyncFileContents[];
|
||||
}
|
||||
|
||||
export type ResourceSync = Resource<ResourceSyncConfig, ResourceSyncInfo>;
|
||||
@@ -2740,8 +2778,8 @@ export interface ResourceSyncListItemInfo {
|
||||
file_contents: boolean;
|
||||
/** Whether sync has `managed` mode enabled. */
|
||||
managed: boolean;
|
||||
/** Resource path to the files. */
|
||||
resource_path: string;
|
||||
/** Resource paths to the files. */
|
||||
resource_path: string[];
|
||||
/** The git provider domain. */
|
||||
git_provider: string;
|
||||
/** The Github repo used as the source of the sync resources */
|
||||
@@ -2879,6 +2917,7 @@ export enum Operation {
|
||||
CreateResourceSync = "CreateResourceSync",
|
||||
UpdateResourceSync = "UpdateResourceSync",
|
||||
DeleteResourceSync = "DeleteResourceSync",
|
||||
WriteSyncContents = "WriteSyncContents",
|
||||
CommitSync = "CommitSync",
|
||||
RunSync = "RunSync",
|
||||
CreateVariable = "CreateVariable",
|
||||
@@ -3105,6 +3144,12 @@ export type DeleteSyncWebhookResponse = NoData;
|
||||
|
||||
export type UpdateTagsOnResourceResponse = NoData;
|
||||
|
||||
export type UpdateUserUsernameResponse = NoData;
|
||||
|
||||
export type UpdateUserPasswordResponse = NoData;
|
||||
|
||||
export type DeleteUserResponse = User;
|
||||
|
||||
export type CreateServiceUserResponse = User;
|
||||
|
||||
export type UpdateServiceUserDescriptionResponse = User;
|
||||
@@ -3737,11 +3782,7 @@ export interface LaunchServer {
|
||||
server_template: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploys the target stack. `docker compose up`. Response: [Update]
|
||||
*
|
||||
* Note. If the stack is already deployed, it will be destroyed first.
|
||||
*/
|
||||
/** Deploys the target stack. `docker compose up`. Response: [Update] */
|
||||
export interface DeployStack {
|
||||
/** Id or name */
|
||||
stack: string;
|
||||
@@ -3752,6 +3793,21 @@ export interface DeployStack {
|
||||
stop_time?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks deployed contents vs latest contents,
|
||||
* and only if any changes found
|
||||
* will `docker compose up`. Response: [Update]
|
||||
*/
|
||||
export interface DeployStackIfChanged {
|
||||
/** Id or name */
|
||||
stack: string;
|
||||
/**
|
||||
* Override the default termination max time.
|
||||
* Only used if the stack needs to be taken down first.
|
||||
*/
|
||||
stop_time?: number;
|
||||
}
|
||||
|
||||
/** Starts the target stack. `docker compose start`. Response: [Update] */
|
||||
export interface StartStack {
|
||||
/** Id or name */
|
||||
@@ -3788,7 +3844,7 @@ export interface UnpauseStack {
|
||||
service?: string;
|
||||
}
|
||||
|
||||
/** Starts the target stack. `docker compose stop`. Response: [Update] */
|
||||
/** Stops the target stack. `docker compose stop`. Response: [Update] */
|
||||
export interface StopStack {
|
||||
/** Id or name */
|
||||
stack: string;
|
||||
@@ -3812,6 +3868,17 @@ export interface DestroyStack {
|
||||
export interface RunSync {
|
||||
/** Id or name */
|
||||
sync: string;
|
||||
/**
|
||||
* Only execute sync on a specific resource type.
|
||||
* Combine with `resource_id` to specify resource.
|
||||
*/
|
||||
resource_type?: ResourceTarget["type"];
|
||||
/**
|
||||
* Only execute sync on a specific resources.
|
||||
* Combine with `resource_type` to specify resources.
|
||||
* Supports name or id.
|
||||
*/
|
||||
resources?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4103,6 +4170,8 @@ export interface GetDeploymentLog {
|
||||
* Max: 5000.
|
||||
*/
|
||||
tail: U64;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
export enum SearchCombinator {
|
||||
@@ -4130,6 +4199,8 @@ export interface SearchDeploymentLog {
|
||||
combinator?: SearchCombinator;
|
||||
/** Invert the results, ie return all lines that DON'T match the terms / combinator. */
|
||||
invert?: boolean;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4571,6 +4642,15 @@ export interface ListDockerContainers {
|
||||
server: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all docker containers on the target server.
|
||||
* Response: [ListDockerContainersResponse].
|
||||
*/
|
||||
export interface ListAllDockerContainers {
|
||||
/** Filter by server id or name. */
|
||||
servers?: string[];
|
||||
}
|
||||
|
||||
/** Inspect a docker container on the server. Response: [Container]. */
|
||||
export interface InspectDockerContainer {
|
||||
/** Id or name */
|
||||
@@ -4596,6 +4676,8 @@ export interface GetContainerLog {
|
||||
* Max: 5000.
|
||||
*/
|
||||
tail: U64;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4620,6 +4702,8 @@ export interface SearchContainerLog {
|
||||
combinator?: SearchCombinator;
|
||||
/** Invert the results, ie return all lines that DON'T match the terms / combinator. */
|
||||
invert?: boolean;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
/** Inspect a docker container on the server. Response: [Container]. */
|
||||
@@ -4810,6 +4894,8 @@ export interface GetStackServiceLog {
|
||||
* Max: 5000.
|
||||
*/
|
||||
tail: U64;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4834,6 +4920,8 @@ export interface SearchStackServiceLog {
|
||||
combinator?: SearchCombinator;
|
||||
/** Invert the results, ie return all lines that DON'T match the terms / combinator. */
|
||||
invert?: boolean;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5878,9 +5966,9 @@ export interface RenameStack {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Rename the stack at id to the given name. Response: [Update]. */
|
||||
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
|
||||
export interface WriteStackFileContents {
|
||||
/** The name or id of the Stack to write the contents to. */
|
||||
/** The name or id of the target Stack. */
|
||||
stack: string;
|
||||
/**
|
||||
* The file path relative to the stack run directory,
|
||||
@@ -5980,6 +6068,21 @@ export interface RefreshResourceSyncPending {
|
||||
sync: string;
|
||||
}
|
||||
|
||||
/** Rename the stack at id to the given name. Response: [Update]. */
|
||||
export interface WriteSyncFileContents {
|
||||
/** The name or id of the target Sync. */
|
||||
sync: string;
|
||||
/**
|
||||
* If this file was under a resource folder, this will be the folder.
|
||||
* Otherwise, it should be empty string.
|
||||
*/
|
||||
resource_path: string;
|
||||
/** The file path relative to the resource path. */
|
||||
file_path: string;
|
||||
/** The contents to write. */
|
||||
contents: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits matching resources updated configuration to the target resource sync. Response: [Update]
|
||||
*
|
||||
@@ -6051,6 +6154,35 @@ export interface UpdateTagsOnResource {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* **Only for local users**. Update the calling users username.
|
||||
* Response: [NoData].
|
||||
*/
|
||||
export interface UpdateUserUsername {
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Only for local users**. Update the calling users password.
|
||||
* Response: [NoData].
|
||||
*/
|
||||
export interface UpdateUserPassword {
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Admin only**. Delete a user.
|
||||
* Admins can delete any non-admin user.
|
||||
* Only Super Admin can delete an admin.
|
||||
* No users can delete a Super Admin user.
|
||||
* User cannot delete themselves.
|
||||
* Response: [NoData].
|
||||
*/
|
||||
export interface DeleteUser {
|
||||
/** User id or username */
|
||||
user: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Admin only.** Create a service user.
|
||||
* Response: [User].
|
||||
@@ -6533,6 +6665,7 @@ export type ExecuteRequest =
|
||||
| { type: "StopDeployment", params: StopDeployment }
|
||||
| { type: "DestroyDeployment", params: DestroyDeployment }
|
||||
| { type: "DeployStack", params: DeployStack }
|
||||
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
|
||||
| { type: "StartStack", params: StartStack }
|
||||
| { type: "RestartStack", params: RestartStack }
|
||||
| { type: "StopStack", params: StopStack }
|
||||
@@ -6591,6 +6724,7 @@ export type ReadRequest =
|
||||
| { type: "InspectDockerImage", params: InspectDockerImage }
|
||||
| { type: "ListDockerImageHistory", params: ListDockerImageHistory }
|
||||
| { type: "InspectDockerVolume", params: InspectDockerVolume }
|
||||
| { type: "ListAllDockerContainers", params: ListAllDockerContainers }
|
||||
| { type: "ListDockerContainers", params: ListDockerContainers }
|
||||
| { type: "ListDockerNetworks", params: ListDockerNetworks }
|
||||
| { type: "ListDockerImages", params: ListDockerImages }
|
||||
@@ -6671,6 +6805,9 @@ export type UserRequest =
|
||||
| { type: "DeleteApiKey", params: DeleteApiKey };
|
||||
|
||||
export type WriteRequest =
|
||||
| { type: "UpdateUserUsername", params: UpdateUserUsername }
|
||||
| { type: "UpdateUserPassword", params: UpdateUserPassword }
|
||||
| { type: "DeleteUser", params: DeleteUser }
|
||||
| { type: "CreateServiceUser", params: CreateServiceUser }
|
||||
| { type: "UpdateServiceUserDescription", params: UpdateServiceUserDescription }
|
||||
| { type: "CreateApiKeyForServiceUser", params: CreateApiKeyForServiceUser }
|
||||
@@ -6730,8 +6867,9 @@ export type WriteRequest =
|
||||
| { type: "CopyResourceSync", params: CopyResourceSync }
|
||||
| { type: "DeleteResourceSync", params: DeleteResourceSync }
|
||||
| { type: "UpdateResourceSync", params: UpdateResourceSync }
|
||||
| { type: "RefreshResourceSyncPending", params: RefreshResourceSyncPending }
|
||||
| { type: "WriteSyncFileContents", params: WriteSyncFileContents }
|
||||
| { type: "CommitSync", params: CommitSync }
|
||||
| { type: "RefreshResourceSyncPending", params: RefreshResourceSyncPending }
|
||||
| { type: "CreateSyncWebhook", params: CreateSyncWebhook }
|
||||
| { type: "DeleteSyncWebhook", params: DeleteSyncWebhook }
|
||||
| { type: "CreateStack", params: CreateStack }
|
||||
|
||||
@@ -6,6 +6,8 @@ use komodo_client::entities::{
|
||||
use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::git::RepoActionResponse;
|
||||
|
||||
/// List the compose project names that are on the host.
|
||||
/// List running `docker compose ls`
|
||||
///
|
||||
@@ -44,9 +46,12 @@ pub struct GetComposeServiceLog {
|
||||
pub project: String,
|
||||
/// The service name
|
||||
pub service: String,
|
||||
/// pass `--tail` for only recent log contents
|
||||
/// Pass `--tail` for only recent log contents. Max of 5000
|
||||
#[serde(default = "default_tail")]
|
||||
pub tail: u64,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
fn default_tail() -> u64 {
|
||||
@@ -72,6 +77,9 @@ pub struct GetComposeServiceLogSearch {
|
||||
/// Invert the search (search for everything not matching terms)
|
||||
#[serde(default)]
|
||||
pub invert: bool,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
//
|
||||
@@ -94,6 +102,25 @@ pub struct WriteComposeContentsToHost {
|
||||
|
||||
//
|
||||
|
||||
/// Write and commit compose contents.
|
||||
/// Only works with git repo based stacks.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
|
||||
#[response(RepoActionResponse)]
|
||||
pub struct WriteCommitComposeContents {
|
||||
/// The stack to write to.
|
||||
pub stack: Stack,
|
||||
/// The username of user which committed the file.
|
||||
pub username: Option<String>,
|
||||
/// Relative to the stack folder + run directory.
|
||||
pub file_path: String,
|
||||
/// The contents to write.
|
||||
pub contents: String,
|
||||
/// If provided, use it to login in. Otherwise check periphery local git providers.
|
||||
pub git_token: Option<String>,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Rewrites the compose directory, pulls any images, takes down existing containers,
|
||||
/// and runs docker compose up.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
|
||||
@@ -105,7 +132,7 @@ pub struct ComposeUp {
|
||||
pub service: Option<String>,
|
||||
/// If provided, use it to login in. Otherwise check periphery local registries.
|
||||
pub git_token: Option<String>,
|
||||
/// If provided, use it to login in. Otherwise check periphery local registries.
|
||||
/// If provided, use it to login in. Otherwise check periphery local git providers.
|
||||
pub registry_token: Option<String>,
|
||||
/// Propogate any secret replacers from core interpolation.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -23,6 +23,9 @@ pub struct GetContainerLog {
|
||||
pub name: String,
|
||||
#[serde(default = "default_tail")]
|
||||
pub tail: u64,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
fn default_tail() -> u64 {
|
||||
@@ -40,6 +43,9 @@ pub struct GetContainerLogSearch {
|
||||
pub combinator: SearchCombinator,
|
||||
#[serde(default)]
|
||||
pub invert: bool,
|
||||
/// Enable `--timestamps`
|
||||
#[serde(default)]
|
||||
pub timestamps: bool,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -74,7 +74,7 @@ pub struct PullOrCloneRepo {
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct RepoActionResponse {
|
||||
pub logs: Vec<Log>,
|
||||
pub commit_hash: Option<String>,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
command: caddy reverse-proxy --from ${KOMODO_HOST} --to core:9120
|
||||
cap_add:
|
||||
|
||||
+10
-1
@@ -18,6 +18,9 @@ COMPOSE_LOGGING_DRIVER=local # Enable log rotation with the local driver.
|
||||
DB_USERNAME=admin
|
||||
DB_PASSWORD=admin
|
||||
|
||||
## Configure a secure passkey to authenticate between Core / Periphery.
|
||||
PASSKEY=a_random_passkey
|
||||
|
||||
#=-------------------------=#
|
||||
#= Komodo Core Environment =#
|
||||
#=-------------------------=#
|
||||
@@ -50,7 +53,7 @@ KOMODO_MONITORING_INTERVAL="15-sec"
|
||||
KOMODO_RESOURCE_POLL_INTERVAL="5-min"
|
||||
|
||||
## Used to auth against periphery. Alt: KOMODO_PASSKEY_FILE
|
||||
KOMODO_PASSKEY=a_random_passkey
|
||||
KOMODO_PASSKEY=${PASSKEY}
|
||||
## Used to auth incoming webhooks. Alt: KOMODO_WEBHOOK_SECRET_FILE
|
||||
KOMODO_WEBHOOK_SECRET=a_random_secret
|
||||
## Used to generate jwt. Alt: KOMODO_JWT_SECRET_FILE
|
||||
@@ -82,6 +85,9 @@ KOMODO_OIDC_ENABLED=false
|
||||
# KOMODO_OIDC_CLIENT_SECRET= # Alt: KOMODO_OIDC_CLIENT_SECRET_FILE
|
||||
## Make usernames the full email.
|
||||
# KOMODO_OIDC_USE_FULL_EMAIL=true
|
||||
## Add additional trusted audiences for token claims verification.
|
||||
## Supports comma separated list, and passing with _FILE (for compose secrets).
|
||||
# KOMODO_OIDC_ADDITIONAL_AUDIENCES=abc,123 # Alt: KOMODO_OIDC_ADDITIONAL_AUDIENCES_FILE
|
||||
|
||||
## Github Oauth
|
||||
KOMODO_GITHUB_OAUTH_ENABLED=false
|
||||
@@ -108,6 +114,9 @@ KOMODO_HETZNER_TOKEN= # Alt: KOMODO_HETZNER_TOKEN_FILE
|
||||
## Full variable list + descriptions are available here:
|
||||
## 🦎 https://github.com/mbecker20/komodo/blob/main/config/periphery.config.toml 🦎
|
||||
|
||||
## Periphery passkeys must include KOMODO_PASSKEY to authenticate
|
||||
PERIPHERY_PASSKEYS=${PASSKEY}
|
||||
|
||||
## Enable SSL using self signed certificates.
|
||||
## Connect to Periphery at https://address:8120.
|
||||
PERIPHERY_SSL_ENABLED=true
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
services:
|
||||
mongo:
|
||||
image: mongo
|
||||
command: --quiet # suppress mongo logs a bit
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
command: --quiet --wiredTigerCacheSizeGB 0.25
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
@@ -27,6 +29,8 @@ services:
|
||||
|
||||
core:
|
||||
image: ghcr.io/mbecker20/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongo
|
||||
@@ -41,19 +45,25 @@ services:
|
||||
KOMODO_DATABASE_ADDRESS: mongo:27017
|
||||
KOMODO_DATABASE_USERNAME: ${DB_USERNAME}
|
||||
KOMODO_DATABASE_PASSWORD: ${DB_PASSWORD}
|
||||
## allows for systemd Periphery connection at
|
||||
volumes:
|
||||
## Core cache for repos for latest commit hash / contents
|
||||
- repo-cache:/repo-cache
|
||||
## Store sync files on server
|
||||
# - /path/to/syncs:/syncs
|
||||
## Optionally mount a custom core.config.toml
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
## Allows for systemd Periphery connection at
|
||||
## "http://host.docker.internal:8120"
|
||||
# extra_hosts:
|
||||
# - host.docker.internal:host-gateway
|
||||
## Optionally mount a custom core.config.toml
|
||||
# volumes:
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
|
||||
## Deploy Periphery container using this block,
|
||||
## or deploy the Periphery binary with systemd using
|
||||
## https://github.com/mbecker20/komodo/tree/main/scripts
|
||||
periphery:
|
||||
image: ghcr.io/mbecker20/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
@@ -78,8 +88,13 @@ services:
|
||||
# - /path/to/compose:/host/compose
|
||||
|
||||
volumes:
|
||||
# Mongo
|
||||
mongo-data:
|
||||
mongo-config:
|
||||
# Core
|
||||
syncs:
|
||||
repo-cache:
|
||||
# Periphery
|
||||
ssl-certs:
|
||||
repos:
|
||||
stacks:
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
@@ -26,6 +28,8 @@ services:
|
||||
|
||||
ferretdb:
|
||||
image: ghcr.io/ferretdb/ferretdb
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres
|
||||
@@ -40,6 +44,8 @@ services:
|
||||
|
||||
core:
|
||||
image: ghcr.io/mbecker20/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ferretdb
|
||||
@@ -52,20 +58,25 @@ services:
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
KOMODO_DATABASE_URI: mongodb://${DB_USERNAME}:${DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN
|
||||
# KOMODO_DATABASE_URI_FILE:
|
||||
## allows for systemd Periphery connection at
|
||||
volumes:
|
||||
## Core cache for repos for latest commit hash / contents
|
||||
- repo-cache:/repo-cache
|
||||
## Store sync files on server
|
||||
# - /path/to/syncs:/syncs
|
||||
## Optionally mount a custom core.config.toml
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
## Allows for systemd Periphery connection at
|
||||
## "http://host.docker.internal:8120"
|
||||
# extra_hosts:
|
||||
# - host.docker.internal:host-gateway
|
||||
## Optionally mount a custom core.config.toml
|
||||
# volumes:
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
|
||||
## Deploy Periphery container using this block,
|
||||
## or deploy the Periphery binary with systemd using
|
||||
## https://github.com/mbecker20/komodo/tree/main/scripts
|
||||
periphery:
|
||||
image: ghcr.io/mbecker20/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
@@ -90,7 +101,12 @@ services:
|
||||
# - /path/to/compose:/host/compose
|
||||
|
||||
volumes:
|
||||
# Postgres
|
||||
pg-data:
|
||||
# Core
|
||||
syncs:
|
||||
repo-cache:
|
||||
# Periphery
|
||||
ssl-certs:
|
||||
repos:
|
||||
stacks:
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
services:
|
||||
ferretdb:
|
||||
image: ghcr.io/ferretdb/ferretdb
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
@@ -24,6 +26,8 @@ services:
|
||||
|
||||
core:
|
||||
image: ghcr.io/mbecker20/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ferretdb
|
||||
@@ -36,19 +40,25 @@ services:
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
KOMODO_DATABASE_ADDRESS: ferretdb
|
||||
## allows for systemd Periphery connection at
|
||||
volumes:
|
||||
## Core cache for repos for latest commit hash / contents
|
||||
- repo-cache:/repo-cache
|
||||
## Store sync files on server
|
||||
# - /path/to/syncs:/syncs
|
||||
## Optionally mount a custom core.config.toml
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
## Allows for systemd Periphery connection at
|
||||
## "http://host.docker.internal:8120"
|
||||
# extra_hosts:
|
||||
# - host.docker.internal:host-gateway
|
||||
## Optionally mount a custom core.config.toml
|
||||
# volumes:
|
||||
# - /path/to/core.config.toml:/config/config.toml
|
||||
|
||||
## Deploy Periphery container using this block,
|
||||
## or deploy the Periphery binary with systemd using
|
||||
## https://github.com/mbecker20/komodo/tree/main/scripts
|
||||
periphery:
|
||||
image: ghcr.io/mbecker20/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
@@ -73,7 +83,12 @@ services:
|
||||
# - /path/to/compose:/host/compose
|
||||
|
||||
volumes:
|
||||
# Sqlite
|
||||
sqlite-data:
|
||||
# Core
|
||||
syncs:
|
||||
repo-cache:
|
||||
# Periphery
|
||||
ssl-certs:
|
||||
repos:
|
||||
stacks:
|
||||
|
||||
+15
-2
@@ -59,6 +59,12 @@ ui_write_disabled = false
|
||||
## Default: false
|
||||
disable_confirm_dialog = false
|
||||
|
||||
## Configure the directory for sync files (inside the container).
|
||||
## There shouldn't be a need to change this, just mount a volume.
|
||||
## Env: KOMODO_SYNC_DIRECTORY
|
||||
## Default: /syncs
|
||||
sync_directory = "/syncs"
|
||||
|
||||
## Configure the repo directory (inside the container).
|
||||
## There shouldn't be a need to change this, just mount a volume.
|
||||
## Env: KOMODO_REPO_DIRECTORY
|
||||
@@ -74,7 +80,7 @@ repo_directory = "/repo-cache"
|
||||
##
|
||||
## NOTE:
|
||||
## Komodo has no API to recover account logins, but if this happens you can doctor the database using Mongo Compass.
|
||||
## Create a new user, login to the database with Compass, note down your old users username and _id.
|
||||
## Create a new Komodo user (Sign Up button), login to the database with Compass, note down your old users username and _id.
|
||||
## Then delete the old user, and update the new user to have the same username and _id.
|
||||
## Make sure to set `enabled: true` and maybe `admin: true` on the new user as well, while using Compass.
|
||||
##
|
||||
@@ -161,10 +167,17 @@ oidc_client_secret = ""
|
||||
## If true, use the full email for usernames.
|
||||
## Otherwise, the @address will be stripped,
|
||||
## making usernames more concise.
|
||||
## Default: false.
|
||||
## Env: KOMODO_OIDC_USE_FULL_EMAIL
|
||||
## Default: false.
|
||||
oidc_use_full_email = false
|
||||
|
||||
## Some providers attach other audiences in addition to the client_id.
|
||||
## If you have this issue, `Invalid audiences: `...` is not a trusted audience"`,
|
||||
## you can add the audience `...` to the list here (assuming it should be trusted).
|
||||
## Env: KOMODO_OIDC_ADDITIONAL_AUDIENCES or KOMODO_OIDC_ADDITIONAL_AUDIENCES_FILE
|
||||
## Default: empty
|
||||
oidc_additional_audiences = []
|
||||
|
||||
#########
|
||||
# OAUTH #
|
||||
#########
|
||||
|
||||
@@ -6,7 +6,7 @@ Komodo just needs a bit of information in order to build your image.
|
||||
Komodo supports cloning repos over http/s, from any provider that supports cloning private repos using `git clone https://<Token>@git-provider.net/<Owner>/<Repo>`.
|
||||
|
||||
Accounts / access tokens can be configured in either the [core config](../setup/advanced.mdx#mount-a-config-file)
|
||||
or in the [periphery config](../setup/connect-servers.mdx#manual-install-steps---binaries).
|
||||
or in the [periphery config](../connect-servers.mdx#manual-install-steps---binaries).
|
||||
|
||||
### Repo configuration
|
||||
To specify the git repo to build, just give it the name of the repo and the branch under *repo config*. The name is given like `mbecker20/komodo`, it includes the username / organization that owns the repo.
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
Connecting a server to Komodo has 2 steps:
|
||||
|
||||
1. Install the Periphery agent on the server
|
||||
2. Adding the server to Komodo via the core API
|
||||
1. Install the Periphery agent on the server (either binary or container).
|
||||
2. Add the server to Komodo via the Core API / UI.
|
||||
|
||||
Once step 1. is complete, you can just connect the server to Komodo Core from the UI.
|
||||
|
||||
## Install
|
||||
## Install Periphery
|
||||
|
||||
You can install Periphery as a systemd managed process, run it as a [docker container](https://github.com/mbecker20/komodo/pkgs/container/periphery), or do whatever you want with the binary.
|
||||
|
||||
@@ -45,6 +43,8 @@ services:
|
||||
periphery:
|
||||
image: ghcr.io/mbecker20/periphery:latest
|
||||
# image: ghcr.io/mbecker20/periphery:latest-aarch64 # use for arm support
|
||||
labels:
|
||||
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
|
||||
logging:
|
||||
driver: local
|
||||
ports:
|
||||
@@ -67,12 +67,16 @@ services:
|
||||
# - /path/to/compose:/host/compose
|
||||
environment:
|
||||
## Full list: `https://github.com/mbecker20/komodo/blob/main/config/periphery.config.toml`
|
||||
## Configure the same passkey given to Komodo Core (KOMODO_PASSKEY)
|
||||
PERIPHERY_PASSKEYS: your_core_passkey # Alt: PERIPHERY_PASSKEYS_FILE
|
||||
## Adding IP here will ensure calling IP is in the list. (optional)
|
||||
PERIPHERY_ALLOWED_IPS:
|
||||
## Enable HTTPS server
|
||||
PERIPHERY_SSL_ENABLED: true
|
||||
## If the disk size is overreporting, can use one of these to
|
||||
## whitelist / blacklist the disks to filter them, whichever is easier.
|
||||
## Accepts comma separated list of paths.
|
||||
## Usually whitelisting just /etc/hostname gives correct size.
|
||||
## Usually whitelisting /etc/hostname gives correct size.
|
||||
PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/hostname
|
||||
# PERIPHERY_EXCLUDE_DISK_MOUNTS: /snap,/etc/repos
|
||||
|
||||
@@ -114,6 +118,10 @@ periphery \
|
||||
--merge-nested-config true
|
||||
```
|
||||
|
||||
:::info
|
||||
You can run `periphery --help` to see the manual.
|
||||
:::
|
||||
|
||||
### Passing config files
|
||||
|
||||
Either file paths or directory paths can be passed to `--config-path`.
|
||||
+2
-2
@@ -16,7 +16,7 @@ const sidebars: SidebarsConfig = {
|
||||
"resources",
|
||||
{
|
||||
type: "category",
|
||||
label: "Setup Komodo",
|
||||
label: "Setup Komodo Core",
|
||||
link: {
|
||||
type: "doc",
|
||||
id: "setup/index",
|
||||
@@ -26,9 +26,9 @@ const sidebars: SidebarsConfig = {
|
||||
"setup/postgres",
|
||||
"setup/sqlite",
|
||||
"setup/advanced",
|
||||
"setup/connect-servers",
|
||||
],
|
||||
},
|
||||
"connect-servers",
|
||||
{
|
||||
type: "category",
|
||||
label: "Build Images",
|
||||
|
||||
+19
-20
@@ -1,27 +1,26 @@
|
||||
# React + TypeScript + Vite
|
||||
# Komodo Frontend
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
Komodo JS stack uses Yarn + Vite + React + Tailwind + shadcn/ui
|
||||
|
||||
Currently, two official plugins are available:
|
||||
## Setup Dev Environment
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
The frontend depends on the local package `@komodo/client` located at `/client/core/ts`.
|
||||
This must first be built and prepared for yarn link.
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
The following command should setup everything up (run with /frontend as working directory):
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
```sh
|
||||
cd ../client/core/ts && yarn && yarn build && yarn link && \
|
||||
cd ../../../frontend && yarn link @komodo/client && yarn
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
You can make a new file `.env.development` (gitignored) which holds:
|
||||
```sh
|
||||
VITE_KOMODO_HOST=https://demo.komo.do
|
||||
```
|
||||
You can point it to any Komodo host you like, including the demo.
|
||||
|
||||
Now you can start the dev frontend server:
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
@@ -35,10 +35,10 @@
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"jotai": "2.9.2",
|
||||
"lightweight-charts": "4.2.0",
|
||||
"lucide-react": "0.437.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"react": "18.3.1",
|
||||
"react-charts": "^3.0.0-beta.57",
|
||||
"react-dom": "18.3.1",
|
||||
"react-minimal-pie-chart": "8.4.0",
|
||||
"react-router-dom": "6.26.0",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const TopbarAlerts = () => {
|
||||
const { data } = useRead(
|
||||
"ListAlerts",
|
||||
{ query: { resolved: false } },
|
||||
{ refetchInterval: 3000 }
|
||||
{ refetchInterval: 10_000 }
|
||||
);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ConfirmUpdate,
|
||||
} from "@components/config/util";
|
||||
import { Section } from "@components/layouts";
|
||||
import { MonacoLanguage } from "@components/monaco";
|
||||
import { Types } from "@komodo/client";
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/button";
|
||||
@@ -31,6 +32,7 @@ export const ConfigLayout = <
|
||||
onReset,
|
||||
selector,
|
||||
titleOther,
|
||||
file_contents_language,
|
||||
}: {
|
||||
original: T;
|
||||
config: Partial<T>;
|
||||
@@ -40,6 +42,7 @@ export const ConfigLayout = <
|
||||
onReset: () => void;
|
||||
selector?: ReactNode;
|
||||
titleOther?: ReactNode;
|
||||
file_contents_language?: MonacoLanguage;
|
||||
}) => {
|
||||
const titleProps = titleOther
|
||||
? { titleOther }
|
||||
@@ -72,8 +75,9 @@ export const ConfigLayout = <
|
||||
<ConfirmUpdate
|
||||
previous={original}
|
||||
content={config}
|
||||
onConfirm={onConfirm}
|
||||
onConfirm={async () => onConfirm()}
|
||||
disabled={disabled}
|
||||
file_contents_language={file_contents_language}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -120,6 +124,7 @@ export const Config = <T,>({
|
||||
components,
|
||||
selector,
|
||||
titleOther,
|
||||
file_contents_language,
|
||||
}: {
|
||||
resource_id: string;
|
||||
resource_type: Types.ResourceTarget["type"];
|
||||
@@ -134,6 +139,7 @@ export const Config = <T,>({
|
||||
string, // sidebar key
|
||||
ConfigComponent<T>[] | false | undefined
|
||||
>;
|
||||
file_contents_language?: MonacoLanguage;
|
||||
}) => {
|
||||
// let component_keys = keys(components);
|
||||
// const [_show, setShow] = useLocalStorage(
|
||||
@@ -164,6 +170,7 @@ export const Config = <T,>({
|
||||
}}
|
||||
onReset={() => set({})}
|
||||
selector={selector}
|
||||
file_contents_language={file_contents_language}
|
||||
>
|
||||
<div className="flex gap-6">
|
||||
<div className="hidden xl:block relative pr-6 border-r">
|
||||
@@ -186,7 +193,7 @@ export const Config = <T,>({
|
||||
key={section + item.label}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
className="justify-end w-full"
|
||||
size="sm"
|
||||
>
|
||||
|
||||
@@ -46,7 +46,11 @@ import {
|
||||
soft_text_color_class_by_intention,
|
||||
text_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { MonacoDiffEditor, MonacoEditor } from "@components/monaco";
|
||||
import {
|
||||
MonacoDiffEditor,
|
||||
MonacoEditor,
|
||||
MonacoLanguage,
|
||||
} from "@components/monaco";
|
||||
|
||||
export const ConfigItem = ({
|
||||
label,
|
||||
@@ -398,6 +402,7 @@ export const AccountSelector = ({
|
||||
provider,
|
||||
selected,
|
||||
onSelect,
|
||||
placeholder = "Select Account",
|
||||
}: {
|
||||
disabled: boolean;
|
||||
type: "Server" | "Builder" | "None";
|
||||
@@ -406,6 +411,7 @@ export const AccountSelector = ({
|
||||
provider: string;
|
||||
selected: string | undefined;
|
||||
onSelect: (id: string) => void;
|
||||
placeholder?: string;
|
||||
}) => {
|
||||
const [db_request, config_request]:
|
||||
| ["ListGitProviderAccounts", "ListGitProvidersFromConfig"]
|
||||
@@ -447,7 +453,7 @@ export const AccountSelector = ({
|
||||
className="w-full lg:w-[200px] max-w-[50%]"
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectValue placeholder="Select Account" />
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"Empty"}>None</SelectItem>
|
||||
@@ -471,12 +477,16 @@ export const AccountSelectorConfig = (params: {
|
||||
provider: string;
|
||||
selected: string | undefined;
|
||||
onSelect: (id: string) => void;
|
||||
placeholder: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem
|
||||
label="Account"
|
||||
description="Select the account used to log in to the provider"
|
||||
description={
|
||||
params.description ??
|
||||
"Select the account used to log in to the provider"
|
||||
}
|
||||
>
|
||||
<AccountSelector {...params} />
|
||||
</ConfigItem>
|
||||
@@ -567,15 +577,21 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
interface ConfirmUpdateProps<T> {
|
||||
previous: T;
|
||||
content: Partial<T>;
|
||||
onConfirm: () => void;
|
||||
onConfirm: () => Promise<void>;
|
||||
loading?: boolean;
|
||||
disabled: boolean;
|
||||
language?: MonacoLanguage;
|
||||
file_contents_language?: MonacoLanguage;
|
||||
}
|
||||
|
||||
export function ConfirmUpdate<T>({
|
||||
previous,
|
||||
content,
|
||||
onConfirm,
|
||||
loading,
|
||||
disabled,
|
||||
language,
|
||||
file_contents_language,
|
||||
}: ConfirmUpdateProps<T>) {
|
||||
const [open, set] = useState(false);
|
||||
useCtrlKeyListener("s", () => {
|
||||
@@ -608,6 +624,8 @@ export function ConfirmUpdate<T>({
|
||||
_key={key as any}
|
||||
val={val as any}
|
||||
previous={previous}
|
||||
language={language}
|
||||
file_contents_language={file_contents_language}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -615,10 +633,11 @@ export function ConfirmUpdate<T>({
|
||||
<ConfirmButton
|
||||
title="Update"
|
||||
icon={<CheckCircle className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onClick={async () => {
|
||||
await onConfirm();
|
||||
set(false);
|
||||
}}
|
||||
loading={loading}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -629,23 +648,24 @@ function ConfirmUpdateItem<T>({
|
||||
_key,
|
||||
val: _val,
|
||||
previous,
|
||||
language,
|
||||
file_contents_language,
|
||||
}: {
|
||||
_key: keyof T;
|
||||
val: T[keyof T];
|
||||
previous: T;
|
||||
language?: MonacoLanguage;
|
||||
file_contents_language?: MonacoLanguage;
|
||||
}) {
|
||||
const [show, setShow] = useState(true);
|
||||
const val =
|
||||
typeof _val === "string"
|
||||
? _key === "environment" ||
|
||||
_key === "build_args" ||
|
||||
_key === "secret_args"
|
||||
? _val
|
||||
.split("\n")
|
||||
.filter((line) => !line.startsWith("#"))
|
||||
.map((line) => line.split(" #")[0])
|
||||
.join("\n")
|
||||
: _val
|
||||
? _val
|
||||
: Array.isArray(_val)
|
||||
? _val.length > 0 &&
|
||||
["string", "number", "boolean"].includes(typeof _val[0])
|
||||
? JSON.stringify(_val)
|
||||
: JSON.stringify(_val, null, 2)
|
||||
: JSON.stringify(_val, null, 2);
|
||||
const prev_val =
|
||||
typeof previous[_key] === "string"
|
||||
@@ -653,7 +673,12 @@ function ConfirmUpdateItem<T>({
|
||||
: _key === "environment" ||
|
||||
_key === "build_args" ||
|
||||
_key === "secret_args"
|
||||
? env_to_text(previous[_key] as any) ?? ""
|
||||
? env_to_text(previous[_key] as any) ?? "" // For backward compat with 1.14
|
||||
: Array.isArray(previous[_key])
|
||||
? previous[_key].length > 0 &&
|
||||
["string", "number", "boolean"].includes(typeof previous[_key][0])
|
||||
? JSON.stringify(previous[_key])
|
||||
: JSON.stringify(previous[_key], null, 2)
|
||||
: JSON.stringify(previous[_key], null, 2);
|
||||
const showDiff =
|
||||
val?.includes("\n") ||
|
||||
@@ -676,7 +701,16 @@ function ConfirmUpdateItem<T>({
|
||||
<MonacoDiffEditor
|
||||
original={prev_val}
|
||||
modified={val}
|
||||
language="toml"
|
||||
language={
|
||||
language ??
|
||||
(["environment", "build_args", "secret_args"].includes(
|
||||
_key as string
|
||||
)
|
||||
? "key_value"
|
||||
: _key === "file_contents"
|
||||
? file_contents_language
|
||||
: "json")
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<pre style={{ minHeight: 0 }}>
|
||||
@@ -718,7 +752,10 @@ export const SystemCommand = ({
|
||||
/>
|
||||
</div>
|
||||
<MonacoEditor
|
||||
value={value?.command}
|
||||
value={
|
||||
value?.command ||
|
||||
" # Add multiple commands on new lines. Supports comments.\n "
|
||||
}
|
||||
language="shell"
|
||||
onValueChange={(command) => set({ ...(value || {}), command })}
|
||||
readOnly={disabled}
|
||||
|
||||
@@ -28,7 +28,11 @@ export const KeysTable = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input className="w-40" value={key} disabled />
|
||||
<Input
|
||||
className="w-[100px] lg:w-[200px] overflow-ellipsis"
|
||||
value={key}
|
||||
disabled
|
||||
/>
|
||||
<CopyButton content={key} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -163,6 +163,7 @@ export const PageXlRow = ({
|
||||
interface SectionProps {
|
||||
title?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
titleRight?: ReactNode;
|
||||
titleOther?: ReactNode;
|
||||
children?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
@@ -173,6 +174,7 @@ interface SectionProps {
|
||||
export const Section = ({
|
||||
title,
|
||||
icon,
|
||||
titleRight,
|
||||
titleOther,
|
||||
actions,
|
||||
children,
|
||||
@@ -189,6 +191,7 @@ export const Section = ({
|
||||
<div className="px-2 flex items-center gap-2 text-muted-foreground">
|
||||
{icon}
|
||||
{title && <h2 className="text-xl">{title}</h2>}
|
||||
{titleRight}
|
||||
</div>
|
||||
) : (
|
||||
titleOther
|
||||
@@ -203,13 +206,13 @@ export const NewLayout = ({
|
||||
entityType,
|
||||
children,
|
||||
enabled,
|
||||
onSuccess,
|
||||
onConfirm,
|
||||
onOpenChange,
|
||||
}: {
|
||||
entityType: string;
|
||||
children: ReactNode;
|
||||
enabled: boolean;
|
||||
onSuccess: () => Promise<unknown>;
|
||||
onConfirm: () => Promise<unknown>;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) => {
|
||||
const [open, set] = useState(false);
|
||||
@@ -234,10 +237,10 @@ export const NewLayout = ({
|
||||
<div className="flex flex-col gap-4 py-8">{children}</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
await onSuccess();
|
||||
await onConfirm();
|
||||
setLoading(false);
|
||||
set(false);
|
||||
}}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const RunBuild = ({ id }: { id: string }) => {
|
||||
const building = useRead(
|
||||
"GetBuildActionState",
|
||||
{ build: id },
|
||||
{ refetchInterval: 5000 }
|
||||
{ refetchInterval: 5_000 }
|
||||
).data?.building;
|
||||
const updates = useRead("ListUpdates", {
|
||||
query: {
|
||||
@@ -22,7 +22,7 @@ export const RunBuild = ({ id }: { id: string }) => {
|
||||
const { mutate: run_mutate, isPending: runPending } = useExecute("RunBuild");
|
||||
const { mutate: cancel_mutate, isPending: cancelPending } =
|
||||
useExecute("CancelBuild");
|
||||
const build = useRead("ListBuilds", {}, { refetchInterval: 5000 }).data?.find(
|
||||
const build = useRead("ListBuilds", {}).data?.find(
|
||||
(d) => d.id === id
|
||||
);
|
||||
const builder = useBuilder(build?.info.builder_id);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user