Compare commits

...

8 Commits

Author SHA1 Message Date
Maxwell Becker
f5a59b0333 1.16.7 (#167)
* 1.16.7

* increase builder max poll to allow User Data more time to setup periphery

* rework to KOMODO_OIDC_REDIRECT_HOST
2024-10-31 21:06:01 -07:00
mbecker20
cacea235f9 replace networks empty with network_mode, replace container: network mode 2024-10-30 02:58:27 -04:00
mbecker20
54ba31dca9 gen ts types 2024-10-30 02:18:57 -04:00
Maxwell Becker
17d7ecb419 1.16.6 (#163)
* remove instrument from validate_cancel_build

* use type safe AllResources map - Action not showing omnisearch

* Stack support replicated services

* server docker nested tables

* fix container networks which use network of another container

* bump version

* add 'address' to ServerListItemInfo

* secrets list on variables page wraps

* fix user data script

* update default template user data

* improve sidebar layout styling

* fix network names shown on containers

* improve stack service / container page

* deleted resource log records Toml backup for later reference

* align all the tables

* add Url Builder type
2024-10-29 23:17:10 -07:00
mbecker20
38f3448790 add Procedures and Actions page 2024-10-28 00:57:42 -04:00
Maxwell Becker
ec88a6fa5a 1.16.5 (#159)
* repo pull lock

* implement BatchRunAction

* other batch methods
2024-10-27 20:56:56 -07:00
mbecker20
3820cd0ca2 make Build Organization configurable with custom value 2024-10-27 14:42:06 -04:00
mbecker20
419aa87bbb update resource toml examples to latest standard 2024-10-26 16:22:12 -04:00
82 changed files with 4285 additions and 1555 deletions

86
Cargo.lock generated
View File

@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"axum",
@@ -120,9 +120,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.90"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
[[package]]
name = "arc-swap"
@@ -201,9 +201,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "aws-config"
version = "1.5.8"
version = "1.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6"
checksum = "2d6448cfb224dd6a9b9ac734f58622dd0d4751f3589f3b777345745f46b2eb14"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -268,9 +268,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ec2"
version = "1.79.0"
version = "1.83.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95559916ae9d6ef69b104546098a4b4c57082db5d11571917916e9a69234c6ea"
checksum = "59ef9cdd731373735434b79a33fd1049525d78674c49b1e278d56544e388fe01"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -292,9 +292,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.46.0"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc2faec3205d496c7e57eff685dd944203df7ce16a4116d0281c44021788a7b"
checksum = "a8776850becacbd3a82a4737a9375ddb5c6832a51379f24443a98e61513f852c"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -314,9 +314,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.47.0"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c93c241f52bc5e0476e259c953234dab7e2a35ee207ee202e86c0095ec4951dc"
checksum = "0007b5b8004547133319b6c4e87193eee2a0bcb3e4c18c75d09febe9dab7b383"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -336,9 +336,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.46.0"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b259429be94a3459fa1b00c5684faee118d74f9577cc50aebadc36e507c63b5f"
checksum = "9fffaa356e7f1c725908b75136d53207fa714e348f365671df14e95a60530ad3"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -432,9 +432,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.7.2"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a065c0fe6fdbdf9f11817eb68582b2ab4aff9e9c39e986ae48f7ec576c6322db"
checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -476,9 +476,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.2.7"
version = "1.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147100a7bea70fa20ef224a6bad700358305f5dc0f84649c53769761395b355b"
checksum = "07c9cdc179e6afbf5d391ab08c85eac817b51c87e1892a5edb5f7bbdc64314b4"
dependencies = [
"base64-simd",
"bytes",
@@ -943,7 +943,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"komodo_client",
"run_command",
@@ -1355,7 +1355,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"thiserror",
]
@@ -1439,7 +1439,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"serror",
]
@@ -1571,7 +1571,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"command",
@@ -2191,7 +2191,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"clap",
@@ -2207,7 +2207,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2238,7 +2238,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2292,11 +2292,12 @@ dependencies = [
"typeshare",
"urlencoding",
"uuid",
"wildcard",
]
[[package]]
name = "komodo_periphery"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2383,7 +2384,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"komodo_client",
@@ -3089,7 +3090,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"komodo_client",
@@ -3307,9 +3308,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@@ -3993,9 +3994,9 @@ dependencies = [
[[package]]
name = "serror"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f432d878d404110352cfbaa031d8a6878a166cb7f50e00ab87d0508f8f68a0"
checksum = "715a997753611604c722411afbe11f83a89e00e39323dc9016db96a86cc04fc8"
dependencies = [
"anyhow",
"axum",
@@ -4327,18 +4328,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
@@ -4791,9 +4792,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "typeshare"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04f17399b76c2e743d58eac0635d7686e9c00f48cd4776f00695d9882a7d3187"
checksum = "19be0f411120091e76e13e5a0186d8e2bcc3e7e244afdb70152197f1a8486ceb"
dependencies = [
"chrono",
"serde",
@@ -4864,7 +4865,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.16.4"
version = "1.16.7"
dependencies = [
"anyhow",
"komodo_client",
@@ -5073,6 +5074,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
[[package]]
name = "wildcard"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36241ad0795516b55e3b60e55c7f979d4f324e4aaea4c70d56b548b9164ee4d2"
dependencies = [
"thiserror",
]
[[package]]
name = "winapi"
version = "0.3.9"

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "1.16.4"
version = "1.16.7"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -32,7 +32,7 @@ git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.4.6", default-features = false }
serror = { version = "0.4.7", default-features = false }
slack = { version = "0.2.0", package = "slack_client_rs" }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
@@ -69,8 +69,8 @@ serde_yaml = "0.9.34"
toml = "0.8.19"
# ERROR
anyhow = "1.0.90"
thiserror = "1.0.64"
anyhow = "1.0.91"
thiserror = "1.0.65"
# LOGGING
opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] }
@@ -104,14 +104,15 @@ bollard = "0.17.1"
sysinfo = "0.32.0"
# CLOUD
aws-config = "1.5.8"
aws-sdk-ec2 = "1.77.0"
aws-config = "1.5.9"
aws-sdk-ec2 = "1.83.0"
# MISC
derive_builder = "0.20.2"
typeshare = "1.0.3"
typeshare = "1.0.4"
octorust = "0.7.0"
dashmap = "6.1.0"
wildcard = "0.2.0"
colored = "2.1.0"
regex = "1.11.0"
regex = "1.11.1"
bson = "2.13.0"

View File

@@ -1,13 +1,21 @@
use std::time::Duration;
use colored::Colorize;
use komodo_client::api::execute::Execution;
use komodo_client::{
api::execute::{BatchExecutionResponse, Execution},
entities::update::Update,
};
use crate::{
helpers::wait_for_enter,
state::{cli_args, komodo_client},
};
pub enum ExecutionResult {
Single(Update),
Batch(BatchExecutionResponse),
}
pub async fn run(execution: Execution) -> anyhow::Result<()> {
if matches!(execution, Execution::None(_)) {
println!("Got 'none' execution. Doing nothing...");
@@ -24,18 +32,30 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchRunBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Deploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeploy(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -54,15 +74,27 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyDeployment(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchCloneRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::PullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchPullRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchBuildRepo(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CancelRepoBuild(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -138,9 +170,15 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDeployStackIfChanged(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::StartStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -159,6 +197,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::DestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::BatchDestroyStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::Sleep(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -171,144 +212,234 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
info!("Running Execution...");
let res = match execution {
Execution::RunAction(request) => {
komodo_client().execute(request).await
}
Execution::RunProcedure(request) => {
komodo_client().execute(request).await
}
Execution::RunBuild(request) => {
komodo_client().execute(request).await
}
Execution::CancelBuild(request) => {
komodo_client().execute(request).await
}
Execution::Deploy(request) => {
komodo_client().execute(request).await
}
Execution::StartDeployment(request) => {
komodo_client().execute(request).await
}
Execution::RestartDeployment(request) => {
komodo_client().execute(request).await
}
Execution::PauseDeployment(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseDeployment(request) => {
komodo_client().execute(request).await
}
Execution::StopDeployment(request) => {
komodo_client().execute(request).await
}
Execution::DestroyDeployment(request) => {
komodo_client().execute(request).await
}
Execution::CloneRepo(request) => {
komodo_client().execute(request).await
}
Execution::PullRepo(request) => {
komodo_client().execute(request).await
}
Execution::BuildRepo(request) => {
komodo_client().execute(request).await
}
Execution::CancelRepoBuild(request) => {
komodo_client().execute(request).await
}
Execution::StartContainer(request) => {
komodo_client().execute(request).await
}
Execution::RestartContainer(request) => {
komodo_client().execute(request).await
}
Execution::PauseContainer(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseContainer(request) => {
komodo_client().execute(request).await
}
Execution::StopContainer(request) => {
komodo_client().execute(request).await
}
Execution::DestroyContainer(request) => {
komodo_client().execute(request).await
}
Execution::StartAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::RestartAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::PauseAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::StopAllContainers(request) => {
komodo_client().execute(request).await
}
Execution::PruneContainers(request) => {
komodo_client().execute(request).await
}
Execution::DeleteNetwork(request) => {
komodo_client().execute(request).await
}
Execution::PruneNetworks(request) => {
komodo_client().execute(request).await
}
Execution::DeleteImage(request) => {
komodo_client().execute(request).await
}
Execution::PruneImages(request) => {
komodo_client().execute(request).await
}
Execution::DeleteVolume(request) => {
komodo_client().execute(request).await
}
Execution::PruneVolumes(request) => {
komodo_client().execute(request).await
}
Execution::PruneDockerBuilders(request) => {
komodo_client().execute(request).await
}
Execution::PruneBuildx(request) => {
komodo_client().execute(request).await
}
Execution::PruneSystem(request) => {
komodo_client().execute(request).await
}
Execution::RunSync(request) => {
komodo_client().execute(request).await
}
Execution::CommitSync(request) => {
komodo_client().write(request).await
}
Execution::DeployStack(request) => {
komodo_client().execute(request).await
}
Execution::DeployStackIfChanged(request) => {
komodo_client().execute(request).await
}
Execution::StartStack(request) => {
komodo_client().execute(request).await
}
Execution::RestartStack(request) => {
komodo_client().execute(request).await
}
Execution::PauseStack(request) => {
komodo_client().execute(request).await
}
Execution::UnpauseStack(request) => {
komodo_client().execute(request).await
}
Execution::StopStack(request) => {
komodo_client().execute(request).await
}
Execution::DestroyStack(request) => {
komodo_client().execute(request).await
}
Execution::RunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchRunAction(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::RunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchRunProcedure(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::RunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchRunBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::CancelBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::Deploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDeploy(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::StartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDestroyDeployment(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::CloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchCloneRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::PullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchPullRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::BuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchBuildRepo(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::CancelRepoBuild(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DestroyContainer(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopAllContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneContainers(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeleteNetwork(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneNetworks(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeleteImage(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneImages(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeleteVolume(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneVolumes(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneDockerBuilders(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneBuildx(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PruneSystem(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RunSync(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::CommitSync(request) => komodo_client()
.write(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDeployStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::DeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDeployStackIfChanged(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::StartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::RestartStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::PauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::UnpauseStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::StopStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::DestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Single(u)),
Execution::BatchDestroyStack(request) => komodo_client()
.execute(request)
.await
.map(|u| ExecutionResult::Batch(u)),
Execution::Sleep(request) => {
let duration =
Duration::from_millis(request.duration_ms as u64);
@@ -320,7 +451,12 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
};
match res {
Ok(update) => println!("\n{}: {update:#?}", "SUCCESS".green()),
Ok(ExecutionResult::Single(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Ok(ExecutionResult::Batch(update)) => {
println!("\n{}: {update:#?}", "SUCCESS".green())
}
Err(e) => println!("{}\n\n{e:#?}", "ERROR".red()),
}

View File

@@ -48,13 +48,14 @@ serde_json.workspace = true
serde_yaml.workspace = true
typeshare.workspace = true
octorust.workspace = true
wildcard.workspace = true
dashmap.workspace = true
tracing.workspace = true
reqwest.workspace = true
futures.workspace = true
nom_pem.workspace = true
anyhow.workspace = true
dotenvy.workspace = true
anyhow.workspace = true
bcrypt.workspace = true
base64.workspace = true
tokio.workspace = true

View File

@@ -9,7 +9,7 @@ use anyhow::Context;
use command::run_komodo_command;
use komodo_client::{
api::{
execute::RunAction,
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
@@ -25,6 +25,7 @@ use resolver_api::Resolve;
use tokio::fs;
use crate::{
api::execute::ExecuteRequest,
config::core_config,
helpers::{
interpolate::{
@@ -39,7 +40,26 @@ use crate::{
state::{action_states, db_client, State},
};
impl super::BatchExecute for BatchRunAction {
type Resource = Action;
fn single_request(action: String) -> ExecuteRequest {
ExecuteRequest::RunAction(RunAction { action })
}
}
impl Resolve<BatchRunAction, (User, Update)> for State {
#[instrument(name = "BatchRunAction", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunAction { pattern }: BatchRunAction,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunAction>(&pattern, &user).await
}
}
impl Resolve<RunAction, (User, Update)> for State {
#[instrument(name = "RunAction", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunAction { action }: RunAction,

View File

@@ -4,7 +4,10 @@ use anyhow::{anyhow, Context};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::{CancelBuild, Deploy, RunBuild},
api::execute::{
BatchExecutionResponse, BatchRunBuild, CancelBuild, Deploy,
RunBuild,
},
entities::{
alert::{Alert, AlertData, SeverityLevel},
all_logs_success,
@@ -51,6 +54,24 @@ use crate::{
use super::ExecuteRequest;
impl super::BatchExecute for BatchRunBuild {
type Resource = Build;
fn single_request(build: String) -> ExecuteRequest {
ExecuteRequest::RunBuild(RunBuild { build })
}
}
impl Resolve<BatchRunBuild, (User, Update)> for State {
#[instrument(name = "BatchRunBuild", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunBuild { pattern }: BatchRunBuild,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunBuild>(&pattern, &user).await
}
}
impl Resolve<RunBuild, (User, Update)> for State {
#[instrument(name = "RunBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -438,7 +459,6 @@ async fn handle_early_return(
Ok(update)
}
#[instrument(skip_all)]
pub async fn validate_cancel_build(
request: &ExecuteRequest,
) -> anyhow::Result<()> {

View File

@@ -37,6 +37,30 @@ use crate::{
state::{action_states, State},
};
use super::ExecuteRequest;
impl super::BatchExecute for BatchDeploy {
type Resource = Deployment;
fn single_request(deployment: String) -> ExecuteRequest {
ExecuteRequest::Deploy(Deploy {
deployment,
stop_signal: None,
stop_time: None,
})
}
}
impl Resolve<BatchDeploy, (User, Update)> for State {
#[instrument(name = "BatchDeploy", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeploy { pattern }: BatchDeploy,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeploy>(&pattern, &user).await
}
}
async fn setup_deployment_execution(
deployment: &str,
user: &User,

View File

@@ -2,13 +2,16 @@ use std::time::Instant;
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use futures::future::join_all;
use komodo_client::{
api::execute::*,
entities::{
update::{Log, Update},
user::User,
Operation,
},
};
use mungos::by_id::find_one_by_id;
@@ -21,6 +24,7 @@ use uuid::Uuid;
use crate::{
auth::auth_request,
helpers::update::{init_execution_update, update_update},
resource::{list_full_for_user_using_pattern, KomodoResource},
state::{db_client, State},
};
@@ -68,6 +72,7 @@ pub enum ExecuteRequest {
// ==== DEPLOYMENT ====
Deploy(Deploy),
BatchDeploy(BatchDeploy),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
@@ -77,29 +82,38 @@ pub enum ExecuteRequest {
// ==== STACK ====
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
StartStack(StartStack),
RestartStack(RestartStack),
StopStack(StopStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// ==== BUILD ====
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
CancelBuild(CancelBuild),
// ==== REPO ====
CloneRepo(CloneRepo),
BatchCloneRepo(BatchCloneRepo),
PullRepo(PullRepo),
BatchPullRepo(BatchPullRepo),
BuildRepo(BuildRepo),
BatchBuildRepo(BatchBuildRepo),
CancelRepoBuild(CancelRepoBuild),
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
// ==== ACTION ====
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer),
@@ -117,7 +131,25 @@ pub fn router() -> Router {
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ExecuteRequest>,
) -> serror::Result<Json<Update>> {
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let res = match inner_handler(request, user).await? {
ExecutionResult::Single(update) => serde_json::to_string(&update)
.context("Failed to serialize Update")?,
ExecutionResult::Batch(res) => res,
};
Ok((TypedHeader(ContentType::json()), res))
}
enum ExecutionResult {
Single(Update),
/// The batch contents will be pre serialized here
Batch(String),
}
async fn inner_handler(
request: ExecuteRequest,
user: User,
) -> anyhow::Result<ExecutionResult> {
let req_id = Uuid::new_v4();
// need to validate no cancel is active before any update is created.
@@ -125,6 +157,17 @@ async fn handler(
let update = init_execution_update(&request, &user).await?;
// This will be the case for the Batch exections,
// they don't have their own updates.
// The batch calls also call "inner_handler" themselves,
// and in their case will spawn tasks, so that isn't necessary
// here either.
if update.operation == Operation::None {
return Ok(ExecutionResult::Batch(
task(req_id, request, user, update).await?,
));
}
let handle =
tokio::spawn(task(req_id, request, user, update.clone()));
@@ -160,7 +203,7 @@ async fn handler(
}
});
Ok(Json(update))
Ok(ExecutionResult::Single(update))
}
#[instrument(
@@ -200,3 +243,40 @@ async fn task(
res
}
trait BatchExecute {
type Resource: KomodoResource;
fn single_request(name: String) -> ExecuteRequest;
}
async fn batch_execute<E: BatchExecute>(
pattern: &str,
user: &User,
) -> anyhow::Result<BatchExecutionResponse> {
let resources = list_full_for_user_using_pattern::<E::Resource>(
&pattern,
Default::default(),
&user,
&[],
)
.await?;
let futures = resources.into_iter().map(|resource| {
let user = user.clone();
async move {
inner_handler(E::single_request(resource.name.clone()), user)
.await
.map(|r| {
let ExecutionResult::Single(update) = r else {
unreachable!()
};
update
})
.map_err(|e| BatchExecutionResponseItemErr {
name: resource.name,
error: e.into(),
})
.into()
}
});
Ok(join_all(futures).await)
}

View File

@@ -2,7 +2,9 @@ use std::pin::Pin;
use formatting::{bold, colored, format_serror, muted, Color};
use komodo_client::{
api::execute::RunProcedure,
api::execute::{
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
},
entities::{
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
@@ -18,6 +20,26 @@ use crate::{
state::{action_states, db_client, State},
};
use super::ExecuteRequest;
impl super::BatchExecute for BatchRunProcedure {
type Resource = Procedure;
fn single_request(procedure: String) -> ExecuteRequest {
ExecuteRequest::RunProcedure(RunProcedure { procedure })
}
}
impl Resolve<BatchRunProcedure, (User, Update)> for State {
#[instrument(name = "BatchRunProcedure", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchRunProcedure { pattern }: BatchRunProcedure,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchRunProcedure>(&pattern, &user).await
}
}
impl Resolve<RunProcedure, (User, Update)> for State {
#[instrument(name = "RunProcedure", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(

View File

@@ -47,6 +47,24 @@ use crate::{
use super::ExecuteRequest;
impl super::BatchExecute for BatchCloneRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<BatchCloneRepo, (User, Update)> for State {
#[instrument(name = "BatchCloneRepo", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchCloneRepo { pattern }: BatchCloneRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchCloneRepo>(&pattern, &user).await
}
}
impl Resolve<CloneRepo, (User, Update)> for State {
#[instrument(name = "CloneRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -138,6 +156,24 @@ impl Resolve<CloneRepo, (User, Update)> for State {
}
}
impl super::BatchExecute for BatchPullRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<BatchPullRepo, (User, Update)> for State {
#[instrument(name = "BatchPullRepo", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchPullRepo { pattern }: BatchPullRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchPullRepo>(&pattern, &user).await
}
}
impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(name = "PullRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -271,6 +307,24 @@ async fn update_last_pulled_time(repo_name: &str) {
}
}
impl super::BatchExecute for BatchBuildRepo {
type Resource = Repo;
fn single_request(repo: String) -> ExecuteRequest {
ExecuteRequest::CloneRepo(CloneRepo { repo })
}
}
impl Resolve<BatchBuildRepo, (User, Update)> for State {
#[instrument(name = "BatchBuildRepo", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchBuildRepo { pattern }: BatchBuildRepo,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchBuildRepo>(&pattern, &user).await
}
}
impl Resolve<BuildRepo, (User, Update)> for State {
#[instrument(name = "BuildRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(

View File

@@ -36,6 +36,29 @@ use crate::{
state::{action_states, db_client, State},
};
use super::ExecuteRequest;
impl super::BatchExecute for BatchDeployStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack {
stack,
stop_time: None,
})
}
}
impl Resolve<BatchDeployStack, (User, Update)> for State {
#[instrument(name = "BatchDeployStack", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeployStack { pattern }: BatchDeployStack,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeployStack>(&pattern, &user).await
}
}
impl Resolve<DeployStack, (User, Update)> for State {
#[instrument(name = "DeployStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
@@ -246,6 +269,28 @@ impl Resolve<DeployStack, (User, Update)> for State {
}
}
impl super::BatchExecute for BatchDeployStackIfChanged {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack,
stop_time: None,
})
}
}
impl Resolve<BatchDeployStackIfChanged, (User, Update)> for State {
#[instrument(name = "BatchDeployStackIfChanged", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDeployStackIfChanged { pattern }: BatchDeployStackIfChanged,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDeployStackIfChanged>(&pattern, &user)
.await
}
}
impl Resolve<DeployStackIfChanged, (User, Update)> for State {
async fn resolve(
&self,
@@ -418,6 +463,28 @@ impl Resolve<StopStack, (User, Update)> for State {
}
}
impl super::BatchExecute for BatchDestroyStack {
type Resource = Stack;
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
remove_orphans: false,
stop_time: None,
})
}
}
impl Resolve<BatchDestroyStack, (User, Update)> for State {
#[instrument(name = "BatchDestroyStack", skip(self, user), fields(user_id = user.id))]
async fn resolve(
&self,
BatchDestroyStack { pattern }: BatchDestroyStack,
(user, _): (User, Update),
) -> anyhow::Result<BatchExecutionResponse> {
super::batch_execute::<BatchDestroyStack>(&pattern, &user).await
}
}
impl Resolve<DestroyStack, (User, Update)> for State {
#[instrument(name = "DestroyStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(

View File

@@ -339,6 +339,7 @@ impl Resolve<ListSecrets, User> for State {
ResourceTarget::Server(id) => Some(id),
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => None,
BuilderConfig::Server(config) => Some(config.server_id),
BuilderConfig::Aws(config) => {
secrets.extend(config.secrets);
@@ -387,6 +388,7 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_git_providers_for_server(
&mut providers,
@@ -485,6 +487,7 @@ impl Resolve<ListDockerRegistriesFromConfig, User> for State {
}
ResourceTarget::Builder(id) => {
match resource::get::<Builder>(&id).await?.config {
BuilderConfig::Url(_) => {}
BuilderConfig::Server(config) => {
merge_docker_registries_for_server(
&mut registries,

View File

@@ -92,13 +92,19 @@ async fn login(
);
let config = core_config();
let redirect = if !config.oidc_redirect.is_empty() {
Redirect::to(
auth_url
.as_str()
.replace(&config.oidc_provider, &config.oidc_redirect)
.as_str(),
)
let redirect = if !config.oidc_redirect_host.is_empty() {
let auth_url = auth_url.as_str();
let (protocol, rest) = auth_url
.split_once("://")
.context("Invalid URL: Missing protocol (eg 'https://')")?;
let host = rest
.split_once(['/', '?'])
.map(|(host, _)| host)
.unwrap_or(rest);
Redirect::to(&auth_url.replace(
&format!("{protocol}://{host}"),
&config.oidc_redirect_host,
))
} else {
Redirect::to(auth_url.as_str())
};

View File

@@ -78,7 +78,7 @@ pub fn core_config() -> &'static CoreConfig {
},
oidc_enabled: env.komodo_oidc_enabled.unwrap_or(config.oidc_enabled),
oidc_provider: env.komodo_oidc_provider.unwrap_or(config.oidc_provider),
oidc_redirect: env.komodo_oidc_redirect.unwrap_or(config.oidc_redirect),
oidc_redirect_host: env.komodo_oidc_redirect_host.unwrap_or(config.oidc_redirect_host),
oidc_client_id: maybe_read_item_from_file(env.komodo_oidc_client_id_file,env
.komodo_oidc_client_id)
.unwrap_or(config.oidc_client_id),

View File

@@ -31,7 +31,7 @@ use crate::{
use super::periphery_client;
const BUILDER_POLL_RATE_SECS: u64 = 2;
const BUILDER_POLL_MAX_TRIES: usize = 30;
const BUILDER_POLL_MAX_TRIES: usize = 60;
#[instrument(skip_all, fields(builder_id = builder.id, update_id = update.id))]
pub async fn get_builder_periphery(
@@ -42,9 +42,32 @@ pub async fn get_builder_periphery(
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
match builder.config {
BuilderConfig::Url(config) => {
if config.address.is_empty() {
return Err(anyhow!("Builder has not yet configured an address"));
}
let periphery = PeripheryClient::new(
config.address,
if config.passkey.is_empty() {
core_config().passkey.clone()
} else {
config.passkey
},
);
periphery
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
return Err(anyhow!("builder has not configured a server"));
return Err(anyhow!("Builder has not configured a server"));
}
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server)?;

View File

@@ -4,9 +4,14 @@ use anyhow::{anyhow, Context};
use formatting::{bold, colored, format_serror, muted, Color};
use futures::future::join_all;
use komodo_client::{
api::execute::Execution,
api::execute::*,
entities::{
action::Action,
build::Build,
deployment::Deployment,
procedure::Procedure,
repo::Repo,
stack::Stack,
update::{Log, Update},
user::procedure_user,
},
@@ -17,6 +22,7 @@ use tokio::sync::Mutex;
use crate::{
api::execute::ExecuteRequest,
resource::{list_full_for_user_using_pattern, KomodoResource},
state::{db_client, State},
};
@@ -79,11 +85,94 @@ pub async fn execute_procedure(
#[allow(dependency_on_unit_never_type_fallback)]
#[instrument(skip(update))]
async fn execute_stage(
executions: Vec<Execution>,
_executions: Vec<Execution>,
parent_id: &str,
parent_name: &str,
update: &Mutex<Update>,
) -> anyhow::Result<()> {
let mut executions = Vec::with_capacity(_executions.capacity());
for execution in _executions {
match execution {
Execution::BatchRunAction(exec) => {
extend_batch_exection::<BatchRunAction>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchRunProcedure(exec) => {
extend_batch_exection::<BatchRunProcedure>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchRunBuild(exec) => {
extend_batch_exection::<BatchRunBuild>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchCloneRepo(exec) => {
extend_batch_exection::<BatchCloneRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchPullRepo(exec) => {
extend_batch_exection::<BatchPullRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchBuildRepo(exec) => {
extend_batch_exection::<BatchBuildRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeploy(exec) => {
extend_batch_exection::<BatchDeploy>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyDeployment(exec) => {
extend_batch_exection::<BatchDestroyDeployment>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStack(exec) => {
extend_batch_exection::<BatchDeployStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStackIfChanged(exec) => {
extend_batch_exection::<BatchDeployStackIfChanged>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyStack(exec) => {
extend_batch_exection::<BatchDestroyStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
execution => executions.push(execution),
}
}
let futures = executions.into_iter().map(|execution| async move {
let now = Instant::now();
add_line_to_update(
@@ -146,6 +235,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchRunProcedure(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunProcedure not implemented correctly"
));
}
Execution::RunAction(req) => {
let req = ExecuteRequest::RunAction(req);
let update = init_execution_update(&req, &user).await?;
@@ -162,6 +257,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchRunAction(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunAction not implemented correctly"
));
}
Execution::RunBuild(req) => {
let req = ExecuteRequest::RunBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -178,6 +279,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchRunBuild(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchRunBuild not implemented correctly"
));
}
Execution::CancelBuild(req) => {
let req = ExecuteRequest::CancelBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -210,6 +317,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDeploy(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeploy not implemented correctly"
));
}
Execution::StartDeployment(req) => {
let req = ExecuteRequest::StartDeployment(req);
let update = init_execution_update(&req, &user).await?;
@@ -306,6 +419,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDestroyDeployment(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDestroyDeployment not implemented correctly"
));
}
Execution::CloneRepo(req) => {
let req = ExecuteRequest::CloneRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -322,6 +441,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchCloneRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchCloneRepo not implemented correctly"
));
}
Execution::PullRepo(req) => {
let req = ExecuteRequest::PullRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -338,6 +463,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchPullRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchPullRepo not implemented correctly"
));
}
Execution::BuildRepo(req) => {
let req = ExecuteRequest::BuildRepo(req);
let update = init_execution_update(&req, &user).await?;
@@ -354,6 +485,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchBuildRepo(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchBuildRepo not implemented correctly"
));
}
Execution::CancelRepoBuild(req) => {
let req = ExecuteRequest::CancelRepoBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -743,6 +880,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDeployStack(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeployStack not implemented correctly"
));
}
Execution::DeployStackIfChanged(req) => {
let req = ExecuteRequest::DeployStackIfChanged(req);
let update = init_execution_update(&req, &user).await?;
@@ -759,6 +902,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDeployStackIfChanged(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDeployStackIfChanged not implemented correctly"
));
}
Execution::StartStack(req) => {
let req = ExecuteRequest::StartStack(req);
let update = init_execution_update(&req, &user).await?;
@@ -855,6 +1004,12 @@ async fn execute_execution(
)
.await?
}
Execution::BatchDestroyStack(_) => {
// All batch executions must be expanded in `execute_stage`
return Err(anyhow!(
"Batch method BatchDestroyStack not implemented correctly"
));
}
Execution::Sleep(req) => {
let duration = Duration::from_millis(req.duration_ms as u64);
tokio::time::sleep(duration).await;
@@ -912,3 +1067,120 @@ async fn add_line_to_update(update: &Mutex<Update>, line: &str) {
error!("Failed to update an update during procedure | {e:#}");
};
}
async fn extend_batch_exection<E: ExtendBatch>(
pattern: &str,
executions: &mut Vec<Execution>,
) -> anyhow::Result<()> {
let more = list_full_for_user_using_pattern::<E::Resource>(
pattern,
Default::default(),
procedure_user(),
&[],
)
.await?
.into_iter()
.map(|resource| E::single_execution(resource.name));
executions.extend(more);
Ok(())
}
trait ExtendBatch {
type Resource: KomodoResource;
fn single_execution(name: String) -> Execution;
}
impl ExtendBatch for BatchRunProcedure {
type Resource = Procedure;
fn single_execution(procedure: String) -> Execution {
Execution::RunProcedure(RunProcedure { procedure })
}
}
impl ExtendBatch for BatchRunAction {
type Resource = Action;
fn single_execution(action: String) -> Execution {
Execution::RunAction(RunAction { action })
}
}
impl ExtendBatch for BatchRunBuild {
type Resource = Build;
fn single_execution(build: String) -> Execution {
Execution::RunBuild(RunBuild { build })
}
}
impl ExtendBatch for BatchCloneRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::CloneRepo(CloneRepo { repo })
}
}
impl ExtendBatch for BatchPullRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::PullRepo(PullRepo { repo })
}
}
impl ExtendBatch for BatchBuildRepo {
type Resource = Repo;
fn single_execution(repo: String) -> Execution {
Execution::BuildRepo(BuildRepo { repo })
}
}
impl ExtendBatch for BatchDeploy {
type Resource = Deployment;
fn single_execution(deployment: String) -> Execution {
Execution::Deploy(Deploy {
deployment,
stop_signal: None,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDestroyDeployment {
type Resource = Deployment;
fn single_execution(deployment: String) -> Execution {
Execution::DestroyDeployment(DestroyDeployment {
deployment,
signal: None,
time: None,
})
}
}
impl ExtendBatch for BatchDeployStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DeployStack(DeployStack {
stack,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDeployStackIfChanged {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DeployStackIfChanged(DeployStackIfChanged {
stack,
stop_time: None,
})
}
}
impl ExtendBatch for BatchDestroyStack {
type Resource = Stack;
fn single_execution(stack: String) -> Execution {
Execution::DestroyStack(DestroyStack {
stack,
remove_orphans: false,
stop_time: None,
})
}
}

View File

@@ -118,7 +118,7 @@ pub fn get_stack_state_from_containers(
if containers.is_empty() {
return StackState::Down;
}
if services.len() != containers.len() {
if services.len() > containers.len() {
return StackState::Unhealthy;
}
let running = containers.iter().all(|container| {

View File

@@ -261,6 +261,9 @@ pub async fn init_execution_update(
resource::get::<Deployment>(&data.deployment).await?.id,
),
),
ExecuteRequest::BatchDeploy(_data) => {
return Ok(Default::default())
}
ExecuteRequest::StartDeployment(data) => (
Operation::StartDeployment,
ResourceTarget::Deployment(
@@ -305,6 +308,9 @@ pub async fn init_execution_update(
resource::get::<Build>(&data.build).await?.id,
),
),
ExecuteRequest::BatchRunBuild(_data) => {
return Ok(Default::default())
}
ExecuteRequest::CancelBuild(data) => (
Operation::CancelBuild,
ResourceTarget::Build(
@@ -319,18 +325,27 @@ pub async fn init_execution_update(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchCloneRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::PullRepo(data) => (
Operation::PullRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchPullRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::BuildRepo(data) => (
Operation::BuildRepo,
ResourceTarget::Repo(
resource::get::<Repo>(&data.repo).await?.id,
),
),
ExecuteRequest::BatchBuildRepo(_data) => {
return Ok(Default::default())
}
ExecuteRequest::CancelRepoBuild(data) => (
Operation::CancelRepoBuild,
ResourceTarget::Repo(
@@ -345,6 +360,9 @@ pub async fn init_execution_update(
resource::get::<Procedure>(&data.procedure).await?.id,
),
),
ExecuteRequest::BatchRunProcedure(_) => {
return Ok(Default::default())
}
// Action
ExecuteRequest::RunAction(data) => (
@@ -353,6 +371,9 @@ pub async fn init_execution_update(
resource::get::<Action>(&data.action).await?.id,
),
),
ExecuteRequest::BatchRunAction(_) => {
return Ok(Default::default())
}
// Server template
ExecuteRequest::LaunchServer(data) => (
@@ -379,12 +400,18 @@ pub async fn init_execution_update(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStack(_data) => {
return Ok(Default::default())
}
ExecuteRequest::DeployStackIfChanged(data) => (
Operation::DeployStack,
ResourceTarget::Stack(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDeployStackIfChanged(_data) => {
return Ok(Default::default())
}
ExecuteRequest::StartStack(data) => (
if data.service.is_some() {
Operation::StartStackService
@@ -441,6 +468,9 @@ pub async fn init_execution_update(
resource::get::<Stack>(&data.stack).await?.id,
),
),
ExecuteRequest::BatchDestroyStack(_data) => {
return Ok(Default::default())
}
};
let mut update = make_update(target, operation, user);
update.in_progress();

View File

@@ -40,6 +40,9 @@ impl super::KomodoResource for Builder {
builder: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let (builder_type, instance_type) = match builder.config {
BuilderConfig::Url(_) => {
(BuilderConfigVariant::Url.to_string(), None)
}
BuilderConfig::Server(config) => (
BuilderConfigVariant::Server.to_string(),
Some(config.server_id),

View File

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Context};
use formatting::format_serror;
use futures::{future::join_all, FutureExt};
use komodo_client::{
api::write::CreateTag,
api::{read::ExportResourcesToToml, write::CreateTag},
entities::{
komodo_timestamp,
permission::PermissionLevel,
@@ -18,6 +18,7 @@ use komodo_client::{
user::{system_user, User},
Operation, ResourceTarget, ResourceTargetVariant,
},
parsers::parse_string_list,
};
use mungos::{
by_id::{delete_one_by_id, update_one_by_id},
@@ -242,6 +243,79 @@ pub async fn get_check_permissions<T: KomodoResource>(
}
}
#[instrument(level = "debug")]
pub async fn get_user_permission_on_resource<T: KomodoResource>(
user: &User,
resource_id: &str,
) -> anyhow::Result<PermissionLevel> {
if user.admin {
return Ok(PermissionLevel::Write);
}
let resource_type = T::resource_type();
// Start with base of Read or None
let mut base = if core_config().transparent_mode {
PermissionLevel::Read
} else {
PermissionLevel::None
};
// Add in the resource level global base permission
let resource_base = get::<T>(resource_id).await?.base_permission;
if resource_base > base {
base = resource_base;
}
// Overlay users base on resource variant
if let Some(level) = user.all.get(&resource_type).cloned() {
if level > base {
base = level;
}
}
if base == PermissionLevel::Write {
// No reason to keep going if already Write at this point.
return Ok(PermissionLevel::Write);
}
// Overlay any user groups base on resource variant
let groups = get_user_user_groups(&user.id).await?;
for group in &groups {
if let Some(level) = group.all.get(&resource_type).cloned() {
if level > base {
base = level;
}
}
}
if base == PermissionLevel::Write {
// No reason to keep going if already Write at this point.
return Ok(PermissionLevel::Write);
}
// Overlay any specific permissions
let permission = find_collect(
&db_client().permissions,
doc! {
"$or": user_target_query(&user.id, &groups)?,
"resource_target.type": resource_type.as_ref(),
"resource_target.id": resource_id
},
None,
)
.await
.context("failed to query db for permissions")?
.into_iter()
// get the max permission user has between personal / any user groups
.fold(base, |level, permission| {
if permission.level > level {
permission.level
} else {
level
}
});
Ok(permission)
}
// ======
// LIST
// ======
@@ -325,79 +399,6 @@ pub async fn get_resource_ids_for_user<T: KomodoResource>(
Ok(Some(ids.into_iter().collect()))
}
#[instrument(level = "debug")]
pub async fn get_user_permission_on_resource<T: KomodoResource>(
user: &User,
resource_id: &str,
) -> anyhow::Result<PermissionLevel> {
if user.admin {
return Ok(PermissionLevel::Write);
}
let resource_type = T::resource_type();
// Start with base of Read or None
let mut base = if core_config().transparent_mode {
PermissionLevel::Read
} else {
PermissionLevel::None
};
// Add in the resource level global base permission
let resource_base = get::<T>(resource_id).await?.base_permission;
if resource_base > base {
base = resource_base;
}
// Overlay users base on resource variant
if let Some(level) = user.all.get(&resource_type).cloned() {
if level > base {
base = level;
}
}
if base == PermissionLevel::Write {
// No reason to keep going if already Write at this point.
return Ok(PermissionLevel::Write);
}
// Overlay any user groups base on resource variant
let groups = get_user_user_groups(&user.id).await?;
for group in &groups {
if let Some(level) = group.all.get(&resource_type).cloned() {
if level > base {
base = level;
}
}
}
if base == PermissionLevel::Write {
// No reason to keep going if already Write at this point.
return Ok(PermissionLevel::Write);
}
// Overlay any specific permissions
let permission = find_collect(
&db_client().permissions,
doc! {
"$or": user_target_query(&user.id, &groups)?,
"resource_target.type": resource_type.as_ref(),
"resource_target.id": resource_id
},
None,
)
.await
.context("failed to query db for permissions")?
.into_iter()
// get the max permission user has between personal / any user groups
.fold(base, |level, permission| {
if permission.level > level {
permission.level
} else {
level
}
});
Ok(permission)
}
#[instrument(level = "debug")]
pub async fn list_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
@@ -410,6 +411,23 @@ pub async fn list_for_user<T: KomodoResource>(
list_for_user_using_document::<T>(filters, user).await
}
#[instrument(level = "debug")]
pub async fn list_for_user_using_pattern<T: KomodoResource>(
pattern: &str,
query: ResourceQuery<T::QuerySpecifics>,
user: &User,
all_tags: &[Tag],
) -> anyhow::Result<Vec<T::ListItem>> {
let list = list_full_for_user_using_pattern::<T>(
pattern, query, user, all_tags,
)
.await?
.into_iter()
.map(|resource| T::to_list_item(resource));
Ok(join_all(list).await)
}
#[instrument(level = "debug")]
pub async fn list_for_user_using_document<T: KomodoResource>(
filters: Document,
user: &User,
@@ -421,6 +439,55 @@ pub async fn list_for_user_using_document<T: KomodoResource>(
Ok(join_all(list).await)
}
/// Lists full resource matching wildcard syntax,
/// or regex if wrapped with "\\"
///
/// ## Example
/// ```
/// let items = list_full_for_user_using_match_string::<Build>("foo-*", Default::default(), user, all_tags).await?;
/// let items = list_full_for_user_using_match_string::<Build>("\\^foo-.*$\\", Default::default(), user, all_tags).await?;
/// ```
#[instrument(level = "debug")]
pub async fn list_full_for_user_using_pattern<T: KomodoResource>(
pattern: &str,
query: ResourceQuery<T::QuerySpecifics>,
user: &User,
all_tags: &[Tag],
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
let resources =
list_full_for_user::<T>(query, user, all_tags).await?;
let patterns = parse_string_list(pattern);
let mut names = HashSet::<String>::new();
for pattern in patterns {
if pattern.starts_with('\\') && pattern.ends_with('\\') {
let regex = regex::Regex::new(&pattern[1..(pattern.len() - 1)])
.context("Regex matching string invalid")?;
for resource in &resources {
if regex.is_match(&resource.name) {
names.insert(resource.name.clone());
}
}
} else {
let wildcard = wildcard::Wildcard::new(pattern.as_bytes())
.context("Wildcard matching string invalid")?;
for resource in &resources {
if wildcard.is_match(resource.name.as_bytes()) {
names.insert(resource.name.clone());
}
}
};
}
Ok(
resources
.into_iter()
.filter(|resource| names.contains(resource.name.as_str()))
.collect(),
)
}
#[instrument(level = "debug")]
pub async fn list_full_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
@@ -831,6 +898,16 @@ pub async fn delete<T: KomodoResource>(
}
let target = resource_target::<T>(resource.id.clone());
let toml = State
.resolve(
ExportResourcesToToml {
targets: vec![target.clone()],
..Default::default()
},
user.clone(),
)
.await?
.toml;
let mut update =
make_update(target.clone(), T::delete_operation(), user);
@@ -843,13 +920,14 @@ pub async fn delete<T: KomodoResource>(
delete_one_by_id(T::coll(), &resource.id, None)
.await
.with_context(|| {
format!("failed to delete {} from database", T::resource_type())
format!("Failed to delete {} from database", T::resource_type())
})?;
update.push_simple_log(
&format!("delete {}", T::resource_type()),
format!("deleted {} {}", T::resource_type(), resource.name),
&format!("Delete {}", T::resource_type()),
format!("Deleted {} {}", T::resource_type(), resource.name),
);
update.push_simple_log("Deleted Toml", toml);
if let Err(e) = T::post_delete(&resource, &mut update).await {
update.push_error_log("post delete", format_serror(&e.into()));

View File

@@ -178,6 +178,13 @@ async fn validate_config(
}
params.procedure = procedure.id;
}
Execution::BatchRunProcedure(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::RunAction(params) => {
let action = super::get_check_permissions::<Action>(
&params.action,
@@ -187,6 +194,13 @@ async fn validate_config(
.await?;
params.action = action.id;
}
Execution::BatchRunAction(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::RunBuild(params) => {
let build = super::get_check_permissions::<Build>(
&params.build,
@@ -196,6 +210,13 @@ async fn validate_config(
.await?;
params.build = build.id;
}
Execution::BatchRunBuild(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::CancelBuild(params) => {
let build = super::get_check_permissions::<Build>(
&params.build,
@@ -215,6 +236,13 @@ async fn validate_config(
.await?;
params.deployment = deployment.id;
}
Execution::BatchDeploy(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::StartDeployment(params) => {
let deployment =
super::get_check_permissions::<Deployment>(
@@ -275,6 +303,13 @@ async fn validate_config(
.await?;
params.deployment = deployment.id;
}
Execution::BatchDestroyDeployment(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::CloneRepo(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -284,6 +319,13 @@ async fn validate_config(
.await?;
params.repo = repo.id;
}
Execution::BatchCloneRepo(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::PullRepo(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -293,6 +335,13 @@ async fn validate_config(
.await?;
params.repo = repo.id;
}
Execution::BatchPullRepo(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::BuildRepo(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -302,6 +351,13 @@ async fn validate_config(
.await?;
params.repo = repo.id;
}
Execution::BatchBuildRepo(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::CancelRepoBuild(params) => {
let repo = super::get_check_permissions::<Repo>(
&params.repo,
@@ -528,6 +584,13 @@ async fn validate_config(
.await?;
params.stack = stack.id;
}
Execution::BatchDeployStack(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::DeployStackIfChanged(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,
@@ -537,6 +600,13 @@ async fn validate_config(
.await?;
params.stack = stack.id;
}
Execution::BatchDeployStackIfChanged(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::StartStack(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,
@@ -591,6 +661,13 @@ async fn validate_config(
.await?;
params.stack = stack.id;
}
Execution::BatchDestroyStack(_params) => {
if !user.admin {
return Err(anyhow!(
"Non admin user cannot configure Batch executions"
));
}
}
Execution::Sleep(_) => {}
}
}

View File

@@ -47,6 +47,7 @@ impl super::KomodoResource for Server {
info: ServerListItemInfo {
state: status.map(|s| s.state).unwrap_or_default(),
region: server.config.region,
address: server.config.address,
send_unreachable_alerts: server
.config
.send_unreachable_alerts,

View File

@@ -1,6 +1,9 @@
use anyhow::Context;
use komodo_client::entities::{
stack::{ComposeFile, ComposeService, Stack, StackServiceNames},
stack::{
ComposeFile, ComposeService, ComposeServiceDeploy, Stack,
StackServiceNames,
},
FileContents,
};
@@ -69,16 +72,40 @@ pub fn extract_services_into_res(
let compose = serde_yaml::from_str::<ComposeFile>(compose_contents)
.context("failed to parse service names from compose contents")?;
let services = compose.services.into_iter().map(
|(service_name, ComposeService { container_name, .. })| {
StackServiceNames {
container_name: container_name.unwrap_or_else(|| {
format!("{project_name}-{service_name}")
}),
service_name,
}
let mut services = Vec::with_capacity(compose.services.capacity());
for (
service_name,
ComposeService {
container_name,
deploy,
..
},
);
) in compose.services
{
match deploy {
Some(ComposeServiceDeploy {
replicas: Some(replicas),
}) if replicas > 1 => {
for i in 1..1 + replicas {
services.push(StackServiceNames {
container_name: format!(
"{project_name}-{service_name}-{i}"
),
service_name: format!("{service_name}-{i}"),
});
}
}
_ => {
services.push(StackServiceNames {
container_name: container_name.unwrap_or_else(|| {
format!("{project_name}-{service_name}")
}),
service_name,
});
}
}
}
res.extend(services);

View File

@@ -360,6 +360,7 @@ impl ResourceSyncTrait for Procedure {
.map(|p| p.name.clone())
.unwrap_or_default();
}
Execution::BatchRunProcedure(_config) => {}
Execution::RunAction(config) => {
config.action = resources
.actions
@@ -367,6 +368,7 @@ impl ResourceSyncTrait for Procedure {
.map(|p| p.name.clone())
.unwrap_or_default();
}
Execution::BatchRunAction(_config) => {}
Execution::RunBuild(config) => {
config.build = resources
.builds
@@ -374,6 +376,7 @@ impl ResourceSyncTrait for Procedure {
.map(|b| b.name.clone())
.unwrap_or_default();
}
Execution::BatchRunBuild(_config) => {}
Execution::CancelBuild(config) => {
config.build = resources
.builds
@@ -388,6 +391,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchDeploy(_config) => {}
Execution::StartDeployment(config) => {
config.deployment = resources
.deployments
@@ -430,6 +434,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchDestroyDeployment(_config) => {}
Execution::CloneRepo(config) => {
config.repo = resources
.repos
@@ -437,6 +442,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchCloneRepo(_config) => {}
Execution::PullRepo(config) => {
config.repo = resources
.repos
@@ -444,6 +450,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchPullRepo(_config) => {}
Execution::BuildRepo(config) => {
config.repo = resources
.repos
@@ -451,6 +458,7 @@ impl ResourceSyncTrait for Procedure {
.map(|d| d.name.clone())
.unwrap_or_default();
}
Execution::BatchBuildRepo(_config) => {}
Execution::CancelRepoBuild(config) => {
config.repo = resources
.repos
@@ -626,6 +634,7 @@ impl ResourceSyncTrait for Procedure {
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::BatchDeployStack(_config) => {}
Execution::DeployStackIfChanged(config) => {
config.stack = resources
.stacks
@@ -633,6 +642,7 @@ impl ResourceSyncTrait for Procedure {
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::BatchDeployStackIfChanged(_config) => {}
Execution::StartStack(config) => {
config.stack = resources
.stacks
@@ -675,6 +685,7 @@ impl ResourceSyncTrait for Procedure {
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::BatchDestroyStack(_config) => {}
Execution::Sleep(_) => {}
}
}

View File

@@ -390,6 +390,7 @@ impl ToToml for Builder {
let empty_params = match resource.config {
PartialBuilderConfig::Aws(config) => config.is_none(),
PartialBuilderConfig::Server(config) => config.is_none(),
PartialBuilderConfig::Url(config) => config.is_none(),
};
if empty_params {
// toml_pretty will remove empty map
@@ -414,6 +415,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchRunProcedure(_exec) => {}
Execution::RunAction(exec) => exec.action.clone_from(
all
.actions
@@ -421,6 +423,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchRunAction(_exec) => {}
Execution::RunBuild(exec) => exec.build.clone_from(
all
.builds
@@ -428,6 +431,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchRunBuild(_exec) => {}
Execution::CancelBuild(exec) => exec.build.clone_from(
all
.builds
@@ -442,6 +446,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchDeploy(_exec) => {}
Execution::StartDeployment(exec) => {
exec.deployment.clone_from(
all
@@ -496,6 +501,7 @@ impl ToToml for Procedure {
.unwrap_or(&String::new()),
)
}
Execution::BatchDestroyDeployment(_exec) => {}
Execution::CloneRepo(exec) => exec.repo.clone_from(
all
.repos
@@ -503,6 +509,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchCloneRepo(_exec) => {}
Execution::PullRepo(exec) => exec.repo.clone_from(
all
.repos
@@ -510,6 +517,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchPullRepo(_exec) => {}
Execution::BuildRepo(exec) => exec.repo.clone_from(
all
.repos
@@ -517,6 +525,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchBuildRepo(_exec) => {}
Execution::CancelRepoBuild(exec) => exec.repo.clone_from(
all
.repos
@@ -710,6 +719,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchDeployStack(_exec) => {}
Execution::DeployStackIfChanged(exec) => {
exec.stack.clone_from(
all
@@ -719,6 +729,7 @@ impl ToToml for Procedure {
.unwrap_or(&String::new()),
)
}
Execution::BatchDeployStackIfChanged(_exec) => {}
Execution::StartStack(exec) => exec.stack.clone_from(
all
.stacks
@@ -761,6 +772,7 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::BatchDestroyStack(_exec) => {}
Execution::Sleep(_) | Execution::None(_) => {}
}
}

View File

@@ -1,4 +1,4 @@
use std::sync::OnceLock;
use std::{collections::HashMap, sync::OnceLock};
use anyhow::{anyhow, Context};
use bollard::{
@@ -40,7 +40,7 @@ impl DockerClient {
pub async fn list_containers(
&self,
) -> anyhow::Result<Vec<ContainerListItem>> {
self
let mut containers = self
.docker
.list_containers(Some(ListContainersOptions::<String> {
all: true,
@@ -48,8 +48,8 @@ impl DockerClient {
}))
.await?
.into_iter()
.map(|container| {
Ok(ContainerListItem {
.flat_map(|container| {
anyhow::Ok(ContainerListItem {
server_id: None,
name: container
.names
@@ -75,9 +75,12 @@ impl DockerClient {
networks: container
.network_settings
.and_then(|settings| {
settings
.networks
.map(|networks| networks.into_keys().collect())
settings.networks.map(|networks| {
let mut keys =
networks.into_keys().collect::<Vec<_>>();
keys.sort();
keys
})
})
.unwrap_or_default(),
volumes: container
@@ -92,7 +95,26 @@ impl DockerClient {
labels: container.labels.unwrap_or_default(),
})
})
.collect()
.collect::<Vec<_>>();
let container_id_to_network = containers
.iter()
.filter_map(|c| Some((c.id.clone()?, c.network_mode.clone()?)))
.collect::<HashMap<_, _>>();
// Fix containers which use `container:container_id` network_mode,
// by replacing with the referenced network mode.
containers.iter_mut().for_each(|container| {
let Some(network_name) = &container.network_mode else {
return;
};
let Some(container_id) =
network_name.strip_prefix("container:")
else {
return;
};
container.network_mode =
container_id_to_network.get(container_id).cloned();
});
Ok(containers)
}
pub async fn inspect_container(
@@ -519,7 +541,7 @@ impl DockerClient {
&self,
containers: &[ContainerListItem],
) -> anyhow::Result<Vec<NetworkListItem>> {
self
let networks = self
.docker
.list_networks::<String>(None)
.await?
@@ -545,7 +567,7 @@ impl DockerClient {
}),
None => false,
};
Ok(NetworkListItem {
NetworkListItem {
name: network.name,
id: network.id,
created: network.created,
@@ -559,9 +581,10 @@ impl DockerClient {
attachable: network.attachable,
ingress: network.ingress,
in_use,
})
}
})
.collect()
.collect();
Ok(networks)
}
pub async fn inspect_network(
@@ -628,7 +651,7 @@ impl DockerClient {
&self,
containers: &[ContainerListItem],
) -> anyhow::Result<Vec<ImageListItem>> {
self
let images = self
.docker
.list_images::<String>(None)
.await?
@@ -641,7 +664,7 @@ impl DockerClient {
.map(|id| id == &image.id)
.unwrap_or_default()
});
Ok(ImageListItem {
ImageListItem {
name: image
.repo_tags
.into_iter()
@@ -652,9 +675,10 @@ impl DockerClient {
created: image.created,
size: image.size,
in_use,
})
}
})
.collect()
.collect();
Ok(images)
}
pub async fn inspect_image(
@@ -761,7 +785,7 @@ impl DockerClient {
&self,
containers: &[ContainerListItem],
) -> anyhow::Result<Vec<VolumeListItem>> {
self
let volumes = self
.docker
.list_volumes::<String>(None)
.await?
@@ -786,7 +810,7 @@ impl DockerClient {
let in_use = containers.iter().any(|container| {
container.volumes.iter().any(|name| &volume.name == name)
});
Ok(VolumeListItem {
VolumeListItem {
name: volume.name,
driver: volume.driver,
mountpoint: volume.mountpoint,
@@ -794,9 +818,10 @@ impl DockerClient {
size: volume.usage_data.map(|data| data.size),
scope,
in_use,
})
}
})
.collect()
.collect();
Ok(volumes)
}
pub async fn inspect_volume(

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Runs the target Action. Response: [Update]
#[typeshare]
@@ -26,3 +26,31 @@ pub struct RunAction {
/// Id or name
pub action: String,
}
/// Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResult]
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchRunAction {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* actions
/// foo-*
/// # add some more
/// extra-action-1, extra-action-2
/// ```
pub pattern: String,
}

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
//
@@ -36,6 +36,36 @@ pub struct RunBuild {
//
/// Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchRunBuild {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* builds
/// foo-*
/// # add some more
/// extra-build-1, extra-build-2
/// ```
pub pattern: String,
}
//
/// Cancels the target build.
/// Only does anything if the build is `building` when called.
/// Response: [Update]

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::{update::Update, TerminationSignal};
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Deploys the container for the target deployment. Response: [Update].
///
@@ -41,6 +41,36 @@ pub struct Deploy {
//
/// Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDeploy {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* deployments
/// foo-*
/// # add some more
/// extra-deployment-1, extra-deployment-2
/// ```
pub pattern: String,
}
//
/// Starts the container for the target deployment. Response: [Update]
///
/// 1. Runs `docker start ${container_name}`.
@@ -187,3 +217,33 @@ pub struct DestroyDeployment {
/// Override the default termination max time.
pub time: Option<i32>,
}
//
/// Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDestroyDeployment {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* deployments
/// foo-*
/// # add some more
/// extra-deployment-1, extra-deployment-2
/// ```
pub pattern: String,
}

View File

@@ -27,7 +27,7 @@ pub use sync::*;
use crate::{
api::write::CommitSync,
entities::{NoData, I64},
entities::{update::Update, NoData, _Serror, I64},
};
pub trait KomodoExecuteRequest: HasResponse {}
@@ -59,27 +59,35 @@ pub enum Execution {
// ACTION
RunAction(RunAction),
BatchRunAction(BatchRunAction),
// PROCEDURE
RunProcedure(RunProcedure),
BatchRunProcedure(BatchRunProcedure),
// BUILD
RunBuild(RunBuild),
BatchRunBuild(BatchRunBuild),
CancelBuild(CancelBuild),
// DEPLOYMENT
Deploy(Deploy),
BatchDeploy(BatchDeploy),
StartDeployment(StartDeployment),
RestartDeployment(RestartDeployment),
PauseDeployment(PauseDeployment),
UnpauseDeployment(UnpauseDeployment),
StopDeployment(StopDeployment),
DestroyDeployment(DestroyDeployment),
BatchDestroyDeployment(BatchDestroyDeployment),
// REPO
CloneRepo(CloneRepo),
BatchCloneRepo(BatchCloneRepo),
PullRepo(PullRepo),
BatchPullRepo(BatchPullRepo),
BuildRepo(BuildRepo),
BatchBuildRepo(BatchBuildRepo),
CancelRepoBuild(CancelRepoBuild),
// SERVER (Container)
@@ -113,13 +121,16 @@ pub enum Execution {
// STACK
DeployStack(DeployStack),
BatchDeployStack(BatchDeployStack),
DeployStackIfChanged(DeployStackIfChanged),
BatchDeployStackIfChanged(BatchDeployStackIfChanged),
StartStack(StartStack),
RestartStack(RestartStack),
PauseStack(PauseStack),
UnpauseStack(UnpauseStack),
StopStack(StopStack),
DestroyStack(DestroyStack),
BatchDestroyStack(BatchDestroyStack),
// SLEEP
Sleep(Sleep),
@@ -131,3 +142,34 @@ pub struct Sleep {
#[serde(default)]
pub duration_ms: I64,
}
#[typeshare]
pub type BatchExecutionResponse = Vec<BatchExecutionResponseItem>;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", content = "data")]
pub enum BatchExecutionResponseItem {
Ok(Update),
Err(BatchExecutionResponseItemErr),
}
impl From<Result<Update, BatchExecutionResponseItemErr>>
for BatchExecutionResponseItem
{
fn from(
value: Result<Update, BatchExecutionResponseItemErr>,
) -> Self {
match value {
Ok(update) => Self::Ok(update),
Err(e) => Self::Err(e),
}
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchExecutionResponseItemErr {
pub name: String,
pub error: _Serror,
}

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Runs the target Procedure. Response: [Update]
#[typeshare]
@@ -26,3 +26,31 @@ pub struct RunProcedure {
/// Id or name
pub procedure: String,
}
/// Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchRunProcedure {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* procedures
/// foo-*
/// # add some more
/// extra-procedure-1, extra-procedure-2
/// ```
pub pattern: String,
}

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
//
@@ -39,6 +39,36 @@ pub struct CloneRepo {
//
/// Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchCloneRepo {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* repos
/// foo-*
/// # add some more
/// extra-repo-1, extra-repo-2
/// ```
pub pattern: String,
}
//
/// Pulls the target repo. Response: [Update].
///
/// Note. Repo must have server attached at `server_id`.
@@ -65,6 +95,36 @@ pub struct PullRepo {
//
/// Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchPullRepo {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* repos
/// foo-*
/// # add some more
/// extra-repo-1, extra-repo-2
/// ```
pub pattern: String,
}
//
/// Builds the target repo, using the attached builder. Response: [Update].
///
/// Note. Repo must have builder attached at `builder_id`.
@@ -95,6 +155,36 @@ pub struct BuildRepo {
//
/// Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchBuildRepo {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* repos
/// foo-*
/// # add some more
/// extra-repo-1, extra-repo-2
/// ```
pub pattern: String,
}
//
/// Cancels the target repo build.
/// Only does anything if the repo build is `building` when called.
/// Response: [Update]

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
use super::{BatchExecutionResponse, KomodoExecuteRequest};
/// Deploys the target stack. `docker compose up`. Response: [Update]
#[typeshare]
@@ -30,6 +30,38 @@ pub struct DeployStack {
pub stop_time: Option<i32>,
}
//
/// Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDeployStack {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* stacks
/// foo-*
/// # add some more
/// extra-stack-1, extra-stack-2
/// ```
pub pattern: String,
}
//
/// Checks deployed contents vs latest contents,
/// and only if any changes found
/// will `docker compose up`. Response: [Update]
@@ -56,6 +88,36 @@ pub struct DeployStackIfChanged {
//
/// Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDeployStackIfChanged {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///
/// Example:
/// ```
/// # match all foo-* stacks
/// foo-*
/// # add some more
/// extra-stack-1, extra-stack-2
/// ```
pub pattern: String,
}
//
/// Starts the target stack. `docker compose start`. Response: [Update]
#[typeshare]
#[derive(
@@ -198,3 +260,33 @@ pub struct DestroyStack {
/// Override the default termination max time.
pub stop_time: Option<i32>,
}
//
/// Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult].
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
PartialEq,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(BatchExecutionResponse)]
pub struct BatchDestroyStack {
/// Id or name or wildcard pattern or regex.
/// Supports multiline and comma delineated combinations of the above.
///d
/// Example:
/// ```
/// # match all foo-* stacks
/// foo-*
/// # add some more
/// extra-stack-1, extra-stack-2
/// ```
pub pattern: String,
}

View File

@@ -0,0 +1,213 @@
use serde::{de::Visitor, Deserializer};
pub fn maybe_string_i64_deserializer<'de, D>(
deserializer: D,
) -> Result<i64, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(MaybeStringI64Visitor)
}
pub fn option_maybe_string_i64_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<i64>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionMaybeStringI64Visitor)
}
struct MaybeStringI64Visitor;
impl<'de> Visitor<'de> for MaybeStringI64Visitor {
type Value = i64;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "number or string number")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse::<i64>().map_err(E::custom)
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v)
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v as i64)
}
}
struct OptionMaybeStringI64Visitor;
impl<'de> Visitor<'de> for OptionMaybeStringI64Visitor {
type Value = Option<i64>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or number or string number")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
MaybeStringI64Visitor.visit_str(v).map(Some)
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v))
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(v as i64))
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -6,6 +6,7 @@ mod file_contents;
mod labels;
mod string_list;
mod term_signal_labels;
mod maybe_string_i64;
pub use conversion::*;
pub use environment::*;
@@ -13,3 +14,4 @@ pub use file_contents::*;
pub use labels::*;
pub use string_list::*;
pub use term_signal_labels::*;
pub use maybe_string_i64::*;

View File

@@ -48,10 +48,13 @@ pub struct BuilderListItemInfo {
#[serde(tag = "type", content = "params")]
#[allow(clippy::large_enum_variant)]
pub enum BuilderConfig {
/// Use a connected server an image builder.
/// Use a Periphery address as a Builder.
Url(UrlBuilderConfig),
/// Use a connected server as a Builder.
Server(ServerBuilderConfig),
/// Use EC2 instances spawned on demand as an image builder.
/// Use EC2 instances spawned on demand as a Builder.
Aws(AwsBuilderConfig),
}
@@ -76,19 +79,21 @@ impl Default for BuilderConfig {
#[serde(tag = "type", content = "params")]
#[allow(clippy::large_enum_variant)]
pub enum PartialBuilderConfig {
Url(#[serde(default)] _PartialUrlBuilderConfig),
Server(#[serde(default)] _PartialServerBuilderConfig),
Aws(#[serde(default)] _PartialAwsBuilderConfig),
}
impl Default for PartialBuilderConfig {
fn default() -> Self {
Self::Aws(Default::default())
Self::Url(Default::default())
}
}
impl MaybeNone for PartialBuilderConfig {
fn is_none(&self) -> bool {
match self {
PartialBuilderConfig::Url(config) => config.is_none(),
PartialBuilderConfig::Server(config) => config.is_none(),
PartialBuilderConfig::Aws(config) => config.is_none(),
}
@@ -98,6 +103,7 @@ impl MaybeNone for PartialBuilderConfig {
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BuilderConfigDiff {
Url(UrlBuilderConfigDiff),
Server(ServerBuilderConfigDiff),
Aws(AwsBuilderConfigDiff),
}
@@ -105,6 +111,9 @@ pub enum BuilderConfigDiff {
impl From<BuilderConfigDiff> for PartialBuilderConfig {
fn from(value: BuilderConfigDiff) -> Self {
match value {
BuilderConfigDiff::Url(diff) => {
PartialBuilderConfig::Url(diff.into())
}
BuilderConfigDiff::Server(diff) => {
PartialBuilderConfig::Server(diff.into())
}
@@ -120,6 +129,9 @@ impl Diff for BuilderConfigDiff {
&self,
) -> impl Iterator<Item = partial_derive2::FieldDiff> {
match self {
BuilderConfigDiff::Url(diff) => {
diff.iter_field_diffs().collect::<Vec<_>>().into_iter()
}
BuilderConfigDiff::Server(diff) => {
diff.iter_field_diffs().collect::<Vec<_>>().into_iter()
}
@@ -138,10 +150,27 @@ impl PartialDiff<PartialBuilderConfig, BuilderConfigDiff>
partial: PartialBuilderConfig,
) -> BuilderConfigDiff {
match self {
BuilderConfig::Url(original) => match partial {
PartialBuilderConfig::Url(partial) => {
BuilderConfigDiff::Url(original.partial_diff(partial))
}
PartialBuilderConfig::Server(partial) => {
let default = ServerBuilderConfig::default();
BuilderConfigDiff::Server(default.partial_diff(partial))
}
PartialBuilderConfig::Aws(partial) => {
let default = AwsBuilderConfig::default();
BuilderConfigDiff::Aws(default.partial_diff(partial))
}
},
BuilderConfig::Server(original) => match partial {
PartialBuilderConfig::Server(partial) => {
BuilderConfigDiff::Server(original.partial_diff(partial))
}
PartialBuilderConfig::Url(partial) => {
let default = UrlBuilderConfig::default();
BuilderConfigDiff::Url(default.partial_diff(partial))
}
PartialBuilderConfig::Aws(partial) => {
let default = AwsBuilderConfig::default();
BuilderConfigDiff::Aws(default.partial_diff(partial))
@@ -151,6 +180,10 @@ impl PartialDiff<PartialBuilderConfig, BuilderConfigDiff>
PartialBuilderConfig::Aws(partial) => {
BuilderConfigDiff::Aws(original.partial_diff(partial))
}
PartialBuilderConfig::Url(partial) => {
let default = UrlBuilderConfig::default();
BuilderConfigDiff::Url(default.partial_diff(partial))
}
PartialBuilderConfig::Server(partial) => {
let default = ServerBuilderConfig::default();
BuilderConfigDiff::Server(default.partial_diff(partial))
@@ -163,6 +196,7 @@ impl PartialDiff<PartialBuilderConfig, BuilderConfigDiff>
impl MaybeNone for BuilderConfigDiff {
fn is_none(&self) -> bool {
match self {
BuilderConfigDiff::Url(config) => config.is_none(),
BuilderConfigDiff::Server(config) => config.is_none(),
BuilderConfigDiff::Aws(config) => config.is_none(),
}
@@ -172,6 +206,9 @@ impl MaybeNone for BuilderConfigDiff {
impl From<PartialBuilderConfig> for BuilderConfig {
fn from(value: PartialBuilderConfig) -> BuilderConfig {
match value {
PartialBuilderConfig::Url(server) => {
BuilderConfig::Url(server.into())
}
PartialBuilderConfig::Server(server) => {
BuilderConfig::Server(server.into())
}
@@ -185,6 +222,9 @@ impl From<PartialBuilderConfig> for BuilderConfig {
impl From<BuilderConfig> for PartialBuilderConfig {
fn from(value: BuilderConfig) -> Self {
match value {
BuilderConfig::Url(config) => {
PartialBuilderConfig::Url(config.into())
}
BuilderConfig::Server(config) => {
PartialBuilderConfig::Server(config.into())
}
@@ -202,6 +242,16 @@ impl MergePartial for BuilderConfig {
partial: PartialBuilderConfig,
) -> BuilderConfig {
match partial {
PartialBuilderConfig::Url(partial) => match self {
BuilderConfig::Url(config) => {
let config = UrlBuilderConfig {
address: partial.address.unwrap_or(config.address),
passkey: partial.passkey.unwrap_or(config.passkey),
};
BuilderConfig::Url(config)
}
_ => BuilderConfig::Url(partial.into()),
},
PartialBuilderConfig::Server(partial) => match self {
BuilderConfig::Server(config) => {
let config = ServerBuilderConfig {
@@ -252,6 +302,42 @@ impl MergePartial for BuilderConfig {
}
}
#[typeshare(serialized_as = "Partial<UrlBuilderConfig>")]
pub type _PartialUrlBuilderConfig = PartialUrlBuilderConfig;
/// Configuration for a Komodo Url Builder.
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
#[partial(skip_serializing_none, from, diff)]
pub struct UrlBuilderConfig {
/// The address of the Periphery agent
#[serde(default = "default_address")]
pub address: String,
/// A custom passkey to use. Otherwise, use the default passkey.
#[serde(default)]
pub passkey: String,
}
fn default_address() -> String {
String::from("https://periphery:8120")
}
impl Default for UrlBuilderConfig {
fn default() -> Self {
Self {
address: default_address(),
passkey: Default::default(),
}
}
}
impl UrlBuilderConfig {
pub fn builder() -> UrlBuilderConfigBuilder {
UrlBuilderConfigBuilder::default()
}
}
#[typeshare(serialized_as = "Partial<ServerBuilderConfig>")]
pub type _PartialServerBuilderConfig = PartialServerBuilderConfig;
@@ -264,11 +350,17 @@ pub type _PartialServerBuilderConfig = PartialServerBuilderConfig;
#[partial(skip_serializing_none, from, diff)]
pub struct ServerBuilderConfig {
/// The server id of the builder
#[serde(alias = "server")]
#[serde(default, alias = "server")]
#[partial_attr(serde(alias = "server"))]
pub server_id: String,
}
impl ServerBuilderConfig {
pub fn builder() -> ServerBuilderConfigBuilder {
ServerBuilderConfigBuilder::default()
}
}
#[typeshare(serialized_as = "Partial<AwsBuilderConfig>")]
pub type _PartialAwsBuilderConfig = PartialAwsBuilderConfig;

View File

@@ -108,8 +108,8 @@ pub struct Env {
pub komodo_oidc_enabled: Option<bool>,
/// Override `oidc_provider`
pub komodo_oidc_provider: Option<String>,
/// Override `oidc_redirect`
pub komodo_oidc_redirect: Option<String>,
/// Override `oidc_redirect_host`
pub komodo_oidc_redirect_host: Option<String>,
/// Override `oidc_client_id`
pub komodo_oidc_client_id: Option<String>,
/// Override `oidc_client_id` from file
@@ -325,18 +325,22 @@ pub struct CoreConfig {
/// Configure OIDC provider address for
/// communcation directly with Komodo Core.
///
/// Note. Needs to be reachable from Komodo Core.
/// Eg. `https://accounts.example.internal/application/o/komodo`
///
/// `https://accounts.example.internal/application/o/komodo`
#[serde(default)]
pub oidc_provider: String,
/// Configure OIDC user redirect address.
/// This is the address users are redirected to in their browser,
/// and may be different from `oidc_provider`.
/// If not provided, the `oidc_provider` will be used.
/// Eg. `https://accounts.example.external/application/o/komodo`
/// Configure OIDC user redirect host.
///
/// This is the host address users are redirected to in their browser,
/// and may be different from `oidc_provider` host.
/// DO NOT include the `path` part, this must be inferred.
/// If not provided, the host will be the same as `oidc_provider`.
/// Eg. `https://accounts.example.external`
#[serde(default)]
pub oidc_redirect: String,
pub oidc_redirect_host: String,
/// Set OIDC client id
#[serde(default)]
@@ -580,7 +584,7 @@ impl CoreConfig {
local_auth: config.local_auth,
oidc_enabled: config.oidc_enabled,
oidc_provider: config.oidc_provider,
oidc_redirect: config.oidc_redirect,
oidc_redirect_host: config.oidc_redirect_host,
oidc_client_id: empty_or_redacted(&config.oidc_client_id),
oidc_client_secret: empty_or_redacted(
&config.oidc_client_secret,

View File

@@ -168,7 +168,7 @@ pub fn get_image_name(
pub fn to_komodo_name(name: &str) -> String {
name
.to_lowercase()
.replace([' ', '.'], "_")
.replace([' ', '.', ',', '\n'], "_")
.trim()
.to_string()
}

View File

@@ -27,6 +27,8 @@ pub struct ServerListItemInfo {
pub state: ServerState,
/// Region of the server.
pub region: String,
/// Address of the server.
pub address: String,
/// Whether server is configured to send unreachable alerts.
pub send_unreachable_alerts: bool,
/// Whether server is configured to send cpu alerts.

View File

@@ -127,7 +127,7 @@ apt upgrade -y
curl -fsSL https://get.docker.com | sh
systemctl enable docker.service
systemctl enable containerd.service
curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py | python3
curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py | HOME=/root python3
systemctl enable periphery.service")
}

View File

@@ -121,7 +121,7 @@ runcmd:
- curl -fsSL https://get.docker.com | sh
- systemctl enable docker.service
- systemctl enable containerd.service
- curl -sSL 'https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py' | python3
- curl -sSL 'https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py' | HOME=/root python3
- systemctl enable periphery.service")
}

View File

@@ -11,6 +11,7 @@ use typeshare::typeshare;
use crate::deserializers::{
env_vars_deserializer, file_contents_deserializer,
option_env_vars_deserializer, option_file_contents_deserializer,
option_maybe_string_i64_deserializer,
option_string_list_deserializer, string_list_deserializer,
};
@@ -563,8 +564,8 @@ impl super::resource::AddFilters for StackQuerySpecifics {
}
}
/// Keeping this minimal for now as its only needed to parse the service names / container names
#[typeshare]
/// Keeping this minimal for now as its only needed to parse the service names / container names,
/// and replica count. Not a typeshared type.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComposeFile {
/// If not provided, will default to the parent folder holding the compose file.
@@ -573,9 +574,18 @@ pub struct ComposeFile {
pub services: HashMap<String, ComposeService>,
}
#[typeshare]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComposeService {
pub image: Option<String>,
pub container_name: Option<String>,
pub deploy: Option<ComposeServiceDeploy>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComposeServiceDeploy {
#[serde(
default,
deserialize_with = "option_maybe_string_i64_deserializer"
)]
pub replicas: Option<i64>,
}

View File

@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "1.16.4",
"version": "1.16.7",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",

View File

@@ -349,28 +349,36 @@ export type ExecuteResponses = {
// ==== DEPLOYMENT ====
Deploy: Types.Update;
BatchDeploy: Types.BatchExecutionResponse;
StartDeployment: Types.Update;
RestartDeployment: Types.Update;
PauseDeployment: Types.Update;
UnpauseDeployment: Types.Update;
StopDeployment: Types.Update;
DestroyDeployment: Types.Update;
BatchDestroyDeployment: Types.BatchExecutionResponse;
// ==== BUILD ====
RunBuild: Types.Update;
BatchRunBuild: Types.BatchExecutionResponse;
CancelBuild: Types.Update;
// ==== REPO ====
CloneRepo: Types.Update;
BatchCloneRepo: Types.BatchExecutionResponse;
PullRepo: Types.Update;
BatchPullRepo: Types.BatchExecutionResponse;
BuildRepo: Types.Update;
BatchBuildRepo: Types.BatchExecutionResponse;
CancelRepoBuild: Types.Update;
// ==== PROCEDURE ====
RunProcedure: Types.Update;
BatchRunProcedure: Types.BatchExecutionResponse;
// ==== ACTION ====
RunAction: Types.Update;
BatchRunAction: Types.BatchExecutionResponse;
// ==== SERVER TEMPLATE ====
LaunchServer: Types.Update;
@@ -380,13 +388,16 @@ export type ExecuteResponses = {
// ==== STACK ====
DeployStack: Types.Update;
BatchDeployStack: Types.BatchExecutionResponse;
DeployStackIfChanged: Types.Update;
BatchDeployStackIfChanged: Types.BatchExecutionResponse;
StartStack: Types.Update;
RestartStack: Types.Update;
StopStack: Types.Update;
PauseStack: Types.Update;
UnpauseStack: Types.Update;
DestroyStack: Types.Update;
BatchDestroyStack: Types.BatchExecutionResponse;
// ==== STACK Service ====
DeployStackService: Types.Update;

View File

@@ -198,6 +198,12 @@ export interface AlerterQuerySpecifics {
export type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;
export type BatchExecutionResponseItem =
| { status: "Ok", data: Update }
| { status: "Err", data: BatchExecutionResponseItemErr };
export type BatchExecutionResponse = BatchExecutionResponseItem[];
export interface Version {
major: number;
minor: number;
@@ -389,9 +395,11 @@ export interface BuildQuerySpecifics {
export type BuildQuery = ResourceQuery<BuildQuerySpecifics>;
export type BuilderConfig =
/** Use a connected server an image builder. */
/** Use a Periphery address as a Builder. */
| { type: "Url", params: UrlBuilderConfig }
/** Use a connected server as a Builder. */
| { type: "Server", params: ServerBuilderConfig }
/** Use EC2 instances spawned on demand as an image builder. */
/** Use EC2 instances spawned on demand as a Builder. */
| { type: "Aws", params: AwsBuilderConfig };
export type Builder = Resource<BuilderConfig, undefined>;
@@ -418,19 +426,27 @@ export type Execution =
/** The "null" execution. Does nothing. */
| { type: "None", params: NoData }
| { type: "RunAction", params: RunAction }
| { type: "BatchRunAction", params: BatchRunAction }
| { type: "RunProcedure", params: RunProcedure }
| { type: "BatchRunProcedure", params: BatchRunProcedure }
| { type: "RunBuild", params: RunBuild }
| { type: "BatchRunBuild", params: BatchRunBuild }
| { type: "CancelBuild", params: CancelBuild }
| { type: "Deploy", params: Deploy }
| { type: "BatchDeploy", params: BatchDeploy }
| { type: "StartDeployment", params: StartDeployment }
| { type: "RestartDeployment", params: RestartDeployment }
| { type: "PauseDeployment", params: PauseDeployment }
| { type: "UnpauseDeployment", params: UnpauseDeployment }
| { type: "StopDeployment", params: StopDeployment }
| { type: "DestroyDeployment", params: DestroyDeployment }
| { type: "BatchDestroyDeployment", params: BatchDestroyDeployment }
| { type: "CloneRepo", params: CloneRepo }
| { type: "BatchCloneRepo", params: BatchCloneRepo }
| { type: "PullRepo", params: PullRepo }
| { type: "BatchPullRepo", params: BatchPullRepo }
| { type: "BuildRepo", params: BuildRepo }
| { type: "BatchBuildRepo", params: BatchBuildRepo }
| { type: "CancelRepoBuild", params: CancelRepoBuild }
| { type: "StartContainer", params: StartContainer }
| { type: "RestartContainer", params: RestartContainer }
@@ -456,13 +472,16 @@ export type Execution =
| { type: "RunSync", params: RunSync }
| { type: "CommitSync", params: CommitSync }
| { type: "DeployStack", params: DeployStack }
| { type: "BatchDeployStack", params: BatchDeployStack }
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
| { type: "BatchDeployStackIfChanged", params: BatchDeployStackIfChanged }
| { type: "StartStack", params: StartStack }
| { type: "RestartStack", params: RestartStack }
| { type: "PauseStack", params: PauseStack }
| { type: "UnpauseStack", params: UnpauseStack }
| { type: "StopStack", params: StopStack }
| { type: "DestroyStack", params: DestroyStack }
| { type: "BatchDestroyStack", params: BatchDestroyStack }
| { type: "Sleep", params: Sleep };
/** Allows to enable / disabled procedures in the sequence / parallel vec on the fly */
@@ -3142,6 +3161,8 @@ export interface ServerListItemInfo {
state: ServerState;
/** Region of the server. */
region: string;
/** Address of the server. */
address: string;
/** Whether server is configured to send unreachable alerts. */
send_unreachable_alerts: boolean;
/** Whether server is configured to send cpu alerts. */
@@ -3387,6 +3408,8 @@ export type _PartialStackConfig = Partial<StackConfig>;
export type _PartialTag = Partial<Tag>;
export type _PartialUrlBuilderConfig = Partial<UrlBuilderConfig>;
export interface __Serror {
error: string;
trace: string[];
@@ -3512,6 +3535,198 @@ export interface AwsServerTemplateConfig {
user_data: string;
}
/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchBuildRepo {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* repos
* foo-*
* # add some more
* extra-repo-1, extra-repo-2
* ```
*/
pattern: string;
}
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchCloneRepo {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* repos
* foo-*
* # add some more
* extra-repo-1, extra-repo-2
* ```
*/
pattern: string;
}
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDeploy {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* deployments
* foo-*
* # add some more
* extra-deployment-1, extra-deployment-2
* ```
*/
pattern: string;
}
/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDeployStack {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* stacks
* foo-*
* # add some more
* extra-stack-1, extra-stack-2
* ```
*/
pattern: string;
}
/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDeployStackIfChanged {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* stacks
* foo-*
* # add some more
* extra-stack-1, extra-stack-2
* ```
*/
pattern: string;
}
/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDestroyDeployment {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* deployments
* foo-*
* # add some more
* extra-deployment-1, extra-deployment-2
* ```
*/
pattern: string;
}
/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDestroyStack {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
* d
* Example:
* ```
* # match all foo-* stacks
* foo-*
* # add some more
* extra-stack-1, extra-stack-2
* ```
*/
pattern: string;
}
export interface BatchExecutionResponseItemErr {
name: string;
error: _Serror;
}
/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchPullRepo {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* repos
* foo-*
* # add some more
* extra-repo-1, extra-repo-2
* ```
*/
pattern: string;
}
/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResult] */
export interface BatchRunAction {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* actions
* foo-*
* # add some more
* extra-action-1, extra-action-2
* ```
*/
pattern: string;
}
/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchRunBuild {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* builds
* foo-*
* # add some more
* extra-build-1, extra-build-2
* ```
*/
pattern: string;
}
/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchRunProcedure {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* procedures
* foo-*
* # add some more
* extra-procedure-1, extra-procedure-2
* ```
*/
pattern: string;
}
/**
* Builds the target repo, using the attached builder. Response: [Update].
*
@@ -3605,18 +3820,6 @@ export interface CommitSync {
sync: string;
}
export interface ComposeService {
image?: string;
container_name?: string;
}
/** Keeping this minimal for now as its only needed to parse the service names / container names */
export interface ComposeFile {
/** If not provided, will default to the parent folder holding the compose file. */
name?: string;
services?: Record<string, ComposeService>;
}
export interface Conversion {
/** reference on the server. */
local: string;
@@ -3811,6 +4014,7 @@ export interface CreateBuildWebhook {
/** Partial representation of [BuilderConfig] */
export type PartialBuilderConfig =
| { type: "Url", params: _PartialUrlBuilderConfig }
| { type: "Server", params: _PartialServerBuilderConfig }
| { type: "Aws", params: _PartialAwsBuilderConfig };
@@ -6452,7 +6656,7 @@ export interface SearchStackServiceLog {
/** Configuration for a Komodo Server Builder. */
export interface ServerBuilderConfig {
/** The server id of the builder */
server_id: string;
server_id?: string;
}
/** The health of a part of the server. */
@@ -6977,6 +7181,14 @@ export interface UpdateVariableValue {
value: string;
}
/** Configuration for a Komodo Url Builder. */
export interface UrlBuilderConfig {
/** The address of the Periphery agent */
address: string;
/** A custom passkey to use. Otherwise, use the default passkey. */
passkey?: string;
}
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteStackFileContents {
/** The name or id of the target Stack. */
@@ -7035,6 +7247,7 @@ export type ExecuteRequest =
| { type: "PruneBuildx", params: PruneBuildx }
| { type: "PruneSystem", params: PruneSystem }
| { type: "Deploy", params: Deploy }
| { type: "BatchDeploy", params: BatchDeploy }
| { type: "StartDeployment", params: StartDeployment }
| { type: "RestartDeployment", params: RestartDeployment }
| { type: "PauseDeployment", params: PauseDeployment }
@@ -7042,21 +7255,30 @@ export type ExecuteRequest =
| { type: "StopDeployment", params: StopDeployment }
| { type: "DestroyDeployment", params: DestroyDeployment }
| { type: "DeployStack", params: DeployStack }
| { type: "BatchDeployStack", params: BatchDeployStack }
| { type: "DeployStackIfChanged", params: DeployStackIfChanged }
| { type: "BatchDeployStackIfChanged", params: BatchDeployStackIfChanged }
| { type: "StartStack", params: StartStack }
| { type: "RestartStack", params: RestartStack }
| { type: "StopStack", params: StopStack }
| { type: "PauseStack", params: PauseStack }
| { type: "UnpauseStack", params: UnpauseStack }
| { type: "DestroyStack", params: DestroyStack }
| { type: "BatchDestroyStack", params: BatchDestroyStack }
| { type: "RunBuild", params: RunBuild }
| { type: "BatchRunBuild", params: BatchRunBuild }
| { type: "CancelBuild", params: CancelBuild }
| { type: "CloneRepo", params: CloneRepo }
| { type: "BatchCloneRepo", params: BatchCloneRepo }
| { type: "PullRepo", params: PullRepo }
| { type: "BatchPullRepo", params: BatchPullRepo }
| { type: "BuildRepo", params: BuildRepo }
| { type: "BatchBuildRepo", params: BatchBuildRepo }
| { type: "CancelRepoBuild", params: CancelRepoBuild }
| { type: "RunProcedure", params: RunProcedure }
| { type: "BatchRunProcedure", params: BatchRunProcedure }
| { type: "RunAction", params: RunAction }
| { type: "BatchRunAction", params: BatchRunAction }
| { type: "LaunchServer", params: LaunchServer }
| { type: "RunSync", params: RunSync };

View File

@@ -78,8 +78,9 @@ KOMODO_JWT_TTL="1-day"
KOMODO_OIDC_ENABLED=false
## Must reachable from Komodo Core container
# KOMODO_OIDC_PROVIDER=https://oidc.provider.internal/application/o/komodo
## Must be reachable by users (optional if it is the same as above).
# KOMODO_OIDC_REDIRECT=https://oidc.provider.external/application/o/komodo
## Change the host to one reachable be reachable by users (optional if it is the same as above).
## DO NOT include the `path` part of the URL.
# KOMODO_OIDC_REDIRECT_HOST=https://oidc.provider.external
## Your client credentials
# KOMODO_OIDC_CLIENT_ID= # Alt: KOMODO_OIDC_CLIENT_ID_FILE
# KOMODO_OIDC_CLIENT_SECRET= # Alt: KOMODO_OIDC_CLIENT_SECRET_FILE

View File

@@ -152,15 +152,18 @@ oidc_enabled = false
## Optional, no default.
oidc_provider = "https://oidc.provider.internal/application/o/komodo"
## Configure OIDC user redirect address.
## Configure OIDC user redirect host.
##
## This is the address users are redirected to in their browser,
## and may be different from `oidc_provider` depending on your networking.
## This is the host address users are redirected to in their browser,
## and may be different from `oidc_provider` host depending on your networking.
## If not provided (or empty string ""), the `oidc_provider` will be used.
##
## Env: KOMODO_OIDC_REDIRECT
## Note. DO NOT include the `path` part of the URL.
## Example: `https://oidc.provider.external`
##
## Env: KOMODO_OIDC_REDIRECT_HOST
## Optional, no default.
oidc_redirect = ""
oidc_redirect_host = ""
## Give the OIDC Client ID.
## Env: KOMODO_OIDC_CLIENT_ID or KOMODO_OIDC_CLIENT_ID_FILE

View File

@@ -22,7 +22,7 @@ apt upgrade -y
curl -fsSL https://get.docker.com | sh
systemctl enable docker.service
systemctl enable containerd.service
curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py | python3
curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py | HOME=/root python3
systemctl enable periphery.service
```

View File

@@ -0,0 +1,86 @@
# Procedures and Actions
For orchestrations involving multiple Resources, Komodo offers the `Procedure` and `Action` resource types.
## Procedures
`Procedures` are compositions of many executions, such as `RunBuild` and `DeployStack`.
The executions are grouped into a series of `Stages`, where each `Stage` contains one or more executions
to run **_all at once_**. The Procedure will wait until all of the executions in a `Stage` are complete before moving
on to the next stage. In short, the executions in a `Stage` are run **_in parallel_**, and the stages themselves are
executed **_sequentially_**.
### Batch Executions
Many executions have a `Batch` version you can select, for example [**BatchDeployStackIfChanged**](https://docs.rs/komodo_client/latest/komodo_client/api/execute/struct.BatchDeployStackIfChanged.html). With this, you can match multiple Stacks by name
using [**wildcard syntax**](https://docs.rs/wildcard/latest/wildcard) and [**regex**](https://docs.rs/regex/latest/regex).
### TOML Example
Like all Resources, `Procedures` have a TOML representation, and can be managed in `ResourceSyncs`.
```toml
[[procedure]]
name = "pull-deploy"
description = "Pulls stack-repo, deploys stacks"
[[procedure.config.stage]]
name = "Pull Repo"
executions = [
{ execution.type = "PullRepo", execution.params.pattern = "stack-repo" },
]
[[procedure.config.stage]]
name = "Deploy if changed"
executions = [
# Uses the Batch version, witch matches many stacks by pattern
# This one matches all stacks prefixed with `foo-` (wildcard) and `bar-` (regex).
{ execution.type = "BatchDeployStackIfChanged", execution.params.pattern = "foo-* , \\^bar-.*$\\" },
]
```
## Actions
`Actions` give users the power of Typescript to write calls to the Komodo API.
For example, an `Action` script like this will align the versions and branches of many `Builds`.
```ts
const VERSION = "1.16.5";
const BRANCH = "dev/" + VERSION;
const APPS = ["core", "periphery"];
const ARCHS = ["x86", "aarch64"];
await komodo.write("UpdateVariableValue", {
name: "KOMODO_DEV_VERSION",
value: VERSION,
});
console.log("Updated KOMODO_DEV_VERSION to " + VERSION);
for (const app of APPS) {
for (const arch of ARCHS) {
const name = `komodo-${app}-${arch}-dev`;
await komodo.write("UpdateBuild", {
id: name,
config: {
version: VERSION as any,
branch: BRANCH,
},
});
console.log(
`Updated Build ${name} to version ${VERSION} and branch ${BRANCH}`,
);
}
}
for (const arch of ARCHS) {
const name = `periphery-bin-${arch}-dev`;
await komodo.write("UpdateRepo", {
id: name,
config: {
branch: BRANCH,
},
});
console.log(`Updated Repo ${name} to branch ${BRANCH}`);
}
```

View File

@@ -23,9 +23,10 @@ automatically execute syncs upon pushes to the configured branch.
name = "server-prod"
description = "the prod server"
tags = ["prod"]
config.address = "http://localhost:8120"
config.region = "AshburnDc1"
config.enabled = true # default: false
[server.config]
address = "http://localhost:8120"
region = "AshburnDc1"
enabled = true # default: false
```
### Builder and build
@@ -38,14 +39,15 @@ config.enabled = true # default: false
name = "builder-01"
tags = []
config.type = "Aws"
config.params.region = "us-east-2"
config.params.ami_id = "ami-0e9bd154667944680"
[builder.config.params]
region = "us-east-2"
ami_id = "ami-0e9bd154667944680"
# These things come from your specific setup
config.params.subnet_id = "subnet-xxxxxxxxxxxxxxxxxx"
config.params.key_pair_name = "xxxxxxxx"
config.params.assign_public_ip = true
config.params.use_public_ip = true
config.params.security_group_ids = [
subnet_id = "subnet-xxxxxxxxxxxxxxxxxx"
key_pair_name = "xxxxxxxx"
assign_public_ip = true
use_public_ip = true
security_group_ids = [
"sg-xxxxxxxxxxxxxxxxxx",
"sg-xxxxxxxxxxxxxxxxxx"
]
@@ -56,19 +58,21 @@ config.params.security_group_ids = [
name = "test_logger"
description = "Logs randomly at INFO, WARN, ERROR levels to test logging setups"
tags = ["test"]
config.builder_id = "builder-01"
config.repo = "mbecker20/test_logger"
config.branch = "master"
config.git_account = "mbecker20"
config.image_registry.type = "Standard"
config.image_registry.params.domain = "github.com" # or your custom domain
config.image_registry.params.account = "your_username"
config.image_registry.params.organization = "your_organization" # optinoal
[build.config]
builder_id = "builder-01"
repo = "mbecker20/test_logger"
branch = "master"
git_account = "mbecker20"
image_registry.type = "Standard"
image_registry.params.domain = "github.com" # or your custom domain
image_registry.params.account = "your_username"
image_registry.params.organization = "your_organization" # optinoal
# Set docker labels
config.labels = """
labels = """
org.opencontainers.image.source = https://github.com/mbecker20/test_logger
org.opencontainers.image.description = Logs randomly at INFO, WARN, ERROR levels to test logging setups
org.opencontainers.image.licenses = GPL-3.0"""
org.opencontainers.image.licenses = GPL-3.0
"""
```
### Deployments
@@ -76,7 +80,8 @@ org.opencontainers.image.licenses = GPL-3.0"""
- [Deployment config schema](https://docs.rs/komodo_client/latest/komodo_client/entities/deployment/struct.DeploymentConfig.html)
```toml
[[variable]] # Declare variables
# Declare variables
[[variable]]
name = "OTLP_ENDPOINT"
value = "http://localhost:4317"
@@ -91,20 +96,26 @@ tags = ["test"]
# - has relevant config updates.
# - the attached build has new version.
deploy = true
config.server_id = "server-01"
config.image.type = "Build"
config.image.params.build = "test_logger"
[deployment.config]
server_id = "server-01"
image.type = "Build"
image.params.build = "test_logger"
# set the volumes / bind mounts
config.volumes = """
volumes = """
# Supports comments
/data/logs = /etc/logs
/data/config = /etc/config"""
# And other formats (eg yaml list)
- "/data/config:/etc/config"
"""
# Set the environment variables
config.environment = """
OTLP_ENDPOINT = [[OTLP_ENDPOINT]] # interpolate variables into the envs. (they also support comments using '#')
environment = """
# Comments supported
OTLP_ENDPOINT = [[OTLP_ENDPOINT]] # interpolate variables into the envs.
VARIABLE_1 = value_1
VARIABLE_2 = value_2"""
VARIABLE_2 = value_2
"""
# Set Docker labels
config.labels = "deployment.type = logger"
labels = "deployment.type = logger"
##
@@ -116,17 +127,19 @@ deploy = true
# Create a dependency on test-logger-01. This deployment will only be deployed after test-logger-01 is deployed.
# Additionally, any sync deploy of test-logger-01 will also trigger sync deploy of this deployment.
after = ["test-logger-01"]
config.server_id = "server-01"
config.image.type = "Build"
config.image.params.build = "test_logger"
config.volumes = """
[deployment.config]
server_id = "server-01"
image.type = "Build"
image.params.build = "test_logger"
volumes = """
/data/logs = /etc/logs
/data/config = /etc/config"""
config.environment = """
environment = """
VARIABLE_1 = value_1
VARIABLE_2 = value_2"""
VARIABLE_2 = value_2
"""
# Set Docker labels
config.labels = "deployment.type = logger"
labels = "deployment.type = logger"
```
### Stack
@@ -140,11 +153,12 @@ description = "stack test"
deploy = true
after = ["test-logger-01"] # Stacks can depend on deployments, and vice versa.
tags = ["test"]
config.server_id = "server-prod"
config.file_paths = ["mongo.yaml", "redis.yaml"]
config.git_provider = "git.mogh.tech"
config.git_account = "mbecker20" # clone private repo by specifying account
config.repo = "mbecker20/stack_test"
[stack.config]
server_id = "server-prod"
file_paths = ["mongo.yaml", "redis.yaml"]
git_provider = "git.mogh.tech"
git_account = "mbecker20" # clone private repo by specifying account
repo = "mbecker20/stack_test"
```
### Procedure
@@ -157,28 +171,28 @@ name = "test-procedure"
description = "Do some things in a specific order"
tags = ["test"]
# Each stage will be executed one after the other (in sequence)
[[procedure.config.stage]]
name = "Build stuff"
enabled = true
# The executions within a stage will be run in parallel. The stage completes when all executions finish.
executions = [
{ execution.type = "RunBuild", execution.params.build = "test_logger", enabled = true },
{ execution.type = "PullRepo", execution.params.repo = "komodo-periphery", enabled = true },
{ execution.type = "RunBuild", execution.params.build = "test_logger" },
# Uses the Batch version, witch matches many builds by pattern
# This one matches all builds prefixed with `foo-` (wildcard) and `bar-` (regex).
{ execution.type = "BatchRunBuild", execution.params.pattern = "foo-* , \\^bar-.*$\\" },
{ execution.type = "PullRepo", execution.params.repo = "komodo-periphery" },
]
[[procedure.config.stage]]
name = "Deploy test logger 1"
enabled = true
executions = [
{ execution.type = "Deploy", execution.params.deployment = "test-logger-01", enabled = true }
{ execution.type = "Deploy", execution.params.deployment = "test-logger-01" },
{ execution.type = "Deploy", execution.params.deployment = "test-logger-03", enabled = false },
]
[[procedure.config.stage]]
name = "Deploy test logger 2"
enabled = true
enabled = false
executions = [
{ execution.type = "Deploy", execution.params.deployment = "test-logger-02", enabled = true }
{ execution.type = "Deploy", execution.params.deployment = "test-logger-02" }
]
```
@@ -191,15 +205,19 @@ executions = [
name = "komodo-periphery"
description = "Builds new versions of the periphery binary. Requires Rust installed on the host."
tags = ["komodo"]
config.server_id = "server-01"
config.git_provider = "git.mogh.tech" # use an alternate git provider (default is github.com)
config.git_account = "mbecker20"
config.repo = "mbecker20/komodo"
[repo.config]
server_id = "server-01"
git_provider = "git.mogh.tech" # use an alternate git provider (default is github.com)
git_account = "mbecker20"
repo = "mbecker20/komodo"
# Run an action after the repo is pulled
config.on_pull.path = "."
config.on_pull.command = """
/root/.cargo/bin/cargo build -p komodo_periphery --release && \
cp ./target/release/periphery /root/periphery"""
on_pull.path = "."
on_pull.command = """
# Supports comments
/root/.cargo/bin/cargo build -p komodo_periphery --release
# Multiple lines will be combined together using '&&'
cp ./target/release/periphery /root/periphery
"""
```
### User Group:

View File

@@ -59,6 +59,7 @@ const sidebars: SidebarsConfig = {
},
"docker-compose",
"variables",
"procedures",
"permissioning",
"sync-resources",
"webhooks",

View File

@@ -262,30 +262,41 @@ export type ExecuteResponses = {
PruneBuildx: Types.Update;
PruneSystem: Types.Update;
Deploy: Types.Update;
BatchDeploy: Types.BatchExecutionResponse;
StartDeployment: Types.Update;
RestartDeployment: Types.Update;
PauseDeployment: Types.Update;
UnpauseDeployment: Types.Update;
StopDeployment: Types.Update;
DestroyDeployment: Types.Update;
BatchDestroyDeployment: Types.BatchExecutionResponse;
RunBuild: Types.Update;
BatchRunBuild: Types.BatchExecutionResponse;
CancelBuild: Types.Update;
CloneRepo: Types.Update;
BatchCloneRepo: Types.BatchExecutionResponse;
PullRepo: Types.Update;
BatchPullRepo: Types.BatchExecutionResponse;
BuildRepo: Types.Update;
BatchBuildRepo: Types.BatchExecutionResponse;
CancelRepoBuild: Types.Update;
RunProcedure: Types.Update;
BatchRunProcedure: Types.BatchExecutionResponse;
RunAction: Types.Update;
BatchRunAction: Types.BatchExecutionResponse;
LaunchServer: Types.Update;
RunSync: Types.Update;
DeployStack: Types.Update;
BatchDeployStack: Types.BatchExecutionResponse;
DeployStackIfChanged: Types.Update;
BatchDeployStackIfChanged: Types.BatchExecutionResponse;
StartStack: Types.Update;
RestartStack: Types.Update;
StopStack: Types.Update;
PauseStack: Types.Update;
UnpauseStack: Types.Update;
DestroyStack: Types.Update;
BatchDestroyStack: Types.BatchExecutionResponse;
DeployStackService: Types.Update;
StartStackService: Types.Update;
RestartStackService: Types.Update;

View File

@@ -203,6 +203,14 @@ export interface AlerterQuerySpecifics {
types: AlerterEndpoint["type"][];
}
export type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;
export type BatchExecutionResponseItem = {
status: "Ok";
data: Update;
} | {
status: "Err";
data: BatchExecutionResponseItemErr;
};
export type BatchExecutionResponse = BatchExecutionResponseItem[];
export interface Version {
major: number;
minor: number;
@@ -383,12 +391,17 @@ export interface BuildQuerySpecifics {
}
export type BuildQuery = ResourceQuery<BuildQuerySpecifics>;
export type BuilderConfig =
/** Use a connected server an image builder. */
/** Use a Periphery address as a Builder. */
{
type: "Url";
params: UrlBuilderConfig;
}
/** Use a connected server as a Builder. */
| {
type: "Server";
params: ServerBuilderConfig;
}
/** Use EC2 instances spawned on demand as an image builder. */
/** Use EC2 instances spawned on demand as a Builder. */
| {
type: "Aws";
params: AwsBuilderConfig;
@@ -416,18 +429,30 @@ export type Execution =
} | {
type: "RunAction";
params: RunAction;
} | {
type: "BatchRunAction";
params: BatchRunAction;
} | {
type: "RunProcedure";
params: RunProcedure;
} | {
type: "BatchRunProcedure";
params: BatchRunProcedure;
} | {
type: "RunBuild";
params: RunBuild;
} | {
type: "BatchRunBuild";
params: BatchRunBuild;
} | {
type: "CancelBuild";
params: CancelBuild;
} | {
type: "Deploy";
params: Deploy;
} | {
type: "BatchDeploy";
params: BatchDeploy;
} | {
type: "StartDeployment";
params: StartDeployment;
@@ -446,15 +471,27 @@ export type Execution =
} | {
type: "DestroyDeployment";
params: DestroyDeployment;
} | {
type: "BatchDestroyDeployment";
params: BatchDestroyDeployment;
} | {
type: "CloneRepo";
params: CloneRepo;
} | {
type: "BatchCloneRepo";
params: BatchCloneRepo;
} | {
type: "PullRepo";
params: PullRepo;
} | {
type: "BatchPullRepo";
params: BatchPullRepo;
} | {
type: "BuildRepo";
params: BuildRepo;
} | {
type: "BatchBuildRepo";
params: BatchBuildRepo;
} | {
type: "CancelRepoBuild";
params: CancelRepoBuild;
@@ -530,9 +567,15 @@ export type Execution =
} | {
type: "DeployStack";
params: DeployStack;
} | {
type: "BatchDeployStack";
params: BatchDeployStack;
} | {
type: "DeployStackIfChanged";
params: DeployStackIfChanged;
} | {
type: "BatchDeployStackIfChanged";
params: BatchDeployStackIfChanged;
} | {
type: "StartStack";
params: StartStack;
@@ -551,6 +594,9 @@ export type Execution =
} | {
type: "DestroyStack";
params: DestroyStack;
} | {
type: "BatchDestroyStack";
params: BatchDestroyStack;
} | {
type: "Sleep";
params: Sleep;
@@ -3049,6 +3095,8 @@ export interface ServerListItemInfo {
state: ServerState;
/** Region of the server. */
region: string;
/** Address of the server. */
address: string;
/** Whether server is configured to send unreachable alerts. */
send_unreachable_alerts: boolean;
/** Whether server is configured to send cpu alerts. */
@@ -3227,6 +3275,7 @@ export type _PartialServerBuilderConfig = Partial<ServerBuilderConfig>;
export type _PartialServerConfig = Partial<ServerConfig>;
export type _PartialStackConfig = Partial<StackConfig>;
export type _PartialTag = Partial<Tag>;
export type _PartialUrlBuilderConfig = Partial<UrlBuilderConfig>;
export interface __Serror {
error: string;
trace: string[];
@@ -3345,6 +3394,186 @@ export interface AwsServerTemplateConfig {
/** The user data to deploy the instance with. */
user_data: string;
}
/** Builds multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchBuildRepo {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* repos
* foo-*
* # add some more
* extra-repo-1, extra-repo-2
* ```
*/
pattern: string;
}
/** Clones multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchCloneRepo {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* repos
* foo-*
* # add some more
* extra-repo-1, extra-repo-2
* ```
*/
pattern: string;
}
/** Deploys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDeploy {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* deployments
* foo-*
* # add some more
* extra-deployment-1, extra-deployment-2
* ```
*/
pattern: string;
}
/** Deploys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDeployStack {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* stacks
* foo-*
* # add some more
* extra-stack-1, extra-stack-2
* ```
*/
pattern: string;
}
/** Deploys multiple Stacks if changed in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDeployStackIfChanged {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* stacks
* foo-*
* # add some more
* extra-stack-1, extra-stack-2
* ```
*/
pattern: string;
}
/** Destroys multiple Deployments in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDestroyDeployment {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* deployments
* foo-*
* # add some more
* extra-deployment-1, extra-deployment-2
* ```
*/
pattern: string;
}
/** Destroys multiple Stacks in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchDestroyStack {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
* d
* Example:
* ```
* # match all foo-* stacks
* foo-*
* # add some more
* extra-stack-1, extra-stack-2
* ```
*/
pattern: string;
}
export interface BatchExecutionResponseItemErr {
name: string;
error: _Serror;
}
/** Pulls multiple Repos in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchPullRepo {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* repos
* foo-*
* # add some more
* extra-repo-1, extra-repo-2
* ```
*/
pattern: string;
}
/** Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResult] */
export interface BatchRunAction {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* actions
* foo-*
* # add some more
* extra-action-1, extra-action-2
* ```
*/
pattern: string;
}
/** Runs multiple builds in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchRunBuild {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* builds
* foo-*
* # add some more
* extra-build-1, extra-build-2
* ```
*/
pattern: string;
}
/** Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResult]. */
export interface BatchRunProcedure {
/**
* Id or name or wildcard pattern or regex.
* Supports multiline and comma delineated combinations of the above.
*
* Example:
* ```
* # match all foo-* procedures
* foo-*
* # add some more
* extra-procedure-1, extra-procedure-2
* ```
*/
pattern: string;
}
/**
* Builds the target repo, using the attached builder. Response: [Update].
*
@@ -3431,16 +3660,6 @@ export interface CommitSync {
/** Id or name */
sync: string;
}
export interface ComposeService {
image?: string;
container_name?: string;
}
/** Keeping this minimal for now as its only needed to parse the service names / container names */
export interface ComposeFile {
/** If not provided, will default to the parent folder holding the compose file. */
name?: string;
services?: Record<string, ComposeService>;
}
export interface Conversion {
/** reference on the server. */
local: string;
@@ -3617,6 +3836,9 @@ export interface CreateBuildWebhook {
}
/** Partial representation of [BuilderConfig] */
export type PartialBuilderConfig = {
type: "Url";
params: _PartialUrlBuilderConfig;
} | {
type: "Server";
params: _PartialServerBuilderConfig;
} | {
@@ -5999,7 +6221,7 @@ export interface SearchStackServiceLog {
/** Configuration for a Komodo Server Builder. */
export interface ServerBuilderConfig {
/** The server id of the builder */
server_id: string;
server_id?: string;
}
/** The health of a part of the server. */
export interface ServerHealthState {
@@ -6478,6 +6700,13 @@ export interface UpdateVariableValue {
/** The value to set. */
value: string;
}
/** Configuration for a Komodo Url Builder. */
export interface UrlBuilderConfig {
/** The address of the Periphery agent */
address: string;
/** A custom passkey to use. Otherwise, use the default passkey. */
passkey?: string;
}
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteStackFileContents {
/** The name or id of the target Stack. */
@@ -6586,6 +6815,9 @@ export type ExecuteRequest = {
} | {
type: "Deploy";
params: Deploy;
} | {
type: "BatchDeploy";
params: BatchDeploy;
} | {
type: "StartDeployment";
params: StartDeployment;
@@ -6607,9 +6839,15 @@ export type ExecuteRequest = {
} | {
type: "DeployStack";
params: DeployStack;
} | {
type: "BatchDeployStack";
params: BatchDeployStack;
} | {
type: "DeployStackIfChanged";
params: DeployStackIfChanged;
} | {
type: "BatchDeployStackIfChanged";
params: BatchDeployStackIfChanged;
} | {
type: "StartStack";
params: StartStack;
@@ -6628,30 +6866,51 @@ export type ExecuteRequest = {
} | {
type: "DestroyStack";
params: DestroyStack;
} | {
type: "BatchDestroyStack";
params: BatchDestroyStack;
} | {
type: "RunBuild";
params: RunBuild;
} | {
type: "BatchRunBuild";
params: BatchRunBuild;
} | {
type: "CancelBuild";
params: CancelBuild;
} | {
type: "CloneRepo";
params: CloneRepo;
} | {
type: "BatchCloneRepo";
params: BatchCloneRepo;
} | {
type: "PullRepo";
params: PullRepo;
} | {
type: "BatchPullRepo";
params: BatchPullRepo;
} | {
type: "BuildRepo";
params: BuildRepo;
} | {
type: "BatchBuildRepo";
params: BatchBuildRepo;
} | {
type: "CancelRepoBuild";
params: CancelRepoBuild;
} | {
type: "RunProcedure";
params: RunProcedure;
} | {
type: "BatchRunProcedure";
params: BatchRunProcedure;
} | {
type: "RunAction";
params: RunAction;
} | {
type: "BatchRunAction";
params: BatchRunAction;
} | {
type: "LaunchServer";
params: LaunchServer;

View File

@@ -340,6 +340,8 @@ export const ProviderSelector = ({
if (value === "Custom") {
onSelect("");
setCustomMode(true);
} else if (value === "None") {
onSelect("");
} else {
onSelect(value);
}
@@ -365,7 +367,8 @@ export const ProviderSelector = ({
!providers.includes(selected) && (
<SelectItem value={selected}>{selected}</SelectItem>
)}
{showCustom && <SelectItem value={"Custom"}>Custom</SelectItem>}
{showCustom && <SelectItem value="Custom">Custom</SelectItem>}
{!showCustom && <SelectItem value="None">None</SelectItem>}
</SelectContent>
</Select>
);
@@ -908,44 +911,45 @@ export const ImageRegistryConfig = ({
/>
</div>
</ConfigItem>
{organizations.length > 0 && (
<ConfigItem
label="Organization"
description="Push the build under an organization namespace, rather than the account namespace."
>
<OrganizationSelector
organizations={organizations}
selected={registry?.organization!}
set={(organization) =>
setRegistry({
...registry,
organization,
})
}
disabled={disabled}
/>
</ConfigItem>
)}
{registry && (
<ConfigItem
label="Account"
description="Select the account used to authenticate against the registry."
>
<AccountSelector
id={resource_id}
type="Builder"
account_type="docker"
provider={registry.domain!}
selected={registry.account}
onSelect={(account) =>
setRegistry({
...registry,
account,
})
}
disabled={disabled}
/>
</ConfigItem>
{registry?.domain && (
<>
<ConfigItem
label="Account"
description="Select the account used to authenticate against the registry."
>
<AccountSelector
id={resource_id}
type="Builder"
account_type="docker"
provider={registry.domain!}
selected={registry.account}
onSelect={(account) =>
setRegistry({
...registry,
account,
})
}
disabled={disabled}
/>
</ConfigItem>
<ConfigItem
label="Organization"
description="Push the build under an organization / project namespace, rather than the account namespace."
>
<OrganizationSelector
organizations={organizations}
selected={registry?.organization!}
set={(organization) =>
setRegistry({
...registry,
organization,
})
}
disabled={disabled}
/>
</ConfigItem>
</>
)}
</>
);
@@ -963,7 +967,7 @@ const OrganizationSelector = ({
disabled: boolean;
}) => {
const [customMode, setCustomMode] = useState(false);
if (customMode || organizations.length === 0) {
if (customMode) {
return (
<Input
placeholder="Input custom organization name"

View File

@@ -36,7 +36,7 @@ export const Layout = () => {
<div className="h-screen overflow-y-scroll">
<div className="container">
<Sidebar />
<div className="lg:ml-64 lg:pl-8 py-24">
<div className="lg:ml-[200px] lg:pl-8 py-[88px]">
<Outlet />
</div>
</div>
@@ -184,7 +184,7 @@ export const Section = ({
{(title || icon || titleRight || titleOther || actions) && (
<div
className={cn(
"flex flex-wrap gap-2 justify-between py-1",
"flex flex-wrap gap-2 justify-between",
itemsCenterTitleRow ? "items-center" : "items-start"
)}
>

View File

@@ -16,6 +16,7 @@ export type MonacoLanguage =
| "toml"
| "json"
| "key_value"
| "string_list"
| "shell"
| "dockerfile"
| "rust"
@@ -28,11 +29,15 @@ export const MonacoEditor = ({
onValueChange,
language,
readOnly,
minHeight,
className,
}: {
value: string | undefined;
onValueChange?: (value: string) => void;
language: MonacoLanguage;
readOnly?: boolean;
minHeight?: number;
className?: string;
}) => {
const [editor, setEditor] =
useState<monaco.editor.IStandaloneCodeEditor | null>(null);
@@ -97,12 +102,8 @@ export const MonacoEditor = ({
containerNode.style.height = `${Math.max(
Math.ceil(contentHeight),
MIN_EDITOR_HEIGHT
minHeight ?? MIN_EDITOR_HEIGHT
)}px`;
// containerNode.style.height = `${Math.max(
// Math.min(Math.ceil(contentHeight), MAX_EDITOR_HEIGHT),
// MIN_EDITOR_HEIGHT
// )}px`;
}, [editor, line_count]);
const { theme: _theme } = useTheme();
@@ -131,7 +132,7 @@ export const MonacoEditor = ({
};
return (
<div className="mx-2 my-1 w-full">
<div className={cn("mx-2 my-1 w-full", className)}>
<Editor
language={language}
value={value}

View File

@@ -1,4 +1,4 @@
import { useRead, useUser } from "@lib/hooks";
import { useAllResources, useUser } from "@lib/hooks";
import { Button } from "@ui/button";
import {
CommandDialog,
@@ -12,18 +12,9 @@ import {
import { Home, Search, User } from "lucide-react";
import { Fragment, ReactNode, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { cn } from "@lib/utils";
import { DeploymentComponents } from "./resources/deployment";
import { BuildComponents } from "./resources/build";
import { ServerComponents } from "./resources/server";
import { ProcedureComponents } from "./resources/procedure";
import { RepoComponents } from "./resources/repo";
import { BuilderComponents } from "./resources/builder";
import { AlerterComponents } from "./resources/alerter";
import { ServerTemplateComponents } from "./resources/server-template";
import { cn, RESOURCE_TARGETS, usableResourcePath } from "@lib/utils";
import { Badge } from "@ui/badge";
import { ResourceSyncComponents } from "./resources/resource-sync";
import { StackComponents } from "./resources/stack";
import { ResourceComponents } from "./resources";
export const OmniSearch = ({
className,
@@ -116,16 +107,7 @@ const useOmniItems = (
search: string
): Record<string, OmniItem[]> => {
const user = useUser().data;
const servers = useRead("ListServers", {}).data;
const deployments = useRead("ListDeployments", {}).data;
const stacks = useRead("ListStacks", {}).data;
const builds = useRead("ListBuilds", {}).data;
const repos = useRead("ListRepos", {}).data;
const procedures = useRead("ListProcedures", {}).data;
const builders = useRead("ListBuilders", {}).data;
const alerters = useRead("ListAlerters", {}).data;
const templates = useRead("ListServerTemplates", {}).data;
const syncs = useRead("ListResourceSyncs", {}).data;
const resources = useAllResources();
const searchTerms = search
.toLowerCase()
.split(" ")
@@ -139,66 +121,21 @@ const useOmniItems = (
icon: <Home className="w-4 h-4" />,
onSelect: () => nav("/"),
},
{
key: "Servers",
label: "Servers",
icon: <ServerComponents.Icon />,
onSelect: () => nav("/servers"),
},
{
key: "Deployments",
label: "Deployments",
icon: <DeploymentComponents.Icon />,
onSelect: () => nav("/deployments"),
},
{
key: "Stacks",
label: "Stacks",
icon: <StackComponents.Icon />,
onSelect: () => nav("/stacks"),
},
{
key: "Builds",
label: "Builds",
icon: <BuildComponents.Icon />,
onSelect: () => nav("/builds"),
},
{
key: "Repos",
label: "Repos",
icon: <RepoComponents.Icon />,
onSelect: () => nav("/repos"),
},
{
key: "Procedures",
label: "Procedures",
icon: <ProcedureComponents.Icon />,
onSelect: () => nav("/procedures"),
},
{
key: "Builders",
label: "Builders",
icon: <BuilderComponents.Icon />,
onSelect: () => nav("/builders"),
},
{
key: "Alerters",
label: "Alerters",
icon: <AlerterComponents.Icon />,
onSelect: () => nav("/alerters"),
},
{
key: "Templates",
label: "Templates",
icon: <ServerTemplateComponents.Icon />,
onSelect: () => nav("/server-templates"),
},
{
key: "Syncs",
label: "Syncs",
icon: <ResourceSyncComponents.Icon />,
onSelect: () => nav("/resource-syncs"),
},
...RESOURCE_TARGETS.map((_type) => {
const type =
_type === "ResourceSync"
? "Sync"
: _type === "ServerTemplate"
? "Template"
: _type;
const Components = ResourceComponents[_type];
return {
key: type + "s",
label: type + "s",
icon: <Components.Icon />,
onSelect: () => nav(usableResourcePath(_type)),
};
}),
(user?.admin && {
key: "Users",
label: "Users",
@@ -214,200 +151,39 @@ const useOmniItems = (
searchTerms.every((term) => label.includes(term))
);
}),
Servers:
servers
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"server".includes(term)
...Object.fromEntries(
RESOURCE_TARGETS.map((_type) => {
const type =
_type === "ResourceSync"
? "Sync"
: _type === "ServerTemplate"
? "Template"
: _type;
const lower = type.toLowerCase();
const Components = ResourceComponents[_type];
return [
type + "s",
resources[_type]
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
lower.includes(term)
)
)
)
.map((server) => ({
key: "server-" + server.name,
label: server.name,
icon: <ServerComponents.Icon id={server.id} />,
onSelect: () => nav(`/servers/${server.id}`),
})) || [],
Deployments:
deployments
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"deployment".includes(term)
)
)
.map((deployment) => ({
key: "deployment-" + deployment.name,
label: deployment.name,
icon: <DeploymentComponents.Icon id={deployment.id} />,
onSelect: () => nav(`/deployments/${deployment.id}`),
})) || [],
Stacks:
stacks
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"stack".includes(term)
)
)
.map((stack) => ({
key: "stack-" + stack.name,
label: stack.name,
icon: <StackComponents.Icon id={stack.id} />,
onSelect: () => nav(`/stacks/${stack.id}`),
})) || [],
Build:
builds
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"build".includes(term)
)
)
.map((build) => ({
key: "build-" + build.name,
label: build.name,
icon: <BuildComponents.Icon id={build.id} />,
onSelect: () => nav(`/builds/${build.id}`),
})) || [],
Repos:
repos
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"repo".includes(term)
)
)
.map((repo) => ({
key: "repo-" + repo.name,
label: repo.name,
icon: <RepoComponents.Icon id={repo.id} />,
onSelect: () => nav(`/repos/${repo.id}`),
})) || [],
Procedures:
procedures
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"procedure".includes(term)
)
)
.map((procedure) => ({
key: "procedure-" + procedure.name,
label: procedure.name,
icon: <ProcedureComponents.Icon id={procedure.id} />,
onSelect: () => nav(`/procedures/${procedure.id}`),
})) || [],
Builders:
builders
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"builder".includes(term)
)
)
.map((builder) => ({
key: "builder-" + builder.name,
label: builder.name,
icon: <BuilderComponents.Icon id={builder.id} />,
onSelect: () => nav(`/builders/${builder.id}`),
})) || [],
Alerters:
alerters
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"alerter".includes(term)
)
)
.map((alerter) => ({
key: "alerter-" + alerter.name,
label: alerter.name,
icon: <AlerterComponents.Icon id={alerter.id} />,
onSelect: () => nav(`/alerters/${alerter.id}`),
})) || [],
Templates:
templates
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"template".includes(term)
)
)
.map((template) => ({
key: "template-" + template.name,
label: template.name,
icon: <ServerTemplateComponents.Icon id={template.id} />,
onSelect: () => nav(`/server-templates/${template.id}`),
})) || [],
Syncs:
syncs
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"sync".includes(term)
)
)
.map((sync) => ({
key: "sync-" + sync.name,
label: sync.name,
icon: <ResourceSyncComponents.Icon id={sync.id} />,
onSelect: () => nav(`/resource-syncs/${sync.id}`),
})) || [],
.map((server) => ({
key: type + "-" + server.name,
label: server.name,
icon: <Components.Icon id={server.id} />,
onSelect: () =>
nav(`/${usableResourcePath(_type)}/${server.id}`),
})) || [],
];
})
),
}),
[
user,
servers,
deployments,
stacks,
builds,
repos,
procedures,
alerters,
builders,
templates,
syncs,
search,
]
[user, resources, search]
);
};

View File

@@ -22,6 +22,7 @@ export const BuilderConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;
if (config?.type === "Aws") return <AwsBuilderConfig id={id} />;
if (config?.type === "Server") return <ServerBuilderConfig id={id} />;
if (config?.type === "Url") return <UrlBuilderConfig id={id} />;
};
const AwsBuilderConfig = ({ id }: { id: string }) => {
@@ -301,6 +302,51 @@ const ServerBuilderConfig = ({ id }: { id: string }) => {
);
};
const UrlBuilderConfig = ({ id }: { id: string }) => {
const perms = useRead("GetPermissionLevel", {
target: { type: "Builder", id },
}).data;
const config = useRead("GetBuilder", { builder: id }).data?.config;
const [update, set] = useLocalStorage<Partial<Types.UrlBuilderConfig>>(
`url-builder-${id}-update-v1`,
{}
);
const { mutateAsync } = useWrite("UpdateBuilder");
if (!config) return null;
const disabled = perms !== Types.PermissionLevel.Write;
return (
<Config
disabled={disabled}
config={config.params as Types.UrlBuilderConfig}
update={update}
set={set}
onSave={async () => {
await mutateAsync({ id, config: { type: "Url", params: update } });
}}
components={{
"": [
{
label: "General",
labelHidden: true,
components: {
address: {
description: "The address of the Periphery agent",
placeholder: "https://periphery:8120",
},
passkey: {
description: "Use a custom passkey to authenticate with Periphery",
placeholder: "Custom passkey",
},
},
},
],
}}
/>
);
};
const ProvidersConfig = (params: {
type: "git" | "docker";
providers: Types.GitProvider[] | Types.DockerRegistry[];

View File

@@ -104,6 +104,7 @@ export const BuilderComponents: RequiredResourceComponents = {
<SelectGroup>
<SelectItem value="Aws">Aws</SelectItem>
<SelectItem value="Server">Server</SelectItem>
<SelectItem value="Url">Url</SelectItem>
</SelectGroup>
</SelectContent>
</Select>

View File

@@ -2,7 +2,7 @@ import {
ActionWithDialog,
ConfirmButton,
CopyButton,
TextUpdateMenu2,
TextUpdateMenuSimple,
} from "@components/util";
import {
useInvalidate,
@@ -70,7 +70,7 @@ export const ResourceDescription = ({
});
return (
<TextUpdateMenu2
<TextUpdateMenuSimple
title="Update Description"
placeholder="Set Description"
value={resource?.description}

View File

@@ -13,7 +13,13 @@ import {
useWrite,
} from "@lib/hooks";
import { Types } from "komodo_client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { Input } from "@ui/input";
import { useEffect, useState } from "react";
import { CopyWebhook, ResourceSelector } from "../common";
@@ -54,6 +60,7 @@ import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { filterBySplit } from "@lib/utils";
import { useToast } from "@ui/use-toast";
import { fmt_upper_camelcase } from "@lib/formatting";
import { TextUpdateMenuMonaco } from "@components/util";
export const ProcedureConfig = ({ id }: { id: string }) => {
const procedure = useRead("GetProcedure", { procedure: id }).data;
@@ -404,6 +411,7 @@ const Stage = ({
columns={[
{
header: "Execution",
size: 250,
cell: ({ row: { original, index } }) => (
<ExecutionTypeSelector
disabled={disabled}
@@ -432,6 +440,7 @@ const Stage = ({
},
{
header: "Target",
size: 250,
cell: ({
row: {
original: {
@@ -464,6 +473,7 @@ const Stage = ({
},
{
header: "Add / Remove",
size: 150,
cell: ({ row: { index } }) => (
<div className="flex items-center gap-2">
<Button
@@ -501,6 +511,7 @@ const Stage = ({
},
{
header: "Enabled",
size: 100,
cell: ({
row: {
original: { enabled },
@@ -652,6 +663,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchRunProcedure: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match procedures"
value={
params.pattern ||
"# Match procedures by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
// Action
RunAction: {
params: { action: "" },
@@ -664,6 +691,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchRunAction: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match actions"
value={
params.pattern ||
"# Match actions by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
// Build
RunBuild: {
params: { build: "" },
@@ -676,6 +719,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchRunBuild: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match builds"
value={
params.pattern ||
"# Match builds by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
CancelBuild: {
params: { build: "" },
Component: ({ params, setParams, disabled }) => (
@@ -701,6 +760,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
);
},
},
BatchDeploy: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match deployments"
value={
params.pattern ||
"# Match deployments by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
StartDeployment: {
params: { deployment: "" },
Component: ({ params, setParams, disabled }) => (
@@ -767,6 +842,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchDestroyDeployment: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match deployments"
value={
params.pattern ||
"# Match deployments by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
// Stack
DeployStack: {
params: { stack: "" },
@@ -779,6 +870,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchDeployStack: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match stacks"
value={
params.pattern ||
"# Match stacks by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
DeployStackIfChanged: {
params: { stack: "" },
Component: ({ params, setParams, disabled }) => (
@@ -790,6 +897,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchDeployStackIfChanged: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match stacks"
value={
params.pattern ||
"# Match stacks by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
StartStack: {
params: { stack: "" },
Component: ({ params, setParams, disabled }) => (
@@ -856,6 +979,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchDestroyStack: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match stacks"
value={
params.pattern ||
"# Match stacks by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
// Repo
CloneRepo: {
params: { repo: "" },
@@ -868,6 +1007,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchCloneRepo: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match repos"
value={
params.pattern ||
"# Match repos by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
PullRepo: {
params: { repo: "" },
Component: ({ params, setParams, disabled }) => (
@@ -879,6 +1034,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchPullRepo: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match repos"
value={
params.pattern ||
"# Match repos by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
BuildRepo: {
params: { repo: "" },
Component: ({ params, setParams, disabled }) => (
@@ -890,6 +1061,22 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
/>
),
},
BatchBuildRepo: {
params: { pattern: "" },
Component: ({ params, setParams, disabled }) => (
<TextUpdateMenuMonaco
title="Match repos"
value={
params.pattern ||
"# Match repos by name, id, wildcard, or \\regex\\.\n"
}
onUpdate={(pattern) => setParams({ pattern })}
disabled={disabled}
language="string_list"
fullWidth
/>
),
},
CancelRepoBuild: {
params: { repo: "" },
Component: ({ params, setParams, disabled }) => (

View File

@@ -1,14 +1,13 @@
import { DockerContainersSection } from "@components/util";
import { useRead } from "@lib/hooks";
import { ReactNode } from "react";
export const Containers = ({
id,
show,
setShow,
titleOther
}: {
id: string;
show: boolean;
setShow: (show: boolean) => void;
titleOther: ReactNode
}) => {
const containers =
useRead("ListDockerContainers", { server: id }, { refetchInterval: 10_000 })
@@ -17,8 +16,7 @@ export const Containers = ({
<DockerContainersSection
server_id={id}
containers={containers}
show={show}
setShow={setShow}
titleOther={titleOther}
pruneButton
/>
);

View File

@@ -1,20 +1,18 @@
import { Section } from "@components/layouts";
import { DockerResourceLink, ShowHideButton } from "@components/util";
import { DockerResourceLink } from "@components/util";
import { format_size_bytes } from "@lib/formatting";
import { useRead } from "@lib/hooks";
import { Badge } from "@ui/badge";
import { DataTable, SortableHeader } from "@ui/data-table";
import { HardDrive } from "lucide-react";
import { ReactNode } from "react";
import { Prune } from "../actions";
export const Images = ({
id,
show,
setShow,
titleOther,
}: {
id: string;
show: boolean;
setShow: (show: boolean) => void;
titleOther: ReactNode;
}) => {
const images =
useRead("ListDockerImages", { server: id }, { refetchInterval: 10_000 })
@@ -23,62 +21,52 @@ export const Images = ({
const allInUse = images.every((image) => image.in_use);
return (
<div className={show ? "mb-8" : undefined}>
<Section
title="Images"
icon={<HardDrive className="w-4 h-4" />}
actions={
<div className="flex items-center gap-2">
{!allInUse && <Prune server_id={id} type="Images" />}
<ShowHideButton show={show} setShow={setShow} />
</div>
}
>
{show && (
<DataTable
tableKey="server-images"
data={images}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<DockerResourceLink
type="image"
server_id={id}
name={row.original.name}
id={row.original.id}
extra={
!row.original.in_use && (
<Badge variant="destructive">Unused</Badge>
)
}
/>
),
size: 200,
},
{
accessorKey: "id",
header: ({ column }) => (
<SortableHeader column={column} title="Id" />
),
},
{
accessorKey: "size",
header: ({ column }) => (
<SortableHeader column={column} title="Size" />
),
cell: ({ row }) =>
row.original.size
? format_size_bytes(row.original.size)
: "Unknown",
},
]}
/>
)}
</Section>
</div>
<Section
titleOther={titleOther}
actions={!allInUse && <Prune server_id={id} type="Images" />}
>
<DataTable
tableKey="server-images"
data={images}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<DockerResourceLink
type="image"
server_id={id}
name={row.original.name}
id={row.original.id}
extra={
!row.original.in_use && (
<Badge variant="destructive">Unused</Badge>
)
}
/>
),
size: 200,
},
{
accessorKey: "id",
header: ({ column }) => (
<SortableHeader column={column} title="Id" />
),
},
{
accessorKey: "size",
header: ({ column }) => (
<SortableHeader column={column} title="Size" />
),
cell: ({ row }) =>
row.original.size
? format_size_bytes(row.original.size)
: "Unknown",
},
]}
/>
</Section>
);
};

View File

@@ -7,7 +7,7 @@ import { useLocalStorage } from "@lib/hooks";
import { Images } from "./images";
import { Containers } from "./containers";
import { Volumes } from "./volumes";
import { Button } from "@ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
export const ServerInfo = ({
id,
@@ -17,17 +17,9 @@ export const ServerInfo = ({
titleOther: ReactNode;
}) => {
const state = useServer(id)?.info.state ?? Types.ServerState.NotOk;
const [show, setShow] = useLocalStorage<{
containers: boolean;
networks: boolean;
images: boolean;
volumes: boolean;
}>("server-info-show-config", {
containers: true,
networks: true,
images: true,
volumes: true,
});
const [show2, setShow2] = useLocalStorage<
"Containers" | "Networks" | "Volumes" | "Images"
>("server-info-show-config-v2", "Containers");
if ([Types.ServerState.NotOk, Types.ServerState.Disabled].includes(state)) {
return (
@@ -39,50 +31,39 @@ export const ServerInfo = ({
);
}
const anyOpen = !Object.values(show).every((val) => !val);
const tabsList = (
<TabsList className="justify-start w-fit">
<TabsTrigger value="Containers" className="w-[110px]">
Containers
</TabsTrigger>
<TabsTrigger value="Networks" className="w-[110px]">
Networks
</TabsTrigger>
<TabsTrigger value="Volumes" className="w-[110px]">
Volumes
</TabsTrigger>
<TabsTrigger value="Images" className="w-[110px]">
Images
</TabsTrigger>
</TabsList>
);
return (
<Section
titleOther={titleOther}
actions={
<Button
size="sm"
variant="outline"
onClick={() =>
setShow({
containers: !anyOpen,
networks: !anyOpen,
images: !anyOpen,
volumes: !anyOpen,
})
}
>
{anyOpen ? "Hide All" : "Show All"}
</Button>
}
>
<div className="flex flex-col gap-4">
<Containers
id={id}
show={show.containers}
setShow={(containers) => setShow({ ...show, containers })}
/>
<Networks
id={id}
show={show.networks}
setShow={(networks) => setShow({ ...show, networks })}
/>
<Volumes
id={id}
show={show.volumes}
setShow={(volumes) => setShow({ ...show, volumes })}
/>
<Images
id={id}
show={show.images}
setShow={(images) => setShow({ ...show, images })}
/>
</div>
<Section titleOther={titleOther}>
<Tabs value={show2} onValueChange={setShow2 as any}>
<TabsContent value="Containers">
<Containers id={id} titleOther={tabsList} />
</TabsContent>
<TabsContent value="Networks">
<Networks id={id} titleOther={tabsList} />
</TabsContent>
<TabsContent value="Volumes">
<Volumes id={id} titleOther={tabsList} />
</TabsContent>
<TabsContent value="Images">
<Images id={id} titleOther={tabsList} />
</TabsContent>
</Tabs>
</Section>
);
};

View File

@@ -1,19 +1,17 @@
import { Section } from "@components/layouts";
import { DockerResourceLink, ShowHideButton } from "@components/util";
import { DockerResourceLink } from "@components/util";
import { useRead } from "@lib/hooks";
import { Badge } from "@ui/badge";
import { DataTable, SortableHeader } from "@ui/data-table";
import { Network } from "lucide-react";
import { ReactNode } from "react";
import { Prune } from "../actions";
export const Networks = ({
id,
show,
setShow,
titleOther,
}: {
id: string;
show: boolean;
setShow: (show: boolean) => void;
titleOther: ReactNode;
}) => {
const networks =
useRead("ListDockerNetworks", { server: id }, { refetchInterval: 10_000 })
@@ -24,82 +22,72 @@ export const Networks = ({
!network.name
? true
: ["none", "host", "bridge"].includes(network.name)
? true
: network.in_use
? true
: network.in_use
);
return (
<div className={show ? "mb-8" : undefined}>
<Section
title="Networks"
icon={<Network className="w-4 h-4" />}
actions={
<div className="flex items-center gap-2">
{!allInUse && <Prune server_id={id} type="Networks" />}
<ShowHideButton show={show} setShow={setShow} />
</div>
}
>
{show && (
<DataTable
tableKey="server-networks"
data={networks}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<div className="flex items-center gap-2">
<DockerResourceLink
type="network"
server_id={id}
name={row.original.name}
extra={
["none", "host", "bridge"].includes(
row.original.name ?? ""
) ? (
<Badge variant="outline">System</Badge>
) : (
!row.original.in_use && (
<Badge variant="destructive">Unused</Badge>
)
)
}
/>
</div>
),
size: 300,
},
{
accessorKey: "driver",
header: ({ column }) => (
<SortableHeader column={column} title="Driver" />
),
},
{
accessorKey: "scope",
header: ({ column }) => (
<SortableHeader column={column} title="Scope" />
),
},
{
accessorKey: "attachable",
header: ({ column }) => (
<SortableHeader column={column} title="Attachable" />
),
},
{
accessorKey: "ipam_driver",
header: ({ column }) => (
<SortableHeader column={column} title="IPAM Driver" />
),
},
]}
/>
)}
</Section>
</div>
<Section
titleOther={titleOther}
actions={!allInUse && <Prune server_id={id} type="Networks" />}
>
<DataTable
tableKey="server-networks"
data={networks}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<div className="flex items-center gap-2">
<DockerResourceLink
type="network"
server_id={id}
name={row.original.name}
extra={
["none", "host", "bridge"].includes(
row.original.name ?? ""
) ? (
<Badge variant="outline">System</Badge>
) : (
!row.original.in_use && (
<Badge variant="destructive">Unused</Badge>
)
)
}
/>
</div>
),
size: 300,
},
{
accessorKey: "driver",
header: ({ column }) => (
<SortableHeader column={column} title="Driver" />
),
},
{
accessorKey: "scope",
header: ({ column }) => (
<SortableHeader column={column} title="Scope" />
),
},
{
accessorKey: "attachable",
header: ({ column }) => (
<SortableHeader column={column} title="Attachable" />
),
},
{
accessorKey: "ipam_driver",
header: ({ column }) => (
<SortableHeader column={column} title="IPAM Driver" />
),
},
]}
/>
</Section>
);
};

View File

@@ -1,19 +1,17 @@
import { Section } from "@components/layouts";
import { DockerResourceLink, ShowHideButton } from "@components/util";
import { DockerResourceLink } from "@components/util";
import { useRead } from "@lib/hooks";
import { Badge } from "@ui/badge";
import { DataTable, SortableHeader } from "@ui/data-table";
import { Database } from "lucide-react";
import { ReactNode } from "react";
import { Prune } from "../actions";
export const Volumes = ({
id,
show,
setShow,
titleOther,
}: {
id: string;
show: boolean;
setShow: (show: boolean) => void;
titleOther: ReactNode;
}) => {
const volumes =
useRead("ListDockerVolumes", { server: id }, { refetchInterval: 10_000 })
@@ -22,57 +20,47 @@ export const Volumes = ({
const allInUse = volumes.every((volume) => volume.in_use);
return (
<div className={show ? "mb-8" : undefined}>
<Section
title="Volumes"
icon={<Database className="w-4 h-4" />}
actions={
<div className="flex items-center gap-2">
{!allInUse && <Prune server_id={id} type="Volumes" />}
<ShowHideButton show={show} setShow={setShow} />
</div>
}
>
{show && (
<DataTable
tableKey="server-volumes"
data={volumes}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<DockerResourceLink
type="volume"
server_id={id}
name={row.original.name}
extra={
!row.original.in_use && (
<Badge variant="destructive">Unused</Badge>
)
}
/>
),
size: 200,
},
{
accessorKey: "driver",
header: ({ column }) => (
<SortableHeader column={column} title="Driver" />
),
},
{
accessorKey: "scope",
header: ({ column }) => (
<SortableHeader column={column} title="Scope" />
),
},
]}
/>
)}
</Section>
</div>
<Section
titleOther={titleOther}
actions={!allInUse && <Prune server_id={id} type="Volumes" />}
>
<DataTable
tableKey="server-volumes"
data={volumes}
columns={[
{
accessorKey: "name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<DockerResourceLink
type="volume"
server_id={id}
name={row.original.name}
extra={
!row.original.in_use && (
<Badge variant="destructive">Unused</Badge>
)
}
/>
),
size: 200,
},
{
accessorKey: "driver",
header: ({ column }) => (
<SortableHeader column={column} title="Driver" />
),
},
{
accessorKey: "scope",
header: ({ column }) => (
<SortableHeader column={column} title="Scope" />
),
},
]}
/>
</Section>
);
};

View File

@@ -8,11 +8,11 @@ import { cn } from "@lib/utils";
import { DataTable, SortableHeader } from "@ui/data-table";
import { useStack } from ".";
import { Types } from "komodo_client";
import { ReactNode } from "react";
import { Fragment, ReactNode } from "react";
import { Link } from "react-router-dom";
import { Button } from "@ui/button";
import { Layers2 } from "lucide-react";
import { StatusBadge } from "@components/util";
import { DockerResourceLink, StatusBadge } from "@components/util";
export const StackServices = ({
id,
@@ -21,7 +21,9 @@ export const StackServices = ({
id: string;
titleOther: ReactNode;
}) => {
const state = useStack(id)?.info.state ?? Types.StackState.Unknown;
const info = useStack(id)?.info;
const server_id = info?.server_id;
const state = info?.state ?? Types.StackState.Unknown;
const services = useRead(
"ListStackServices",
{ stack: id },
@@ -43,6 +45,7 @@ export const StackServices = ({
columns={[
{
accessorKey: "service",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Service" />
),
@@ -66,18 +69,10 @@ export const StackServices = ({
</Link>
);
},
// size: 200,
},
{
accessorKey: "container.image",
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
cell: ({ row }) => <>{row.original.container?.image}</>,
// size: 200,
},
{
accessorKey: "container.state",
size: 160,
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
@@ -90,7 +85,58 @@ export const StackServices = ({
/>
);
},
// size: 120,
},
{
accessorKey: "container.image",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
cell: ({ row }) =>
server_id && (
<DockerResourceLink
type="image"
server_id={server_id}
name={row.original.container?.image}
id={row.original.container?.image_id}
/>
),
// size: 200,
},
{
accessorKey: "container.networks.0",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Networks" />
),
cell: ({ row }) =>
(row.original.container?.networks.length ?? 0) > 0 ? (
<div className="flex items-center gap-2 flex-wrap">
{server_id &&
row.original.container?.networks.map((network, i) => (
<Fragment key={network}>
<DockerResourceLink
type="network"
server_id={server_id}
name={network}
/>
{i !==
row.original.container!.networks.length - 1 && (
<div className="text-muted-foreground">|</div>
)}
</Fragment>
))}
</div>
) : (
server_id &&
row.original.container?.network_mode && (
<DockerResourceLink
type="network"
server_id={server_id}
name={row.original.container.network_mode}
/>
)
),
},
]}
/>

View File

@@ -18,7 +18,7 @@ import { homeViewAtom } from "@main";
export const Sidebar = () => {
const [view, setView] = useAtom(homeViewAtom);
return (
<div className="fixed top-0 pt-24 w-64 border-r hidden lg:block pr-8 pb-8 h-screen overflow-y-auto">
<div className="fixed top-0 pt-[84px] w-[200px] border-r hidden lg:block pr-8 pb-8 h-screen overflow-y-auto">
<div className="flex flex-col gap-1">
<SidebarLink
label="Dashboard"

View File

@@ -1,5 +1,6 @@
import {
FocusEventHandler,
Fragment,
MouseEventHandler,
ReactNode,
forwardRef,
@@ -54,7 +55,7 @@ import { Section } from "./layouts";
import { DataTable, SortableHeader } from "@ui/data-table";
import { useRead, useUser } from "@lib/hooks";
import { Prune } from "./resources/server/actions";
import { MonacoEditor } from "./monaco";
import { MonacoEditor, MonacoLanguage } from "./monaco";
import { UsableResource } from "@types";
import { ResourceComponents } from "./resources";
@@ -351,7 +352,7 @@ export const CopyButton = ({
);
};
export const TextUpdateMenu = ({
export const TextUpdateMenuMonaco = ({
title,
titleRight,
value = "",
@@ -378,7 +379,7 @@ export const TextUpdateMenu = ({
open?: boolean;
setOpen?: (open: boolean) => void;
triggerHidden?: boolean;
language?: "toml" | "yaml" | "json" | "key_value";
language?: MonacoLanguage;
}) => {
const [_open, _setOpen] = useState(false);
const [__open, __setOpen] = [open ?? _open, setOpen ?? _setOpen];
@@ -406,7 +407,7 @@ export const TextUpdateMenu = ({
triggerClassName
)}
>
{value || placeholder}
{value.split("\n")[0] || placeholder}
</div>
</Card>
</DialogTrigger>
@@ -601,10 +602,10 @@ export const DOCKER_LINK_ICONS: {
!name
? "Warning"
: no_containers
? ["none", "host", "bridge"].includes(name)
? "None"
: "Critical"
: "Good"
? ["none", "host", "bridge"].includes(name)
? "None"
: "Critical"
: "Good"
)
)}
/>
@@ -677,7 +678,7 @@ export const DockerResourceLink = ({
<Icon server_id={server_id} name={type === "image" ? id : name} />
<div
title={name}
className="max-w-[200px] lg:max-w-[300px] overflow-hidden overflow-ellipsis"
className="max-w-[200px] lg:max-w-[250px] overflow-hidden overflow-ellipsis"
>
{name}
</div>
@@ -705,12 +706,14 @@ export const DockerContainersSection = ({
show = true,
setShow,
pruneButton,
titleOther,
}: {
server_id: string;
containers: Types.ListDockerContainersResponse;
show?: boolean;
setShow?: (show: boolean) => void;
pruneButton?: boolean;
titleOther?: ReactNode;
}) => {
const allRunning = useRead("ListDockerContainers", {
server: server_id,
@@ -720,8 +723,9 @@ export const DockerContainersSection = ({
return (
<div className={cn(setShow && show && "mb-8")}>
<Section
title="Containers"
icon={<Box className="w-4 h-4" />}
titleOther={titleOther}
title={!titleOther ? "Containers" : undefined}
icon={!titleOther ? <Box className="w-4 h-4" /> : undefined}
actions={
<div className="flex items-center gap-2">
{pruneButton && !allRunning && (
@@ -738,6 +742,7 @@ export const DockerContainersSection = ({
columns={[
{
accessorKey: "name",
size: 260,
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
@@ -748,10 +753,26 @@ export const DockerContainersSection = ({
name={row.original.name}
/>
),
size: 200,
},
{
accessorKey: "state",
size: 160,
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
cell: ({ row }) => {
const state = row.original?.state;
return (
<StatusBadge
text={state}
intent={container_state_intention(state)}
/>
);
},
},
{
accessorKey: "image",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
@@ -765,32 +786,36 @@ export const DockerContainersSection = ({
),
},
{
accessorKey: "network_mode",
accessorKey: "networks.0",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Network" />
<SortableHeader column={column} title="Networks" />
),
cell: ({ row }) => (
<DockerResourceLink
type="network"
server_id={server_id}
name={row.original.network_mode}
/>
),
},
{
accessorKey: "state",
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
cell: ({ row }) => {
const state = row.original?.state;
return (
<StatusBadge
text={state}
intent={container_state_intention(state)}
/>
);
},
cell: ({ row }) =>
row.original.networks.length > 0 ? (
<div className="flex items-center gap-x-2 flex-wrap">
{row.original.networks.map((network, i) => (
<Fragment key={network}>
<DockerResourceLink
type="network"
server_id={server_id}
name={network}
/>
{i !== row.original.networks.length - 1 && (
<div className="text-muted-foreground">|</div>
)}
</Fragment>
))}
</div>
) : (
row.original.network_mode && (
<DockerResourceLink
type="network"
server_id={server_id}
name={row.original.network_mode}
/>
)
),
},
]}
/>
@@ -833,7 +858,7 @@ export const ResourcePageHeader = ({
);
};
export const TextUpdateMenu2 = ({
export const TextUpdateMenuSimple = ({
title,
titleRight,
value = "",
@@ -875,7 +900,7 @@ export const TextUpdateMenu2 = ({
triggerClassName
)}
>
{value || placeholder}
{value.split("\n")[0] || placeholder}
</div>
</DialogTrigger>
<DialogContent className="min-w-[50vw]">

View File

@@ -19,6 +19,7 @@ import { useToast } from "@ui/use-toast";
import { atom, useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { RESOURCE_TARGETS } from "./utils";
// ============== RESOLVER ==============
@@ -235,6 +236,68 @@ export const useResourceParamType = () => {
return (type[0].toUpperCase() + type.slice(1, -1)) as UsableResource;
};
type ResourceMap = {
[Resource in UsableResource]: Types.ResourceListItem<unknown>[] | undefined;
};
export const useAllResources = (): ResourceMap => {
return {
Server: useRead("ListServers", {}).data,
Stack: useRead("ListStacks", {}).data,
Deployment: useRead("ListDeployments", {}).data,
Build: useRead("ListBuilds", {}).data,
Repo: useRead("ListRepos", {}).data,
Procedure: useRead("ListProcedures", {}).data,
Action: useRead("ListActions", {}).data,
Builder: useRead("ListBuilders", {}).data,
Alerter: useRead("ListAlerters", {}).data,
ServerTemplate: useRead("ListServerTemplates", {}).data,
ResourceSync: useRead("ListResourceSyncs", {}).data,
};
};
// Returns true if Komodo has no resources.
export const useNoResources = () => {
const resources = useAllResources();
for (const target of RESOURCE_TARGETS) {
if (resources[target] && resources[target].length) {
return false;
}
}
return true;
};
/** returns function that takes a resource target and checks if it exists */
export const useCheckResourceExists = () => {
const resources = useAllResources();
return (target: Types.ResourceTarget) => {
return (
resources[target.type as UsableResource]?.some(
(resource) => resource.id === target.id
) || false
);
};
};
export const useFilterResources = <Info>(
resources?: Types.ResourceListItem<Info>[],
search?: string
) => {
const tags = useTagsFilter();
const searchSplit = search?.toLowerCase()?.split(" ") || [];
return (
resources?.filter(
(resource) =>
tags.every((tag: string) => resource.tags.includes(tag)) &&
(searchSplit.length > 0
? searchSplit.every((search) =>
resource.name.toLowerCase().includes(search)
)
: true)
) ?? []
);
};
export const usePushRecentlyViewed = ({ type, id }: Types.ResourceTarget) => {
const userInvalidate = useUserInvalidate();
@@ -285,60 +348,6 @@ export const useTagsFilter = () => {
return tags;
};
/** returns function that takes a resource target and checks if it exists */
export const useCheckResourceExists = () => {
const servers = useRead("ListServers", {}).data;
const deployments = useRead("ListDeployments", {}).data;
const builds = useRead("ListBuilds", {}).data;
const repos = useRead("ListRepos", {}).data;
const procedures = useRead("ListProcedures", {}).data;
const builders = useRead("ListBuilders", {}).data;
const alerters = useRead("ListAlerters", {}).data;
return (target: Types.ResourceTarget) => {
switch (target.type) {
case "Server":
return servers?.some((resource) => resource.id === target.id) || false;
case "Deployment":
return (
deployments?.some((resource) => resource.id === target.id) || false
);
case "Build":
return builds?.some((resource) => resource.id === target.id) || false;
case "Repo":
return repos?.some((resource) => resource.id === target.id) || false;
case "Procedure":
return (
procedures?.some((resource) => resource.id === target.id) || false
);
case "Builder":
return builders?.some((resource) => resource.id === target.id) || false;
case "Alerter":
return alerters?.some((resource) => resource.id === target.id) || false;
default:
return false;
}
};
};
export const useFilterResources = <Info>(
resources?: Types.ResourceListItem<Info>[],
search?: string
) => {
const tags = useTagsFilter();
const searchSplit = search?.toLowerCase()?.split(" ") || [];
return (
resources?.filter(
(resource) =>
tags.every((tag: string) => resource.tags.includes(tag)) &&
(searchSplit.length > 0
? searchSplit.every((search) =>
resource.name.toLowerCase().includes(search)
)
: true)
) ?? []
);
};
export type LocalStorageSetter<T> = (state: T) => T;
export const useLocalStorage = <T>(
@@ -409,44 +418,6 @@ export const useCtrlKeyListener = (listenKey: string, onPress: () => void) => {
});
};
// Returns true if Komodo has no resources.
export const useNoResources = () => {
const servers =
useRead("ListServers", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const deployments =
useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const stacks =
useRead("ListStacks", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const builds =
useRead("ListBuilds", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const repos =
useRead("ListRepos", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const procedures =
useRead("ListProcedures", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const builders =
useRead("ListBuilders", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const alerters =
useRead("ListAlerters", {}, { refetchInterval: 5000 }).data?.length ?? 0;
const templates =
useRead("ListServerTemplates", {}, { refetchInterval: 5000 }).data
?.length ?? 0;
const syncs =
useRead("ListResourceSyncs", {}, { refetchInterval: 5000 }).data?.length ??
0;
return (
servers === 0 &&
deployments === 0 &&
stacks === 0 &&
builds === 0 &&
repos === 0 &&
procedures === 0 &&
builders === 0 &&
alerters === 0 &&
templates === 0 &&
syncs === 0
);
};
export type WebhookIntegration = "Github" | "Gitlab";
export type WebhookIntegrations = {
[key: string]: WebhookIntegration;

View File

@@ -32,6 +32,7 @@ import "./theme";
import "./yaml";
import "./toml";
import "./key_value";
import "./string_list";
import "./shell";
import "./dockerfile";
import "./rust";

View File

@@ -0,0 +1,55 @@
import * as monaco from "monaco-editor";
const string_list_conf: monaco.languages.LanguageConfiguration = {
comments: {
lineComment: "#",
},
autoClosingPairs: [
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};
const string_list_language = <monaco.languages.IMonarchLanguage>{
defaultToken: "",
tokenPostfix: ".string_list",
tokenizer: {
root: [
// Comments
[/#.*$/, "comment"],
// Comma as a delimiter
[/,/, "comment"],
[/\*/, "keyword"],
[/\?/, "keyword"],
// Special syntax: text surrounded by \
// [/\\[^\\]*\\/, "keyword"],
[/\\/, { token: "keyword", next: "@regex" }],
// Main strings separated by spaces or newlines
[/[^\*\?,#\\\s]+/, ""],
// Whitespace
[/[ \t\r\n]+/, ""],
],
regex: [
// Regex tokens
[/\[[^\]]*\]/, ""], // Character classes like [abc]
[/[*+?\.]+/, "keyword"], // Quantifiers like *, +, ?
[/\\./, "string.regexp constant.character.escape"], // Escape sequences like \d, \w
[/[^\\]/, "string"], // Any other regex content
[/\\/, { token: "keyword", next: "@pop" }], // Closing backslash returns to root
],
},
};
// Register the custom language and configuration with Monaco
monaco.languages.register({ id: "string_list" });
monaco.languages.setLanguageConfiguration("string_list", string_list_conf);
monaco.languages.setMonarchTokensProvider("string_list", string_list_language);

View File

@@ -6,7 +6,7 @@ import { useRead } from "@lib/hooks";
import { DataTable, SortableHeader } from "@ui/data-table";
import { Input } from "@ui/input";
import { Box, Search } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { Fragment, useCallback, useMemo, useState } from "react";
export const ContainersPage = () => {
const [search, setSearch] = useState("");
@@ -54,6 +54,7 @@ export const ContainersPage = () => {
columns={[
{
accessorKey: "name",
size: 260,
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
@@ -64,10 +65,26 @@ export const ContainersPage = () => {
name={row.original.name}
/>
),
size: 200,
},
{
accessorKey: "state",
size: 160,
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
cell: ({ row }) => {
const state = row.original?.state;
return (
<StatusBadge
text={state}
intent={container_state_intention(state)}
/>
);
},
},
{
accessorKey: "server_id",
size: 200,
sortingFn: (a, b) => {
const sa = serverName(a.original.server_id!);
const sb = serverName(b.original.server_id!);
@@ -89,6 +106,7 @@ export const ContainersPage = () => {
},
{
accessorKey: "image",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
@@ -102,33 +120,60 @@ export const ContainersPage = () => {
),
},
{
accessorKey: "network_mode",
accessorKey: "networks.0",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Network" />
),
cell: ({ row }) => (
<DockerResourceLink
type="network"
server_id={row.original.server_id!}
name={row.original.network_mode}
/>
<SortableHeader column={column} title="Networks" />
),
cell: ({ row }) =>
row.original.networks.length > 0 ? (
<div className="flex items-center gap-x-2 flex-wrap">
{row.original.networks.map((network, i) => (
<Fragment key={network}>
<DockerResourceLink
type="network"
server_id={row.original.server_id!}
name={network}
/>
{i !== row.original.networks.length - 1 && (
<div className="text-muted-foreground">|</div>
)}
</Fragment>
))}
</div>
) : (
row.original.network_mode && (
<DockerResourceLink
type="network"
server_id={row.original.server_id!}
name={row.original.network_mode}
/>
)
),
},
{
accessorKey: "state",
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
cell: ({ row }) => {
const state = row.original?.state;
return (
<StatusBadge
text={state}
intent={container_state_intention(state)}
/>
);
},
},
// {
// accessorKey: "volumes.0",
// minSize: 300,
// header: ({ column }) => (
// <SortableHeader column={column} title="Volumes" />
// ),
// cell: ({ row }) => (
// <div className="flex items-center gap-x-2 flex-wrap">
// {row.original.volumes.map((volume, i) => (
// <Fragment key={volume}>
// <DockerResourceLink
// type="volume"
// server_id={row.original.server_id!}
// name={volume}
// />
// {i !== row.original.volumes.length - 1 && (
// <div className="text-muted-foreground">|</div>
// )}
// </Fragment>
// ))}
// </div>
// ),
// },
]}
/>
</div>

View File

@@ -25,7 +25,6 @@ import { Link } from "react-router-dom";
export const Dashboard = () => {
const noResources = useNoResources();
const user = useUser().data!;
return (
<>
<ActiveResources />

View File

@@ -4,21 +4,31 @@ import { useServer } from "@components/resources/server";
import {
DOCKER_LINK_ICONS,
DockerLabelsSection,
DockerResourcePageName,
StatusBadge,
DockerResourceLink,
ResourcePageHeader,
ShowHideButton,
} from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { Button } from "@ui/button";
import { DataTable } from "@ui/data-table";
import { ChevronLeft, Clapperboard, Info, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import {
ChevronLeft,
Clapperboard,
Info,
Loader2,
SearchCode,
} from "lucide-react";
import { Link, useParams } from "react-router-dom";
import { ContainerLogs } from "./log";
import { Actions } from "./actions";
import { has_minimum_permissions } from "@lib/utils";
import { Types } from "komodo_client";
import { ResourceUpdates } from "@components/updates/resource";
import { container_state_intention } from "@lib/color";
import { UsableResource } from "@types";
import { Fragment } from "react/jsx-runtime";
import { useEditPermissions } from "@pages/resource";
import { ResourceNotifications } from "@pages/resource-notifications";
import { MonacoEditor } from "@components/monaco";
import { useState } from "react";
export const ContainerPage = () => {
const { type, id, container } = useParams() as {
@@ -41,12 +51,10 @@ const ContainerPageInner = ({
id: string;
container: string;
}) => {
const [showInspect, setShowInspect] = useState(false);
const server = useServer(id);
useSetTitle(`${server?.name} | container | ${container_name}`);
const nav = useNavigate();
const perms = useRead("GetPermissionLevel", {
target: { type: "Server", id },
}).data;
const { canExecute } = useEditPermissions({ type: "Server", id });
const {
data: container,
isPending,
@@ -81,96 +89,231 @@ const ContainerPageInner = ({
);
}
const canExecute = has_minimum_permissions(
perms,
Types.PermissionLevel.Execute
);
const state = list_container?.state ?? Types.ContainerStateStatusEnum.Empty;
const status = list_container?.status;
const intention = container_state_intention(state);
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
<div className="flex flex-col gap-4">
{/* BACK */}
<div className="flex items-center justify-between mb-4">
<Button
className="gap-2"
variant="secondary"
onClick={() => nav("/servers/" + id)}
>
<ChevronLeft className="w-4" /> Back
<div>
<div className="w-full flex items-center justify-between mb-12">
<Link to={"/servers/" + id}>
<Button className="gap-2" variant="secondary">
<ChevronLeft className="w-4" />
Back
</Button>
<NewDeployment id={id} container={container_name} />
</div>
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<DOCKER_LINK_ICONS.container
server_id={id}
</Link>
<NewDeployment id={id} container={container_name} />
</div>
<div className="flex flex-col xl:flex-row gap-4">
{/** HEADER */}
<div className="w-full flex flex-col gap-4">
<div className="flex flex-col gap-2 border rounded-md">
{/* <Components.ResourcePageHeader id={id} /> */}
<ResourcePageHeader
intent={intention}
icon={
<DOCKER_LINK_ICONS.container
server_id={id}
name={container_name}
size={8}
/>
}
name={container_name}
size={8}
state={state}
status={list_container?.status}
/>
<div className="flex flex-col pb-2 px-4">
<div className="flex items-center gap-x-4 gap-y-1 flex-wrap text-muted-foreground">
<ResourceLink type="Server" id={id} />
<AttachedResource id={id} container={container_name} />
{list_container?.image && (
<>
|
<DockerResourceLink
type="image"
server_id={id}
name={list_container.image}
id={list_container.image_id}
muted
/>
</>
)}
{list_container?.networks.map((network) => (
<Fragment key={network}>
|
<DockerResourceLink
type="network"
server_id={id}
name={network}
muted
/>
</Fragment>
))}
{list_container?.volumes.map((volume) => (
<Fragment key={volume}>
|
<DockerResourceLink
type="volume"
server_id={id}
name={volume}
muted
/>
</Fragment>
))}
</div>
</div>
</div>
<DockerResourcePageName name={container_name} />
<div className="flex items-center gap-4 flex-wrap">
<StatusBadge
text={state}
intent={container_state_intention(state)}
/>
{status && (
<p className="text-sm text-muted-foreground">{status}</p>
)}
</div>
</div>
{/* INFO */}
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
<ResourceLink type="Server" id={id} />
<AttachedResource id={id} container={container_name} />
{/* <ResourceDescription type="Server" id={id} disabled={!canWrite} /> */}
</div>
{/** NOTIFICATIONS */}
<ResourceNotifications type="Server" id={id} />
</div>
{/* Actions */}
{canExecute && (
<Section title="Actions" icon={<Clapperboard className="w-4 h-4" />}>
<div className="flex gap-4 items-center flex-wrap">
{Object.entries(Actions).map(([key, Action]) => (
<Action key={key} id={id} container={container_name} />
))}
</div>
<div className="mt-8 flex flex-col gap-12">
{/* Actions */}
{canExecute && (
<Section title="Actions" icon={<Clapperboard className="w-4 h-4" />}>
<div className="flex gap-4 items-center flex-wrap">
{Object.entries(Actions).map(([key, Action]) => (
<Action key={key} id={id} container={container_name} />
))}
</div>
</Section>
)}
{/* Logs */}
<ContainerLogs id={id} container_name={container_name} />
{/* TOP LEVEL CONTAINER INFO */}
<Section title="Details" icon={<Info className="w-4 h-4" />}>
<DataTable
tableKey="container-info"
data={[container]}
columns={[
{
accessorKey: "Id",
header: "Id",
},
{
accessorKey: "Image",
header: "Image",
},
{
accessorKey: "Driver",
header: "Driver",
},
]}
/>
</Section>
)}
{/* Updates */}
<ResourceUpdates type="Server" id={id} />
<DockerLabelsSection labels={container.Config?.Labels} />
<ContainerLogs id={id} container_name={container_name} />
{/* TOP LEVEL CONTAINER INFO */}
<Section title="Details" icon={<Info className="w-4 h-4" />}>
<DataTable
tableKey="container-info"
data={[container]}
columns={[
{
accessorKey: "Image",
header: "Image",
},
{
accessorKey: "Driver",
header: "Driver",
},
]}
/>
</Section>
<DockerLabelsSection labels={container.Config?.Labels} />
<Section
title="Inspect"
icon={<SearchCode className="w-4 h-4" />}
titleRight={
<div className="pl-2">
<ShowHideButton show={showInspect} setShow={setShowInspect} />
</div>
}
>
{showInspect && (
<MonacoEditor
value={JSON.stringify(container, null, 2)}
language="json"
readOnly
/>
)}
</Section>
</div>
</div>
);
// return (
// <div className="flex flex-col gap-16 mb-24">
// {/* HEADER */}
// <div className="flex flex-col gap-4">
// {/* BACK */}
// <div className="flex items-center justify-between mb-4">
// <Button
// className="gap-2"
// variant="secondary"
// onClick={() => nav("/servers/" + id)}
// >
// <ChevronLeft className="w-4" /> Back
// </Button>
// <NewDeployment id={id} container={container_name} />
// </div>
// {/* TITLE */}
// <div className="flex items-center gap-4">
// <div className="mt-1">
// <DOCKER_LINK_ICONS.container
// server_id={id}
// name={container_name}
// size={8}
// />
// </div>
// <DockerResourcePageName name={container_name} />
// <div className="flex items-center gap-4 flex-wrap">
// <StatusBadge
// text={state}
// intent={container_state_intention(state)}
// />
// {status && (
// <p className="text-sm text-muted-foreground">{status}</p>
// )}
// </div>
// </div>
// {/* INFO */}
// <div className="flex flex-wrap gap-4 items-center text-muted-foreground">
// <ResourceLink type="Server" id={id} />
// <AttachedResource id={id} container={container_name} />
// </div>
// </div>
// {/* Actions */}
// {canExecute && (
// <Section title="Actions" icon={<Clapperboard className="w-4 h-4" />}>
// <div className="flex gap-4 items-center flex-wrap">
// {Object.entries(Actions).map(([key, Action]) => (
// <Action key={key} id={id} container={container_name} />
// ))}
// </div>
// </Section>
// )}
// {/* Updates */}
// <ResourceUpdates type="Server" id={id} />
// <ContainerLogs id={id} container_name={container_name} />
// {/* TOP LEVEL CONTAINER INFO */}
// <Section title="Details" icon={<Info className="w-4 h-4" />}>
// <DataTable
// tableKey="container-info"
// data={[container]}
// columns={[
// {
// accessorKey: "Id",
// header: "Id",
// },
// {
// accessorKey: "Image",
// header: "Image",
// },
// {
// accessorKey: "Driver",
// header: "Driver",
// },
// ]}
// />
// </Section>
// <DockerLabelsSection labels={container.Config?.Labels} />
// </div>
// );
};
const AttachedResource = ({
@@ -197,13 +340,10 @@ const AttachedResource = ({
return (
<>
|
<div className="flex gap-2">
<div>{attached.resource.type}:</div>
<ResourceLink
type={attached.resource.type as UsableResource}
id={attached.resource.id}
/>
</div>
<ResourceLink
type={attached.resource.type as UsableResource}
id={attached.resource.id}
/>
</>
);
};

View File

@@ -7,6 +7,7 @@ import {
DockerContainersSection,
DockerLabelsSection,
DockerResourcePageName,
ShowHideButton,
} from "@components/util";
import { fmt_date_with_minutes, format_size_bytes } from "@lib/formatting";
import { useExecute, useRead, useSetTitle } from "@lib/hooks";
@@ -15,8 +16,17 @@ import { Types } from "komodo_client";
import { Badge } from "@ui/badge";
import { Button } from "@ui/button";
import { DataTable } from "@ui/data-table";
import { ChevronLeft, HistoryIcon, Info, Loader2, Trash } from "lucide-react";
import {
ChevronLeft,
HistoryIcon,
Info,
Loader2,
SearchCode,
Trash,
} from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { useState } from "react";
import { MonacoEditor } from "@components/monaco";
export const ImagePage = () => {
const { type, id, image } = useParams() as {
@@ -37,6 +47,7 @@ const ImagePageInner = ({
id: string;
image: string;
}) => {
const [showInspect, setShowInspect] = useState(false);
const server = useServer(id);
useSetTitle(`${server?.name} | image | ${image_name}`);
const nav = useNavigate();
@@ -212,6 +223,24 @@ const ImagePageInner = ({
)}
<DockerLabelsSection labels={image?.Config?.Labels} />
<Section
title="Inspect"
icon={<SearchCode className="w-4 h-4" />}
titleRight={
<div className="pl-2">
<ShowHideButton show={showInspect} setShow={setShowInspect} />
</div>
}
>
{showInspect && (
<MonacoEditor
value={JSON.stringify(image, null, 2)}
language="json"
readOnly
/>
)}
</Section>
</div>
);
};

View File

@@ -8,6 +8,7 @@ import {
DockerOptions,
DockerResourceLink,
DockerResourcePageName,
ShowHideButton,
} from "@components/util";
import { useExecute, useRead, useSetTitle } from "@lib/hooks";
import { has_minimum_permissions } from "@lib/utils";
@@ -20,10 +21,13 @@ import {
ChevronLeft,
Info,
Loader2,
SearchCode,
Trash,
Waypoints,
} from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { useState } from "react";
import { MonacoEditor } from "@components/monaco";
export const NetworkPage = () => {
const { type, id, network } = useParams() as {
@@ -44,6 +48,7 @@ const NetworkPageInner = ({
id: string;
network: string;
}) => {
const [showInspect, setShowInspect] = useState(false);
const server = useServer(id);
useSetTitle(`${server?.name} | network | ${network_name}`);
const nav = useNavigate();
@@ -277,6 +282,24 @@ const NetworkPageInner = ({
)}
<DockerLabelsSection labels={network.Labels} />
<Section
title="Inspect"
icon={<SearchCode className="w-4 h-4" />}
titleRight={
<div className="pl-2">
<ShowHideButton show={showInspect} setShow={setShowInspect} />
</div>
}
>
{showInspect && (
<MonacoEditor
value={JSON.stringify(network, null, 2)}
language="json"
readOnly
/>
)}
</Section>
</div>
);
};

View File

@@ -8,6 +8,7 @@ import {
DockerLabelsSection,
DockerOptions,
DockerResourcePageName,
ShowHideButton,
} from "@components/util";
import { useExecute, useRead, useSetTitle } from "@lib/hooks";
import { has_minimum_permissions } from "@lib/utils";
@@ -15,8 +16,10 @@ import { Types } from "komodo_client";
import { Badge } from "@ui/badge";
import { Button } from "@ui/button";
import { DataTable } from "@ui/data-table";
import { ChevronLeft, Info, Loader2, Trash } from "lucide-react";
import { ChevronLeft, Info, Loader2, SearchCode, Trash } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { useState } from "react";
import { MonacoEditor } from "@components/monaco";
export const VolumePage = () => {
const { type, id, volume } = useParams() as {
@@ -37,6 +40,7 @@ const VolumePageInner = ({
id: string;
volume: string;
}) => {
const [showInspect, setShowInspect] = useState(false);
const server = useServer(id);
useSetTitle(`${server?.name} | volume | ${volume_name}`);
const nav = useNavigate();
@@ -114,7 +118,7 @@ const VolumePageInner = ({
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<DOCKER_LINK_ICONS.container
<DOCKER_LINK_ICONS.volume
server_id={id}
name={volume_name}
size={8}
@@ -175,6 +179,24 @@ const VolumePageInner = ({
</Section>
<DockerLabelsSection labels={volume.Labels} />
<Section
title="Inspect"
icon={<SearchCode className="w-4 h-4" />}
titleRight={
<div className="pl-2">
<ShowHideButton show={showInspect} setShow={setShowInspect} />
</div>
}
>
{showInspect && (
<MonacoEditor
value={JSON.stringify(volume, null, 2)}
language="json"
readOnly
/>
)}
</Section>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { ConfirmButton, CopyButton, TextUpdateMenu } from "@components/util";
import { ConfirmButton, CopyButton, TextUpdateMenuMonaco } from "@components/util";
import {
useInvalidate,
useRead,
@@ -233,7 +233,7 @@ const Providers = ({ type }: { type: "GitProvider" | "DockerRegistry" }) => {
]}
/>
{updateMenuData && (
<TextUpdateMenu
<TextUpdateMenuMonaco
title={updateMenuData.title}
titleRight={updateMenuData.titleRight}
placeholder={updateMenuData.placeholder}

View File

@@ -1,4 +1,8 @@
import { ConfirmButton, CopyButton, TextUpdateMenu } from "@components/util";
import {
ConfirmButton,
CopyButton,
TextUpdateMenuMonaco,
} from "@components/util";
import {
useInvalidate,
useRead,
@@ -84,7 +88,7 @@ export const Variables = () => {
</div>
{updateMenuData && (
<TextUpdateMenu
<TextUpdateMenuMonaco
title={updateMenuData.title}
placeholder={updateMenuData.placeholder}
value={updateMenuData.value}
@@ -209,7 +213,7 @@ export const Variables = () => {
{/** SECRETS */}
{secrets.length ? (
<div className="flex items-center gap-2 text-muted-foreground">
<div className="flex items-center gap-2 flex-wrap text-muted-foreground">
<div>Core Secrets:</div>
{secrets.map((secret) => (
<Badge variant="secondary">{secret}</Badge>

View File

@@ -14,16 +14,17 @@ import {
stroke_color_class_by_intention,
} from "@lib/color";
import { useRead, useSetTitle } from "@lib/hooks";
import { cn, has_minimum_permissions } from "@lib/utils";
import { cn } from "@lib/utils";
import { Types } from "komodo_client";
import { ChevronLeft, Clapperboard, Layers2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { Link, useParams } from "react-router-dom";
import { StackServiceLogs } from "./log";
import { ResourceUpdates } from "@components/updates/resource";
import { Button } from "@ui/button";
import { ExportButton } from "@components/export";
import { AddTags, ResourceTags } from "@components/tags";
import { DockerResourceLink, StatusBadge } from "@components/util";
import { DockerResourceLink, ResourcePageHeader } from "@components/util";
import { useEditPermissions } from "@pages/resource";
import { ResourceNotifications } from "@pages/resource-notifications";
import { Fragment } from "react/jsx-runtime";
type IdServiceComponent = React.FC<{ id: string; service?: string }>;
@@ -54,15 +55,10 @@ const StackServicePageInner = ({
}) => {
const stack = useStack(stack_id);
useSetTitle(`${stack?.name} | ${service}`);
const nav = useNavigate();
const perms = useRead("GetPermissionLevel", {
target: { type: "Stack", id: stack_id },
}).data;
const canExecute = has_minimum_permissions(
perms,
Types.PermissionLevel.Execute
);
const canWrite = has_minimum_permissions(perms, Types.PermissionLevel.Write);
const { canExecute, canWrite } = useEditPermissions({
type: "Stack",
id: stack_id,
});
const services = useRead("ListStackServices", { stack: stack_id }).data;
const container = services?.find((s) => s.service === service)?.container;
const state = container?.state ?? Types.ContainerStateStatusEnum.Empty;
@@ -70,98 +66,119 @@ const StackServicePageInner = ({
const stroke_color = stroke_color_class_by_intention(intention);
return (
<div className="flex flex-col gap-16">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between mb-4">
<Button
className="gap-2"
variant="secondary"
onClick={() => nav("/stacks/" + stack_id)}
>
<ChevronLeft className="w-4" /> Back
<div>
<div className="w-full flex items-center justify-between mb-12">
<Link to={"/stacks/" + stack_id}>
<Button className="gap-2" variant="secondary">
<ChevronLeft className="w-4" />
Back
</Button>
</Link>
<div className="flex items-center gap-4">
<ExportButton targets={[{ type: "Stack", id: stack_id }]} />
</div>
<div className="flex flex-col gap-4">
<div className="flex gap-4 justify-between flex-wrap">
<div className="flex items-center gap-4">
<div className="mt-1">
<Layers2 className={cn("w-8 h-8", stroke_color)} />
</div>
<h1 className="text-3xl">{service}</h1>
<div className="flex flex-wrap gap-4 items-center">
<StatusBadge text={state} intent={intention} />
{container?.status && <div>{container?.status}</div>}
</div>
<div className="flex flex-col xl:flex-row gap-4">
{/** HEADER */}
<div className="w-full flex flex-col gap-4">
<div className="flex flex-col gap-2 border rounded-md">
{/* <Components.ResourcePageHeader id={id} /> */}
<ResourcePageHeader
intent={intention}
icon={<Layers2 className={cn("w-8 h-8", stroke_color)} />}
name={service}
state={state}
status={container?.status}
/>
<div className="flex flex-col pb-2 px-4">
<div className="flex items-center gap-x-4 gap-y-0 flex-wrap text-muted-foreground">
<ResourceLink type="Stack" id={stack_id} />
{stack?.info.server_id && (
<>
|
<ResourceLink type="Server" id={stack.info.server_id} />
</>
)}
{stack?.info.server_id && container?.name && (
<>
|
<DockerResourceLink
type="container"
server_id={stack.info.server_id}
name={container.name}
muted
/>
</>
)}
{stack?.info.server_id && container?.image && (
<>
|
<DockerResourceLink
type="image"
server_id={stack.info.server_id}
name={container.image}
id={container.image_id}
muted
/>
</>
)}
{stack?.info.server_id &&
container?.networks.map((network) => (
<Fragment key={network}>
|
<DockerResourceLink
type="network"
server_id={stack.info.server_id}
name={network}
muted
/>
</Fragment>
))}
{stack?.info.server_id &&
container &&
container.volumes.map((volume) => (
<Fragment key={volume}>
|
<DockerResourceLink
type="volume"
server_id={stack.info.server_id}
name={volume}
muted
/>
</Fragment>
))}
</div>
</div>
<div className="flex items-center gap-2">
<p className="text-sm text-muted-foreground">Description: </p>
<ResourceDescription
type="Stack"
id={stack_id}
disabled={!canWrite}
/>
</div>
</div>
<div className="flex gap-4 justify-between flex-wrap">
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
<ResourceLink type="Stack" id={stack_id} />
{stack?.info.server_id && (
<>
|
<ResourceLink type="Server" id={stack.info.server_id} />
</>
)}
{stack?.info.server_id && container && container.name && (
<>
|
<DockerResourceLink
type="container"
server_id={stack?.info.server_id}
name={container.name}
muted
/>
</>
)}
</div>
<div className="flex items-center gap-2 h-7 lg:justify-self-end">
<p className="text-sm text-muted-foreground">Tags:</p>
<ResourceTags
target={{ id: stack_id, type: "Stack" }}
className="text-sm"
disabled={!canWrite}
click_to_delete
/>
{canWrite && <AddTags target={{ id: stack_id, type: "Stack" }} />}
</div>
</div>
<ResourceDescription
type="Stack"
id={stack_id}
disabled={!canWrite}
/>
</div>
{/** NOTIFICATIONS */}
<ResourceNotifications type="Stack" id={stack_id} />
</div>
{/* Actions */}
{canExecute && (
<Section
title="Actions (Service)"
icon={<Clapperboard className="w-4 h-4" />}
>
<div className="flex gap-4 items-center flex-wrap">
{Object.entries(Actions).map(([key, Action]) => (
<Action key={key} id={stack_id} service={service} />
))}
</div>
</Section>
)}
<div className="mt-8 flex flex-col gap-12">
{/* Actions */}
{canExecute && (
<Section
title="Actions (Service)"
icon={<Clapperboard className="w-4 h-4" />}
>
<div className="flex gap-4 items-center flex-wrap">
{Object.entries(Actions).map(([key, Action]) => (
<Action key={key} id={stack_id} service={service} />
))}
</div>
</Section>
)}
{/* Updates */}
<ResourceUpdates type="Stack" id={stack_id} />
{/* Logs */}
<div className="pt-4">
<StackServiceLogs id={stack_id} service={service} />
{/* Logs */}
<div className="pt-4">
<StackServiceLogs id={stack_id} service={service} />
</div>
</div>
</div>
);

View File

@@ -1,14 +1,22 @@
use std::{collections::HashMap, path::Path};
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use anyhow::Context;
use command::run_komodo_command;
use formatting::format_serror;
use komodo_client::entities::{
update::Log, CloneArgs, EnvironmentVar,
komodo_timestamp, update::Log, CloneArgs, EnvironmentVar,
};
use tokio::sync::Mutex;
use crate::{get_commit_hash_log, GitRes};
/// Wait this long after a pull to allow another pull through
const PULL_TIMEOUT: i64 = 5_000;
/// This will pull in a way that handles edge cases
/// from possible state of the repo. For example, the user
/// can change branch after clone, or even the remote.
@@ -38,187 +46,266 @@ where
{
let args: CloneArgs = clone_args.into();
let path = args.path(repo_dir);
let repo_url = args.remote_url(access_token.as_deref())?;
// Set remote url
let mut set_remote = run_komodo_command(
"set git remote",
path.as_ref(),
format!("git remote set-url origin {repo_url}"),
false,
)
.await;
// Acquire the path lock
let lock = pull_cache().get_lock(path.clone()).await;
if !set_remote.success {
if let Some(token) = access_token {
set_remote.command =
set_remote.command.replace(&token, "<TOKEN>");
set_remote.stdout =
set_remote.stdout.replace(&token, "<TOKEN>");
set_remote.stderr =
set_remote.stderr.replace(&token, "<TOKEN>");
}
return Ok(GitRes {
logs: vec![set_remote],
hash: None,
message: None,
env_file_path: None,
});
// Lock the path lock, prevents simultaneous pulls by
// ensuring simultaneous pulls will wait for first to finish
// and checking cached results.
let mut locked = lock.lock().await;
// Early return from cache if lasted pulled with PULL_TIMEOUT
if locked.last_pulled + PULL_TIMEOUT > komodo_timestamp() {
return clone_entry_res(&locked.res);
}
let checkout = run_komodo_command(
"checkout branch",
path.as_ref(),
format!("git checkout -f {}", args.branch),
false,
)
.await;
let res = async {
let repo_url = args.remote_url(access_token.as_deref())?;
if !checkout.success {
return Ok(GitRes {
logs: vec![checkout],
hash: None,
message: None,
env_file_path: None,
});
}
let pull_log = run_komodo_command(
"git pull",
path.as_ref(),
format!("git pull --rebase --force origin {}", args.branch),
false,
)
.await;
let mut logs = vec![pull_log];
if !logs[0].success {
return Ok(GitRes {
logs,
hash: None,
message: None,
env_file_path: None,
});
}
if let Some(commit) = args.commit {
let reset_log = run_komodo_command(
"set commit",
// Set remote url
let mut set_remote = run_komodo_command(
"set git remote",
path.as_ref(),
format!("git reset --hard {commit}"),
format!("git remote set-url origin {repo_url}"),
false,
)
.await;
logs.push(reset_log);
}
let (hash, message) = match get_commit_hash_log(&path).await {
Ok((log, hash, message)) => {
logs.push(log);
(Some(hash), Some(message))
if !set_remote.success {
if let Some(token) = access_token {
set_remote.command =
set_remote.command.replace(&token, "<TOKEN>");
set_remote.stdout =
set_remote.stdout.replace(&token, "<TOKEN>");
set_remote.stderr =
set_remote.stderr.replace(&token, "<TOKEN>");
}
return Ok(GitRes {
logs: vec![set_remote],
hash: None,
message: None,
env_file_path: None,
});
}
Err(e) => {
logs.push(Log::simple(
"latest commit",
format_serror(
&e.context("failed to get latest commit").into(),
),
));
(None, None)
}
};
let Ok(env_file_path) = crate::environment::write_file(
environment,
env_file_path,
secrets,
&path,
&mut logs,
)
.await
else {
return Ok(GitRes {
let checkout = run_komodo_command(
"checkout branch",
path.as_ref(),
format!("git checkout -f {}", args.branch),
false,
)
.await;
if !checkout.success {
return Ok(GitRes {
logs: vec![checkout],
hash: None,
message: None,
env_file_path: None,
});
}
let pull_log = run_komodo_command(
"git pull",
path.as_ref(),
format!("git pull --rebase --force origin {}", args.branch),
false,
)
.await;
let mut logs = vec![pull_log];
if !logs[0].success {
return Ok(GitRes {
logs,
hash: None,
message: None,
env_file_path: None,
});
}
if let Some(commit) = args.commit {
let reset_log = run_komodo_command(
"set commit",
path.as_ref(),
format!("git reset --hard {commit}"),
false,
)
.await;
logs.push(reset_log);
}
let (hash, message) = match get_commit_hash_log(&path).await {
Ok((log, hash, message)) => {
logs.push(log);
(Some(hash), Some(message))
}
Err(e) => {
logs.push(Log::simple(
"latest commit",
format_serror(
&e.context("failed to get latest commit").into(),
),
));
(None, None)
}
};
let Ok(env_file_path) = crate::environment::write_file(
environment,
env_file_path,
secrets,
&path,
&mut logs,
)
.await
else {
return Ok(GitRes {
logs,
hash,
message,
env_file_path: None,
});
};
if let Some(command) = args.on_pull {
if !command.command.is_empty() {
let on_pull_path = path.join(&command.path);
if let Some(secrets) = secrets {
let (full_command, mut replacers) =
match svi::interpolate_variables(
&command.command,
secrets,
svi::Interpolator::DoubleBrackets,
true,
)
.context(
"failed to interpolate secrets into on_pull command",
) {
Ok(res) => res,
Err(e) => {
logs.push(Log::error(
"interpolate secrets - on_pull",
format_serror(&e.into()),
));
return Ok(GitRes {
logs,
hash,
message,
env_file_path: None,
});
}
};
replacers.extend(core_replacers.to_owned());
let mut on_pull_log = run_komodo_command(
"on pull",
on_pull_path.as_ref(),
&full_command,
true,
)
.await;
on_pull_log.command =
svi::replace_in_string(&on_pull_log.command, &replacers);
on_pull_log.stdout =
svi::replace_in_string(&on_pull_log.stdout, &replacers);
on_pull_log.stderr =
svi::replace_in_string(&on_pull_log.stderr, &replacers);
tracing::debug!(
"run repo on_pull command | command: {} | cwd: {:?}",
on_pull_log.command,
on_pull_path
);
logs.push(on_pull_log);
} else {
let on_pull_log = run_komodo_command(
"on pull",
on_pull_path.as_ref(),
&command.command,
true,
)
.await;
tracing::debug!(
"run repo on_pull command | command: {} | cwd: {:?}",
command.command,
on_pull_path
);
logs.push(on_pull_log);
}
}
}
anyhow::Ok(GitRes {
logs,
hash,
message,
env_file_path: None,
});
};
env_file_path,
})
}
.await;
if let Some(command) = args.on_pull {
if !command.command.is_empty() {
let on_pull_path = path.join(&command.path);
if let Some(secrets) = secrets {
let (full_command, mut replacers) =
match svi::interpolate_variables(
&command.command,
secrets,
svi::Interpolator::DoubleBrackets,
true,
)
.context(
"failed to interpolate secrets into on_pull command",
) {
Ok(res) => res,
Err(e) => {
logs.push(Log::error(
"interpolate secrets - on_pull",
format_serror(&e.into()),
));
return Ok(GitRes {
logs,
hash,
message,
env_file_path: None,
});
}
};
replacers.extend(core_replacers.to_owned());
let mut on_pull_log = run_komodo_command(
"on pull",
on_pull_path.as_ref(),
&full_command,
true,
)
.await;
// Set the cache with results
locked.last_pulled = komodo_timestamp();
locked.res = clone_entry_res(&res);
on_pull_log.command =
svi::replace_in_string(&on_pull_log.command, &replacers);
on_pull_log.stdout =
svi::replace_in_string(&on_pull_log.stdout, &replacers);
on_pull_log.stderr =
svi::replace_in_string(&on_pull_log.stderr, &replacers);
res
}
tracing::debug!(
"run repo on_pull command | command: {} | cwd: {:?}",
on_pull_log.command,
on_pull_path
);
fn pull_cache() -> &'static PullCache {
static LAST_PULLED_MAP: OnceLock<PullCache> = OnceLock::new();
LAST_PULLED_MAP.get_or_init(|| Default::default())
}
logs.push(on_pull_log);
} else {
let on_pull_log = run_komodo_command(
"on pull",
on_pull_path.as_ref(),
&command.command,
true,
)
.await;
tracing::debug!(
"run repo on_pull command | command: {} | cwd: {:?}",
command.command,
on_pull_path
);
logs.push(on_pull_log);
}
struct PullCacheEntry {
last_pulled: i64,
res: anyhow::Result<GitRes>,
}
fn clone_entry_res(
res: &anyhow::Result<GitRes>,
) -> anyhow::Result<GitRes> {
match res {
Ok(res) => Ok(res.clone()),
Err(e) => Err(clone_anyhow_error(e)),
}
}
impl Default for PullCacheEntry {
fn default() -> Self {
PullCacheEntry {
last_pulled: 0,
res: Ok(GitRes::default()),
}
}
Ok(GitRes {
logs,
hash,
message,
env_file_path,
})
}
/// Prevents simulataneous pulls on the same repo,
/// as well as prevents redudant pulls within [PULL_TIMEOUT] milliseconds.
#[derive(Default)]
struct PullCache(Mutex<HashMap<PathBuf, Arc<Mutex<PullCacheEntry>>>>);
impl PullCache {
async fn get_lock(
&self,
path: PathBuf,
) -> Arc<Mutex<PullCacheEntry>> {
let mut lock = self.0.lock().await;
lock.entry(path).or_default().clone()
}
}
fn clone_anyhow_error(e: &anyhow::Error) -> anyhow::Error {
let mut reasons =
e.chain().map(|e| e.to_string()).collect::<Vec<_>>();
// Always guaranteed to be at least one reason
// Need to start the chain with the last reason
let mut e = anyhow::Error::msg(reasons.pop().unwrap());
// Need to reverse reason application from lowest context to highest context.
for reason in reasons.into_iter().rev() {
e = e.context(reason)
}
e
}