Compare commits

...

29 Commits

Author SHA1 Message Date
Maxwell Becker
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 Becker
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
mbecker20
3be2b5163b 1.15.1 do not add trailing slash OIDC provider 2024-10-07 13:23:40 -04:00
mbecker20
6a145f58ff pass provider as-is. Authentik users should add a trailing slash 2024-10-07 13:16:25 -04:00
mbecker20
f1cede2ebd update dark / light stack screenshot to have action buttons 2024-10-07 08:05:39 -04:00
mbecker20
a5cfa1d412 update screenshots 2024-10-07 07:30:18 -04:00
mbecker20
a0674654c1 update screenshots 2024-10-07 07:30:11 -04:00
mbecker20
3faa1c58c1 update screenshots 2024-10-07 07:30:05 -04:00
mbecker20
7e296f34af screenshots 2024-10-07 07:29:58 -04:00
mbecker20
9f8ced190c update screenshots 2024-10-07 07:29:02 -04:00
mbecker20
c194bb16d8 update screenshots 2024-10-07 07:28:45 -04:00
mbecker20
39fec9b55e update screenshots 2024-10-07 07:27:52 -04:00
mbecker20
e97ed9888d update screenshots 1 2024-10-07 07:27:16 -04:00
mbecker20
559102ffe3 update readme 2024-10-07 07:25:36 -04:00
mbecker20
6bf80ddcc7 update screenshots readme 2024-10-07 07:25:24 -04:00
mbecker20
89dbe1b4d9 stack file_contents editor respects readOnly / disabled 2024-10-07 06:58:00 -04:00
mbecker20
334e16d646 OIDC use preferred username 2024-10-07 06:35:46 -04:00
mbecker20
a7bbe519f4 add build server link 2024-10-07 06:15:53 -04:00
mbecker20
5827486c5a add redirect uri for OIDC 2024-10-07 06:15:00 -04:00
mbecker20
8ca8f7eddd add context to oidc init error 2024-10-07 06:10:12 -04:00
mbecker20
0600276b43 fix parse KOMODO_MONGO_ in envs 2024-10-07 05:43:09 -04:00
mbecker20
a77a1495c7 active resources mb-12 not always there 2024-10-07 05:14:54 -04:00
112 changed files with 1292 additions and 623 deletions

View File

@@ -5,4 +5,10 @@ LICENSE
*.code-workspace
*/node_modules
*/dist
*/dist
creds.toml
.core-repos
.repos
.stacks
.ssl

9
.gitignore vendored
View File

@@ -1,11 +1,14 @@
target
/frontend/build
node_modules
/lib/ts_client/build
node_modules
dist
.env
.env.development
.DS_Store
creds.toml
.syncs
.core-repos
.repos
.stacks
.DS_Store
.ssl

107
Cargo.lock generated
View File

@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"axum",
@@ -201,9 +201,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "aws-config"
version = "1.5.7"
version = "1.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8191fb3091fa0561d1379ef80333c3c7191c6f0435d986e85821bcf7acbd1126"
checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -268,9 +268,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ec2"
version = "1.75.0"
version = "1.77.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6787d920877cca6a4ee3953093f6a47cefe26de95a4f7b3681c5850bfe657b4"
checksum = "4bb6f841697b994ec3a020c560b52693bc9fcb7b9c69210088ab56e03df23b5e"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -292,9 +292,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.44.0"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b90cfe6504115e13c41d3ea90286ede5aa14da294f3fe077027a6e83850843c"
checksum = "e33ae899566f3d395cbf42858e433930682cc9c1889fa89318896082fef45efb"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -314,9 +314,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.45.0"
version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167c0fad1f212952084137308359e8e4c4724d1c643038ce163f06de9662c1d0"
checksum = "f39c09e199ebd96b9f860b0fce4b6625f211e064ad7c8693b72ecf7ef03881e0"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -336,9 +336,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.44.0"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb5f98188ec1435b68097daa2a37d74b9d17c9caa799466338a8d1544e71b9d"
checksum = "3d95f93a98130389eb6233b9d615249e543f6c24a68ca1f109af9ca5164a8765"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -887,9 +887,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.19"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -897,9 +897,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.19"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
"anstream",
"anstyle",
@@ -943,7 +943,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"komodo_client",
"run_command",
@@ -1149,18 +1149,18 @@ dependencies = [
[[package]]
name = "derive_builder"
version = "0.20.1"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.1"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
@@ -1170,9 +1170,9 @@ dependencies = [
[[package]]
name = "derive_builder_macro"
version = "0.20.1"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.77",
@@ -1355,7 +1355,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"thiserror",
]
@@ -1439,7 +1439,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"serror",
]
@@ -1452,9 +1452,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
@@ -1467,9 +1467,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
@@ -1477,15 +1477,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
@@ -1494,15 +1494,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
@@ -1511,21 +1511,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
@@ -1571,7 +1571,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "git"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"command",
@@ -2192,7 +2192,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"clap",
@@ -2208,7 +2208,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2239,7 +2239,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2296,7 +2296,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2306,6 +2306,7 @@ dependencies = [
"bollard",
"clap",
"command",
"derive_variants",
"dotenvy",
"environment_file",
"envy",
@@ -2382,7 +2383,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"komodo_client",
@@ -2446,7 +2447,7 @@ dependencies = [
[[package]]
name = "migrator"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"dotenvy",
@@ -3101,7 +3102,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"komodo_client",
@@ -4249,9 +4250,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.31.4"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be"
checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791"
dependencies = [
"core-foundation-sys",
"libc",
@@ -4879,7 +4880,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.15.0"
version = "1.15.4"
dependencies = [
"anyhow",
"komodo_client",

View File

@@ -3,7 +3,7 @@ resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
[workspace.package]
version = "1.15.0"
version = "1.15.4"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -44,8 +44,8 @@ svi = "1.0.1"
reqwest = { version = "0.12.8", features = ["json"] }
tokio = { version = "1.38.1", features = ["full"] }
tokio-util = "0.7.12"
futures = "0.3.30"
futures-util = "0.3.30"
futures = "0.3.31"
futures-util = "0.3.31"
# SERVER
axum-extra = { version = "0.9.4", features = ["typed-header"] }
@@ -76,7 +76,7 @@ opentelemetry = "0.25.0"
tracing = "0.1.40"
# CONFIG
clap = { version = "4.5.19", features = ["derive"] }
clap = { version = "4.5.20", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
@@ -95,14 +95,14 @@ hex = "0.4.3"
# SYSTEM
bollard = "0.17.1"
sysinfo = "0.31.4"
sysinfo = "0.32.0"
# CLOUD
aws-config = "1.5.7"
aws-sdk-ec2 = "1.75.0"
aws-config = "1.5.8"
aws-sdk-ec2 = "1.77.0"
# MISC
derive_builder = "0.20.1"
derive_builder = "0.20.2"
typeshare = "1.0.3"
octorust = "0.7.0"
dashmap = "6.1.0"

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")]

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();

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")]
@@ -154,7 +158,15 @@ async fn handler(
Ok(Json(update))
}
#[instrument(name = "ExecuteRequest", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
name = "ExecuteRequest",
skip(user, update),
fields(
user_id = user.id,
update_id = update.id,
request = format!("{:?}", request.extract_variant()))
)
]
async fn task(
req_id: Uuid,
request: ExecuteRequest,

View File

@@ -19,6 +19,7 @@ use crate::{
add_interp_update_log,
interpolate_variables_secrets_into_extra_args,
interpolate_variables_secrets_into_string,
interpolate_variables_secrets_into_system_command,
},
periphery_client,
query::get_variables_and_secrets,
@@ -102,6 +103,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut stack.config.pre_deploy,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,

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")

View File

@@ -404,6 +404,7 @@ impl Resolve<GetContainerLog, User> for State {
server,
container,
tail,
timestamps,
}: GetContainerLog,
user: User,
) -> anyhow::Result<Log> {
@@ -417,6 +418,7 @@ impl Resolve<GetContainerLog, User> for State {
.request(periphery::container::GetContainerLog {
name: container,
tail: cmp::min(tail, MAX_LOG_LENGTH),
timestamps,
})
.await
.context("failed at call to periphery")
@@ -432,6 +434,7 @@ impl Resolve<SearchContainerLog, User> for State {
terms,
combinator,
invert,
timestamps,
}: SearchContainerLog,
user: User,
) -> anyhow::Result<Log> {
@@ -447,6 +450,7 @@ impl Resolve<SearchContainerLog, User> for State {
terms,
combinator,
invert,
timestamps,
})
.await
.context("failed at call to periphery")

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")

View File

@@ -188,7 +188,10 @@ async fn handler(
#[instrument(
name = "WriteRequest",
skip(user, request),
fields(user_id = user.id, request = format!("{:?}", request.extract_variant()))
fields(
user_id = user.id,
request = format!("{:?}", request.extract_variant())
)
)]
async fn task(
req_id: Uuid,

View File

@@ -32,17 +32,15 @@ pub async fn init_default_oidc_client() {
return;
}
async {
let provider = config.oidc_provider.to_string();
// Use OpenID Connect Discovery to fetch the provider metadata.
let provider_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new(if provider.ends_with('/') {
provider
} else {
provider + "/"
})?,
IssuerUrl::new(config.oidc_provider.clone())?,
async_http_client,
)
.await?;
.await
.context(
"Failed to get OIDC /.well-known/openid-configuration",
)?;
// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
// and token URL.

View File

@@ -135,7 +135,9 @@ async fn callback(
.context("CSRF Token invalid")?;
if komodo_timestamp() > valid_until {
return Err(anyhow!("CSRF token invalid (Timed out). The token must be "));
return Err(anyhow!(
"CSRF token invalid (Timed out). The token must be "
));
}
let token_response = client
@@ -150,8 +152,21 @@ async fn callback(
let id_token = token_response
.id_token()
.context("OIDC Server did not return an ID token")?;
// Some providers attach additional audiences, they must be added here
// so token verification succeeds.
let verifier = client.id_token_verifier();
let additional_audiences = &core_config().oidc_additional_audiences;
let verifier = if additional_audiences.is_empty() {
verifier
} else {
verifier.set_other_audience_verifier_fn(|aud| {
additional_audiences.contains(aud)
})
};
let claims = id_token
.claims(&client.id_token_verifier(), &nonce)
.claims(&verifier, &nonce)
.context("Failed to verify token claims")?;
// Verify the access token hash to ensure that the access token hasn't been substituted for
@@ -191,20 +206,25 @@ async fn callback(
if !no_users_exist && core_config.disable_user_registration {
return Err(anyhow!("User registration is disabled"));
}
// Email will use user_id if it isn't available.
let email = claims
.email()
.map(|email| email.as_str())
.unwrap_or(user_id);
let username = if core_config.oidc_use_full_email {
email
} else {
email
.split_once('@')
.map(|(username, _)| username)
.unwrap_or(email)
}
.to_string();
// Will use preferred_username, then email, then user_id if it isn't available.
let username = claims
.preferred_username()
.map(|username| username.to_string())
.unwrap_or_else(|| {
let email = claims
.email()
.map(|email| email.as_str())
.unwrap_or(user_id);
if core_config.oidc_use_full_email {
email
} else {
email
.split_once('@')
.map(|(username, _)| username)
.unwrap_or(email)
}
.to_string()
});
let user = User {
id: Default::default(),
username,

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

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},
@@ -280,8 +281,8 @@ async fn startup_open_alert_cleanup() {
}
}
/// Ensures a default server exists with the defined address
pub async fn ensure_first_server() {
/// Ensures a default server / builder exists with the defined address
pub async fn ensure_first_server_and_builder() {
let first_server = &core_config().first_server;
if first_server.is_empty() {
return;
@@ -295,23 +296,49 @@ pub async fn ensure_first_server() {
else {
return;
};
if server.is_some() {
return;
}
let server = if let Some(server) = server {
server
} else {
match State
.resolve(
CreateServer {
name: format!("server-{}", random_string(5)),
config: PartialServerConfig {
address: Some(first_server.to_string()),
enabled: Some(true),
..Default::default()
},
},
system_user().to_owned(),
)
.await
{
Ok(server) => server,
Err(e) => {
error!("Failed to initialize 'first_server'. Failed to CreateServer. {e:?}");
return;
}
}
};
let Ok(None) = db.builders
.find_one(Document::new()).await
.inspect_err(|e| error!("Failed to initialize 'first_builder'. Failed to query db. {e:?}")) else {
return;
};
if let Err(e) = State
.resolve(
CreateServer {
name: format!("server-{}", random_string(5)),
config: PartialServerConfig {
address: Some(first_server.to_string()),
enabled: Some(true),
..Default::default()
},
CreateBuilder {
name: String::from("local"),
config: PartialBuilderConfig::Server(
PartialServerBuilderConfig {
server_id: Some(server.id),
},
),
},
system_user().to_owned(),
)
.await
{
error!("Failed to initialize 'first_server'. Failed to CreateServer. {e:?}");
error!("Failed to initialize 'first_builder'. Failed to CreateBuilder. {e:?}");
}
}

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(),
);

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,

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,
},
};
@@ -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?;

View File

@@ -274,7 +274,7 @@ impl ToToml for Build {
config
.into_iter()
.map(|(key, value)| match key.as_str() {
"builder_id" => return Ok((String::from("builder"), value)),
"builder_id" => Ok((String::from("builder"), value)),
"version" => {
match (
&resource.config.version,
@@ -353,7 +353,7 @@ impl ToToml for ServerTemplate {
if empty_params {
// toml_pretty will remove empty map
// but in this case its needed to deserialize the enums.
toml.push_str("\nconfig.params = {}");
toml.push_str("\nparams = {}");
}
}
}
@@ -385,7 +385,7 @@ impl ToToml for Builder {
if empty_params {
// toml_pretty will remove empty map
// but in this case its needed to deserialize the enums.
toml.push_str("\nconfig.params = {}");
toml.push_str("\nparams = {}");
}
}
}

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

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)
}
}

View File

@@ -28,6 +28,7 @@ impl Resolve<ListComposeProjects, ()> for State {
let docker_compose = docker_compose();
let res = run_komodo_command(
"list projects",
None,
format!("{docker_compose} ls --all --format json"),
)
.await;
@@ -76,6 +77,61 @@ pub struct DockerComposeLsItem {
//
impl Resolve<GetComposeServiceLog> for State {
#[instrument(
name = "GetComposeServiceLog",
level = "debug",
skip(self)
)]
async fn resolve(
&self,
GetComposeServiceLog {
project,
service,
tail,
timestamps,
}: GetComposeServiceLog,
_: (),
) -> anyhow::Result<Log> {
let docker_compose = docker_compose();
let timestamps =
timestamps.then_some(" --timestamps").unwrap_or_default();
let command = format!(
"{docker_compose} -p {project} logs {service} --tail {tail}{timestamps}"
);
Ok(run_komodo_command("get stack log", None, command).await)
}
}
impl Resolve<GetComposeServiceLogSearch> for State {
#[instrument(
name = "GetComposeServiceLogSearch",
level = "debug",
skip(self)
)]
async fn resolve(
&self,
GetComposeServiceLogSearch {
project,
service,
terms,
combinator,
invert,
timestamps,
}: GetComposeServiceLogSearch,
_: (),
) -> anyhow::Result<Log> {
let docker_compose = docker_compose();
let grep = log_grep(&terms, combinator, invert);
let timestamps =
timestamps.then_some(" --timestamps").unwrap_or_default();
let command = format!("{docker_compose} -p {project} logs {service} --tail 5000{timestamps} 2>&1 | {grep}");
Ok(run_komodo_command("get stack log grep", None, command).await)
}
}
//
const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎
services:
hello_world:
@@ -95,6 +151,11 @@ services:
";
impl Resolve<GetComposeContentsOnHost, ()> for State {
#[instrument(
name = "GetComposeContentsOnHost",
level = "debug",
skip(self)
)]
async fn resolve(
&self,
GetComposeContentsOnHost {
@@ -115,16 +176,11 @@ impl Resolve<GetComposeContentsOnHost, ()> for State {
.context("Failed to initialize run directory")?;
}
let file_paths = file_paths
.iter()
.map(|path| {
run_directory.join(path).components().collect::<PathBuf>()
})
.collect::<Vec<_>>();
let mut res = GetComposeContentsOnHostResponse::default();
for full_path in &file_paths {
for path in file_paths {
let full_path =
run_directory.join(&path).components().collect::<PathBuf>();
if !full_path.exists() {
fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS)
.await
@@ -136,74 +192,25 @@ impl Resolve<GetComposeContentsOnHost, ()> for State {
)
}) {
Ok(contents) => {
res.contents.push(FileContents {
path: full_path.display().to_string(),
contents,
});
// The path we store here has to be the same as incoming file path in the array,
// in order for WriteComposeContentsToHost to write to the correct path.
res.contents.push(FileContents { path, contents });
}
Err(e) => {
res.errors.push(FileContents {
path: full_path.display().to_string(),
path,
contents: format_serror(&e.into()),
});
}
}
}
Ok(res)
}
}
//
impl Resolve<GetComposeServiceLog> for State {
#[instrument(
name = "GetComposeServiceLog",
level = "debug",
skip(self)
)]
async fn resolve(
&self,
GetComposeServiceLog {
project,
service,
tail,
}: GetComposeServiceLog,
_: (),
) -> anyhow::Result<Log> {
let docker_compose = docker_compose();
let command = format!(
"{docker_compose} -p {project} logs {service} --tail {tail}"
);
Ok(run_komodo_command("get stack log", command).await)
}
}
impl Resolve<GetComposeServiceLogSearch> for State {
#[instrument(
name = "GetComposeServiceLogSearch",
level = "debug",
skip(self)
)]
async fn resolve(
&self,
GetComposeServiceLogSearch {
project,
service,
terms,
combinator,
invert,
}: GetComposeServiceLogSearch,
_: (),
) -> anyhow::Result<Log> {
let docker_compose = docker_compose();
let grep = log_grep(&terms, combinator, invert);
let command = format!("{docker_compose} -p {project} logs {service} --tail 5000 2>&1 | {grep}");
Ok(run_komodo_command("get stack log grep", command).await)
}
}
//
impl Resolve<WriteComposeContentsToHost> for State {
#[instrument(name = "WriteComposeContentsToHost", skip(self))]
async fn resolve(
@@ -216,13 +223,10 @@ impl Resolve<WriteComposeContentsToHost> for State {
}: WriteComposeContentsToHost,
_: (),
) -> anyhow::Result<Log> {
let root =
periphery_config().stack_dir.join(to_komodo_name(&name));
let run_directory = root.join(&run_directory);
let run_directory = run_directory.canonicalize().context(
"failed to validate run directory on host (canonicalize error)",
)?;
let file_path = run_directory
let file_path = periphery_config()
.stack_dir
.join(to_komodo_name(&name))
.join(&run_directory)
.join(file_path)
.components()
.collect::<PathBuf>();
@@ -292,6 +296,7 @@ impl Resolve<ComposeExecution> for State {
let docker_compose = docker_compose();
let log = run_komodo_command(
"compose command",
None,
format!("{docker_compose} -p {project} {command}"),
)
.await;

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)
}
}
@@ -297,14 +318,12 @@ impl Resolve<StartAllContainers> for State {
.await
.context("failed to list all containers on host")?;
let futures =
containers.iter().map(
|ContainerListItem { name, .. }| {
let command = format!("docker start {name}");
async move {
run_komodo_command(&command.clone(), command).await
}
},
);
containers.iter().map(|ContainerListItem { name, .. }| {
let command = format!("docker start {name}");
async move {
run_komodo_command(&command.clone(), None, command).await
}
});
Ok(join_all(futures).await)
}
}
@@ -322,14 +341,13 @@ impl Resolve<RestartAllContainers> for State {
.list_containers()
.await
.context("failed to list all containers on host")?;
let futures = containers.iter().map(
|ContainerListItem { name, .. }| {
let futures =
containers.iter().map(|ContainerListItem { name, .. }| {
let command = format!("docker restart {name}");
async move {
run_komodo_command(&command.clone(), command).await
run_komodo_command(&command.clone(), None, command).await
}
},
);
});
Ok(join_all(futures).await)
}
}
@@ -347,14 +365,13 @@ impl Resolve<PauseAllContainers> for State {
.list_containers()
.await
.context("failed to list all containers on host")?;
let futures = containers.iter().map(
|ContainerListItem { name, .. }| {
let futures =
containers.iter().map(|ContainerListItem { name, .. }| {
let command = format!("docker pause {name}");
async move {
run_komodo_command(&command.clone(), command).await
run_komodo_command(&command.clone(), None, command).await
}
},
);
});
Ok(join_all(futures).await)
}
}
@@ -372,14 +389,13 @@ impl Resolve<UnpauseAllContainers> for State {
.list_containers()
.await
.context("failed to list all containers on host")?;
let futures = containers.iter().map(
|ContainerListItem { name, .. }| {
let futures =
containers.iter().map(|ContainerListItem { name, .. }| {
let command = format!("docker unpause {name}");
async move {
run_komodo_command(&command.clone(), command).await
run_komodo_command(&command.clone(), None, command).await
}
},
);
});
Ok(join_all(futures).await)
}
}
@@ -401,6 +417,7 @@ impl Resolve<StopAllContainers> for State {
|ContainerListItem { name, .. }| async move {
run_komodo_command(
&format!("docker stop {name}"),
None,
stop_container_command(name, None, None),
)
.await

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);

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)
}
}

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)]
@@ -206,7 +210,7 @@ impl ResolveToString<ListSecrets> for State {
}
impl Resolve<GetDockerLists> for State {
#[instrument(name = "GetDockerLists", skip(self))]
#[instrument(name = "GetDockerLists", level = "debug", skip(self))]
async fn resolve(
&self,
GetDockerLists {}: GetDockerLists,
@@ -251,7 +255,7 @@ impl Resolve<RunCommand> for State {
} else {
format!("cd {path} && {command}")
};
run_komodo_command("run command", command).await
run_komodo_command("run command", None, command).await
})
.await
.context("failure in spawned task")
@@ -266,6 +270,6 @@ impl Resolve<PruneSystem> for State {
_: (),
) -> anyhow::Result<Log> {
let command = String::from("docker system prune -a -f --volumes");
Ok(run_komodo_command("prune system", command).await)
Ok(run_komodo_command("prune system", None, command).await)
}
}

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)
}
}

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)
}
}

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,46 @@ async fn write_stack(
}
};
let clone_or_pull_res = if stack.config.reclone {
State
.resolve(
CloneRepo {
args,
git_token,
environment: env_vars,
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await
} else {
State
.resolve(
PullOrCloneRepo {
args,
git_token,
environment: env_vars,
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await
};
let RepoActionResponse {
logs,
commit_hash,
commit_message,
env_file_path,
} = match State
.resolve(
PullOrCloneRepo {
args,
git_token,
environment: env_vars,
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await
{
} = match clone_or_pull_res {
Ok(res) => res,
Err(e) => {
let error = format_serror(
@@ -407,7 +513,12 @@ async fn write_stack(
return Err(anyhow!("Stopped after repo pull failure"));
}
Ok((run_directory, env_file_path))
Ok((
run_directory,
env_file_path
.is_some()
.then_some(&stack.config.env_file_path),
))
}
}
@@ -422,7 +533,8 @@ async fn compose_down(
.map(|service| format!(" {service}"))
.unwrap_or_default();
let log = run_komodo_command(
"destroy container",
"compose down",
None,
format!("{docker_compose} -p {project} down{service_arg}"),
)
.await;

View File

@@ -935,7 +935,7 @@ pub async fn docker_login(
#[instrument]
pub async fn pull_image(image: &str) -> Log {
let command = format!("docker pull {image}");
run_komodo_command("docker pull", command).await
run_komodo_command("docker pull", None, command).await
}
pub fn stop_container_command(

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,
)
}

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,

View File

@@ -128,7 +128,7 @@ pub struct UnpauseStack {
//
/// Starts the target stack. `docker compose stop`. Response: [Update]
/// Stops the target stack. `docker compose stop`. Response: [Update]
#[typeshare]
#[derive(
Debug,

View File

@@ -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]

View File

@@ -303,6 +303,9 @@ pub struct GetContainerLog {
/// Max: 5000.
#[serde(default = "default_tail")]
pub tail: U64,
/// Enable `--timestamps`
#[serde(default)]
pub timestamps: bool,
}
fn default_tail() -> u64 {
@@ -341,6 +344,9 @@ pub struct SearchContainerLog {
/// Invert the results, ie return all lines that DON'T match the terms / combinator.
#[serde(default)]
pub invert: bool,
/// Enable `--timestamps`
#[serde(default)]
pub timestamps: bool,
}
#[typeshare]

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]

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

View File

@@ -116,6 +116,10 @@ pub struct Env {
pub komodo_oidc_client_secret_file: Option<PathBuf>,
/// Override `oidc_use_full_email`
pub komodo_oidc_use_full_email: Option<bool>,
/// Override `oidc_additional_audiences`
pub komodo_oidc_additional_audiences: Option<Vec<String>>,
/// Override `oidc_additional_audiences` from file
pub komodo_oidc_additional_audiences_file: Option<PathBuf>,
/// Override `google_oauth.enabled`
pub komodo_google_oauth_enabled: Option<bool>,
@@ -159,31 +163,31 @@ pub struct Env {
pub komodo_github_webhook_app_pk_path: Option<String>,
/// Override `database.uri`
#[serde(alias = "KOMODO_MONGO_URI")]
#[serde(alias = "komodo_mongo_uri")]
pub komodo_database_uri: Option<String>,
/// Override `database.uri` from file
#[serde(alias = "KOMODO_MONGO_URI_FILE")]
#[serde(alias = "komodo_mongo_uri_file")]
pub komodo_database_uri_file: Option<PathBuf>,
/// Override `database.address`
#[serde(alias = "KOMODO_MONGO_ADDRESS")]
#[serde(alias = "komodo_mongo_address")]
pub komodo_database_address: Option<String>,
/// Override `database.username`
#[serde(alias = "KOMODO_MONGO_USERNAME")]
#[serde(alias = "komodo_mongo_username")]
pub komodo_database_username: Option<String>,
/// Override `database.username` with file
#[serde(alias = "KOMODO_MONGO_USERNAME_FILE")]
#[serde(alias = "komodo_mongo_username_file")]
pub komodo_database_username_file: Option<PathBuf>,
/// Override `database.password`
#[serde(alias = "KOMODO_MONGO_PASSWORD")]
#[serde(alias = "komodo_mongo_password")]
pub komodo_database_password: Option<String>,
/// Override `database.password` with file
#[serde(alias = "KOMODO_MONGO_PASSWORD_FILE")]
#[serde(alias = "komodo_mongo_password_file")]
pub komodo_database_password_file: Option<PathBuf>,
/// Override `database.app_name`
#[serde(alias = "KOMODO_MONGO_APP_NAME")]
#[serde(alias = "komodo_mongo_app_name")]
pub komodo_database_app_name: Option<String>,
/// Override `database.db_name`
#[serde(alias = "KOMODO_MONGO_DB_NAME")]
#[serde(alias = "komodo_mongo_db_name")]
pub komodo_database_db_name: Option<String>,
/// Override `aws.access_key_id`
@@ -344,6 +348,11 @@ pub struct CoreConfig {
#[serde(default)]
pub oidc_use_full_email: bool,
/// Your OIDC provider may set additional audiences other than `client_id`,
/// they must be added here to make claims verification work.
#[serde(default)]
pub oidc_additional_audiences: Vec<String>,
// =========
// = Oauth =
// =========
@@ -548,6 +557,11 @@ impl CoreConfig {
&config.oidc_client_secret,
),
oidc_use_full_email: config.oidc_use_full_email,
oidc_additional_audiences: config
.oidc_additional_audiences
.iter()
.map(|aud| empty_or_redacted(aud))
.collect(),
google_oauth: OauthCredentials {
enabled: config.google_oauth.enabled,
id: empty_or_redacted(&config.google_oauth.id),

View File

@@ -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

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
@@ -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)
}
}

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>,
}

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()")]
@@ -312,6 +323,11 @@ pub struct StackConfig {
#[builder(default)]
pub registry_account: String,
/// The optional command to run before the Stack is deployed.
#[serde(default)]
#[builder(default)]
pub pre_deploy: SystemCommand,
/// The extra arguments to pass after `docker compose up -d`.
/// If empty, no extra arguments will be passed.
#[serde(default)]
@@ -410,10 +426,12 @@ impl Default for StackConfig {
file_contents: Default::default(),
auto_pull: default_auto_pull(),
ignore_services: Default::default(),
pre_deploy: Default::default(),
extra_args: Default::default(),
environment: Default::default(),
env_file_path: default_env_file_path(),
run_build: Default::default(),
destroy_before_deploy: Default::default(),
build_extra_args: Default::default(),
skip_secret_interp: Default::default(),
git_provider: default_git_provider(),
@@ -421,6 +439,7 @@ impl Default for StackConfig {
repo: Default::default(),
branch: default_branch(),
commit: Default::default(),
reclone: Default::default(),
git_account: Default::default(),
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),

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))

View File

@@ -438,8 +438,6 @@ export interface BuildConfig {
webhook_secret?: string;
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/** Configuration for the registry to push the built image to. */
image_registry?: ImageRegistryConfig;
/**
* The path of the docker build context relative to the root of the repo.
* Default: "." (the root of the repo).
@@ -447,6 +445,8 @@ export interface BuildConfig {
build_path: string;
/** The path of the dockerfile relative to the build path. */
dockerfile_path: string;
/** Configuration for the registry to push the built image to. */
image_registry?: ImageRegistryConfig;
/** Whether to skip secret interpolation in the build_args. */
skip_secret_interp?: boolean;
/** Whether to use buildx to build (eg `docker buildx build ...`) */
@@ -512,6 +512,8 @@ export interface BuildListItemInfo {
builder_id: string;
/** The git provider domain */
git_provider: string;
/** The image registry domain */
image_registry_domain: string;
/** The repo used as the source of the build */
repo: string;
/** The branch of the repo */
@@ -2321,6 +2323,8 @@ export interface StackConfig {
* Combine with build_extra_args for custom behaviors.
*/
run_build?: boolean;
/** Whether to run `docker compose down` before `compose up`. */
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
skip_secret_interp?: boolean;
/**
@@ -2365,6 +2369,11 @@ export interface StackConfig {
branch: string;
/** Optionally set a specific commit hash. */
commit?: string;
/**
* By default, the Stack will `git pull` the repo after it is first cloned.
* If this option is enabled, the repo folder will be deleted and recloned instead.
*/
reclone?: boolean;
/** Whether incoming webhooks actually trigger action. */
webhook_enabled: boolean;
/**
@@ -2378,6 +2387,8 @@ export interface StackConfig {
registry_provider?: string;
/** Used with `registry_provider` to login to a registry before docker compose up. */
registry_account?: string;
/** The optional command to run before the Stack is deployed. */
pre_deploy?: SystemCommand;
/**
* The extra arguments to pass after `docker compose up -d`.
* If empty, no extra arguments will be passed.
@@ -3788,7 +3799,7 @@ export interface UnpauseStack {
service?: string;
}
/** Starts the target stack. `docker compose stop`. Response: [Update] */
/** Stops the target stack. `docker compose stop`. Response: [Update] */
export interface StopStack {
/** Id or name */
stack: string;
@@ -4103,6 +4114,8 @@ export interface GetDeploymentLog {
* Max: 5000.
*/
tail: U64;
/** Enable `--timestamps` */
timestamps?: boolean;
}
export enum SearchCombinator {
@@ -4130,6 +4143,8 @@ export interface SearchDeploymentLog {
combinator?: SearchCombinator;
/** Invert the results, ie return all lines that DON'T match the terms / combinator. */
invert?: boolean;
/** Enable `--timestamps` */
timestamps?: boolean;
}
/**
@@ -4596,6 +4611,8 @@ export interface GetContainerLog {
* Max: 5000.
*/
tail: U64;
/** Enable `--timestamps` */
timestamps?: boolean;
}
/**
@@ -4620,6 +4637,8 @@ export interface SearchContainerLog {
combinator?: SearchCombinator;
/** Invert the results, ie return all lines that DON'T match the terms / combinator. */
invert?: boolean;
/** Enable `--timestamps` */
timestamps?: boolean;
}
/** Inspect a docker container on the server. Response: [Container]. */
@@ -4810,6 +4829,8 @@ export interface GetStackServiceLog {
* Max: 5000.
*/
tail: U64;
/** Enable `--timestamps` */
timestamps?: boolean;
}
/**
@@ -4834,6 +4855,8 @@ export interface SearchStackServiceLog {
combinator?: SearchCombinator;
/** Invert the results, ie return all lines that DON'T match the terms / combinator. */
invert?: boolean;
/** Enable `--timestamps` */
timestamps?: boolean;
}
/**

View File

@@ -44,9 +44,12 @@ pub struct GetComposeServiceLog {
pub project: String,
/// The service name
pub service: String,
/// pass `--tail` for only recent log contents
/// Pass `--tail` for only recent log contents. Max of 5000
#[serde(default = "default_tail")]
pub tail: u64,
/// Enable `--timestamps`
#[serde(default)]
pub timestamps: bool,
}
fn default_tail() -> u64 {
@@ -72,6 +75,9 @@ pub struct GetComposeServiceLogSearch {
/// Invert the search (search for everything not matching terms)
#[serde(default)]
pub invert: bool,
/// Enable `--timestamps`
#[serde(default)]
pub timestamps: bool,
}
//

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,
}
//

View File

@@ -82,6 +82,9 @@ KOMODO_OIDC_ENABLED=false
# KOMODO_OIDC_CLIENT_SECRET= # Alt: KOMODO_OIDC_CLIENT_SECRET_FILE
## Make usernames the full email.
# KOMODO_OIDC_USE_FULL_EMAIL=true
## Add additional trusted audiences for token claims verification.
## Supports comma separated list, and passing with _FILE (for compose secrets).
# KOMODO_OIDC_ADDITIONAL_AUDIENCES=abc,123 # Alt: KOMODO_OIDC_ADDITIONAL_AUDIENCES_FILE
## Github Oauth
KOMODO_GITHUB_OAUTH_ENABLED=false

View File

@@ -161,10 +161,17 @@ oidc_client_secret = ""
## If true, use the full email for usernames.
## Otherwise, the @address will be stripped,
## making usernames more concise.
## Default: false.
## Env: KOMODO_OIDC_USE_FULL_EMAIL
## Default: false.
oidc_use_full_email = false
## Some providers attach other audiences in addition to the client_id.
## If you have this issue, `Invalid audiences: `...` is not a trusted audience"`,
## you can add the audience `...` to the list here (assuming it should be trusted).
## Env: KOMODO_OIDC_ADDITIONAL_AUDIENCES or KOMODO_OIDC_ADDITIONAL_AUDIENCES_FILE
## Default: empty
oidc_additional_audiences = []
#########
# OAUTH #
#########

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.

View File

@@ -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.

View File

@@ -1,15 +1,18 @@
# Advanced Configuration
### Oauth2
### OIDC / Oauth2
To enable OAuth2 login, you must create a client on the respective OAuth provider,
for example [Github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
or [Google](https://developers.google.com/identity/protocols/oauth2).
Komodo also supports self hosted Oauth2 providers like [Authentik](https://docs.goauthentik.io/docs/providers/oauth2/) or [Gitea](https://docs.gitea.com/development/oauth2-provider).
- Komodo uses the `web application` login flow.
- The redirect uri is:
- `<KOMODO_HOST>/auth/github/callback` for Github.
- `<KOMODO_HOST>/auth/google/callback` for Google.
- `<KOMODO_HOST>/auth/oidc/callback` for OIDC.
### Mount a config file

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",

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
```

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 }
@@ -74,6 +77,7 @@ export const ConfigLayout = <
content={config}
onConfirm={onConfirm}
disabled={disabled}
file_contents_language={file_contents_language}
/>
)}
</div>
@@ -120,6 +124,7 @@ export const Config = <T,>({
components,
selector,
titleOther,
file_contents_language,
}: {
resource_id: string;
resource_type: Types.ResourceTarget["type"];
@@ -134,6 +139,7 @@ export const Config = <T,>({
string, // sidebar key
ConfigComponent<T>[] | false | undefined
>;
file_contents_language?: MonacoLanguage;
}) => {
// let component_keys = keys(components);
// const [_show, setShow] = useLocalStorage(
@@ -164,6 +170,7 @@ export const Config = <T,>({
}}
onReset={() => set({})}
selector={selector}
file_contents_language={file_contents_language}
>
<div className="flex gap-6">
<div className="hidden xl:block relative pr-6 border-r">
@@ -186,7 +193,7 @@ export const Config = <T,>({
key={section + item.label}
>
<Button
variant="outline"
variant="secondary"
className="justify-end w-full"
size="sm"
>

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>
@@ -569,6 +579,8 @@ interface ConfirmUpdateProps<T> {
content: Partial<T>;
onConfirm: () => void;
disabled: boolean;
language?: MonacoLanguage;
file_contents_language?: MonacoLanguage;
}
export function ConfirmUpdate<T>({
@@ -576,6 +588,8 @@ export function ConfirmUpdate<T>({
content,
onConfirm,
disabled,
language,
file_contents_language,
}: ConfirmUpdateProps<T>) {
const [open, set] = useState(false);
useCtrlKeyListener("s", () => {
@@ -608,6 +622,8 @@ export function ConfirmUpdate<T>({
_key={key as any}
val={val as any}
previous={previous}
language={language}
file_contents_language={file_contents_language}
/>
))}
</div>
@@ -629,23 +645,24 @@ function ConfirmUpdateItem<T>({
_key,
val: _val,
previous,
language,
file_contents_language,
}: {
_key: keyof T;
val: T[keyof T];
previous: T;
language?: MonacoLanguage;
file_contents_language?: MonacoLanguage;
}) {
const [show, setShow] = useState(true);
const val =
typeof _val === "string"
? _key === "environment" ||
_key === "build_args" ||
_key === "secret_args"
? _val
.split("\n")
.filter((line) => !line.startsWith("#"))
.map((line) => line.split(" #")[0])
.join("\n")
: _val
? _val
: Array.isArray(_val)
? _val.length > 0 &&
["string", "number", "boolean"].includes(typeof _val[0])
? JSON.stringify(_val)
: JSON.stringify(_val, null, 2)
: JSON.stringify(_val, null, 2);
const prev_val =
typeof previous[_key] === "string"
@@ -653,7 +670,12 @@ function ConfirmUpdateItem<T>({
: _key === "environment" ||
_key === "build_args" ||
_key === "secret_args"
? env_to_text(previous[_key] as any) ?? ""
? env_to_text(previous[_key] as any) ?? "" // For backward compat with 1.14
: Array.isArray(previous[_key])
? previous[_key].length > 0 &&
["string", "number", "boolean"].includes(typeof previous[_key][0])
? JSON.stringify(previous[_key])
: JSON.stringify(previous[_key], null, 2)
: JSON.stringify(previous[_key], null, 2);
const showDiff =
val?.includes("\n") ||
@@ -676,7 +698,16 @@ function ConfirmUpdateItem<T>({
<MonacoDiffEditor
original={prev_val}
modified={val}
language="toml"
language={
language ??
(["environment", "build_args", "secret_args"].includes(
_key as string
)
? "key_value"
: _key === "file_contents"
? file_contents_language
: "json")
}
/>
) : (
<pre style={{ minHeight: 0 }}>
@@ -718,7 +749,7 @@ export const SystemCommand = ({
/>
</div>
<MonacoEditor
value={value?.command}
value={value?.command || " # Add multiple commands on new lines. Supports comments.\n "}
language="shell"
onValueChange={(command) => set({ ...(value || {}), command })}
readOnly={disabled}

View File

@@ -203,13 +203,13 @@ export const NewLayout = ({
entityType,
children,
enabled,
onSuccess,
onConfirm,
onOpenChange,
}: {
entityType: string;
children: ReactNode;
enabled: boolean;
onSuccess: () => Promise<unknown>;
onConfirm: () => Promise<unknown>;
onOpenChange?: (open: boolean) => void;
}) => {
const [open, set] = useState(false);
@@ -237,7 +237,7 @@ export const NewLayout = ({
variant="outline"
onClick={async () => {
setLoading(true);
await onSuccess();
await onConfirm();
setLoading(false);
set(false);
}}

View File

@@ -100,7 +100,7 @@ export const BuildConfig = ({
<ConfigInput
className="text-lg w-[200px]"
label="Version"
description="Version the image tag using server (major.minor.patch)"
description="Version the image with major.minor.patch. It can be interpolated using [[$VERSION]]."
placeholder="0.0.0"
value={version}
onChange={(version) => set({ version: version as any })}

View File

@@ -115,9 +115,17 @@ export const BuildComponents: RequiredResourceComponents = {
New: () => {
const user = useUser().data;
const builders = useRead("ListBuilders", {}).data;
if (!user) return null;
if (!user.admin && !user.create_build_permissions) return null;
return <NewResource type="Build" />;
return (
<NewResource
type="Build"
builder_id={
builders && builders.length === 1 ? builders[0].id : undefined
}
/>
);
},
Table: ({ resources }) => (

View File

@@ -74,7 +74,7 @@ export const BuilderComponents: RequiredResourceComponents = {
return (
<NewLayout
entityType="Builder"
onSuccess={async () => {
onConfirm={async () => {
if (!type) return;
const id = (await mutateAsync({ name, config: { type, params: {} } }))
._id?.$oid!;

View File

@@ -119,7 +119,11 @@ export const ResourceSelector = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" className="flex justify-start gap-2 w-fit max-w-[200px]" disabled={disabled}>
<Button
variant="secondary"
className="flex justify-start gap-2 w-fit max-w-[200px]"
disabled={disabled}
>
{name || `Select ${type}`}
{!disabled && <ChevronsUpDown className="w-3 h-3" />}
</Button>
@@ -258,16 +262,19 @@ export const NewResource = ({
type,
readable_type,
server_id,
builder_id,
build_id,
name: _name = "",
}: {
type: UsableResource;
readable_type?: string;
server_id?: string;
builder_id?: string;
build_id?: string;
name?: string;
}) => {
const nav = useNavigate();
const { toast } = useToast();
const { mutateAsync } = useWrite(`Create${type}`);
const [name, setName] = useState(_name);
const type_display =
@@ -276,7 +283,7 @@ export const NewResource = ({
: type === "ResourceSync"
? "resource-sync"
: type.toLowerCase();
const config: Types._PartialDeploymentConfig =
const config: Types._PartialDeploymentConfig | Types._PartialRepoConfig =
type === "Deployment"
? {
server_id,
@@ -287,15 +294,19 @@ export const NewResource = ({
: type === "Stack"
? { server_id }
: type === "Repo"
? { server_id }
? { server_id, builder_id }
: type === "Build"
? { builder_id }
: {};
const onConfirm = async () => {
if (!name) toast({ title: "Name cannot be empty" });
const id = (await mutateAsync({ name, config }))._id?.$oid!;
nav(`/${usableResourcePath(type)}/${id}`);
};
return (
<NewLayout
entityType={readable_type ?? type}
onSuccess={async () => {
const id = (await mutateAsync({ name, config }))._id?.$oid!;
nav(`/${usableResourcePath(type)}/${id}`);
}}
onConfirm={onConfirm}
enabled={!!name}
onOpenChange={() => setName("")}
>
@@ -305,6 +316,12 @@ export const NewResource = ({
placeholder={`${type_display}-name`}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (!name) return;
if (e.key === "Enter") {
onConfirm();
}
}}
/>
</div>
</NewLayout>

View File

@@ -2,6 +2,7 @@ import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { ReactNode, useState } from "react";
import {
AccountSelectorConfig,
AddExtraArgMenu,
ConfigItem,
ConfigList,
@@ -19,6 +20,7 @@ import {
DefaultTerminationSignal,
TerminationTimeout,
} from "./components/term-signal";
import { extract_registry_domain } from "@lib/utils";
export const DeploymentConfig = ({
id,
@@ -31,6 +33,7 @@ export const DeploymentConfig = ({
target: { type: "Deployment", id },
}).data;
const config = useRead("GetDeployment", { deployment: id }).data?.config;
const builds = useRead("ListBuilds", {}).data;
const global_disabled =
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
const [update, set] = useState<Partial<Types.DeploymentConfig>>({});
@@ -100,6 +103,37 @@ export const DeploymentConfig = ({
image: (value, set) => (
<ImageConfig image={value} set={set} disabled={disabled} />
),
image_registry_account: (account, set) => {
const image = update.image ?? config.image;
const provider =
image?.type === "Image" && image.params.image
? extract_registry_domain(image.params.image)
: image?.type === "Build" && image.params.build_id
? builds?.find((b) => b.id === image.params.build_id)?.info
.image_registry_domain
: undefined;
return (
<AccountSelectorConfig
id={update.server_id ?? config.server_id ?? undefined}
type="Server"
account_type="docker"
provider={provider ?? "docker.io"}
selected={account}
onSelect={(image_registry_account) =>
set({ image_registry_account })
}
disabled={disabled}
placeholder={
image?.type === "Build" ? "Same as Build" : undefined
}
description={
image?.type === "Build"
? "Select an alternate account used to log in to the provider"
: undefined
}
/>
);
},
redeploy_on_build: (update.image?.type ?? config.image?.type) ===
"Build" && {
description: "Automatically redeploy when the image is built.",
@@ -182,6 +216,53 @@ export const DeploymentConfig = ({
),
},
},
{
label: "Restart",
labelHidden: true,
components: {
restart: (value, set) => (
<RestartModeSelector
selected={value}
set={set}
disabled={disabled}
/>
),
},
},
],
advanced: [
{
label: "Command",
labelHidden: true,
components: {
command: (value, set) => (
<ConfigItem
label="Command"
description={
<div className="flex flex-row flex-wrap gap-2">
<div>Replace the CMD, or extend the ENTRYPOINT.</div>
<Link
to="https://docs.docker.com/engine/reference/run/#commands-and-arguments"
target="_blank"
className="text-primary"
>
See docker docs.
{/* <Button variant="link" className="p-0">
</Button> */}
</Link>
</div>
}
>
<MonacoEditor
value={value}
language="shell"
onValueChange={(command) => set({ command })}
readOnly={disabled}
/>
</ConfigItem>
),
},
},
{
label: "Labels",
description: "Attach --labels to the container.",
@@ -197,19 +278,6 @@ export const DeploymentConfig = ({
),
},
},
{
label: "Restart",
labelHidden: true,
components: {
restart: (value, set) => (
<RestartModeSelector
selected={value}
set={set}
disabled={disabled}
/>
),
},
},
{
label: "Extra Args",
labelHidden: true,
@@ -255,38 +323,6 @@ export const DeploymentConfig = ({
),
},
},
{
label: "Command",
labelHidden: true,
components: {
command: (value, set) => (
<ConfigItem
label="Command"
description={
<div className="flex flex-row flex-wrap gap-2">
<div>Replace the CMD, or extend the ENTRYPOINT.</div>
<Link
to="https://docs.docker.com/engine/reference/run/#commands-and-arguments"
target="_blank"
className="text-primary"
>
See docker docs.
{/* <Button variant="link" className="p-0">
</Button> */}
</Link>
</div>
}
>
<MonacoEditor
value={value}
language="shell"
onValueChange={(command) => set({ command })}
readOnly={disabled}
/>
</ConfigItem>
),
},
},
{
label: "Termination",
boldLabel: false,

View File

@@ -1,7 +1,7 @@
import { useLocalStorage, useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { RequiredResourceComponents } from "@types";
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
import { HardDrive, Rocket, Server } from "lucide-react";
import { cn } from "@lib/utils";
import { useServer } from "../server";
import {
@@ -22,7 +22,6 @@ import { DeleteResource, NewResource, ResourceLink } from "../common";
import { RunBuild } from "../build/actions";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { DeploymentConfig } from "./config";
import { Link } from "react-router-dom";
import { DashboardPieChart } from "@pages/home/dashboard";
import { ResourcePageHeader, StatusBadge } from "@components/util";
@@ -145,9 +144,21 @@ export const DeploymentComponents: RequiredResourceComponents = {
);
},
New: ({ server_id, build_id }) => (
<NewResource type="Deployment" server_id={server_id} build_id={build_id} />
),
New: ({ server_id: _server_id, build_id }) => {
const servers = useRead("ListServers", {}).data;
const server_id = _server_id
? _server_id
: servers && servers.length === 1
? servers[0].id
: undefined;
return (
<NewResource
type="Deployment"
server_id={server_id}
build_id={build_id}
/>
);
},
Table: ({ resources }) => {
return (
@@ -192,17 +203,6 @@ export const DeploymentComponents: RequiredResourceComponents = {
</div>
);
},
Alerts: ({ id }) => {
return (
<Link
to={`/deployments/${id}/alerts`}
className="flex gap-2 items-center"
>
<AlertTriangle className="w-4 h-4" />
Alerts
</Link>
);
},
},
Actions: {

View File

@@ -43,6 +43,10 @@ const DeploymentLogsInner = ({
const [invert, setInvert] = useState(false);
const [search, setSearch] = useState("");
const [poll, setPoll] = useLocalStorage("log-poll-v1", false);
const [timestamps, setTimestamps] = useLocalStorage(
"log-timestamps-v1",
false
);
const addTerm = () => {
if (!search.length) return;
@@ -61,8 +65,8 @@ const DeploymentLogsInner = ({
};
const { Log, refetch, stderr } = terms.length
? SearchLogs(id, terms, invert)
: NoSearchLogs(id, tail, stream);
? SearchLogs(id, terms, invert, timestamps)
: NoSearchLogs(id, tail, timestamps, stream);
useEffect(() => {
const interval = setInterval(() => {
@@ -77,9 +81,8 @@ const DeploymentLogsInner = ({
actions={
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<div className="text-muted-foreground flex gap-1">
<div>Invert</div>
<div className="hidden xl:block">Search</div>
<div className="text-muted-foreground flex gap-1 text-sm">
Invert
</div>
<Switch checked={invert} onCheckedChange={setInvert} />
</div>
@@ -126,9 +129,19 @@ const DeploymentLogsInner = ({
<Button variant="secondary" size="icon" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4" />
</Button>
<div className="flex items-center gap-2">
<div className="text-muted-foreground">Poll</div>
<Switch checked={poll} onCheckedChange={setPoll} />
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setTimestamps((t) => !t)}
>
<div className="text-muted-foreground text-sm">Timestamps</div>
<Switch checked={timestamps} />
</div>
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setPoll((p) => !p)}
>
<div className="text-muted-foreground text-sm">Poll</div>
<Switch checked={poll} />
</div>
<TailLengthSelector
selected={tail}
@@ -143,12 +156,17 @@ const DeploymentLogsInner = ({
);
};
const NoSearchLogs = (id: string, tail: string, stream: string) => {
const { data: log, refetch } = useRead(
"GetDeploymentLog",
{ deployment: id, tail: Number(tail) },
{ refetchInterval: 30000 }
);
const NoSearchLogs = (
id: string,
tail: string,
timestamps: boolean,
stream: string
) => {
const { data: log, refetch } = useRead("GetDeploymentLog", {
deployment: id,
tail: Number(tail),
timestamps,
});
return {
Log: (
<div className="relative">
@@ -160,12 +178,18 @@ const NoSearchLogs = (id: string, tail: string, stream: string) => {
};
};
const SearchLogs = (id: string, terms: string[], invert: boolean) => {
const SearchLogs = (
id: string,
terms: string[],
invert: boolean,
timestamps: boolean
) => {
const { data: log, refetch } = useRead("SearchDeploymentLog", {
deployment: id,
terms,
combinator: Types.SearchCombinator.And,
invert,
timestamps,
});
return {
Log: (

View File

@@ -57,6 +57,7 @@ export const ResourceSyncConfig = ({
onSave={async () => {
await mutateAsync({ id, config: update });
}}
file_contents_language="toml"
components={{
"": [
{

View File

@@ -58,7 +58,7 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
return (
<NewLayout
entityType="Server Template"
onSuccess={async () => {
onConfirm={async () => {
if (!type) return;
const id = (await mutateAsync({ name, config: { type, params: {} } }))
._id?.$oid!;

View File

@@ -6,6 +6,7 @@ import {
ConfigList,
InputList,
ProviderSelectorConfig,
SystemCommand,
} from "@components/config/util";
import { Types } from "@komodo/client";
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
@@ -233,10 +234,24 @@ export const StackConfig = ({
placeholder: "Compose project name",
boldLabel: true,
description:
"Optionally set a different compose project name. It should match the compose project name on your host.",
"Optionally set a different compose project name. If importing existing stack, this should match the compose project name on your host.",
},
},
},
{
label: "Pre Deploy",
description:
"Execute a shell command before running docker compose up. The 'path' is relative to the Run Directory",
components: {
pre_deploy: (value, set) => (
<SystemCommand
value={value}
set={(value) => set({ pre_deploy: value })}
disabled={disabled}
/>
),
},
},
{
label: "Extra Args",
labelHidden: true,
@@ -374,6 +389,17 @@ export const StackConfig = ({
),
},
},
{
label: "Destroy",
labelHidden: true,
components: {
run_build: {
label: "Destroy Before Deploy",
description:
"Ensure 'docker compose down' is run before redeploying the Stack.",
},
},
},
];
if (mode === undefined) {
@@ -467,6 +493,10 @@ export const StackConfig = ({
description:
"Switch to a specific hash after cloning the branch.",
},
reclone: {
description:
"Delete the repo folder and clone it again, instead of using 'git pull'.",
},
},
},
{
@@ -693,6 +723,7 @@ export const StackConfig = ({
}
onValueChange={(file_contents) => set({ file_contents })}
language="yaml"
readOnly={disabled}
/>
);
},
@@ -717,6 +748,7 @@ export const StackConfig = ({
await mutateAsync({ id, config: update });
}}
components={components}
file_contents_language="yaml"
/>
);
};

View File

@@ -151,7 +151,15 @@ export const StackComponents: RequiredResourceComponents = {
);
},
New: ({ server_id }) => <NewResource type="Stack" server_id={server_id} />,
New: ({ server_id: _server_id }) => {
const servers = useRead("ListServers", {}).data;
const server_id = _server_id
? _server_id
: servers && servers.length === 1
? servers[0].id
: undefined;
return <NewResource type="Stack" server_id={server_id} />;
},
Table: ({ resources }) => (
<StackTable stacks={resources as Types.StackListItem[]} />

View File

@@ -80,7 +80,7 @@ export const StackInfo = ({
</CardHeader>
</Card>
)}
{/* Update deployed contents with diff */}
{/* {!is_down && deployed_contents.length > 0 && (
<Card>
@@ -189,6 +189,7 @@ export const StackInfo = ({
}
}}
disabled={!edits[content.path]}
language="yaml"
/>
</div>
)}

View File

@@ -17,7 +17,7 @@ export const NewUserGroup = () => {
return (
<NewLayout
entityType="User Group"
onSuccess={() => mutateAsync({ name })}
onConfirm={() => mutateAsync({ name })}
enabled={!!name}
onOpenChange={() => setName("")}
>
@@ -46,7 +46,7 @@ export const NewServiceUser = () => {
return (
<NewLayout
entityType="Service User"
onSuccess={() => mutateAsync({ username, description: "" })}
onConfirm={() => mutateAsync({ username, description: "" })}
enabled={!!username}
onOpenChange={() => setUsername("")}
>

View File

@@ -244,3 +244,13 @@ export const is_service_user = (user_id: string) => {
user_id === "Repo Manager"
);
};
export const extract_registry_domain = (image_name: string) => {
if (!image_name) return "docker.io";
const maybe_domain = image_name.split("/")[0];
if (maybe_domain.includes(".")) {
return maybe_domain
} else {
return "docker.io"
}
}

View File

@@ -27,9 +27,7 @@ export const Dashboard = () => {
return (
<>
<div className="mb-12">
<ActiveResources />
</div>
<ActiveResources />
<Page
title="Dashboard"
icon={<Box className="w-8 h-8" />}
@@ -253,37 +251,39 @@ const ActiveResources = () => {
if (resources.length === 0) return null;
return (
<Section
title="Active"
icon={
<Circle className="w-4 h-4 stroke-none transition-colors fill-green-500" />
}
>
<DataTable
tableKey="active-resources"
data={resources}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<ResourceLink type={row.original.type} id={row.original.id} />
),
},
{
accessorKey: "type",
header: ({ column }) => (
<SortableHeader column={column} title="Resource" />
),
},
{
header: "State",
cell: ({ row }) => row.original.state,
},
]}
/>
</Section>
<div className="mb-12">
<Section
title="Active"
icon={
<Circle className="w-4 h-4 stroke-none transition-colors fill-green-500" />
}
>
<DataTable
tableKey="active-resources"
data={resources}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<ResourceLink type={row.original.type} id={row.original.id} />
),
},
{
accessorKey: "type",
header: ({ column }) => (
<SortableHeader column={column} title="Resource" />
),
},
{
header: "State",
cell: ({ row }) => row.original.state,
},
]}
/>
</Section>
</div>
);
};

View File

@@ -25,6 +25,10 @@ export const ContainerLogs = ({
const [invert, setInvert] = useState(false);
const [search, setSearch] = useState("");
const [poll, setPoll] = useLocalStorage("log-poll-v1", false);
const [timestamps, setTimestamps] = useLocalStorage(
"log-timestamps-v1",
false
);
const addTerm = () => {
if (!search.length) return;
@@ -43,8 +47,8 @@ export const ContainerLogs = ({
};
const { Log, refetch, stderr } = terms.length
? SearchLogs(id, container_name, terms, invert)
: NoSearchLogs(id, container_name, tail, stream);
? SearchLogs(id, container_name, terms, invert, timestamps)
: NoSearchLogs(id, container_name, tail, timestamps, stream);
useEffect(() => {
const interval = setInterval(() => {
@@ -61,10 +65,7 @@ export const ContainerLogs = ({
actions={
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<div className="text-muted-foreground flex gap-1">
<div>Invert</div>
<div className="hidden xl:block">Search</div>
</div>
<div className="text-muted-foreground flex gap-1">Invert</div>
<Switch checked={invert} onCheckedChange={setInvert} />
</div>
{terms.map((term, index) => (
@@ -110,9 +111,19 @@ export const ContainerLogs = ({
<Button variant="secondary" size="icon" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4" />
</Button>
<div className="flex items-center gap-2">
<div className="text-muted-foreground">Poll</div>
<Switch checked={poll} onCheckedChange={setPoll} />
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setTimestamps((t) => !t)}
>
<div className="text-muted-foreground text-sm">Timestamps</div>
<Switch checked={timestamps} />
</div>
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setPoll((p) => !p)}
>
<div className="text-muted-foreground text-sm">Poll</div>
<Switch checked={poll} />
</div>
<TailLengthSelector
selected={tail}
@@ -131,13 +142,15 @@ const NoSearchLogs = (
id: string,
container: string,
tail: string,
timestamps: boolean,
stream: string
) => {
const { data: log, refetch } = useRead(
"GetContainerLog",
{ server: id, container, tail: Number(tail) },
{ refetchInterval: 30000 }
);
const { data: log, refetch } = useRead("GetContainerLog", {
server: id,
container,
tail: Number(tail),
timestamps,
});
return {
Log: (
<div className="relative">
@@ -153,7 +166,8 @@ const SearchLogs = (
id: string,
container: string,
terms: string[],
invert: boolean
invert: boolean,
timestamps: boolean
) => {
const { data: log, refetch } = useRead("SearchContainerLog", {
server: id,
@@ -161,6 +175,7 @@ const SearchLogs = (
terms,
combinator: Types.SearchCombinator.And,
invert,
timestamps,
});
return {
Log: (

View File

@@ -45,6 +45,10 @@ const StackLogsInner = ({
const [invert, setInvert] = useState(false);
const [search, setSearch] = useState("");
const [poll, setPoll] = useLocalStorage("log-poll-v1", false);
const [timestamps, setTimestamps] = useLocalStorage(
"log-timestamps-v1",
false
);
const addTerm = () => {
if (!search.length) return;
@@ -63,8 +67,8 @@ const StackLogsInner = ({
};
const { Log, refetch, stderr } = terms.length
? SearchLogs(id, service, terms, invert)
: NoSearchLogs(id, service, tail, stream);
? SearchLogs(id, service, terms, invert, timestamps)
: NoSearchLogs(id, service, tail, timestamps, stream);
useEffect(() => {
const interval = setInterval(() => {
@@ -81,9 +85,8 @@ const StackLogsInner = ({
actions={
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<div className="text-muted-foreground flex gap-1">
<div>Invert</div>
<div className="hidden xl:block">Search</div>
<div className="text-muted-foreground flex gap-1 text-sm">
Invert
</div>
<Switch checked={invert} onCheckedChange={setInvert} />
</div>
@@ -130,9 +133,19 @@ const StackLogsInner = ({
<Button variant="secondary" size="icon" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4" />
</Button>
<div className="flex items-center gap-2">
<div className="text-muted-foreground">Poll</div>
<Switch checked={poll} onCheckedChange={setPoll} />
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setTimestamps((t) => !t)}
>
<div className="text-muted-foreground text-sm">Timestamps</div>
<Switch checked={timestamps} />
</div>
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setPoll((p) => !p)}
>
<div className="text-muted-foreground text-sm">Poll</div>
<Switch checked={poll} />
</div>
<TailLengthSelector
selected={tail}
@@ -151,13 +164,15 @@ const NoSearchLogs = (
id: string,
service: string,
tail: string,
timestamps: boolean,
stream: string
) => {
const { data: log, refetch } = useRead(
"GetStackServiceLog",
{ stack: id, service, tail: Number(tail) },
{ refetchInterval: 30000 }
);
const { data: log, refetch } = useRead("GetStackServiceLog", {
stack: id,
service,
tail: Number(tail),
timestamps,
});
return {
Log: (
<div className="relative">
@@ -173,7 +188,8 @@ const SearchLogs = (
id: string,
service: string,
terms: string[],
invert: boolean
invert: boolean,
timestamps: boolean
) => {
const { data: log, refetch } = useRead("SearchStackServiceLog", {
stack: id,
@@ -181,6 +197,7 @@ const SearchLogs = (
terms,
combinator: Types.SearchCombinator.And,
invert,
timestamps,
});
return {
Log: (

View File

@@ -1,12 +1,57 @@
use std::path::Path;
use komodo_client::entities::{komodo_timestamp, update::Log};
use run_command::{async_run_command, CommandOutput};
pub async fn run_komodo_command(stage: &str, command: String) -> Log {
/// Parses commands out of multiline string
/// and chains them together with '&&'
///
/// Supports full line and end of line comments. See [parse_multiline_command].
pub async fn run_komodo_command(
stage: &str,
path: impl Into<Option<&Path>>,
command: impl AsRef<str>,
) -> Log {
let command = parse_multiline_command(command);
let command = if let Some(path) = path.into() {
format!("cd {} && {command}", path.display(),)
} else {
command
};
let start_ts = komodo_timestamp();
let output = async_run_command(&command).await;
output_into_log(stage, command, start_ts, output)
}
/// Parses commands out of multiline string
/// and chains them together with '&&'
///
/// Supports full line and end of line comments.
///
/// ## Example:
/// ```sh
/// # comments supported
/// sh ./shell1.sh # end of line supported
/// sh ./shell2.sh
/// # print done
/// echo done
/// ```
/// becomes
/// ```sh
/// sh ./shell1.sh && sh ./shell2.sh && echo done
/// ```
pub fn parse_multiline_command(command: impl AsRef<str>) -> String {
command
.as_ref()
.split('\n')
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split(" #").next())
.map(str::trim)
.collect::<Vec<_>>()
.join(" && ")
}
pub fn output_into_log(
stage: &str,
command: String,

View File

@@ -39,8 +39,7 @@ where
{
let args: CloneArgs = clone_args.into();
let repo_dir = args.path(repo_dir);
let repo_url =
args.remote_url(access_token.as_ref().map(String::as_str))?;
let repo_url = args.remote_url(access_token.as_deref())?;
let mut logs = clone_inner(
&repo_url,
@@ -100,7 +99,7 @@ where
};
if let Some(command) = args.on_clone {
if !command.path.is_empty() && !command.command.is_empty() {
if !command.command.is_empty() {
let on_clone_path = repo_dir.join(&command.path);
if let Some(secrets) = secrets {
let (full_command, mut replacers) =
@@ -116,7 +115,8 @@ where
replacers.extend(core_replacers.to_owned());
let mut on_clone_log = run_komodo_command(
"on clone",
format!("cd {} && {full_command}", on_clone_path.display()),
on_clone_path.as_ref(),
full_command,
)
.await;
@@ -137,11 +137,8 @@ where
} else {
let on_clone_log = run_komodo_command(
"on clone",
format!(
"cd {} && {}",
on_clone_path.display(),
command.command
),
on_clone_path.as_ref(),
&command.command,
)
.await;
tracing::debug!(
@@ -154,7 +151,7 @@ where
}
}
if let Some(command) = args.on_pull {
if !command.path.is_empty() && !command.command.is_empty() {
if !command.command.is_empty() {
let on_pull_path = repo_dir.join(&command.path);
if let Some(secrets) = secrets {
let (full_command, mut replacers) =
@@ -170,7 +167,8 @@ where
replacers.extend(core_replacers.to_owned());
let mut on_pull_log = run_komodo_command(
"on pull",
format!("cd {} && {full_command}", on_pull_path.display()),
on_pull_path.as_ref(),
&full_command,
)
.await;
@@ -191,11 +189,8 @@ where
} else {
let on_pull_log = run_komodo_command(
"on pull",
format!(
"cd {} && {}",
on_pull_path.display(),
command.command
),
on_pull_path.as_ref(),
&command.command,
)
.await;
tracing::debug!(
@@ -256,10 +251,8 @@ async fn clone_inner(
if let Some(commit) = commit {
let reset_log = run_komodo_command(
"set commit",
format!(
"cd {} && git reset --hard {commit}",
destination.display()
),
destination,
format!("git reset --hard {commit}",),
)
.await;
logs.push(reset_log);

View File

@@ -38,16 +38,13 @@ where
{
let args: CloneArgs = clone_args.into();
let path = args.path(repo_dir);
let path_display = path.display();
let repo_url =
args.remote_url(access_token.as_ref().map(String::as_str))?;
let repo_url = args.remote_url(access_token.as_deref())?;
// Set remote url
let mut set_remote = run_komodo_command(
"set git remote",
format!(
"cd {path_display} && git remote set-url origin {repo_url}"
),
path.as_ref(),
format!("git remote set-url origin {repo_url}"),
)
.await;
@@ -70,7 +67,8 @@ where
let checkout = run_komodo_command(
"checkout branch",
format!("cd {path_display} && git checkout -f {}", args.branch),
path.as_ref(),
format!("git checkout -f {}", args.branch),
)
.await;
@@ -83,12 +81,12 @@ where
});
}
let command = format!(
"cd {path_display} && git pull --rebase --force origin {}",
args.branch
);
let pull_log = run_komodo_command("git pull", command).await;
let pull_log = run_komodo_command(
"git pull",
path.as_ref(),
format!("git pull --rebase --force origin {}", args.branch),
)
.await;
let mut logs = vec![pull_log];
@@ -104,7 +102,8 @@ where
if let Some(commit) = args.commit {
let reset_log = run_komodo_command(
"set commit",
format!("cd {path_display} && git reset --hard {commit}"),
path.as_ref(),
format!("git reset --hard {commit}"),
)
.await;
logs.push(reset_log);
@@ -144,7 +143,7 @@ where
};
if let Some(command) = args.on_pull {
if !command.path.is_empty() && !command.command.is_empty() {
if !command.command.is_empty() {
let on_pull_path = path.join(&command.path);
if let Some(secrets) = secrets {
let (full_command, mut replacers) =
@@ -174,7 +173,8 @@ where
replacers.extend(core_replacers.to_owned());
let mut on_pull_log = run_komodo_command(
"on pull",
format!("cd {} && {full_command}", on_pull_path.display()),
on_pull_path.as_ref(),
&full_command,
)
.await;
@@ -195,11 +195,8 @@ where
} else {
let on_pull_log = run_komodo_command(
"on pull",
format!(
"cd {} && {}",
on_pull_path.display(),
command.command
),
on_pull_path.as_ref(),
&command.command,
)
.await;
tracing::debug!(

View File

@@ -6,6 +6,8 @@ A tool to build and deploy software across many servers.
🦎 [Try the Demo](https://demo.komo.do)
🦎 [See the Build Server](https://build.komo.do)
🦎 [Join the Discord](https://discord.gg/DRqE8Fvg5c)
## About
@@ -31,25 +33,21 @@ there are no warranties. Use at your own risk.
### Light Theme
![Dashboard](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Dashboard.png)
![Resources](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Resources.png)
![Deployment](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Deployment.png)
![Server](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Server.png)
![Stack](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Stack.png)
![Compose](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Compose.png)
![Env](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Env.png)
![Sync](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Sync.png)
![Procedure](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Procedure.png)
![UserGroup](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-UserGroup.png)
![Update](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Update.png)
![Search](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Search.png)
![Stats](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Stats.png)
![Export](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Light-Export.png)
### Dark Theme
![Dashboard](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Dashboard.png)
![Resources](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Resources.png)
![Deployment](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Deployment.png)
![Server](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Server.png)
![Stack](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Stack.png)
![Compose](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Compose.png)
![Env](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Env.png)
![Sync](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Sync.png)
![Procedure](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Procedure.png)
![UserGroup](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-UserGroup.png)
![Update](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Update.png)
![Search](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Search.png)
![Stats](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Stats.png)
![Export](https://raw.githubusercontent.com/mbecker20/komodo/main/screenshots/Dark-Export.png)

View File

@@ -30,6 +30,10 @@ docker compose -p komodo-dev -f test.compose.yaml build"""
description = "runs core --release pointing to test.core.config.toml"
cmd = "KOMODO_CONFIG_PATH=test.core.config.toml cargo run -p komodo_core --release"
[test-periphery]
description = "runs periphery --release pointing to test.periphery.config.toml"
cmd = "PERIPHERY_CONFIG_PATH=test.periphery.config.toml cargo run -p komodo_periphery --release"
[docsite-start]
path = "docsite"
cmd = "yarn start"
@@ -38,5 +42,5 @@ cmd = "yarn start"
path = "docsite"
cmd = "yarn deploy"
[rustdoc-server]
cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc"
# [rustdoc-server]
# cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc"

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

BIN
screenshots/Dark-Env.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

BIN
screenshots/Dark-Stack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

BIN
screenshots/Dark-Stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

BIN
screenshots/Light-Env.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 495 KiB

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