Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee270d045 | ||
|
|
2cfae525e9 | ||
|
|
80e5d2a972 | ||
|
|
6f22c011a6 | ||
|
|
401cccee79 | ||
|
|
654b923f98 | ||
|
|
61261be70f | ||
|
|
46418125e3 | ||
|
|
e029e94f0d | ||
|
|
3be2b5163b | ||
|
|
6a145f58ff | ||
|
|
f1cede2ebd | ||
|
|
a5cfa1d412 | ||
|
|
a0674654c1 | ||
|
|
3faa1c58c1 | ||
|
|
7e296f34af | ||
|
|
9f8ced190c | ||
|
|
c194bb16d8 | ||
|
|
39fec9b55e | ||
|
|
e97ed9888d | ||
|
|
559102ffe3 | ||
|
|
6bf80ddcc7 | ||
|
|
89dbe1b4d9 | ||
|
|
334e16d646 | ||
|
|
a7bbe519f4 | ||
|
|
5827486c5a | ||
|
|
8ca8f7eddd | ||
|
|
0600276b43 | ||
|
|
a77a1495c7 |
@@ -5,4 +5,10 @@ LICENSE
|
||||
*.code-workspace
|
||||
|
||||
*/node_modules
|
||||
*/dist
|
||||
*/dist
|
||||
|
||||
creds.toml
|
||||
.core-repos
|
||||
.repos
|
||||
.stacks
|
||||
.ssl
|
||||
9
.gitignore
vendored
@@ -1,11 +1,14 @@
|
||||
target
|
||||
/frontend/build
|
||||
node_modules
|
||||
/lib/ts_client/build
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.development
|
||||
.DS_Store
|
||||
|
||||
creds.toml
|
||||
.syncs
|
||||
.core-repos
|
||||
.repos
|
||||
.stacks
|
||||
.DS_Store
|
||||
.ssl
|
||||
107
Cargo.lock
generated
@@ -41,7 +41,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
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.0"
|
||||
version = "1.15.4"
|
||||
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.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1439,7 +1439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
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.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2192,7 +2192,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2208,7 +2208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2239,7 +2239,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2296,7 +2296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
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.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -2446,7 +2447,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "migrator"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenvy",
|
||||
@@ -3101,7 +3102,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
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.0"
|
||||
version = "1.15.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
|
||||
16
Cargo.toml
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.15.0"
|
||||
version = "1.15.4"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -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"
|
||||
|
||||
@@ -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")]
|
||||
@@ -154,7 +158,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,
|
||||
|
||||
@@ -19,6 +19,7 @@ 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,
|
||||
@@ -102,6 +103,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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -404,6 +404,7 @@ impl Resolve<GetContainerLog, User> for State {
|
||||
server,
|
||||
container,
|
||||
tail,
|
||||
timestamps,
|
||||
}: GetContainerLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -417,6 +418,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 +434,7 @@ impl Resolve<SearchContainerLog, User> for State {
|
||||
terms,
|
||||
combinator,
|
||||
invert,
|
||||
timestamps,
|
||||
}: SearchContainerLog,
|
||||
user: User,
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -447,6 +450,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")
|
||||
|
||||
@@ -188,7 +188,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,
|
||||
|
||||
@@ -32,17 +32,15 @@ pub async fn init_default_oidc_client() {
|
||||
return;
|
||||
}
|
||||
async {
|
||||
let provider = config.oidc_provider.to_string();
|
||||
// Use OpenID Connect Discovery to fetch the provider metadata.
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||
IssuerUrl::new(if provider.ends_with('/') {
|
||||
provider
|
||||
} else {
|
||||
provider + "/"
|
||||
})?,
|
||||
IssuerUrl::new(config.oidc_provider.clone())?,
|
||||
async_http_client,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context(
|
||||
"Failed to get OIDC /.well-known/openid-configuration",
|
||||
)?;
|
||||
|
||||
// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
|
||||
// and token URL.
|
||||
|
||||
@@ -135,7 +135,9 @@ async fn callback(
|
||||
.context("CSRF Token invalid")?;
|
||||
|
||||
if komodo_timestamp() > valid_until {
|
||||
return Err(anyhow!("CSRF token invalid (Timed out). The token must be "));
|
||||
return Err(anyhow!(
|
||||
"CSRF token invalid (Timed out). The token must be "
|
||||
));
|
||||
}
|
||||
|
||||
let token_response = client
|
||||
@@ -150,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
|
||||
@@ -191,20 +206,25 @@ async fn callback(
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled"));
|
||||
}
|
||||
// Email will use user_id if it isn't available.
|
||||
let email = claims
|
||||
.email()
|
||||
.map(|email| email.as_str())
|
||||
.unwrap_or(user_id);
|
||||
let username = if core_config.oidc_use_full_email {
|
||||
email
|
||||
} else {
|
||||
email
|
||||
.split_once('@')
|
||||
.map(|(username, _)| username)
|
||||
.unwrap_or(email)
|
||||
}
|
||||
.to_string();
|
||||
// Will use preferred_username, then email, then user_id if it isn't available.
|
||||
let username = claims
|
||||
.preferred_username()
|
||||
.map(|username| username.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
let email = claims
|
||||
.email()
|
||||
.map(|email| email.as_str())
|
||||
.unwrap_or(user_id);
|
||||
if core_config.oidc_use_full_email {
|
||||
email
|
||||
} else {
|
||||
email
|
||||
.split_once('@')
|
||||
.map(|(username, _)| username)
|
||||
.unwrap_or(email)
|
||||
}
|
||||
.to_string()
|
||||
});
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
@@ -280,8 +281,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 +296,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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,7 @@ use komodo_client::{
|
||||
tag::Tag,
|
||||
to_komodo_name,
|
||||
update::Update,
|
||||
user::User,
|
||||
user::{system_user, User},
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
},
|
||||
};
|
||||
@@ -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?;
|
||||
|
||||
@@ -274,7 +274,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 +353,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 +385,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 = {}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,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 +77,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 +151,11 @@ services:
|
||||
";
|
||||
|
||||
impl Resolve<GetComposeContentsOnHost, ()> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeContentsOnHost",
|
||||
level = "debug",
|
||||
skip(self)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetComposeContentsOnHost {
|
||||
@@ -115,16 +176,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 +192,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 +223,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>();
|
||||
@@ -292,6 +296,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,14 +318,12 @@ impl Resolve<StartAllContainers> for State {
|
||||
.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
|
||||
}
|
||||
},
|
||||
);
|
||||
containers.iter().map(|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker start {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
}
|
||||
});
|
||||
Ok(join_all(futures).await)
|
||||
}
|
||||
}
|
||||
@@ -322,14 +341,13 @@ impl Resolve<RestartAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures = containers.iter().map(
|
||||
|ContainerListItem { name, .. }| {
|
||||
let futures =
|
||||
containers.iter().map(|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker restart {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), command).await
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Ok(join_all(futures).await)
|
||||
}
|
||||
}
|
||||
@@ -347,14 +365,13 @@ impl Resolve<PauseAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures = containers.iter().map(
|
||||
|ContainerListItem { name, .. }| {
|
||||
let futures =
|
||||
containers.iter().map(|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker pause {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), command).await
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Ok(join_all(futures).await)
|
||||
}
|
||||
}
|
||||
@@ -372,14 +389,13 @@ impl Resolve<UnpauseAllContainers> for State {
|
||||
.list_containers()
|
||||
.await
|
||||
.context("failed to list all containers on host")?;
|
||||
let futures = containers.iter().map(
|
||||
|ContainerListItem { name, .. }| {
|
||||
let futures =
|
||||
containers.iter().map(|ContainerListItem { name, .. }| {
|
||||
let command = format!("docker unpause {name}");
|
||||
async move {
|
||||
run_komodo_command(&command.clone(), command).await
|
||||
run_komodo_command(&command.clone(), None, command).await
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Ok(join_all(futures).await)
|
||||
}
|
||||
}
|
||||
@@ -401,6 +417,7 @@ impl Resolve<StopAllContainers> for State {
|
||||
|ContainerListItem { name, .. }| async move {
|
||||
run_komodo_command(
|
||||
&format!("docker stop {name}"),
|
||||
None,
|
||||
stop_container_command(name, None, None),
|
||||
)
|
||||
.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)]
|
||||
@@ -206,7 +210,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 +255,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 +270,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,46 @@ async fn write_stack(
|
||||
}
|
||||
};
|
||||
|
||||
let clone_or_pull_res = if stack.config.reclone {
|
||||
State
|
||||
.resolve(
|
||||
CloneRepo {
|
||||
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
|
||||
} else {
|
||||
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
|
||||
};
|
||||
|
||||
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 +513,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 +533,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;
|
||||
|
||||
@@ -935,7 +935,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,
|
||||
|
||||
@@ -128,7 +128,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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -303,6 +303,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 +344,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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -116,6 +116,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>,
|
||||
@@ -159,31 +163,31 @@ pub struct Env {
|
||||
pub komodo_github_webhook_app_pk_path: Option<String>,
|
||||
|
||||
/// Override `database.uri`
|
||||
#[serde(alias = "KOMODO_MONGO_URI")]
|
||||
#[serde(alias = "komodo_mongo_uri")]
|
||||
pub komodo_database_uri: Option<String>,
|
||||
/// Override `database.uri` from file
|
||||
#[serde(alias = "KOMODO_MONGO_URI_FILE")]
|
||||
#[serde(alias = "komodo_mongo_uri_file")]
|
||||
pub komodo_database_uri_file: Option<PathBuf>,
|
||||
/// Override `database.address`
|
||||
#[serde(alias = "KOMODO_MONGO_ADDRESS")]
|
||||
#[serde(alias = "komodo_mongo_address")]
|
||||
pub komodo_database_address: Option<String>,
|
||||
/// Override `database.username`
|
||||
#[serde(alias = "KOMODO_MONGO_USERNAME")]
|
||||
#[serde(alias = "komodo_mongo_username")]
|
||||
pub komodo_database_username: Option<String>,
|
||||
/// Override `database.username` with file
|
||||
#[serde(alias = "KOMODO_MONGO_USERNAME_FILE")]
|
||||
#[serde(alias = "komodo_mongo_username_file")]
|
||||
pub komodo_database_username_file: Option<PathBuf>,
|
||||
/// Override `database.password`
|
||||
#[serde(alias = "KOMODO_MONGO_PASSWORD")]
|
||||
#[serde(alias = "komodo_mongo_password")]
|
||||
pub komodo_database_password: Option<String>,
|
||||
/// Override `database.password` with file
|
||||
#[serde(alias = "KOMODO_MONGO_PASSWORD_FILE")]
|
||||
#[serde(alias = "komodo_mongo_password_file")]
|
||||
pub komodo_database_password_file: Option<PathBuf>,
|
||||
/// Override `database.app_name`
|
||||
#[serde(alias = "KOMODO_MONGO_APP_NAME")]
|
||||
#[serde(alias = "komodo_mongo_app_name")]
|
||||
pub komodo_database_app_name: Option<String>,
|
||||
/// Override `database.db_name`
|
||||
#[serde(alias = "KOMODO_MONGO_DB_NAME")]
|
||||
#[serde(alias = "komodo_mongo_db_name")]
|
||||
pub komodo_database_db_name: Option<String>,
|
||||
|
||||
/// Override `aws.access_key_id`
|
||||
@@ -344,6 +348,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 =
|
||||
// =========
|
||||
@@ -548,6 +557,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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()")]
|
||||
@@ -312,6 +323,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 +426,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,6 +439,7 @@ 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(),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 */
|
||||
@@ -2321,6 +2323,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 +2369,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;
|
||||
/**
|
||||
@@ -2378,6 +2387,8 @@ export interface StackConfig {
|
||||
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.
|
||||
@@ -3788,7 +3799,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;
|
||||
@@ -4103,6 +4114,8 @@ export interface GetDeploymentLog {
|
||||
* Max: 5000.
|
||||
*/
|
||||
tail: U64;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
export enum SearchCombinator {
|
||||
@@ -4130,6 +4143,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4596,6 +4611,8 @@ export interface GetContainerLog {
|
||||
* Max: 5000.
|
||||
*/
|
||||
tail: U64;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4620,6 +4637,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 +4829,8 @@ export interface GetStackServiceLog {
|
||||
* Max: 5000.
|
||||
*/
|
||||
tail: U64;
|
||||
/** Enable `--timestamps` */
|
||||
timestamps?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4834,6 +4855,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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,9 +44,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 +75,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,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -82,6 +82,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
|
||||
|
||||
@@ -161,10 +161,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.
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
# Advanced Configuration
|
||||
|
||||
### Oauth2
|
||||
### OIDC / Oauth2
|
||||
|
||||
To enable OAuth2 login, you must create a client on the respective OAuth provider,
|
||||
for example [Github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
or [Google](https://developers.google.com/identity/protocols/oauth2).
|
||||
|
||||
Komodo also supports self hosted Oauth2 providers like [Authentik](https://docs.goauthentik.io/docs/providers/oauth2/) or [Gitea](https://docs.gitea.com/development/oauth2-provider).
|
||||
|
||||
- Komodo uses the `web application` login flow.
|
||||
- The redirect uri is:
|
||||
- `<KOMODO_HOST>/auth/github/callback` for Github.
|
||||
- `<KOMODO_HOST>/auth/google/callback` for Google.
|
||||
- `<KOMODO_HOST>/auth/oidc/callback` for OIDC.
|
||||
|
||||
### Mount a config file
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 }
|
||||
@@ -74,6 +77,7 @@ export const ConfigLayout = <
|
||||
content={config}
|
||||
onConfirm={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>
|
||||
@@ -569,6 +579,8 @@ interface ConfirmUpdateProps<T> {
|
||||
content: Partial<T>;
|
||||
onConfirm: () => void;
|
||||
disabled: boolean;
|
||||
language?: MonacoLanguage;
|
||||
file_contents_language?: MonacoLanguage;
|
||||
}
|
||||
|
||||
export function ConfirmUpdate<T>({
|
||||
@@ -576,6 +588,8 @@ export function ConfirmUpdate<T>({
|
||||
content,
|
||||
onConfirm,
|
||||
disabled,
|
||||
language,
|
||||
file_contents_language,
|
||||
}: ConfirmUpdateProps<T>) {
|
||||
const [open, set] = useState(false);
|
||||
useCtrlKeyListener("s", () => {
|
||||
@@ -608,6 +622,8 @@ export function ConfirmUpdate<T>({
|
||||
_key={key as any}
|
||||
val={val as any}
|
||||
previous={previous}
|
||||
language={language}
|
||||
file_contents_language={file_contents_language}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -629,23 +645,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 +670,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 +698,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 +749,7 @@ 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}
|
||||
|
||||
@@ -203,13 +203,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);
|
||||
@@ -237,7 +237,7 @@ export const NewLayout = ({
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
await onSuccess();
|
||||
await onConfirm();
|
||||
setLoading(false);
|
||||
set(false);
|
||||
}}
|
||||
|
||||
@@ -100,7 +100,7 @@ export const BuildConfig = ({
|
||||
<ConfigInput
|
||||
className="text-lg w-[200px]"
|
||||
label="Version"
|
||||
description="Version the image tag using server (major.minor.patch)"
|
||||
description="Version the image with major.minor.patch. It can be interpolated using [[$VERSION]]."
|
||||
placeholder="0.0.0"
|
||||
value={version}
|
||||
onChange={(version) => set({ version: version as any })}
|
||||
|
||||
@@ -115,9 +115,17 @@ export const BuildComponents: RequiredResourceComponents = {
|
||||
|
||||
New: () => {
|
||||
const user = useUser().data;
|
||||
const builders = useRead("ListBuilders", {}).data;
|
||||
if (!user) return null;
|
||||
if (!user.admin && !user.create_build_permissions) return null;
|
||||
return <NewResource type="Build" />;
|
||||
return (
|
||||
<NewResource
|
||||
type="Build"
|
||||
builder_id={
|
||||
builders && builders.length === 1 ? builders[0].id : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
Table: ({ resources }) => (
|
||||
|
||||
@@ -74,7 +74,7 @@ export const BuilderComponents: RequiredResourceComponents = {
|
||||
return (
|
||||
<NewLayout
|
||||
entityType="Builder"
|
||||
onSuccess={async () => {
|
||||
onConfirm={async () => {
|
||||
if (!type) return;
|
||||
const id = (await mutateAsync({ name, config: { type, params: {} } }))
|
||||
._id?.$oid!;
|
||||
|
||||
@@ -119,7 +119,11 @@ export const ResourceSelector = ({
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="secondary" className="flex justify-start gap-2 w-fit max-w-[200px]" disabled={disabled}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex justify-start gap-2 w-fit max-w-[200px]"
|
||||
disabled={disabled}
|
||||
>
|
||||
{name || `Select ${type}`}
|
||||
{!disabled && <ChevronsUpDown className="w-3 h-3" />}
|
||||
</Button>
|
||||
@@ -258,16 +262,19 @@ export const NewResource = ({
|
||||
type,
|
||||
readable_type,
|
||||
server_id,
|
||||
builder_id,
|
||||
build_id,
|
||||
name: _name = "",
|
||||
}: {
|
||||
type: UsableResource;
|
||||
readable_type?: string;
|
||||
server_id?: string;
|
||||
builder_id?: string;
|
||||
build_id?: string;
|
||||
name?: string;
|
||||
}) => {
|
||||
const nav = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync } = useWrite(`Create${type}`);
|
||||
const [name, setName] = useState(_name);
|
||||
const type_display =
|
||||
@@ -276,7 +283,7 @@ export const NewResource = ({
|
||||
: type === "ResourceSync"
|
||||
? "resource-sync"
|
||||
: type.toLowerCase();
|
||||
const config: Types._PartialDeploymentConfig =
|
||||
const config: Types._PartialDeploymentConfig | Types._PartialRepoConfig =
|
||||
type === "Deployment"
|
||||
? {
|
||||
server_id,
|
||||
@@ -287,15 +294,19 @@ export const NewResource = ({
|
||||
: type === "Stack"
|
||||
? { server_id }
|
||||
: type === "Repo"
|
||||
? { server_id }
|
||||
? { server_id, builder_id }
|
||||
: type === "Build"
|
||||
? { builder_id }
|
||||
: {};
|
||||
const onConfirm = async () => {
|
||||
if (!name) toast({ title: "Name cannot be empty" });
|
||||
const id = (await mutateAsync({ name, config }))._id?.$oid!;
|
||||
nav(`/${usableResourcePath(type)}/${id}`);
|
||||
};
|
||||
return (
|
||||
<NewLayout
|
||||
entityType={readable_type ?? type}
|
||||
onSuccess={async () => {
|
||||
const id = (await mutateAsync({ name, config }))._id?.$oid!;
|
||||
nav(`/${usableResourcePath(type)}/${id}`);
|
||||
}}
|
||||
onConfirm={onConfirm}
|
||||
enabled={!!name}
|
||||
onOpenChange={() => setName("")}
|
||||
>
|
||||
@@ -305,6 +316,12 @@ export const NewResource = ({
|
||||
placeholder={`${type_display}-name`}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (!name) return;
|
||||
if (e.key === "Enter") {
|
||||
onConfirm();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</NewLayout>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRead, useWrite } from "@lib/hooks";
|
||||
import { Types } from "@komodo/client";
|
||||
import { ReactNode, useState } from "react";
|
||||
import {
|
||||
AccountSelectorConfig,
|
||||
AddExtraArgMenu,
|
||||
ConfigItem,
|
||||
ConfigList,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
DefaultTerminationSignal,
|
||||
TerminationTimeout,
|
||||
} from "./components/term-signal";
|
||||
import { extract_registry_domain } from "@lib/utils";
|
||||
|
||||
export const DeploymentConfig = ({
|
||||
id,
|
||||
@@ -31,6 +33,7 @@ export const DeploymentConfig = ({
|
||||
target: { type: "Deployment", id },
|
||||
}).data;
|
||||
const config = useRead("GetDeployment", { deployment: id }).data?.config;
|
||||
const builds = useRead("ListBuilds", {}).data;
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
const [update, set] = useState<Partial<Types.DeploymentConfig>>({});
|
||||
@@ -100,6 +103,37 @@ export const DeploymentConfig = ({
|
||||
image: (value, set) => (
|
||||
<ImageConfig image={value} set={set} disabled={disabled} />
|
||||
),
|
||||
image_registry_account: (account, set) => {
|
||||
const image = update.image ?? config.image;
|
||||
const provider =
|
||||
image?.type === "Image" && image.params.image
|
||||
? extract_registry_domain(image.params.image)
|
||||
: image?.type === "Build" && image.params.build_id
|
||||
? builds?.find((b) => b.id === image.params.build_id)?.info
|
||||
.image_registry_domain
|
||||
: undefined;
|
||||
return (
|
||||
<AccountSelectorConfig
|
||||
id={update.server_id ?? config.server_id ?? undefined}
|
||||
type="Server"
|
||||
account_type="docker"
|
||||
provider={provider ?? "docker.io"}
|
||||
selected={account}
|
||||
onSelect={(image_registry_account) =>
|
||||
set({ image_registry_account })
|
||||
}
|
||||
disabled={disabled}
|
||||
placeholder={
|
||||
image?.type === "Build" ? "Same as Build" : undefined
|
||||
}
|
||||
description={
|
||||
image?.type === "Build"
|
||||
? "Select an alternate account used to log in to the provider"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
redeploy_on_build: (update.image?.type ?? config.image?.type) ===
|
||||
"Build" && {
|
||||
description: "Automatically redeploy when the image is built.",
|
||||
@@ -182,6 +216,53 @@ export const DeploymentConfig = ({
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
restart: (value, set) => (
|
||||
<RestartModeSelector
|
||||
selected={value}
|
||||
set={set}
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
advanced: [
|
||||
{
|
||||
label: "Command",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
command: (value, set) => (
|
||||
<ConfigItem
|
||||
label="Command"
|
||||
description={
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
<div>Replace the CMD, or extend the ENTRYPOINT.</div>
|
||||
<Link
|
||||
to="https://docs.docker.com/engine/reference/run/#commands-and-arguments"
|
||||
target="_blank"
|
||||
className="text-primary"
|
||||
>
|
||||
See docker docs.
|
||||
{/* <Button variant="link" className="p-0">
|
||||
</Button> */}
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MonacoEditor
|
||||
value={value}
|
||||
language="shell"
|
||||
onValueChange={(command) => set({ command })}
|
||||
readOnly={disabled}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Labels",
|
||||
description: "Attach --labels to the container.",
|
||||
@@ -197,19 +278,6 @@ export const DeploymentConfig = ({
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
restart: (value, set) => (
|
||||
<RestartModeSelector
|
||||
selected={value}
|
||||
set={set}
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Extra Args",
|
||||
labelHidden: true,
|
||||
@@ -255,38 +323,6 @@ export const DeploymentConfig = ({
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Command",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
command: (value, set) => (
|
||||
<ConfigItem
|
||||
label="Command"
|
||||
description={
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
<div>Replace the CMD, or extend the ENTRYPOINT.</div>
|
||||
<Link
|
||||
to="https://docs.docker.com/engine/reference/run/#commands-and-arguments"
|
||||
target="_blank"
|
||||
className="text-primary"
|
||||
>
|
||||
See docker docs.
|
||||
{/* <Button variant="link" className="p-0">
|
||||
</Button> */}
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MonacoEditor
|
||||
value={value}
|
||||
language="shell"
|
||||
onValueChange={(command) => set({ command })}
|
||||
readOnly={disabled}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Termination",
|
||||
boldLabel: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useLocalStorage, useRead } from "@lib/hooks";
|
||||
import { Types } from "@komodo/client";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
|
||||
import { HardDrive, Rocket, Server } from "lucide-react";
|
||||
import { cn } from "@lib/utils";
|
||||
import { useServer } from "../server";
|
||||
import {
|
||||
@@ -22,7 +22,6 @@ import { DeleteResource, NewResource, ResourceLink } from "../common";
|
||||
import { RunBuild } from "../build/actions";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { DeploymentConfig } from "./config";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { ResourcePageHeader, StatusBadge } from "@components/util";
|
||||
|
||||
@@ -145,9 +144,21 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
New: ({ server_id, build_id }) => (
|
||||
<NewResource type="Deployment" server_id={server_id} build_id={build_id} />
|
||||
),
|
||||
New: ({ server_id: _server_id, build_id }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const server_id = _server_id
|
||||
? _server_id
|
||||
: servers && servers.length === 1
|
||||
? servers[0].id
|
||||
: undefined;
|
||||
return (
|
||||
<NewResource
|
||||
type="Deployment"
|
||||
server_id={server_id}
|
||||
build_id={build_id}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
Table: ({ resources }) => {
|
||||
return (
|
||||
@@ -192,17 +203,6 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
Alerts: ({ id }) => {
|
||||
return (
|
||||
<Link
|
||||
to={`/deployments/${id}/alerts`}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Alerts
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Actions: {
|
||||
|
||||
@@ -43,6 +43,10 @@ const DeploymentLogsInner = ({
|
||||
const [invert, setInvert] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const [poll, setPoll] = useLocalStorage("log-poll-v1", false);
|
||||
const [timestamps, setTimestamps] = useLocalStorage(
|
||||
"log-timestamps-v1",
|
||||
false
|
||||
);
|
||||
|
||||
const addTerm = () => {
|
||||
if (!search.length) return;
|
||||
@@ -61,8 +65,8 @@ const DeploymentLogsInner = ({
|
||||
};
|
||||
|
||||
const { Log, refetch, stderr } = terms.length
|
||||
? SearchLogs(id, terms, invert)
|
||||
: NoSearchLogs(id, tail, stream);
|
||||
? SearchLogs(id, terms, invert, timestamps)
|
||||
: NoSearchLogs(id, tail, timestamps, stream);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
@@ -77,9 +81,8 @@ const DeploymentLogsInner = ({
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground flex gap-1">
|
||||
<div>Invert</div>
|
||||
<div className="hidden xl:block">Search</div>
|
||||
<div className="text-muted-foreground flex gap-1 text-sm">
|
||||
Invert
|
||||
</div>
|
||||
<Switch checked={invert} onCheckedChange={setInvert} />
|
||||
</div>
|
||||
@@ -126,9 +129,19 @@ const DeploymentLogsInner = ({
|
||||
<Button variant="secondary" size="icon" onClick={() => refetch()}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground">Poll</div>
|
||||
<Switch checked={poll} onCheckedChange={setPoll} />
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setTimestamps((t) => !t)}
|
||||
>
|
||||
<div className="text-muted-foreground text-sm">Timestamps</div>
|
||||
<Switch checked={timestamps} />
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setPoll((p) => !p)}
|
||||
>
|
||||
<div className="text-muted-foreground text-sm">Poll</div>
|
||||
<Switch checked={poll} />
|
||||
</div>
|
||||
<TailLengthSelector
|
||||
selected={tail}
|
||||
@@ -143,12 +156,17 @@ const DeploymentLogsInner = ({
|
||||
);
|
||||
};
|
||||
|
||||
const NoSearchLogs = (id: string, tail: string, stream: string) => {
|
||||
const { data: log, refetch } = useRead(
|
||||
"GetDeploymentLog",
|
||||
{ deployment: id, tail: Number(tail) },
|
||||
{ refetchInterval: 30000 }
|
||||
);
|
||||
const NoSearchLogs = (
|
||||
id: string,
|
||||
tail: string,
|
||||
timestamps: boolean,
|
||||
stream: string
|
||||
) => {
|
||||
const { data: log, refetch } = useRead("GetDeploymentLog", {
|
||||
deployment: id,
|
||||
tail: Number(tail),
|
||||
timestamps,
|
||||
});
|
||||
return {
|
||||
Log: (
|
||||
<div className="relative">
|
||||
@@ -160,12 +178,18 @@ const NoSearchLogs = (id: string, tail: string, stream: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const SearchLogs = (id: string, terms: string[], invert: boolean) => {
|
||||
const SearchLogs = (
|
||||
id: string,
|
||||
terms: string[],
|
||||
invert: boolean,
|
||||
timestamps: boolean
|
||||
) => {
|
||||
const { data: log, refetch } = useRead("SearchDeploymentLog", {
|
||||
deployment: id,
|
||||
terms,
|
||||
combinator: Types.SearchCombinator.And,
|
||||
invert,
|
||||
timestamps,
|
||||
});
|
||||
return {
|
||||
Log: (
|
||||
|
||||
@@ -57,6 +57,7 @@ export const ResourceSyncConfig = ({
|
||||
onSave={async () => {
|
||||
await mutateAsync({ id, config: update });
|
||||
}}
|
||||
file_contents_language="toml"
|
||||
components={{
|
||||
"": [
|
||||
{
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
|
||||
return (
|
||||
<NewLayout
|
||||
entityType="Server Template"
|
||||
onSuccess={async () => {
|
||||
onConfirm={async () => {
|
||||
if (!type) return;
|
||||
const id = (await mutateAsync({ name, config: { type, params: {} } }))
|
||||
._id?.$oid!;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ConfigList,
|
||||
InputList,
|
||||
ProviderSelectorConfig,
|
||||
SystemCommand,
|
||||
} from "@components/config/util";
|
||||
import { Types } from "@komodo/client";
|
||||
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
@@ -233,10 +234,24 @@ export const StackConfig = ({
|
||||
placeholder: "Compose project name",
|
||||
boldLabel: true,
|
||||
description:
|
||||
"Optionally set a different compose project name. It should match the compose project name on your host.",
|
||||
"Optionally set a different compose project name. If importing existing stack, this should match the compose project name on your host.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Pre Deploy",
|
||||
description:
|
||||
"Execute a shell command before running docker compose up. The 'path' is relative to the Run Directory",
|
||||
components: {
|
||||
pre_deploy: (value, set) => (
|
||||
<SystemCommand
|
||||
value={value}
|
||||
set={(value) => set({ pre_deploy: value })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Extra Args",
|
||||
labelHidden: true,
|
||||
@@ -374,6 +389,17 @@ export const StackConfig = ({
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Destroy",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
run_build: {
|
||||
label: "Destroy Before Deploy",
|
||||
description:
|
||||
"Ensure 'docker compose down' is run before redeploying the Stack.",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (mode === undefined) {
|
||||
@@ -467,6 +493,10 @@ export const StackConfig = ({
|
||||
description:
|
||||
"Switch to a specific hash after cloning the branch.",
|
||||
},
|
||||
reclone: {
|
||||
description:
|
||||
"Delete the repo folder and clone it again, instead of using 'git pull'.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -693,6 +723,7 @@ export const StackConfig = ({
|
||||
}
|
||||
onValueChange={(file_contents) => set({ file_contents })}
|
||||
language="yaml"
|
||||
readOnly={disabled}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -717,6 +748,7 @@ export const StackConfig = ({
|
||||
await mutateAsync({ id, config: update });
|
||||
}}
|
||||
components={components}
|
||||
file_contents_language="yaml"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -151,7 +151,15 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
);
|
||||
},
|
||||
|
||||
New: ({ server_id }) => <NewResource type="Stack" server_id={server_id} />,
|
||||
New: ({ server_id: _server_id }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const server_id = _server_id
|
||||
? _server_id
|
||||
: servers && servers.length === 1
|
||||
? servers[0].id
|
||||
: undefined;
|
||||
return <NewResource type="Stack" server_id={server_id} />;
|
||||
},
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<StackTable stacks={resources as Types.StackListItem[]} />
|
||||
|
||||
@@ -80,7 +80,7 @@ export const StackInfo = ({
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
|
||||
{/* Update deployed contents with diff */}
|
||||
{/* {!is_down && deployed_contents.length > 0 && (
|
||||
<Card>
|
||||
@@ -189,6 +189,7 @@ export const StackInfo = ({
|
||||
}
|
||||
}}
|
||||
disabled={!edits[content.path]}
|
||||
language="yaml"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const NewUserGroup = () => {
|
||||
return (
|
||||
<NewLayout
|
||||
entityType="User Group"
|
||||
onSuccess={() => mutateAsync({ name })}
|
||||
onConfirm={() => mutateAsync({ name })}
|
||||
enabled={!!name}
|
||||
onOpenChange={() => setName("")}
|
||||
>
|
||||
@@ -46,7 +46,7 @@ export const NewServiceUser = () => {
|
||||
return (
|
||||
<NewLayout
|
||||
entityType="Service User"
|
||||
onSuccess={() => mutateAsync({ username, description: "" })}
|
||||
onConfirm={() => mutateAsync({ username, description: "" })}
|
||||
enabled={!!username}
|
||||
onOpenChange={() => setUsername("")}
|
||||
>
|
||||
|
||||
@@ -244,3 +244,13 @@ export const is_service_user = (user_id: string) => {
|
||||
user_id === "Repo Manager"
|
||||
);
|
||||
};
|
||||
|
||||
export const extract_registry_domain = (image_name: string) => {
|
||||
if (!image_name) return "docker.io";
|
||||
const maybe_domain = image_name.split("/")[0];
|
||||
if (maybe_domain.includes(".")) {
|
||||
return maybe_domain
|
||||
} else {
|
||||
return "docker.io"
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,7 @@ export const Dashboard = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-12">
|
||||
<ActiveResources />
|
||||
</div>
|
||||
<ActiveResources />
|
||||
<Page
|
||||
title="Dashboard"
|
||||
icon={<Box className="w-8 h-8" />}
|
||||
@@ -253,37 +251,39 @@ const ActiveResources = () => {
|
||||
if (resources.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Active"
|
||||
icon={
|
||||
<Circle className="w-4 h-4 stroke-none transition-colors fill-green-500" />
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
tableKey="active-resources"
|
||||
data={resources}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ResourceLink type={row.original.type} id={row.original.id} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Resource" />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "State",
|
||||
cell: ({ row }) => row.original.state,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
<div className="mb-12">
|
||||
<Section
|
||||
title="Active"
|
||||
icon={
|
||||
<Circle className="w-4 h-4 stroke-none transition-colors fill-green-500" />
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
tableKey="active-resources"
|
||||
data={resources}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ResourceLink type={row.original.type} id={row.original.id} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Resource" />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "State",
|
||||
cell: ({ row }) => row.original.state,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,10 @@ export const ContainerLogs = ({
|
||||
const [invert, setInvert] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const [poll, setPoll] = useLocalStorage("log-poll-v1", false);
|
||||
const [timestamps, setTimestamps] = useLocalStorage(
|
||||
"log-timestamps-v1",
|
||||
false
|
||||
);
|
||||
|
||||
const addTerm = () => {
|
||||
if (!search.length) return;
|
||||
@@ -43,8 +47,8 @@ export const ContainerLogs = ({
|
||||
};
|
||||
|
||||
const { Log, refetch, stderr } = terms.length
|
||||
? SearchLogs(id, container_name, terms, invert)
|
||||
: NoSearchLogs(id, container_name, tail, stream);
|
||||
? SearchLogs(id, container_name, terms, invert, timestamps)
|
||||
: NoSearchLogs(id, container_name, tail, timestamps, stream);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
@@ -61,10 +65,7 @@ export const ContainerLogs = ({
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground flex gap-1">
|
||||
<div>Invert</div>
|
||||
<div className="hidden xl:block">Search</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex gap-1">Invert</div>
|
||||
<Switch checked={invert} onCheckedChange={setInvert} />
|
||||
</div>
|
||||
{terms.map((term, index) => (
|
||||
@@ -110,9 +111,19 @@ export const ContainerLogs = ({
|
||||
<Button variant="secondary" size="icon" onClick={() => refetch()}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground">Poll</div>
|
||||
<Switch checked={poll} onCheckedChange={setPoll} />
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setTimestamps((t) => !t)}
|
||||
>
|
||||
<div className="text-muted-foreground text-sm">Timestamps</div>
|
||||
<Switch checked={timestamps} />
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setPoll((p) => !p)}
|
||||
>
|
||||
<div className="text-muted-foreground text-sm">Poll</div>
|
||||
<Switch checked={poll} />
|
||||
</div>
|
||||
<TailLengthSelector
|
||||
selected={tail}
|
||||
@@ -131,13 +142,15 @@ const NoSearchLogs = (
|
||||
id: string,
|
||||
container: string,
|
||||
tail: string,
|
||||
timestamps: boolean,
|
||||
stream: string
|
||||
) => {
|
||||
const { data: log, refetch } = useRead(
|
||||
"GetContainerLog",
|
||||
{ server: id, container, tail: Number(tail) },
|
||||
{ refetchInterval: 30000 }
|
||||
);
|
||||
const { data: log, refetch } = useRead("GetContainerLog", {
|
||||
server: id,
|
||||
container,
|
||||
tail: Number(tail),
|
||||
timestamps,
|
||||
});
|
||||
return {
|
||||
Log: (
|
||||
<div className="relative">
|
||||
@@ -153,7 +166,8 @@ const SearchLogs = (
|
||||
id: string,
|
||||
container: string,
|
||||
terms: string[],
|
||||
invert: boolean
|
||||
invert: boolean,
|
||||
timestamps: boolean
|
||||
) => {
|
||||
const { data: log, refetch } = useRead("SearchContainerLog", {
|
||||
server: id,
|
||||
@@ -161,6 +175,7 @@ const SearchLogs = (
|
||||
terms,
|
||||
combinator: Types.SearchCombinator.And,
|
||||
invert,
|
||||
timestamps,
|
||||
});
|
||||
return {
|
||||
Log: (
|
||||
|
||||
@@ -45,6 +45,10 @@ const StackLogsInner = ({
|
||||
const [invert, setInvert] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const [poll, setPoll] = useLocalStorage("log-poll-v1", false);
|
||||
const [timestamps, setTimestamps] = useLocalStorage(
|
||||
"log-timestamps-v1",
|
||||
false
|
||||
);
|
||||
|
||||
const addTerm = () => {
|
||||
if (!search.length) return;
|
||||
@@ -63,8 +67,8 @@ const StackLogsInner = ({
|
||||
};
|
||||
|
||||
const { Log, refetch, stderr } = terms.length
|
||||
? SearchLogs(id, service, terms, invert)
|
||||
: NoSearchLogs(id, service, tail, stream);
|
||||
? SearchLogs(id, service, terms, invert, timestamps)
|
||||
: NoSearchLogs(id, service, tail, timestamps, stream);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
@@ -81,9 +85,8 @@ const StackLogsInner = ({
|
||||
actions={
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground flex gap-1">
|
||||
<div>Invert</div>
|
||||
<div className="hidden xl:block">Search</div>
|
||||
<div className="text-muted-foreground flex gap-1 text-sm">
|
||||
Invert
|
||||
</div>
|
||||
<Switch checked={invert} onCheckedChange={setInvert} />
|
||||
</div>
|
||||
@@ -130,9 +133,19 @@ const StackLogsInner = ({
|
||||
<Button variant="secondary" size="icon" onClick={() => refetch()}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground">Poll</div>
|
||||
<Switch checked={poll} onCheckedChange={setPoll} />
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setTimestamps((t) => !t)}
|
||||
>
|
||||
<div className="text-muted-foreground text-sm">Timestamps</div>
|
||||
<Switch checked={timestamps} />
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setPoll((p) => !p)}
|
||||
>
|
||||
<div className="text-muted-foreground text-sm">Poll</div>
|
||||
<Switch checked={poll} />
|
||||
</div>
|
||||
<TailLengthSelector
|
||||
selected={tail}
|
||||
@@ -151,13 +164,15 @@ const NoSearchLogs = (
|
||||
id: string,
|
||||
service: string,
|
||||
tail: string,
|
||||
timestamps: boolean,
|
||||
stream: string
|
||||
) => {
|
||||
const { data: log, refetch } = useRead(
|
||||
"GetStackServiceLog",
|
||||
{ stack: id, service, tail: Number(tail) },
|
||||
{ refetchInterval: 30000 }
|
||||
);
|
||||
const { data: log, refetch } = useRead("GetStackServiceLog", {
|
||||
stack: id,
|
||||
service,
|
||||
tail: Number(tail),
|
||||
timestamps,
|
||||
});
|
||||
return {
|
||||
Log: (
|
||||
<div className="relative">
|
||||
@@ -173,7 +188,8 @@ const SearchLogs = (
|
||||
id: string,
|
||||
service: string,
|
||||
terms: string[],
|
||||
invert: boolean
|
||||
invert: boolean,
|
||||
timestamps: boolean
|
||||
) => {
|
||||
const { data: log, refetch } = useRead("SearchStackServiceLog", {
|
||||
stack: id,
|
||||
@@ -181,6 +197,7 @@ const SearchLogs = (
|
||||
terms,
|
||||
combinator: Types.SearchCombinator.And,
|
||||
invert,
|
||||
timestamps,
|
||||
});
|
||||
return {
|
||||
Log: (
|
||||
|
||||
@@ -1,12 +1,57 @@
|
||||
use std::path::Path;
|
||||
|
||||
use komodo_client::entities::{komodo_timestamp, update::Log};
|
||||
use run_command::{async_run_command, CommandOutput};
|
||||
|
||||
pub async fn run_komodo_command(stage: &str, command: String) -> Log {
|
||||
/// Parses commands out of multiline string
|
||||
/// and chains them together with '&&'
|
||||
///
|
||||
/// Supports full line and end of line comments. See [parse_multiline_command].
|
||||
pub async fn run_komodo_command(
|
||||
stage: &str,
|
||||
path: impl Into<Option<&Path>>,
|
||||
command: impl AsRef<str>,
|
||||
) -> Log {
|
||||
let command = parse_multiline_command(command);
|
||||
let command = if let Some(path) = path.into() {
|
||||
format!("cd {} && {command}", path.display(),)
|
||||
} else {
|
||||
command
|
||||
};
|
||||
let start_ts = komodo_timestamp();
|
||||
let output = async_run_command(&command).await;
|
||||
output_into_log(stage, command, start_ts, output)
|
||||
}
|
||||
|
||||
/// Parses commands out of multiline string
|
||||
/// and chains them together with '&&'
|
||||
///
|
||||
/// Supports full line and end of line comments.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ```sh
|
||||
/// # comments supported
|
||||
/// sh ./shell1.sh # end of line supported
|
||||
/// sh ./shell2.sh
|
||||
/// # print done
|
||||
/// echo done
|
||||
/// ```
|
||||
/// becomes
|
||||
/// ```sh
|
||||
/// sh ./shell1.sh && sh ./shell2.sh && echo done
|
||||
/// ```
|
||||
pub fn parse_multiline_command(command: impl AsRef<str>) -> String {
|
||||
command
|
||||
.as_ref()
|
||||
.split('\n')
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.filter_map(|line| line.split(" #").next())
|
||||
.map(str::trim)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" && ")
|
||||
}
|
||||
|
||||
pub fn output_into_log(
|
||||
stage: &str,
|
||||
command: String,
|
||||
|
||||
@@ -39,8 +39,7 @@ where
|
||||
{
|
||||
let args: CloneArgs = clone_args.into();
|
||||
let repo_dir = args.path(repo_dir);
|
||||
let repo_url =
|
||||
args.remote_url(access_token.as_ref().map(String::as_str))?;
|
||||
let repo_url = args.remote_url(access_token.as_deref())?;
|
||||
|
||||
let mut logs = clone_inner(
|
||||
&repo_url,
|
||||
@@ -100,7 +99,7 @@ where
|
||||
};
|
||||
|
||||
if let Some(command) = args.on_clone {
|
||||
if !command.path.is_empty() && !command.command.is_empty() {
|
||||
if !command.command.is_empty() {
|
||||
let on_clone_path = repo_dir.join(&command.path);
|
||||
if let Some(secrets) = secrets {
|
||||
let (full_command, mut replacers) =
|
||||
@@ -116,7 +115,8 @@ where
|
||||
replacers.extend(core_replacers.to_owned());
|
||||
let mut on_clone_log = run_komodo_command(
|
||||
"on clone",
|
||||
format!("cd {} && {full_command}", on_clone_path.display()),
|
||||
on_clone_path.as_ref(),
|
||||
full_command,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -137,11 +137,8 @@ where
|
||||
} else {
|
||||
let on_clone_log = run_komodo_command(
|
||||
"on clone",
|
||||
format!(
|
||||
"cd {} && {}",
|
||||
on_clone_path.display(),
|
||||
command.command
|
||||
),
|
||||
on_clone_path.as_ref(),
|
||||
&command.command,
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(
|
||||
@@ -154,7 +151,7 @@ where
|
||||
}
|
||||
}
|
||||
if let Some(command) = args.on_pull {
|
||||
if !command.path.is_empty() && !command.command.is_empty() {
|
||||
if !command.command.is_empty() {
|
||||
let on_pull_path = repo_dir.join(&command.path);
|
||||
if let Some(secrets) = secrets {
|
||||
let (full_command, mut replacers) =
|
||||
@@ -170,7 +167,8 @@ where
|
||||
replacers.extend(core_replacers.to_owned());
|
||||
let mut on_pull_log = run_komodo_command(
|
||||
"on pull",
|
||||
format!("cd {} && {full_command}", on_pull_path.display()),
|
||||
on_pull_path.as_ref(),
|
||||
&full_command,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -191,11 +189,8 @@ where
|
||||
} else {
|
||||
let on_pull_log = run_komodo_command(
|
||||
"on pull",
|
||||
format!(
|
||||
"cd {} && {}",
|
||||
on_pull_path.display(),
|
||||
command.command
|
||||
),
|
||||
on_pull_path.as_ref(),
|
||||
&command.command,
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(
|
||||
@@ -256,10 +251,8 @@ async fn clone_inner(
|
||||
if let Some(commit) = commit {
|
||||
let reset_log = run_komodo_command(
|
||||
"set commit",
|
||||
format!(
|
||||
"cd {} && git reset --hard {commit}",
|
||||
destination.display()
|
||||
),
|
||||
destination,
|
||||
format!("git reset --hard {commit}",),
|
||||
)
|
||||
.await;
|
||||
logs.push(reset_log);
|
||||
|
||||
@@ -38,16 +38,13 @@ where
|
||||
{
|
||||
let args: CloneArgs = clone_args.into();
|
||||
let path = args.path(repo_dir);
|
||||
let path_display = path.display();
|
||||
let repo_url =
|
||||
args.remote_url(access_token.as_ref().map(String::as_str))?;
|
||||
let repo_url = args.remote_url(access_token.as_deref())?;
|
||||
|
||||
// Set remote url
|
||||
let mut set_remote = run_komodo_command(
|
||||
"set git remote",
|
||||
format!(
|
||||
"cd {path_display} && git remote set-url origin {repo_url}"
|
||||
),
|
||||
path.as_ref(),
|
||||
format!("git remote set-url origin {repo_url}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -70,7 +67,8 @@ where
|
||||
|
||||
let checkout = run_komodo_command(
|
||||
"checkout branch",
|
||||
format!("cd {path_display} && git checkout -f {}", args.branch),
|
||||
path.as_ref(),
|
||||
format!("git checkout -f {}", args.branch),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -83,12 +81,12 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
let command = format!(
|
||||
"cd {path_display} && git pull --rebase --force origin {}",
|
||||
args.branch
|
||||
);
|
||||
|
||||
let pull_log = run_komodo_command("git pull", command).await;
|
||||
let pull_log = run_komodo_command(
|
||||
"git pull",
|
||||
path.as_ref(),
|
||||
format!("git pull --rebase --force origin {}", args.branch),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut logs = vec![pull_log];
|
||||
|
||||
@@ -104,7 +102,8 @@ where
|
||||
if let Some(commit) = args.commit {
|
||||
let reset_log = run_komodo_command(
|
||||
"set commit",
|
||||
format!("cd {path_display} && git reset --hard {commit}"),
|
||||
path.as_ref(),
|
||||
format!("git reset --hard {commit}"),
|
||||
)
|
||||
.await;
|
||||
logs.push(reset_log);
|
||||
@@ -144,7 +143,7 @@ where
|
||||
};
|
||||
|
||||
if let Some(command) = args.on_pull {
|
||||
if !command.path.is_empty() && !command.command.is_empty() {
|
||||
if !command.command.is_empty() {
|
||||
let on_pull_path = path.join(&command.path);
|
||||
if let Some(secrets) = secrets {
|
||||
let (full_command, mut replacers) =
|
||||
@@ -174,7 +173,8 @@ where
|
||||
replacers.extend(core_replacers.to_owned());
|
||||
let mut on_pull_log = run_komodo_command(
|
||||
"on pull",
|
||||
format!("cd {} && {full_command}", on_pull_path.display()),
|
||||
on_pull_path.as_ref(),
|
||||
&full_command,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -195,11 +195,8 @@ where
|
||||
} else {
|
||||
let on_pull_log = run_komodo_command(
|
||||
"on pull",
|
||||
format!(
|
||||
"cd {} && {}",
|
||||
on_pull_path.display(),
|
||||
command.command
|
||||
),
|
||||
on_pull_path.as_ref(),
|
||||
&command.command,
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(
|
||||
|
||||
22
readme.md
@@ -6,6 +6,8 @@ A tool to build and deploy software across many servers.
|
||||
|
||||
🦎 [Try the Demo](https://demo.komo.do)
|
||||
|
||||
🦎 [See the Build Server](https://build.komo.do)
|
||||
|
||||
🦎 [Join the Discord](https://discord.gg/DRqE8Fvg5c)
|
||||
|
||||
## About
|
||||
@@ -31,25 +33,21 @@ there are no warranties. Use at your own risk.
|
||||
### Light Theme
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### Dark Theme
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -30,6 +30,10 @@ docker compose -p komodo-dev -f test.compose.yaml build"""
|
||||
description = "runs core --release pointing to test.core.config.toml"
|
||||
cmd = "KOMODO_CONFIG_PATH=test.core.config.toml cargo run -p komodo_core --release"
|
||||
|
||||
[test-periphery]
|
||||
description = "runs periphery --release pointing to test.periphery.config.toml"
|
||||
cmd = "PERIPHERY_CONFIG_PATH=test.periphery.config.toml cargo run -p komodo_periphery --release"
|
||||
|
||||
[docsite-start]
|
||||
path = "docsite"
|
||||
cmd = "yarn start"
|
||||
@@ -38,5 +42,5 @@ cmd = "yarn start"
|
||||
path = "docsite"
|
||||
cmd = "yarn deploy"
|
||||
|
||||
[rustdoc-server]
|
||||
cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc"
|
||||
# [rustdoc-server]
|
||||
# cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc"
|
||||
BIN
screenshots/Dark-Compose.png
Normal file
|
After Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 197 KiB |
BIN
screenshots/Dark-Env.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 140 KiB |
BIN
screenshots/Dark-Stack.png
Normal file
|
After Width: | Height: | Size: 405 KiB |
BIN
screenshots/Dark-Stats.png
Normal file
|
After Width: | Height: | Size: 405 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 451 KiB |
|
Before Width: | Height: | Size: 133 KiB |
BIN
screenshots/Light-Compose.png
Normal file
|
After Width: | Height: | Size: 580 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 375 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 182 KiB |
BIN
screenshots/Light-Env.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 495 KiB |