Compare commits

..
16 Commits
Author SHA1 Message Date
mbecker20 d71e9dca11 fix version 2024-10-13 03:21:56 -04:00
Maxwell BeckerandGitHub 165131bdf8 1.15.7 (#119)
* 1.15.7-dev ensure git config set

* add username to commit msg
2024-10-13 00:01:14 -07:00
mbecker20 0a81d2a0d0 add labels to mongo compose 2024-10-13 00:57:13 -04:00
Maxwell BeckerandGitHub 44ab5eb804 1.15.6 (#117)
* add periphery.skip label, skip in StopAllContainers

* add core config sync directory

* deploy stack if changed

* fix stack env_file_path when git repo and using run_directory

* deploy stack if changed

* write sync contents

* commit to git based sync, managed git based sync

* can sync non UI defined resource syncs

* sync UI control

* clippy

* init new stack compose file in repo

* better error message when attached Server / Builder invalid

* specify multiple resource file paths (mixed files + folders)

* use react charts

* tweak stats charts

* add Containers page

* 1.15.6

* stack deploy check if deployes vs remote has changed

* improve ux with loading indicators

* sync diff accounts for deploy / after

* fix new chart time axes
2024-10-12 21:42:46 -07:00
Maxwell BeckerandGitHub e3d8e603ec 1.15.5 (#116)
* 1.15.5
- Update your user's username and password
- **Admin**: Delete Users

* update username / password / delete user backend

* bump version

* alerter default disabled

* delete users and update username / password

* set password "" after update
2024-10-11 19:42:43 -07:00
mbecker20 8b5c179473 account recover note 2024-10-11 19:16:01 -04:00
mbecker20 8582bc92da fix Destroy Before Deploy config 2024-10-10 04:17:17 -04:00
Maxwell BeckerandGitHub 8ee270d045 1.15.4 (#114)
* stack destroy before deploy option

* add timestamps. Fix log polling even when poll not selected

* Add build [[$VERSION]] support. VERSION build arg default

* fix clippy lint

* initialize `first_builder`

* run_komodo_command uses parse_multiline_command

* comment UI for $VERSION and new command feature

* bump some deps

* support multiline commands in pre_deploy / pre_build
2024-10-10 00:37:23 -07:00
Maxwell BeckerandGitHub 2cfae525e9 1.15.3 (#109)
* fix parser support single quote '

* add stack reclone toggle

* git clone with token uses token:<TOKEN> for gitlab compatability

* support stack pre deploy shell command

* rename compose down update log stage

* deployment configure registry login account

* local testing setup

* bump version to 1.15.3

* new resources auto assign server if only one

* better error log when try to create resource with duplicate name

* end description with .

* ConfirmUpdate multi language

* fix compose write to host logic

* improve instrumentation

* improve update diff when small array

improve 2

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

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