forked from github-starred/komodo
Compare commits
3 Commits
v1.17.1
...
v1.17.4-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f2f61be5 | ||
|
|
b43e2918da | ||
|
|
f45205011e |
124
Cargo.lock
generated
124
Cargo.lock
generated
@@ -106,9 +106,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@@ -898,7 +898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cache"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"tokio",
|
||||
@@ -951,6 +951,27 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -1038,7 +1059,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -1114,6 +1135,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "croner"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38fd53511eaf0b00a185613875fee58b208dfce016577d0ad4bb548e1c4fb3ee"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -1484,6 +1514,15 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "english-to-cron"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a13a7d5e0ab3872c3ee478366eae624d89ab953d30276b0eee08169774ceb73"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
@@ -1498,7 +1537,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment_file"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
@@ -1567,7 +1606,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -1729,7 +1768,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cache",
|
||||
@@ -2466,7 +2505,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2482,7 +2521,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2513,7 +2552,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -2527,10 +2566,14 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bcrypt",
|
||||
"cache",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"command",
|
||||
"croner",
|
||||
"dashmap",
|
||||
"derive_variants",
|
||||
"dotenvy",
|
||||
"english-to-cron",
|
||||
"environment_file",
|
||||
"envy",
|
||||
"formatting",
|
||||
@@ -2577,7 +2620,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2689,7 +2732,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3346,6 +3389,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse_link_header"
|
||||
version = "0.3.3"
|
||||
@@ -3413,7 +3465,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3425,6 +3477,44 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
@@ -3877,7 +3967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "response"
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4468,6 +4558,12 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.17.1"
|
||||
version = "1.17.3"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -67,7 +67,7 @@ serde_yaml = "0.9.34"
|
||||
toml = "0.8.20"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.97"
|
||||
anyhow = "1.0.98"
|
||||
thiserror = "2.0.12"
|
||||
|
||||
# LOGGING
|
||||
@@ -107,6 +107,12 @@ aws-config = "1.6.1"
|
||||
aws-sdk-ec2 = "1.121.1"
|
||||
aws-credential-types = "1.2.2"
|
||||
|
||||
## CRON
|
||||
english-to-cron = "0.1.4"
|
||||
chrono-tz = "0.10.3"
|
||||
chrono = "0.4.40"
|
||||
croner = "2.1.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.2"
|
||||
typeshare = "1.0.4"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Builds the Komodo Core and Periphery binaries
|
||||
## for a specific architecture.
|
||||
|
||||
FROM rust:1.85.1-bullseye AS builder
|
||||
FROM rust:1.86.0-bullseye AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
@@ -23,5 +23,5 @@ COPY --from=builder /builder/target/release/core /core
|
||||
COPY --from=builder /builder/target/release/periphery /periphery
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.description="Komodo Binaries"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
@@ -39,7 +39,9 @@ svi.workspace = true
|
||||
# external
|
||||
aws-credential-types.workspace = true
|
||||
ordered_hash_map.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
openidconnect.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
axum-server.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
@@ -50,6 +52,7 @@ tower-http.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
typeshare.workspace = true
|
||||
chrono-tz.workspace = true
|
||||
octorust.workspace = true
|
||||
wildcard.workspace = true
|
||||
arc-swap.workspace = true
|
||||
@@ -60,6 +63,8 @@ futures.workspace = true
|
||||
nom_pem.workspace = true
|
||||
dotenvy.workspace = true
|
||||
anyhow.workspace = true
|
||||
croner.workspace = true
|
||||
chrono.workspace = true
|
||||
bcrypt.workspace = true
|
||||
base64.workspace = true
|
||||
rustls.workspace = true
|
||||
@@ -73,5 +78,4 @@ envy.workspace = true
|
||||
rand.workspace = true
|
||||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
# Build Core
|
||||
FROM rust:1.85.1-bullseye AS core-builder
|
||||
FROM rust:1.86.0-bullseye AS core-builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
@@ -189,6 +189,24 @@ pub async fn send_alert(
|
||||
let link = resource_link(ResourceTargetVariant::Repo, id);
|
||||
format!("{level} | Repo build for **{name}** failed\n{link}")
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Procedure, id);
|
||||
format!("{level} | Procedure **{name}** failed\n{link}")
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Action, id);
|
||||
format!("{level} | Action **{name}** failed\n{link}")
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let link = resource_link(*resource_type, id);
|
||||
format!(
|
||||
"{level} | **{name}** ({resource_type}) | Scheduled run started 🕝\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
if !content.is_empty() {
|
||||
|
||||
@@ -18,8 +18,9 @@ use crate::helpers::query::get_variables_and_secrets;
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
mod discord;
|
||||
mod slack;
|
||||
mod ntfy;
|
||||
mod pushover;
|
||||
mod slack;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn send_alerts(alerts: &[Alert]) {
|
||||
@@ -131,7 +132,18 @@ pub async fn send_alert_to_alerter(
|
||||
}
|
||||
AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url }) => {
|
||||
ntfy::send_alert(url, alert).await.with_context(|| {
|
||||
format!("Failed to send alert to ntfy Alerter {}", alerter.name)
|
||||
format!(
|
||||
"Failed to send alert to ntfy Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {
|
||||
pushover::send_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send alert to Pushover Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,24 @@ pub async fn send_alert(
|
||||
let link = resource_link(ResourceTargetVariant::Repo, id);
|
||||
format!("{level} | Repo build for {} failed\n{link}", name,)
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Procedure, id);
|
||||
format!("{level} | Procedure {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Action, id);
|
||||
format!("{level} | Action {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let link = resource_link(*resource_type, id);
|
||||
format!(
|
||||
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
|
||||
|
||||
270
bin/core/src/alert/pushover.rs
Normal file
270
bin/core/src/alert/pushover.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn send_alert(
|
||||
url: &str,
|
||||
alert: &Alert,
|
||||
) -> anyhow::Result<()> {
|
||||
let level = fmt_level(alert.level);
|
||||
let content = match &alert.data {
|
||||
AlertData::Test { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Alerter, id);
|
||||
format!(
|
||||
"{level} | If you see this message, then Alerter {} is working\n{link}",
|
||||
name,
|
||||
)
|
||||
}
|
||||
AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!(
|
||||
"{level} | {}{} is now reachable\n{link}",
|
||||
name, region
|
||||
)
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {:#?}", e))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{level} | {}{} is unreachable ❌\n{link}{err}",
|
||||
name, region
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
format!(
|
||||
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
|
||||
name, region,
|
||||
)
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
|
||||
name, region,
|
||||
)
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let link = resource_link(ResourceTargetVariant::Server, id);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
format!(
|
||||
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
|
||||
name, region, path,
|
||||
)
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let to_state = fmt_docker_container_state(to);
|
||||
format!(
|
||||
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
|
||||
name, to_state, server_name, from,
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
format!(
|
||||
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
|
||||
name, server_name, image,
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
format!(
|
||||
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
|
||||
name, server_name, image,
|
||||
)
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let to_state = fmt_stack_state(to);
|
||||
format!(
|
||||
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
|
||||
name, to_state, server_name, from,
|
||||
)
|
||||
}
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
format!(
|
||||
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
|
||||
name, server_name, service, image,
|
||||
)
|
||||
}
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images_str = images.join(", ");
|
||||
format!(
|
||||
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
|
||||
name, server_name, images_label, images_str,
|
||||
)
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
instance_id,
|
||||
message,
|
||||
} => {
|
||||
format!(
|
||||
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
|
||||
instance_id, message,
|
||||
)
|
||||
}
|
||||
AlertData::ResourceSyncPendingUpdates { id, name } => {
|
||||
let link =
|
||||
resource_link(ResourceTargetVariant::ResourceSync, id);
|
||||
format!(
|
||||
"{level} | Pending resource sync updates on {}\n{link}",
|
||||
name,
|
||||
)
|
||||
}
|
||||
AlertData::BuildFailed { id, name, version } => {
|
||||
let link = resource_link(ResourceTargetVariant::Build, id);
|
||||
format!(
|
||||
"{level} | Build {name} failed\nversion: v{version}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::RepoBuildFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Repo, id);
|
||||
format!("{level} | Repo build for {} failed\n{link}", name,)
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Procedure, id);
|
||||
format!("{level} | Procedure {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let link = resource_link(ResourceTargetVariant::Action, id);
|
||||
format!("{level} | Action {name} failed\n{link}")
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let link = resource_link(*resource_type, id);
|
||||
format!(
|
||||
"{level} | {name} ({resource_type}) | Scheduled run started 🕝\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
|
||||
if !content.is_empty() {
|
||||
send_message(url, content).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
url: &str,
|
||||
content: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// pushover needs all information to be encoded in the URL. At minimum they need
|
||||
// the user key, the application token, and the message (url encoded).
|
||||
// other optional params here: https://pushover.net/api (just add them to the
|
||||
// webhook url along with the application token and the user key).
|
||||
let content = [("message", content)];
|
||||
|
||||
let response = http_client()
|
||||
.post(url)
|
||||
.form(&content)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send message")?;
|
||||
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
debug!("pushover alert sent successfully: {}", status);
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send message to pushover | {} | failed to get response text",
|
||||
status
|
||||
)
|
||||
})?;
|
||||
Err(anyhow!(
|
||||
"Failed to send message to pushover | {} | {}",
|
||||
status,
|
||||
text
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
@@ -373,9 +373,7 @@ pub async fn send_alert(
|
||||
let text = format!("{level} | Build {name} has failed");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"build name: *{name}*\nversion: *v{version}*",
|
||||
)),
|
||||
Block::section(format!("version: *v{version}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Build,
|
||||
id,
|
||||
@@ -388,7 +386,6 @@ pub async fn send_alert(
|
||||
format!("{level} | Repo build for *{name}* has *failed*");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!("repo name: *{name}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Repo,
|
||||
id,
|
||||
@@ -396,6 +393,42 @@ pub async fn send_alert(
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ProcedureFailed { id, name } => {
|
||||
let text = format!("{level} | Procedure *{name}* has *failed*");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Procedure,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ActionFailed { id, name } => {
|
||||
let text = format!("{level} | Action *{name}* has *failed*");
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Action,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::ScheduleRun {
|
||||
resource_type,
|
||||
id,
|
||||
name,
|
||||
} => {
|
||||
let text = format!(
|
||||
"{level} | *{name}* ({resource_type}) | Scheduled run started 🕝"
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(resource_link(*resource_type, id)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::None {} => Default::default(),
|
||||
};
|
||||
if !text.is_empty() {
|
||||
@@ -411,7 +444,7 @@ pub async fn send_alert(
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
|
||||
let slack = ::slack::Client::new(url_interpolated);
|
||||
slack.send_message(text, blocks).await.map_err(|e| {
|
||||
let replacers =
|
||||
|
||||
@@ -13,8 +13,13 @@ use komodo_client::{
|
||||
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
|
||||
},
|
||||
entities::{
|
||||
action::Action, config::core::CoreConfig,
|
||||
permission::PermissionLevel, update::Update, user::action_user,
|
||||
action::Action,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
config::core::CoreConfig,
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
update::Update,
|
||||
user::action_user,
|
||||
},
|
||||
};
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
@@ -22,6 +27,7 @@ use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::{execute::ExecuteRequest, user::UserArgs},
|
||||
config::core_config,
|
||||
helpers::{
|
||||
@@ -178,6 +184,26 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if !update.success && action.config.failure_alert {
|
||||
warn!("action unsuccessful, alerting...");
|
||||
let target = update.target.clone();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::ActionFailed {
|
||||
id: action.id,
|
||||
name: action.name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ use komodo_client::{
|
||||
BatchExecutionResponse, BatchRunProcedure, RunProcedure,
|
||||
},
|
||||
entities::{
|
||||
permission::PermissionLevel, procedure::Procedure,
|
||||
update::Update, user::User,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
update::Update,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
@@ -15,6 +19,7 @@ use resolver_api::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
helpers::{procedure::execute_procedure, update::update_update},
|
||||
resource::{self, refresh_procedure_state_cache},
|
||||
state::{action_states, db_client},
|
||||
@@ -137,6 +142,26 @@ fn resolve_inner(
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if !update.success && procedure.config.failure_alert {
|
||||
warn!("procedure unsuccessful, alerting...");
|
||||
let target = update.target.clone();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::ProcedureFailed {
|
||||
id: procedure.id,
|
||||
name: procedure.name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -432,9 +432,7 @@ async fn get_on_host_periphery(
|
||||
|
||||
match builder.config {
|
||||
BuilderConfig::Aws(_) => {
|
||||
return Err(anyhow!(
|
||||
"Files on host doesn't work with AWS builder"
|
||||
));
|
||||
Err(anyhow!("Files on host doesn't work with AWS builder"))
|
||||
}
|
||||
BuilderConfig::Url(config) => {
|
||||
let periphery = PeripheryClient::new(
|
||||
|
||||
@@ -829,7 +829,9 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
.context("failed to open existing pending resource sync updates alert")
|
||||
.inspect_err(|e| warn!("{e:#}"))
|
||||
.ok();
|
||||
send_alerts(&[alert]).await;
|
||||
if sync.config.pending_alert {
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
}
|
||||
// CLOSE ALERT
|
||||
(Some(existing), false) => {
|
||||
|
||||
@@ -194,6 +194,7 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
stdio: env
|
||||
.komodo_logging_stdio
|
||||
.unwrap_or(config.logging.stdio),
|
||||
pretty: env.komodo_logging_pretty.unwrap_or(config.logging.pretty),
|
||||
otlp_endpoint: env
|
||||
.komodo_logging_otlp_endpoint
|
||||
.unwrap_or(config.logging.otlp_endpoint),
|
||||
|
||||
@@ -23,6 +23,7 @@ mod helpers;
|
||||
mod listener;
|
||||
mod monitor;
|
||||
mod resource;
|
||||
mod schedule;
|
||||
mod stack;
|
||||
mod state;
|
||||
mod sync;
|
||||
@@ -59,6 +60,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
resource::spawn_repo_state_refresh_loop();
|
||||
resource::spawn_procedure_state_refresh_loop();
|
||||
resource::spawn_action_state_refresh_loop();
|
||||
schedule::spawn_schedule_executor();
|
||||
helpers::prune::spawn_prune_loop();
|
||||
|
||||
// Setup static frontend services
|
||||
@@ -86,9 +88,9 @@ async fn app() -> anyhow::Result<()> {
|
||||
)
|
||||
.into_make_service();
|
||||
|
||||
let addr = format!("{}:{}", core_config().bind_ip, core_config().port);
|
||||
let socket_addr =
|
||||
SocketAddr::from_str(&addr)
|
||||
let addr =
|
||||
format!("{}:{}", core_config().bind_ip, core_config().port);
|
||||
let socket_addr = SocketAddr::from_str(&addr)
|
||||
.context("failed to parse listen address")?;
|
||||
|
||||
if config.ssl_enabled {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
action::{
|
||||
Action, ActionConfig, ActionConfigDiff, ActionInfo,
|
||||
ActionListItem, ActionListItemInfo, ActionQuerySpecifics,
|
||||
@@ -17,7 +17,12 @@ use mungos::{
|
||||
mongodb::{Collection, bson::doc, options::FindOneOptions},
|
||||
};
|
||||
|
||||
use crate::state::{action_state_cache, action_states, db_client};
|
||||
use crate::{
|
||||
schedule::{
|
||||
cancel_schedule, get_schedule_item_info, update_schedule,
|
||||
},
|
||||
state::{action_state_cache, action_states, db_client},
|
||||
};
|
||||
|
||||
impl super::KomodoResource for Action {
|
||||
type Config = ActionConfig;
|
||||
@@ -31,6 +36,10 @@ impl super::KomodoResource for Action {
|
||||
ResourceTargetVariant::Action
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Action(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().actions
|
||||
@@ -40,6 +49,9 @@ impl super::KomodoResource for Action {
|
||||
action: Resource<Self::Config, Self::Info>,
|
||||
) -> Self::ListItem {
|
||||
let state = get_action_state(&action.id).await;
|
||||
let (next_scheduled_run, schedule_error) = get_schedule_item_info(
|
||||
&ResourceTarget::Action(action.id.clone()),
|
||||
);
|
||||
ActionListItem {
|
||||
name: action.name,
|
||||
id: action.id,
|
||||
@@ -48,6 +60,8 @@ impl super::KomodoResource for Action {
|
||||
info: ActionListItemInfo {
|
||||
state,
|
||||
last_run_at: action.info.last_run_at,
|
||||
next_scheduled_run,
|
||||
schedule_error,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -83,9 +97,10 @@ impl super::KomodoResource for Action {
|
||||
}
|
||||
|
||||
async fn post_create(
|
||||
_created: &Resource<Self::Config, Self::Info>,
|
||||
created: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
update_schedule(created);
|
||||
refresh_action_state_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -131,9 +146,10 @@ impl super::KomodoResource for Action {
|
||||
}
|
||||
|
||||
async fn post_delete(
|
||||
_resource: &Resource<Self::Config, Self::Info>,
|
||||
resource: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
cancel_schedule(&ResourceTarget::Action(resource.id.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use derive_variants::ExtractVariant;
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
alerter::{
|
||||
Alerter, AlerterConfig, AlerterConfigDiff, AlerterListItem,
|
||||
AlerterListItemInfo, AlerterQuerySpecifics, PartialAlerterConfig,
|
||||
@@ -25,6 +25,10 @@ impl super::KomodoResource for Alerter {
|
||||
ResourceTargetVariant::Alerter
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Alerter(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().alerters
|
||||
|
||||
@@ -5,7 +5,7 @@ use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::RefreshBuildCache,
|
||||
entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
build::{
|
||||
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
|
||||
BuildListItemInfo, BuildQuerySpecifics, BuildState,
|
||||
@@ -44,6 +44,10 @@ impl super::KomodoResource for Build {
|
||||
ResourceTargetVariant::Build
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Build(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().builds
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{
|
||||
MergePartial, Operation, ResourceTargetVariant,
|
||||
MergePartial, Operation, ResourceTarget, ResourceTargetVariant,
|
||||
builder::{
|
||||
Builder, BuilderConfig, BuilderConfigDiff, BuilderConfigVariant,
|
||||
BuilderListItem, BuilderListItemInfo, BuilderQuerySpecifics,
|
||||
@@ -31,6 +31,10 @@ impl super::KomodoResource for Builder {
|
||||
ResourceTargetVariant::Builder
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Builder(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().builders
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
build::Build,
|
||||
deployment::{
|
||||
Deployment, DeploymentConfig, DeploymentConfigDiff,
|
||||
@@ -43,6 +43,10 @@ impl super::KomodoResource for Deployment {
|
||||
ResourceTargetVariant::Deployment
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Deployment(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().deployments
|
||||
|
||||
@@ -29,7 +29,7 @@ use mungos::{
|
||||
options::FindOptions,
|
||||
},
|
||||
};
|
||||
use partial_derive2::{Diff, FieldDiff, MaybeNone, PartialDiff};
|
||||
use partial_derive2::{Diff, MaybeNone, PartialDiff};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
@@ -107,6 +107,7 @@ pub trait KomodoResource {
|
||||
type QuerySpecifics: AddFilters + Default + std::fmt::Debug;
|
||||
|
||||
fn resource_type() -> ResourceTargetVariant;
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget;
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>;
|
||||
|
||||
@@ -693,13 +694,17 @@ pub async fn update<T: KomodoResource>(
|
||||
return Ok(resource);
|
||||
}
|
||||
|
||||
let mut diff_log = String::from("diff");
|
||||
|
||||
for FieldDiff { field, from, to } in diff.iter_field_diffs() {
|
||||
diff_log.push_str(&format!(
|
||||
"\n\n<span class=\"text-muted-foreground\">field</span>: '{field}'\n<span class=\"text-muted-foreground\">from</span>: <span class=\"text-red-700 dark:text-red-400\">{from}</span>\n<span class=\"text-muted-foreground\">to</span>: <span class=\"text-green-700 dark:text-green-400\">{to}</span>",
|
||||
));
|
||||
// Leave this Result unhandled for now
|
||||
let prev_toml = ExportResourcesToToml {
|
||||
targets: vec![T::resource_target(&resource.id)],
|
||||
..Default::default()
|
||||
}
|
||||
.resolve(&ReadArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed to export resource toml before update");
|
||||
|
||||
// This minimizes the update against the existing config
|
||||
let config: T::PartialConfig = diff.into();
|
||||
@@ -715,13 +720,35 @@ pub async fn update<T: KomodoResource>(
|
||||
.await
|
||||
.context("failed to update resource on database")?;
|
||||
|
||||
let curr_toml = ExportResourcesToToml {
|
||||
targets: vec![T::resource_target(&id)],
|
||||
..Default::default()
|
||||
}
|
||||
.resolve(&ReadArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed to export resource toml after update");
|
||||
|
||||
let mut update = make_update(
|
||||
resource_target::<T>(id),
|
||||
T::update_operation(),
|
||||
user,
|
||||
);
|
||||
|
||||
update.push_simple_log("update config", diff_log);
|
||||
match prev_toml {
|
||||
Ok(res) => update.prev_toml = res.toml,
|
||||
Err(e) => update
|
||||
// These logs are pushed with success == true, so user still knows the update was succesful.
|
||||
.push_simple_log("Failed export", format_serror(&e.into())),
|
||||
}
|
||||
match curr_toml {
|
||||
Ok(res) => update.current_toml = res.toml,
|
||||
Err(e) => update
|
||||
// These logs are pushed with success == true, so user still knows the update was succesful.
|
||||
.push_simple_log("Failed export", format_serror(&e.into())),
|
||||
}
|
||||
|
||||
let updated = get::<T>(id_or_name).await?;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{Context, anyhow};
|
||||
use komodo_client::{
|
||||
api::execute::Execution,
|
||||
entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
@@ -31,6 +31,9 @@ use mungos::{
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
schedule::{
|
||||
cancel_schedule, get_schedule_item_info, update_schedule,
|
||||
},
|
||||
state::{action_states, db_client, procedure_state_cache},
|
||||
};
|
||||
|
||||
@@ -46,6 +49,10 @@ impl super::KomodoResource for Procedure {
|
||||
ResourceTargetVariant::Procedure
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Procedure(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().procedures
|
||||
@@ -55,6 +62,9 @@ impl super::KomodoResource for Procedure {
|
||||
procedure: Resource<Self::Config, Self::Info>,
|
||||
) -> Self::ListItem {
|
||||
let state = get_procedure_state(&procedure.id).await;
|
||||
let (next_scheduled_run, schedule_error) = get_schedule_item_info(
|
||||
&ResourceTarget::Procedure(procedure.id.clone()),
|
||||
);
|
||||
ProcedureListItem {
|
||||
name: procedure.name,
|
||||
id: procedure.id,
|
||||
@@ -63,6 +73,8 @@ impl super::KomodoResource for Procedure {
|
||||
info: ProcedureListItemInfo {
|
||||
stages: procedure.config.stages.len() as i64,
|
||||
state,
|
||||
next_scheduled_run,
|
||||
schedule_error,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -94,9 +106,10 @@ impl super::KomodoResource for Procedure {
|
||||
}
|
||||
|
||||
async fn post_create(
|
||||
_created: &Resource<Self::Config, Self::Info>,
|
||||
created: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
update_schedule(created);
|
||||
refresh_procedure_state_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -142,9 +155,10 @@ impl super::KomodoResource for Procedure {
|
||||
}
|
||||
|
||||
async fn post_delete(
|
||||
_resource: &Resource<Self::Config, Self::Info>,
|
||||
resource: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
cancel_schedule(&ResourceTarget::Procedure(resource.id.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::Duration;
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
builder::Builder,
|
||||
permission::PermissionLevel,
|
||||
repo::{
|
||||
@@ -44,6 +44,10 @@ impl super::KomodoResource for Repo {
|
||||
ResourceTargetVariant::Repo
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Repo(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().repos
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTargetVariant, komodo_timestamp,
|
||||
Operation, ResourceTarget, ResourceTargetVariant, komodo_timestamp,
|
||||
resource::Resource,
|
||||
server::{
|
||||
PartialServerConfig, Server, ServerConfig, ServerConfigDiff,
|
||||
@@ -29,6 +29,10 @@ impl super::KomodoResource for Server {
|
||||
ResourceTargetVariant::Server
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Server(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().servers
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use komodo_client::entities::{
|
||||
MergePartial, Operation, ResourceTargetVariant,
|
||||
MergePartial, Operation, ResourceTarget, ResourceTargetVariant,
|
||||
resource::Resource,
|
||||
server_template::{
|
||||
PartialServerTemplateConfig, ServerTemplate,
|
||||
@@ -29,6 +29,10 @@ impl super::KomodoResource for ServerTemplate {
|
||||
ResourceTargetVariant::ServerTemplate
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::ServerTemplate(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().server_templates
|
||||
|
||||
@@ -3,7 +3,7 @@ use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::RefreshStackCache,
|
||||
entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
permission::PermissionLevel,
|
||||
resource::Resource,
|
||||
server::Server,
|
||||
@@ -44,6 +44,10 @@ impl super::KomodoResource for Stack {
|
||||
ResourceTargetVariant::Stack
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::Stack(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().stacks
|
||||
|
||||
@@ -3,7 +3,8 @@ use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::RefreshResourceSyncPending,
|
||||
entities::{
|
||||
Operation, ResourceTargetVariant, komodo_timestamp,
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
komodo_timestamp,
|
||||
resource::Resource,
|
||||
sync::{
|
||||
PartialResourceSyncConfig, ResourceSync, ResourceSyncConfig,
|
||||
@@ -36,6 +37,10 @@ impl super::KomodoResource for ResourceSync {
|
||||
ResourceTargetVariant::ResourceSync
|
||||
}
|
||||
|
||||
fn resource_target(id: impl Into<String>) -> ResourceTarget {
|
||||
ResourceTarget::ResourceSync(id.into())
|
||||
}
|
||||
|
||||
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
|
||||
{
|
||||
&db_client().resource_syncs
|
||||
|
||||
378
bin/core/src/schedule.rs
Normal file
378
bin/core/src/schedule.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{OnceLock, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::Timelength;
|
||||
use chrono::Local;
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::execute::{RunAction, RunProcedure},
|
||||
entities::{
|
||||
ResourceTarget, ResourceTargetVariant, ScheduleFormat,
|
||||
action::Action,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
komodo_timestamp,
|
||||
procedure::Procedure,
|
||||
user::{action_user, procedure_user},
|
||||
},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::execute::{ExecuteArgs, ExecuteRequest},
|
||||
helpers::update::init_execution_update,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
pub fn spawn_schedule_executor() {
|
||||
// Executor thread
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let current_time = async_timing_util::wait_until_timelength(
|
||||
Timelength::OneSecond,
|
||||
0,
|
||||
)
|
||||
.await as i64;
|
||||
let mut lock = schedules().write().unwrap();
|
||||
let drained = lock.drain().collect::<Vec<_>>();
|
||||
for (target, next_run) in drained {
|
||||
match next_run {
|
||||
Ok(next_run_time) if current_time >= next_run_time => {
|
||||
tokio::spawn(async move {
|
||||
match &target {
|
||||
ResourceTarget::Action(id) => {
|
||||
let action = match crate::resource::get::<Action>(
|
||||
id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(action) => action,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Scheduled action run on {id} failed | failed to get procedure | {e:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let request =
|
||||
ExecuteRequest::RunAction(RunAction {
|
||||
action: id.clone(),
|
||||
});
|
||||
let update = match init_execution_update(
|
||||
&request,
|
||||
action_user(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(update) => update,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to make update for scheduled action run, action {id} is not being run | {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let ExecuteRequest::RunAction(request) = request
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
if let Err(e) = request
|
||||
.resolve(&ExecuteArgs {
|
||||
user: action_user().to_owned(),
|
||||
update,
|
||||
})
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Scheduled action run on {id} failed | {e:?}"
|
||||
);
|
||||
}
|
||||
update_schedule(&action);
|
||||
if action.config.schedule_alert {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Ok,
|
||||
data: AlertData::ScheduleRun {
|
||||
resource_type: ResourceTargetVariant::Action,
|
||||
id: action.id,
|
||||
name: action.name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
}
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
let procedure = match crate::resource::get::<
|
||||
Procedure,
|
||||
>(id)
|
||||
.await
|
||||
{
|
||||
Ok(procedure) => procedure,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Scheduled procedure run on {id} failed | failed to get procedure | {e:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let request =
|
||||
ExecuteRequest::RunProcedure(RunProcedure {
|
||||
procedure: id.clone(),
|
||||
});
|
||||
let update = match init_execution_update(
|
||||
&request,
|
||||
procedure_user(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(update) => update,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to make update for scheduled procedure run, procedure {id} is not being run | {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let ExecuteRequest::RunProcedure(request) = request
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
if let Err(e) = request
|
||||
.resolve(&ExecuteArgs {
|
||||
user: procedure_user().to_owned(),
|
||||
update,
|
||||
})
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Scheduled procedure run on {id} failed | {e:?}"
|
||||
);
|
||||
}
|
||||
update_schedule(&procedure);
|
||||
if procedure.config.schedule_alert {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: komodo_timestamp(),
|
||||
resolved_ts: Some(komodo_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Ok,
|
||||
data: AlertData::ScheduleRun {
|
||||
resource_type:
|
||||
ResourceTargetVariant::Procedure,
|
||||
id: procedure.id,
|
||||
name: procedure.name,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
other => {
|
||||
lock.insert(target, other);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
// Updater thread
|
||||
tokio::spawn(async move {
|
||||
update_schedules().await;
|
||||
loop {
|
||||
async_timing_util::wait_until_timelength(
|
||||
Timelength::FiveMinutes,
|
||||
500,
|
||||
)
|
||||
.await;
|
||||
update_schedules().await
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type UnixTimestampMs = i64;
|
||||
type Schedules =
|
||||
HashMap<ResourceTarget, Result<UnixTimestampMs, String>>;
|
||||
|
||||
fn schedules() -> &'static RwLock<Schedules> {
|
||||
static SCHEDULES: OnceLock<RwLock<Schedules>> = OnceLock::new();
|
||||
SCHEDULES.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub fn get_schedule_item_info(
|
||||
target: &ResourceTarget,
|
||||
) -> (Option<i64>, Option<String>) {
|
||||
match schedules().read().unwrap().get(target) {
|
||||
Some(Ok(time)) => (Some(*time), None),
|
||||
Some(Err(e)) => (None, Some(e.clone())),
|
||||
None => (None, None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel_schedule(target: &ResourceTarget) {
|
||||
schedules().write().unwrap().remove(target);
|
||||
}
|
||||
|
||||
pub async fn update_schedules() {
|
||||
let (procedures, actions) = tokio::join!(
|
||||
find_collect(&db_client().procedures, None, None),
|
||||
find_collect(&db_client().actions, None, None),
|
||||
);
|
||||
let procedures = match procedures
|
||||
.context("failed to get all procedures from db")
|
||||
{
|
||||
Ok(procedures) => procedures,
|
||||
Err(e) => {
|
||||
error!("failed to get procedures for schedule update | {e:#}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
let actions =
|
||||
match actions.context("failed to get all actions from db") {
|
||||
Ok(actions) => actions,
|
||||
Err(e) => {
|
||||
error!("failed to get actions for schedule update | {e:#}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
// clear out any schedules which don't match to existing resources
|
||||
{
|
||||
let mut lock = schedules().write().unwrap();
|
||||
lock.retain(|target, _| match target {
|
||||
ResourceTarget::Action(id) => {
|
||||
actions.iter().any(|action| &action.id == id)
|
||||
}
|
||||
ResourceTarget::Procedure(id) => {
|
||||
procedures.iter().any(|procedure| &procedure.id == id)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
});
|
||||
}
|
||||
for procedure in procedures {
|
||||
update_schedule(&procedure);
|
||||
}
|
||||
for action in actions {
|
||||
update_schedule(&action);
|
||||
}
|
||||
}
|
||||
|
||||
/// Re/spawns the schedule for the given procedure
|
||||
pub fn update_schedule(schedule: impl HasSchedule) {
|
||||
// Cancel any existing schedule for the procedure
|
||||
cancel_schedule(&schedule.target());
|
||||
|
||||
if !schedule.enabled() || schedule.schedule().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
schedules().write().unwrap().insert(
|
||||
schedule.target(),
|
||||
find_next_occurrence(schedule)
|
||||
.map_err(|e| format_serror(&e.into())),
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds the next run occurence in UTC ms.
|
||||
fn find_next_occurrence(
|
||||
schedule: impl HasSchedule,
|
||||
) -> anyhow::Result<i64> {
|
||||
let cron = match schedule.format() {
|
||||
ScheduleFormat::Cron => croner::Cron::new(schedule.schedule())
|
||||
.with_seconds_required()
|
||||
.with_dom_and_dow()
|
||||
.parse()
|
||||
.context("Failed to parse schedule CRON")?,
|
||||
ScheduleFormat::English => {
|
||||
let cron =
|
||||
english_to_cron::str_cron_syntax(schedule.schedule())
|
||||
.map_err(|e| {
|
||||
anyhow!("Failed to parse english to cron | {e:?}")
|
||||
})?
|
||||
.split(' ')
|
||||
// croner does not accept year
|
||||
.take(6)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
croner::Cron::new(&cron)
|
||||
.with_seconds_required()
|
||||
.with_dom_and_dow()
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!("Failed to parse schedule CRON: {cron}")
|
||||
})?
|
||||
}
|
||||
};
|
||||
let next = if schedule.timezone().is_empty() {
|
||||
let tz_time = chrono::Local::now().with_timezone(&Local);
|
||||
cron
|
||||
.find_next_occurrence(&tz_time, false)
|
||||
.context("Failed to find next run time")?
|
||||
.timestamp_millis()
|
||||
} else {
|
||||
let tz: chrono_tz::Tz = schedule
|
||||
.timezone()
|
||||
.parse()
|
||||
.context("Failed to parse schedule timezone")?;
|
||||
let tz_time = chrono::Local::now().with_timezone(&tz);
|
||||
cron
|
||||
.find_next_occurrence(&tz_time, false)
|
||||
.context("Failed to find next run time")?
|
||||
.timestamp_millis()
|
||||
};
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub trait HasSchedule {
|
||||
fn target(&self) -> ResourceTarget;
|
||||
fn enabled(&self) -> bool;
|
||||
fn format(&self) -> ScheduleFormat;
|
||||
fn schedule(&self) -> &str;
|
||||
fn timezone(&self) -> &str;
|
||||
}
|
||||
|
||||
impl HasSchedule for &Procedure {
|
||||
fn target(&self) -> ResourceTarget {
|
||||
ResourceTarget::Procedure(self.id.clone())
|
||||
}
|
||||
fn enabled(&self) -> bool {
|
||||
self.config.schedule_enabled
|
||||
}
|
||||
fn format(&self) -> ScheduleFormat {
|
||||
self.config.schedule_format
|
||||
}
|
||||
fn schedule(&self) -> &str {
|
||||
&self.config.schedule
|
||||
}
|
||||
fn timezone(&self) -> &str {
|
||||
&self.config.schedule_timezone
|
||||
}
|
||||
}
|
||||
|
||||
impl HasSchedule for &Action {
|
||||
fn target(&self) -> ResourceTarget {
|
||||
ResourceTarget::Action(self.id.clone())
|
||||
}
|
||||
fn enabled(&self) -> bool {
|
||||
self.config.schedule_enabled
|
||||
}
|
||||
fn format(&self) -> ScheduleFormat {
|
||||
self.config.schedule_format
|
||||
}
|
||||
fn schedule(&self) -> &str {
|
||||
&self.config.schedule
|
||||
}
|
||||
fn timezone(&self) -> &str {
|
||||
&self.config.schedule_timezone
|
||||
}
|
||||
}
|
||||
@@ -424,7 +424,7 @@ fn build_cache_for_deployment<'a>(
|
||||
let deployed_version = status
|
||||
.container
|
||||
.as_ref()
|
||||
.and_then(|c| c.image.as_ref()?.split(':').last())
|
||||
.and_then(|c| c.image.as_ref()?.split(':').next_back())
|
||||
.unwrap_or("0.0.0");
|
||||
match build_version_cache.get(build_id) {
|
||||
Some(version) if deployed_version != version => {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget, ResourceTargetVariant,
|
||||
ResourceTargetVariant,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
@@ -55,8 +55,6 @@ pub struct ToUpdateItem<T: Default> {
|
||||
}
|
||||
|
||||
pub trait ResourceSyncTrait: ToToml + Sized {
|
||||
fn resource_target(id: String) -> ResourceTarget;
|
||||
|
||||
/// To exclude resource syncs with "file_contents" (they aren't compatible)
|
||||
fn include_resource(
|
||||
name: &String,
|
||||
|
||||
@@ -4,7 +4,7 @@ use formatting::{Color, bold, colored, muted};
|
||||
use komodo_client::{
|
||||
api::execute::Execution,
|
||||
entities::{
|
||||
ResourceTarget, ResourceTargetVariant,
|
||||
ResourceTargetVariant,
|
||||
action::Action,
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
@@ -40,10 +40,6 @@ use super::{
|
||||
};
|
||||
|
||||
impl ResourceSyncTrait for Server {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Server(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -56,10 +52,6 @@ impl ResourceSyncTrait for Server {
|
||||
impl ExecuteResourceSync for Server {}
|
||||
|
||||
impl ResourceSyncTrait for Deployment {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Deployment(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
mut original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -93,10 +85,6 @@ impl ResourceSyncTrait for Deployment {
|
||||
impl ExecuteResourceSync for Deployment {}
|
||||
|
||||
impl ResourceSyncTrait for Stack {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Stack(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
mut original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -116,10 +104,6 @@ impl ResourceSyncTrait for Stack {
|
||||
impl ExecuteResourceSync for Stack {}
|
||||
|
||||
impl ResourceSyncTrait for Build {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Build(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
mut original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -149,10 +133,6 @@ impl ResourceSyncTrait for Build {
|
||||
impl ExecuteResourceSync for Build {}
|
||||
|
||||
impl ResourceSyncTrait for Repo {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Repo(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
mut original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -179,10 +159,6 @@ impl ResourceSyncTrait for Repo {
|
||||
impl ExecuteResourceSync for Repo {}
|
||||
|
||||
impl ResourceSyncTrait for Alerter {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Alerter(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -195,10 +171,6 @@ impl ResourceSyncTrait for Alerter {
|
||||
impl ExecuteResourceSync for Alerter {}
|
||||
|
||||
impl ResourceSyncTrait for Builder {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Builder(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
mut original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -220,10 +192,6 @@ impl ResourceSyncTrait for Builder {
|
||||
impl ExecuteResourceSync for Builder {}
|
||||
|
||||
impl ResourceSyncTrait for ServerTemplate {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::ServerTemplate(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -236,10 +204,6 @@ impl ResourceSyncTrait for ServerTemplate {
|
||||
impl ExecuteResourceSync for ServerTemplate {}
|
||||
|
||||
impl ResourceSyncTrait for Action {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Action(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
@@ -252,10 +216,6 @@ impl ResourceSyncTrait for Action {
|
||||
impl ExecuteResourceSync for Action {}
|
||||
|
||||
impl ResourceSyncTrait for ResourceSync {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::ResourceSync(id)
|
||||
}
|
||||
|
||||
fn include_resource(
|
||||
name: &String,
|
||||
config: &Self::Config,
|
||||
@@ -341,10 +301,6 @@ impl ResourceSyncTrait for ResourceSync {
|
||||
impl ExecuteResourceSync for ResourceSync {}
|
||||
|
||||
impl ResourceSyncTrait for Procedure {
|
||||
fn resource_target(id: String) -> ResourceTarget {
|
||||
ResourceTarget::Procedure(id)
|
||||
}
|
||||
|
||||
fn get_diff(
|
||||
mut original: Self::Config,
|
||||
update: Self::PartialConfig,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## All in one, multi stage compile + runtime Docker build for your architecture.
|
||||
|
||||
FROM rust:1.85.1-bullseye AS builder
|
||||
FROM rust:1.86.0-bullseye AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Resolve<super::Args> for GetDockerfileContentsOnHost {
|
||||
} = self;
|
||||
|
||||
let root =
|
||||
periphery_config().build_dir.join(to_komodo_name(&name));
|
||||
periphery_config().build_dir().join(to_komodo_name(&name));
|
||||
let build_dir =
|
||||
root.join(&build_path).components().collect::<PathBuf>();
|
||||
|
||||
@@ -91,7 +91,7 @@ impl Resolve<super::Args> for WriteDockerfileContentsToHost {
|
||||
contents,
|
||||
} = self;
|
||||
let full_path = periphery_config()
|
||||
.build_dir
|
||||
.build_dir()
|
||||
.join(to_komodo_name(&name))
|
||||
.join(&build_path)
|
||||
.join(dockerfile_path)
|
||||
@@ -180,7 +180,7 @@ impl Resolve<super::Args> for build::Build {
|
||||
let name = to_komodo_name(name);
|
||||
|
||||
let build_path =
|
||||
periphery_config().build_dir.join(&name).join(build_path);
|
||||
periphery_config().build_dir().join(&name).join(build_path);
|
||||
let dockerfile_path = optional_string(dockerfile_path)
|
||||
.unwrap_or("Dockerfile".to_owned());
|
||||
|
||||
@@ -248,7 +248,7 @@ impl Resolve<super::Args> for build::Build {
|
||||
} {
|
||||
let success = log.success;
|
||||
logs.push(log);
|
||||
if success {
|
||||
if !success {
|
||||
return Ok(logs);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Resolve<super::Args> for GetComposeContentsOnHost {
|
||||
file_paths,
|
||||
} = self;
|
||||
let root =
|
||||
periphery_config().stack_dir.join(to_komodo_name(&name));
|
||||
periphery_config().stack_dir().join(to_komodo_name(&name));
|
||||
let run_directory =
|
||||
root.join(&run_directory).components().collect::<PathBuf>();
|
||||
|
||||
@@ -196,7 +196,7 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
|
||||
contents,
|
||||
} = self;
|
||||
let file_path = periphery_config()
|
||||
.stack_dir
|
||||
.stack_dir()
|
||||
.join(to_komodo_name(&name))
|
||||
.join(&run_directory)
|
||||
.join(file_path)
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Resolve<super::Args> for GetLatestCommit {
|
||||
) -> serror::Result<LatestCommit> {
|
||||
let repo_path = match self.path {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => periphery_config().repo_dir.join(self.name),
|
||||
None => periphery_config().repo_dir().join(self.name),
|
||||
};
|
||||
if !repo_path.is_dir() {
|
||||
return Err(
|
||||
@@ -70,13 +70,13 @@ impl Resolve<super::Args> for CloneRepo {
|
||||
),
|
||||
};
|
||||
let parent_dir = if args.is_build {
|
||||
&periphery_config().build_dir
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
&periphery_config().repo_dir
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
git::clone(
|
||||
args,
|
||||
parent_dir,
|
||||
&parent_dir,
|
||||
token,
|
||||
&environment,
|
||||
&env_file_path,
|
||||
@@ -140,13 +140,13 @@ impl Resolve<super::Args> for PullRepo {
|
||||
),
|
||||
};
|
||||
let parent_dir = if args.is_build {
|
||||
&periphery_config().build_dir
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
&periphery_config().repo_dir
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
git::pull(
|
||||
args,
|
||||
parent_dir,
|
||||
&parent_dir,
|
||||
token,
|
||||
&environment,
|
||||
&env_file_path,
|
||||
@@ -210,13 +210,13 @@ impl Resolve<super::Args> for PullOrCloneRepo {
|
||||
),
|
||||
};
|
||||
let parent_dir = if args.is_build {
|
||||
&periphery_config().build_dir
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
&periphery_config().repo_dir
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
git::pull_or_clone(
|
||||
args,
|
||||
parent_dir,
|
||||
&parent_dir,
|
||||
token,
|
||||
&environment,
|
||||
&env_file_path,
|
||||
@@ -252,11 +252,10 @@ impl Resolve<super::Args> for RenameRepo {
|
||||
curr_name,
|
||||
new_name,
|
||||
} = self;
|
||||
let renamed = fs::rename(
|
||||
periphery_config().repo_dir.join(&curr_name),
|
||||
periphery_config().repo_dir.join(&new_name),
|
||||
)
|
||||
.await;
|
||||
let repo_dir = periphery_config().repo_dir();
|
||||
let renamed =
|
||||
fs::rename(repo_dir.join(&curr_name), repo_dir.join(&new_name))
|
||||
.await;
|
||||
let msg = match renamed {
|
||||
Ok(_) => String::from("Renamed Repo directory on Server"),
|
||||
Err(_) => format!("No Repo cloned at {curr_name} to rename"),
|
||||
@@ -274,9 +273,9 @@ impl Resolve<super::Args> for DeleteRepo {
|
||||
// If using custom clone path, it will be passed by core instead of name.
|
||||
// So the join will resolve to just the absolute path.
|
||||
let root = if is_build {
|
||||
&periphery_config().build_dir
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
&periphery_config().repo_dir
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
let full_path = root.join(&name);
|
||||
let deleted =
|
||||
|
||||
@@ -430,7 +430,7 @@ pub async fn write_stack(
|
||||
Option<Vec<(String, String)>>,
|
||||
)> {
|
||||
let root = periphery_config()
|
||||
.stack_dir
|
||||
.stack_dir()
|
||||
.join(to_komodo_name(&stack.name));
|
||||
let run_directory = root.join(&stack.config.run_directory);
|
||||
// This will remove any intermediate '/./' in the path, which is a problem for some OS.
|
||||
|
||||
@@ -36,9 +36,12 @@ pub fn periphery_config() -> &'static PeripheryConfig {
|
||||
PeripheryConfig {
|
||||
port: env.periphery_port.unwrap_or(config.port),
|
||||
bind_ip: env.periphery_bind_ip.unwrap_or(config.bind_ip),
|
||||
repo_dir: env.periphery_repo_dir.unwrap_or(config.repo_dir),
|
||||
stack_dir: env.periphery_stack_dir.unwrap_or(config.stack_dir),
|
||||
build_dir: env.periphery_build_dir.unwrap_or(config.build_dir),
|
||||
root_directory: env
|
||||
.periphery_root_directory
|
||||
.unwrap_or(config.root_directory),
|
||||
repo_dir: env.periphery_repo_dir.or(config.repo_dir),
|
||||
stack_dir: env.periphery_stack_dir.or(config.stack_dir),
|
||||
build_dir: env.periphery_build_dir.or(config.build_dir),
|
||||
stats_polling_rate: env
|
||||
.periphery_stats_polling_rate
|
||||
.unwrap_or(config.stats_polling_rate),
|
||||
@@ -54,6 +57,9 @@ pub fn periphery_config() -> &'static PeripheryConfig {
|
||||
stdio: env
|
||||
.periphery_logging_stdio
|
||||
.unwrap_or(config.logging.stdio),
|
||||
pretty: env
|
||||
.periphery_logging_pretty
|
||||
.unwrap_or(config.logging.pretty),
|
||||
otlp_endpoint: env
|
||||
.periphery_logging_otlp_endpoint
|
||||
.unwrap_or(config.logging.otlp_endpoint),
|
||||
@@ -80,10 +86,10 @@ pub fn periphery_config() -> &'static PeripheryConfig {
|
||||
.unwrap_or(config.ssl_enabled),
|
||||
ssl_key_file: env
|
||||
.periphery_ssl_key_file
|
||||
.unwrap_or(config.ssl_key_file),
|
||||
.or(config.ssl_key_file),
|
||||
ssl_cert_file: env
|
||||
.periphery_ssl_cert_file
|
||||
.unwrap_or(config.ssl_cert_file),
|
||||
.or(config.ssl_cert_file),
|
||||
secrets: config.secrets,
|
||||
git_providers: config.git_providers,
|
||||
docker_registries: config.docker_registries,
|
||||
|
||||
@@ -101,7 +101,7 @@ pub async fn pull_or_clone_stack(
|
||||
}
|
||||
|
||||
let root = periphery_config()
|
||||
.stack_dir
|
||||
.stack_dir()
|
||||
.join(to_komodo_name(&stack.name));
|
||||
|
||||
let mut args: CloneArgs = stack.into();
|
||||
|
||||
@@ -26,10 +26,13 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
stats::spawn_system_stats_polling_thread();
|
||||
|
||||
let addr = format!("{}:{}", config::periphery_config().bind_ip, config::periphery_config().port);
|
||||
let addr = format!(
|
||||
"{}:{}",
|
||||
config::periphery_config().bind_ip,
|
||||
config::periphery_config().port
|
||||
);
|
||||
|
||||
let socket_addr =
|
||||
SocketAddr::from_str(&addr)
|
||||
let socket_addr = SocketAddr::from_str(&addr)
|
||||
.context("failed to parse listen address")?;
|
||||
|
||||
let app = router::router()
|
||||
@@ -43,8 +46,8 @@ async fn app() -> anyhow::Result<()> {
|
||||
ssl::ensure_certs().await;
|
||||
info!("Komodo Periphery starting on https://{}", socket_addr);
|
||||
let ssl_config = RustlsConfig::from_pem_file(
|
||||
&config.ssl_cert_file,
|
||||
&config.ssl_key_file,
|
||||
config.ssl_cert_file(),
|
||||
config.ssl_key_file(),
|
||||
)
|
||||
.await
|
||||
.context("Invalid ssl cert / key")?;
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::config::periphery_config;
|
||||
|
||||
pub async fn ensure_certs() {
|
||||
let config = periphery_config();
|
||||
if !config.ssl_cert_file.is_file() || !config.ssl_key_file.is_file()
|
||||
if !config.ssl_cert_file().is_file()
|
||||
|| !config.ssl_key_file().is_file()
|
||||
{
|
||||
generate_self_signed_ssl_certs().await
|
||||
}
|
||||
@@ -14,16 +15,19 @@ async fn generate_self_signed_ssl_certs() {
|
||||
|
||||
let config = periphery_config();
|
||||
|
||||
let ssl_key_file = config.ssl_key_file();
|
||||
let ssl_cert_file = config.ssl_cert_file();
|
||||
|
||||
// ensure cert folders exist
|
||||
if let Some(parent) = config.ssl_key_file.parent() {
|
||||
if let Some(parent) = ssl_key_file.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if let Some(parent) = config.ssl_cert_file.parent() {
|
||||
if let Some(parent) = ssl_cert_file.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
let key_path = &config.ssl_key_file.display();
|
||||
let cert_path = &config.ssl_cert_file.display();
|
||||
let key_path = ssl_key_file.display();
|
||||
let cert_path = ssl_cert_file.display();
|
||||
|
||||
let command = format!(
|
||||
"openssl req -x509 -newkey rsa:4096 -keyout {key_path} -out {cert_path} -sha256 -days 3650 -nodes -subj \"/C=XX/CN=periphery\""
|
||||
|
||||
@@ -13,7 +13,10 @@ use crate::{
|
||||
entities::I64,
|
||||
};
|
||||
|
||||
use super::resource::{Resource, ResourceListItem, ResourceQuery};
|
||||
use super::{
|
||||
ScheduleFormat,
|
||||
resource::{Resource, ResourceListItem, ResourceQuery},
|
||||
};
|
||||
|
||||
#[typeshare]
|
||||
pub type ActionListItem = ResourceListItem<ActionListItemInfo>;
|
||||
@@ -25,6 +28,12 @@ pub struct ActionListItemInfo {
|
||||
pub last_run_at: I64,
|
||||
/// Whether last action run successful
|
||||
pub state: ActionState,
|
||||
/// If the procedure has schedule enabled, this is the
|
||||
/// next scheduled run time in unix ms.
|
||||
pub next_scheduled_run: Option<I64>,
|
||||
/// If there is an error parsing schedule expression,
|
||||
/// it will be given here.
|
||||
pub schedule_error: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -62,15 +71,55 @@ pub type _PartialActionConfig = PartialActionConfig;
|
||||
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct ActionConfig {
|
||||
/// Typescript file contents using pre-initialized `komodo` client.
|
||||
/// Supports variable / secret interpolation.
|
||||
#[serde(default, deserialize_with = "file_contents_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_file_contents_deserializer"
|
||||
))]
|
||||
/// Choose whether to specify schedule as regular CRON, or using the english to CRON parser.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub file_contents: String,
|
||||
pub schedule_format: ScheduleFormat,
|
||||
|
||||
/// Optionally provide a schedule for the procedure to run on.
|
||||
///
|
||||
/// There are 2 ways to specify a schedule:
|
||||
///
|
||||
/// 1. Regular CRON expression:
|
||||
///
|
||||
/// (second, minute, hour, day, month, day-of-week)
|
||||
/// ```
|
||||
/// 0 0 0 1,15 * ?
|
||||
/// ```
|
||||
///
|
||||
/// 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
|
||||
///
|
||||
/// ```
|
||||
/// at midnight on the 1st and 15th of the month
|
||||
/// ```
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub schedule: String,
|
||||
|
||||
/// Whether schedule is enabled if one is provided.
|
||||
/// Can be used to temporarily disable the schedule.
|
||||
#[serde(default = "default_schedule_enabled")]
|
||||
#[builder(default = "default_schedule_enabled()")]
|
||||
#[partial_default(default_schedule_enabled())]
|
||||
pub schedule_enabled: bool,
|
||||
|
||||
/// Optional. A TZ Identifier. If not provided, will use Core local timezone.
|
||||
/// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub schedule_timezone: String,
|
||||
|
||||
/// Whether to send alerts when the schedule was run.
|
||||
#[serde(default = "default_schedule_alert")]
|
||||
#[builder(default = "default_schedule_alert()")]
|
||||
#[partial_default(default_schedule_alert())]
|
||||
pub schedule_alert: bool,
|
||||
|
||||
/// Whether to send alerts when this action fails.
|
||||
#[serde(default = "default_failure_alert")]
|
||||
#[builder(default = "default_failure_alert()")]
|
||||
#[partial_default(default_failure_alert())]
|
||||
pub failure_alert: bool,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
@@ -83,6 +132,28 @@ pub struct ActionConfig {
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub webhook_secret: String,
|
||||
|
||||
/// Typescript file contents using pre-initialized `komodo` client.
|
||||
/// Supports variable / secret interpolation.
|
||||
#[serde(default, deserialize_with = "file_contents_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_file_contents_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub file_contents: String,
|
||||
}
|
||||
|
||||
fn default_schedule_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_schedule_alert() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_failure_alert() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_webhook_enabled() -> bool {
|
||||
@@ -98,9 +169,15 @@ impl ActionConfig {
|
||||
impl Default for ActionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_contents: Default::default(),
|
||||
schedule_format: Default::default(),
|
||||
schedule: Default::default(),
|
||||
schedule_enabled: default_schedule_enabled(),
|
||||
schedule_timezone: Default::default(),
|
||||
schedule_alert: default_schedule_alert(),
|
||||
failure_alert: default_failure_alert(),
|
||||
webhook_enabled: default_webhook_enabled(),
|
||||
webhook_secret: Default::default(),
|
||||
file_contents: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use typeshare::typeshare;
|
||||
use crate::entities::{I64, MongoId};
|
||||
|
||||
use super::{
|
||||
_Serror, ResourceTarget, Version, deployment::DeploymentState,
|
||||
stack::StackState,
|
||||
_Serror, ResourceTarget, ResourceTargetVariant, Version,
|
||||
deployment::DeploymentState, stack::StackState,
|
||||
};
|
||||
|
||||
/// Representation of an alert in the system.
|
||||
@@ -260,6 +260,32 @@ pub enum AlertData {
|
||||
/// The name of the repo
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// A procedure has failed
|
||||
ProcedureFailed {
|
||||
/// The id of the procedure
|
||||
id: String,
|
||||
/// The name of the procedure
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// An action has failed
|
||||
ActionFailed {
|
||||
/// The id of the action
|
||||
id: String,
|
||||
/// The name of the action
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// A schedule was run
|
||||
ScheduleRun {
|
||||
/// Procedure or Action
|
||||
resource_type: ResourceTargetVariant,
|
||||
/// The resource id
|
||||
id: String,
|
||||
/// The resource name
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for AlertData {
|
||||
|
||||
@@ -114,6 +114,9 @@ pub enum AlerterEndpoint {
|
||||
|
||||
/// Send alert to Ntfy
|
||||
Ntfy(NtfyAlerterEndpoint),
|
||||
|
||||
/// Send alert to Pushover
|
||||
Pushover(PushoverAlerterEndpoint),
|
||||
}
|
||||
|
||||
impl Default for AlerterEndpoint {
|
||||
@@ -222,9 +225,33 @@ fn default_ntfy_url() -> String {
|
||||
String::from("http://localhost:8080/komodo")
|
||||
}
|
||||
|
||||
/// Configuration for a Pushover alerter.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Serialize, Deserialize, Builder,
|
||||
)]
|
||||
pub struct PushoverAlerterEndpoint {
|
||||
/// The pushover URL including application and user tokens in parameters.
|
||||
#[serde(default = "default_pushover_url")]
|
||||
#[builder(default = "default_pushover_url()")]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl Default for PushoverAlerterEndpoint {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: default_pushover_url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_pushover_url() -> String {
|
||||
String::from(
|
||||
"https://api.pushover.net/1/messages.json?token=XXXXXXXXXXXXX&user=XXXXXXXXXXXXX",
|
||||
)
|
||||
}
|
||||
|
||||
// QUERY
|
||||
|
||||
#[typeshare]
|
||||
pub type AlerterQuery = ResourceQuery<AlerterQuerySpecifics>;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct BuildListItemInfo {
|
||||
|
||||
/// Whether build is in files on host mode.
|
||||
pub files_on_host: bool,
|
||||
|
||||
|
||||
/// The git provider domain
|
||||
pub git_provider: Option<String>,
|
||||
/// The repo used as the source of the build
|
||||
|
||||
@@ -85,6 +85,8 @@ pub struct Env {
|
||||
pub komodo_logging_level: Option<LogLevel>,
|
||||
/// Override `logging.stdio`
|
||||
pub komodo_logging_stdio: Option<StdioLogMode>,
|
||||
/// Override `logging.pretty`
|
||||
pub komodo_logging_pretty: Option<bool>,
|
||||
/// Override `logging.otlp_endpoint`
|
||||
pub komodo_logging_otlp_endpoint: Option<String>,
|
||||
/// Override `logging.opentelemetry_service_name`
|
||||
@@ -260,7 +262,7 @@ pub struct CoreConfig {
|
||||
|
||||
/// IP address the core server binds to.
|
||||
/// Default: [::].
|
||||
#[serde(default = "default_core_bind_ip")]
|
||||
#[serde(default = "default_core_bind_ip")]
|
||||
pub bind_ip: String,
|
||||
|
||||
/// Sent in auth header with req to periphery.
|
||||
|
||||
@@ -116,6 +116,8 @@ pub struct Env {
|
||||
pub periphery_port: Option<u16>,
|
||||
/// Override `bind_ip`
|
||||
pub periphery_bind_ip: Option<String>,
|
||||
/// Override `root_directory`
|
||||
pub periphery_root_directory: Option<PathBuf>,
|
||||
/// Override `repo_dir`
|
||||
pub periphery_repo_dir: Option<PathBuf>,
|
||||
/// Override `stack_dir`
|
||||
@@ -132,6 +134,8 @@ pub struct Env {
|
||||
pub periphery_logging_level: Option<LogLevel>,
|
||||
/// Override `logging.stdio`
|
||||
pub periphery_logging_stdio: Option<StdioLogMode>,
|
||||
/// Override `logging.pretty`
|
||||
pub periphery_logging_pretty: Option<bool>,
|
||||
/// Override `logging.otlp_endpoint`
|
||||
pub periphery_logging_otlp_endpoint: Option<String>,
|
||||
/// Override `logging.opentelemetry_service_name`
|
||||
@@ -171,20 +175,33 @@ pub struct PeripheryConfig {
|
||||
#[serde(default = "default_periphery_bind_ip")]
|
||||
pub bind_ip: String,
|
||||
|
||||
/// The directory Komodo will use as the default root for the specific (repo, stack, build) directories.
|
||||
///
|
||||
/// repo: ${root_directory}/repos
|
||||
/// stack: ${root_directory}/stacks
|
||||
/// build: ${root_directory}/builds
|
||||
///
|
||||
/// Note. These can each be overridden with a specific directory
|
||||
/// by specifying `repo_dir`, `stack_dir`, or `build_dir` explicitly
|
||||
///
|
||||
/// Default: `/etc/komodo`
|
||||
#[serde(default = "default_root_directory")]
|
||||
pub root_directory: PathBuf,
|
||||
|
||||
/// The system directory where Komodo managed repos will be cloned.
|
||||
/// Default: `/etc/komodo/repos`
|
||||
#[serde(default = "default_repo_dir")]
|
||||
pub repo_dir: PathBuf,
|
||||
/// If not provided, will default to `${root_directory}/repos`.
|
||||
/// Default: empty
|
||||
pub repo_dir: Option<PathBuf>,
|
||||
|
||||
/// The system directory where stacks will managed.
|
||||
/// Default: `/etc/komodo/stacks`
|
||||
#[serde(default = "default_stack_dir")]
|
||||
pub stack_dir: PathBuf,
|
||||
/// If not provided, will default to `${root_directory}/stacks`.
|
||||
/// Default: empty
|
||||
pub stack_dir: Option<PathBuf>,
|
||||
|
||||
/// The system directory where builds will managed.
|
||||
/// Default: `/etc/komodo/builds`
|
||||
#[serde(default = "default_build_dir")]
|
||||
pub build_dir: PathBuf,
|
||||
/// If not provided, will default to `${root_directory}/builds`.
|
||||
/// Default: empty
|
||||
pub build_dir: Option<PathBuf>,
|
||||
|
||||
/// The rate at which the system stats will be polled to update the cache.
|
||||
/// Default: `5-sec`
|
||||
@@ -244,14 +261,12 @@ pub struct PeripheryConfig {
|
||||
pub ssl_enabled: bool,
|
||||
|
||||
/// Path to the ssl key.
|
||||
/// Default: `/etc/komodo/ssl/periphery/key.pem`.
|
||||
#[serde(default = "default_ssl_key_file")]
|
||||
pub ssl_key_file: PathBuf,
|
||||
/// Default: `${root_directory}/ssl/key.pem`.
|
||||
pub ssl_key_file: Option<PathBuf>,
|
||||
|
||||
/// Path to the ssl cert.
|
||||
/// Default: `/etc/komodo/ssl/periphery/cert.pem`.
|
||||
#[serde(default = "default_ssl_cert_file")]
|
||||
pub ssl_cert_file: PathBuf,
|
||||
/// Default: `${root_directory}/ssl/cert.pem`.
|
||||
pub ssl_cert_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn default_periphery_port() -> u16 {
|
||||
@@ -262,16 +277,8 @@ fn default_periphery_bind_ip() -> String {
|
||||
"[::]".to_string()
|
||||
}
|
||||
|
||||
fn default_repo_dir() -> PathBuf {
|
||||
"/etc/komodo/repos".parse().unwrap()
|
||||
}
|
||||
|
||||
fn default_stack_dir() -> PathBuf {
|
||||
"/etc/komodo/stacks".parse().unwrap()
|
||||
}
|
||||
|
||||
fn default_build_dir() -> PathBuf {
|
||||
"/etc/komodo/builds".parse().unwrap()
|
||||
fn default_root_directory() -> PathBuf {
|
||||
"/etc/komodo".parse().unwrap()
|
||||
}
|
||||
|
||||
fn default_stats_polling_rate() -> Timelength {
|
||||
@@ -282,22 +289,15 @@ fn default_ssl_enabled() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_ssl_key_file() -> PathBuf {
|
||||
"/etc/komodo/ssl/key.pem".parse().unwrap()
|
||||
}
|
||||
|
||||
fn default_ssl_cert_file() -> PathBuf {
|
||||
"/etc/komodo/ssl/cert.pem".parse().unwrap()
|
||||
}
|
||||
|
||||
impl Default for PeripheryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: default_periphery_port(),
|
||||
bind_ip: default_periphery_bind_ip(),
|
||||
repo_dir: default_repo_dir(),
|
||||
stack_dir: default_stack_dir(),
|
||||
build_dir: default_build_dir(),
|
||||
root_directory: default_root_directory(),
|
||||
repo_dir: None,
|
||||
stack_dir: None,
|
||||
build_dir: None,
|
||||
stats_polling_rate: default_stats_polling_rate(),
|
||||
legacy_compose_cli: Default::default(),
|
||||
logging: Default::default(),
|
||||
@@ -309,8 +309,8 @@ impl Default for PeripheryConfig {
|
||||
git_providers: Default::default(),
|
||||
docker_registries: Default::default(),
|
||||
ssl_enabled: default_ssl_enabled(),
|
||||
ssl_key_file: default_ssl_key_file(),
|
||||
ssl_cert_file: default_ssl_cert_file(),
|
||||
ssl_key_file: None,
|
||||
ssl_cert_file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,6 +320,7 @@ impl PeripheryConfig {
|
||||
PeripheryConfig {
|
||||
port: self.port,
|
||||
bind_ip: self.bind_ip.clone(),
|
||||
root_directory: self.root_directory.clone(),
|
||||
repo_dir: self.repo_dir.clone(),
|
||||
stack_dir: self.stack_dir.clone(),
|
||||
build_dir: self.build_dir.clone(),
|
||||
@@ -378,4 +379,44 @@ impl PeripheryConfig {
|
||||
ssl_cert_file: self.ssl_cert_file.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repo_dir(&self) -> PathBuf {
|
||||
if let Some(dir) = &self.repo_dir {
|
||||
dir.to_owned()
|
||||
} else {
|
||||
self.root_directory.join("repos")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stack_dir(&self) -> PathBuf {
|
||||
if let Some(dir) = &self.stack_dir {
|
||||
dir.to_owned()
|
||||
} else {
|
||||
self.root_directory.join("stacks")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_dir(&self) -> PathBuf {
|
||||
if let Some(dir) = &self.build_dir {
|
||||
dir.to_owned()
|
||||
} else {
|
||||
self.root_directory.join("builds")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ssl_key_file(&self) -> PathBuf {
|
||||
if let Some(dir) = &self.ssl_key_file {
|
||||
dir.to_owned()
|
||||
} else {
|
||||
self.root_directory.join("ssl/key.pem")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ssl_cert_file(&self) -> PathBuf {
|
||||
if let Some(dir) = &self.ssl_cert_file {
|
||||
dir.to_owned()
|
||||
} else {
|
||||
self.root_directory.join("ssl/cert.pem")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ pub struct LogConfig {
|
||||
#[serde(default)]
|
||||
pub stdio: StdioLogMode,
|
||||
|
||||
#[serde(default)]
|
||||
pub pretty: bool,
|
||||
|
||||
/// Enable opentelemetry exporting
|
||||
#[serde(default)]
|
||||
pub otlp_endpoint: String,
|
||||
@@ -27,6 +30,7 @@ impl Default for LogConfig {
|
||||
Self {
|
||||
level: Default::default(),
|
||||
stdio: Default::default(),
|
||||
pretty: Default::default(),
|
||||
otlp_endpoint: Default::default(),
|
||||
opentelemetry_service_name: default_opentelemetry_service_name(
|
||||
),
|
||||
|
||||
@@ -999,3 +999,13 @@ impl ResourceTargetVariant {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum ScheduleFormat {
|
||||
#[default]
|
||||
English,
|
||||
Cron,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use typeshare::typeshare;
|
||||
use crate::api::execute::Execution;
|
||||
|
||||
use super::{
|
||||
I64,
|
||||
I64, ScheduleFormat,
|
||||
resource::{Resource, ResourceListItem, ResourceQuery},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,12 @@ pub struct ProcedureListItemInfo {
|
||||
pub stages: I64,
|
||||
/// Reflect whether last run successful / currently running.
|
||||
pub state: ProcedureState,
|
||||
/// If the procedure has schedule enabled, this is the
|
||||
/// next scheduled run time in unix ms.
|
||||
pub next_scheduled_run: Option<I64>,
|
||||
/// If there is an error parsing schedule expression,
|
||||
/// it will be given here.
|
||||
pub schedule_error: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -61,6 +67,56 @@ pub struct ProcedureConfig {
|
||||
#[builder(default)]
|
||||
pub stages: Vec<ProcedureStage>,
|
||||
|
||||
/// Choose whether to specify schedule as regular CRON, or using the english to CRON parser.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub schedule_format: ScheduleFormat,
|
||||
|
||||
/// Optionally provide a schedule for the procedure to run on.
|
||||
///
|
||||
/// There are 2 ways to specify a schedule:
|
||||
///
|
||||
/// 1. Regular CRON expression:
|
||||
///
|
||||
/// (second, minute, hour, day, month, day-of-week)
|
||||
/// ```
|
||||
/// 0 0 0 1,15 * ?
|
||||
/// ```
|
||||
///
|
||||
/// 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
|
||||
///
|
||||
/// ```
|
||||
/// at midnight on the 1st and 15th of the month
|
||||
/// ```
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub schedule: String,
|
||||
|
||||
/// Whether schedule is enabled if one is provided.
|
||||
/// Can be used to temporarily disable the schedule.
|
||||
#[serde(default = "default_schedule_enabled")]
|
||||
#[builder(default = "default_schedule_enabled()")]
|
||||
#[partial_default(default_schedule_enabled())]
|
||||
pub schedule_enabled: bool,
|
||||
|
||||
/// Optional. A TZ Identifier. If not provided, will use Core local timezone.
|
||||
/// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub schedule_timezone: String,
|
||||
|
||||
/// Whether to send alerts when the schedule was run.
|
||||
#[serde(default = "default_schedule_alert")]
|
||||
#[builder(default = "default_schedule_alert()")]
|
||||
#[partial_default(default_schedule_alert())]
|
||||
pub schedule_alert: bool,
|
||||
|
||||
/// Whether to send alerts when this procedure fails.
|
||||
#[serde(default = "default_failure_alert")]
|
||||
#[builder(default = "default_failure_alert()")]
|
||||
#[partial_default(default_failure_alert())]
|
||||
pub failure_alert: bool,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
#[builder(default = "default_webhook_enabled()")]
|
||||
@@ -80,6 +136,18 @@ impl ProcedureConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_schedule_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_schedule_alert() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_failure_alert() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_webhook_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -88,6 +156,12 @@ impl Default for ProcedureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stages: Default::default(),
|
||||
schedule_format: Default::default(),
|
||||
schedule: Default::default(),
|
||||
schedule_enabled: default_schedule_enabled(),
|
||||
schedule_timezone: Default::default(),
|
||||
schedule_alert: default_schedule_alert(),
|
||||
failure_alert: default_failure_alert(),
|
||||
webhook_enabled: default_webhook_enabled(),
|
||||
webhook_secret: Default::default(),
|
||||
}
|
||||
|
||||
@@ -271,6 +271,13 @@ pub struct ResourceSyncConfig {
|
||||
#[builder(default)]
|
||||
pub include_user_groups: bool,
|
||||
|
||||
/// Whether sync should send alert when it enters Pending state.
|
||||
/// Default: true
|
||||
#[serde(default = "default_pending_alert")]
|
||||
#[builder(default = "default_pending_alert()")]
|
||||
#[partial_default(default_pending_alert())]
|
||||
pub pending_alert: bool,
|
||||
|
||||
/// Manage the file contents in the UI.
|
||||
#[serde(default, deserialize_with = "file_contents_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
@@ -318,6 +325,10 @@ fn default_include_resources() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_pending_alert() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for ResourceSyncConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -338,6 +349,7 @@ impl Default for ResourceSyncConfig {
|
||||
delete: Default::default(),
|
||||
webhook_enabled: default_webhook_enabled(),
|
||||
webhook_secret: Default::default(),
|
||||
pending_alert: default_pending_alert(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,12 @@ pub struct Update {
|
||||
/// Some unstructured, operation specific data. Not for general usage.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub other_data: String,
|
||||
/// If the update is for resource config update, give the previous toml contents
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub prev_toml: String,
|
||||
/// If the update is for resource config update, give the current (at time of Update) toml contents
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub current_toml: String,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
|
||||
@@ -170,4 +170,42 @@ impl KomodoClient {
|
||||
self.reqwest = reqwest;
|
||||
self
|
||||
}
|
||||
|
||||
/// Poll an [Update][entities::update::Update] (returned by the `execute` calls) until the
|
||||
/// [UpdateStatus][entities::update::UpdateStatus] is `Complete`, and then return it.
|
||||
#[cfg(not(feature = "blocking"))]
|
||||
pub async fn poll_update_until_complete(
|
||||
&self,
|
||||
update_id: impl Into<String>,
|
||||
) -> anyhow::Result<entities::update::Update> {
|
||||
let update_id = update_id.into();
|
||||
loop {
|
||||
let update = self
|
||||
.read(api::read::GetUpdate {
|
||||
id: update_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
if update.status == entities::update::UpdateStatus::Complete {
|
||||
return Ok(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll an [Update][entities::update::Update] (returned by the `execute` calls) until the
|
||||
/// [UpdateStatus][entities::update::UpdateStatus] is `Complete`, and then return it.
|
||||
#[cfg(feature = "blocking")]
|
||||
pub fn poll_update_until_complete(
|
||||
&self,
|
||||
update_id: impl Into<String>,
|
||||
) -> anyhow::Result<entities::update::Update> {
|
||||
let update_id = update_id.into();
|
||||
loop {
|
||||
let update = self.read(api::read::GetUpdate {
|
||||
id: update_id.clone(),
|
||||
})?;
|
||||
if update.status == entities::update::UpdateStatus::Complete {
|
||||
return Ok(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.17.1",
|
||||
"version": "1.17.3",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
} from "./responses.js";
|
||||
import {
|
||||
AuthRequest,
|
||||
BatchExecutionResponse,
|
||||
ExecuteRequest,
|
||||
ReadRequest,
|
||||
Update,
|
||||
UpdateListItem,
|
||||
UpdateStatus,
|
||||
UserRequest,
|
||||
WriteRequest,
|
||||
WsLoginMessage,
|
||||
@@ -155,6 +158,42 @@ export function KomodoClient(url: string, options: InitOptions) {
|
||||
ExecuteResponses[Req["type"]]
|
||||
>("/execute", { type, params });
|
||||
|
||||
const execute_and_poll = async <
|
||||
T extends ExecuteRequest["type"],
|
||||
Req extends Extract<ExecuteRequest, { type: T }>
|
||||
>(
|
||||
type: T,
|
||||
params: Req["params"]
|
||||
) => {
|
||||
const res = await execute(type, params);
|
||||
// Check if its a batch of updates or a single update;
|
||||
if (Array.isArray(res)) {
|
||||
const batch = res as any as BatchExecutionResponse;
|
||||
return await Promise.all(
|
||||
batch.map(async (item) => {
|
||||
if (item.status === "Err") {
|
||||
return item;
|
||||
}
|
||||
return await poll_update_until_complete(item.data._id?.$oid!);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// it is a single update
|
||||
const update = res as any as Update;
|
||||
return await poll_update_until_complete(update._id?.$oid!);
|
||||
}
|
||||
};
|
||||
|
||||
const poll_update_until_complete = async (update_id: string) => {
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const update = await read("GetUpdate", { id: update_id });
|
||||
if (update.status === UpdateStatus.Complete) {
|
||||
return update;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const core_version = () => read("GetVersion", {}).then((res) => res.version);
|
||||
|
||||
const subscribe_to_update_websocket = async ({
|
||||
@@ -290,9 +329,29 @@ export function KomodoClient(url: string, options: InitOptions) {
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* NOTE. These calls return immediately when the update is created, NOT when the execution task finishes.
|
||||
* To have the call only return when the task finishes, use [execute_and_poll_until_complete].
|
||||
*
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
|
||||
*/
|
||||
execute,
|
||||
/**
|
||||
* Call the `/execute` api, and poll the update until the task has completed.
|
||||
*
|
||||
* ```
|
||||
* const update = await komodo.execute_and_poll("DeployStack", {
|
||||
* stack: "my-stack"
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
|
||||
*/
|
||||
execute_and_poll,
|
||||
/**
|
||||
* Poll an Update (returned by the `execute` calls) until the `status` is `Complete`.
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status.
|
||||
*/
|
||||
poll_update_until_complete,
|
||||
/** Returns the version of Komodo Core the client is calling to. */
|
||||
core_version,
|
||||
/**
|
||||
|
||||
@@ -51,12 +51,47 @@ export interface Resource<Config, Info> {
|
||||
base_permission?: PermissionLevel;
|
||||
}
|
||||
|
||||
export enum ScheduleFormat {
|
||||
English = "English",
|
||||
Cron = "Cron",
|
||||
}
|
||||
|
||||
export interface ActionConfig {
|
||||
/** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */
|
||||
schedule_format?: ScheduleFormat;
|
||||
/**
|
||||
* Typescript file contents using pre-initialized `komodo` client.
|
||||
* Supports variable / secret interpolation.
|
||||
* Optionally provide a schedule for the procedure to run on.
|
||||
*
|
||||
* There are 2 ways to specify a schedule:
|
||||
*
|
||||
* 1. Regular CRON expression:
|
||||
*
|
||||
* (second, minute, hour, day, month, day-of-week)
|
||||
* ```
|
||||
* 0 0 0 1,15 * ?
|
||||
* ```
|
||||
*
|
||||
* 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
|
||||
*
|
||||
* ```
|
||||
* at midnight on the 1st and 15th of the month
|
||||
* ```
|
||||
*/
|
||||
file_contents?: string;
|
||||
schedule?: string;
|
||||
/**
|
||||
* Whether schedule is enabled if one is provided.
|
||||
* Can be used to temporarily disable the schedule.
|
||||
*/
|
||||
schedule_enabled: boolean;
|
||||
/**
|
||||
* Optional. A TZ Identifier. If not provided, will use Core local timezone.
|
||||
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
|
||||
*/
|
||||
schedule_timezone?: string;
|
||||
/** Whether to send alerts when the schedule was run. */
|
||||
schedule_alert: boolean;
|
||||
/** Whether to send alerts when this action fails. */
|
||||
failure_alert: boolean;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
@@ -64,6 +99,11 @@ export interface ActionConfig {
|
||||
* If its an empty string, use the default secret from the config.
|
||||
*/
|
||||
webhook_secret?: string;
|
||||
/**
|
||||
* Typescript file contents using pre-initialized `komodo` client.
|
||||
* Supports variable / secret interpolation.
|
||||
*/
|
||||
file_contents?: string;
|
||||
}
|
||||
|
||||
export interface ActionInfo {
|
||||
@@ -102,6 +142,16 @@ export interface ActionListItemInfo {
|
||||
last_run_at: I64;
|
||||
/** Whether last action run successful */
|
||||
state: ActionState;
|
||||
/**
|
||||
* If the procedure has schedule enabled, this is the
|
||||
* next scheduled run time in unix ms.
|
||||
*/
|
||||
next_scheduled_run?: I64;
|
||||
/**
|
||||
* If there is an error parsing schedule expression,
|
||||
* it will be given here.
|
||||
*/
|
||||
schedule_error?: string;
|
||||
}
|
||||
|
||||
export type ActionListItem = ResourceListItem<ActionListItemInfo>;
|
||||
@@ -135,7 +185,9 @@ export type AlerterEndpoint =
|
||||
/** Send alert to a Discord app */
|
||||
| { type: "Discord", params: DiscordAlerterEndpoint }
|
||||
/** Send alert to Ntfy */
|
||||
| { type: "Ntfy", params: NtfyAlerterEndpoint };
|
||||
| { type: "Ntfy", params: NtfyAlerterEndpoint }
|
||||
/** Send alert to Pushover */
|
||||
| { type: "Pushover", params: PushoverAlerterEndpoint };
|
||||
|
||||
/** Used to reference a specific resource across all resource types */
|
||||
export type ResourceTarget =
|
||||
@@ -544,6 +596,41 @@ export interface ProcedureStage {
|
||||
export interface ProcedureConfig {
|
||||
/** The stages to be run by the procedure. */
|
||||
stages?: ProcedureStage[];
|
||||
/** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */
|
||||
schedule_format?: ScheduleFormat;
|
||||
/**
|
||||
* Optionally provide a schedule for the procedure to run on.
|
||||
*
|
||||
* There are 2 ways to specify a schedule:
|
||||
*
|
||||
* 1. Regular CRON expression:
|
||||
*
|
||||
* (second, minute, hour, day, month, day-of-week)
|
||||
* ```
|
||||
* 0 0 0 1,15 * ?
|
||||
* ```
|
||||
*
|
||||
* 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
|
||||
*
|
||||
* ```
|
||||
* at midnight on the 1st and 15th of the month
|
||||
* ```
|
||||
*/
|
||||
schedule?: string;
|
||||
/**
|
||||
* Whether schedule is enabled if one is provided.
|
||||
* Can be used to temporarily disable the schedule.
|
||||
*/
|
||||
schedule_enabled: boolean;
|
||||
/**
|
||||
* Optional. A TZ Identifier. If not provided, will use Core local timezone.
|
||||
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
|
||||
*/
|
||||
schedule_timezone?: string;
|
||||
/** Whether to send alerts when the schedule was run. */
|
||||
schedule_alert: boolean;
|
||||
/** Whether to send alerts when this procedure fails. */
|
||||
failure_alert: boolean;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
@@ -1146,6 +1233,29 @@ export type AlertData =
|
||||
id: string;
|
||||
/** The name of the repo */
|
||||
name: string;
|
||||
}}
|
||||
/** A procedure has failed */
|
||||
| { type: "ProcedureFailed", data: {
|
||||
/** The id of the procedure */
|
||||
id: string;
|
||||
/** The name of the procedure */
|
||||
name: string;
|
||||
}}
|
||||
/** An action has failed */
|
||||
| { type: "ActionFailed", data: {
|
||||
/** The id of the action */
|
||||
id: string;
|
||||
/** The name of the action */
|
||||
name: string;
|
||||
}}
|
||||
/** A schedule was run */
|
||||
| { type: "ScheduleRun", data: {
|
||||
/** Procedure or Action */
|
||||
resource_type: ResourceTarget["type"];
|
||||
/** The resource id */
|
||||
id: string;
|
||||
/** The resource name */
|
||||
name: string;
|
||||
}};
|
||||
|
||||
/** Representation of an alert in the system. */
|
||||
@@ -1424,6 +1534,11 @@ export interface ResourceSyncConfig {
|
||||
include_variables?: boolean;
|
||||
/** Whether sync should include user groups. */
|
||||
include_user_groups?: boolean;
|
||||
/**
|
||||
* Whether sync should send alert when it enters Pending state.
|
||||
* Default: true
|
||||
*/
|
||||
pending_alert: boolean;
|
||||
/** Manage the file contents in the UI. */
|
||||
file_contents?: string;
|
||||
}
|
||||
@@ -2198,6 +2313,10 @@ export interface Update {
|
||||
commit_hash?: string;
|
||||
/** Some unstructured, operation specific data. Not for general usage. */
|
||||
other_data?: string;
|
||||
/** If the update is for resource config update, give the previous toml contents */
|
||||
prev_toml?: string;
|
||||
/** If the update is for resource config update, give the current (at time of Update) toml contents */
|
||||
current_toml?: string;
|
||||
}
|
||||
|
||||
export type GetUpdateResponse = Update;
|
||||
@@ -3286,6 +3405,16 @@ export interface ProcedureListItemInfo {
|
||||
stages: I64;
|
||||
/** Reflect whether last run successful / currently running. */
|
||||
state: ProcedureState;
|
||||
/**
|
||||
* If the procedure has schedule enabled, this is the
|
||||
* next scheduled run time in unix ms.
|
||||
*/
|
||||
next_scheduled_run?: I64;
|
||||
/**
|
||||
* If there is an error parsing schedule expression,
|
||||
* it will be given here.
|
||||
*/
|
||||
schedule_error?: string;
|
||||
}
|
||||
|
||||
export type ProcedureListItem = ResourceListItem<ProcedureListItemInfo>;
|
||||
@@ -6609,6 +6738,12 @@ export interface PushRecentlyViewed {
|
||||
resource: ResourceTarget;
|
||||
}
|
||||
|
||||
/** Configuration for a Pushover alerter. */
|
||||
export interface PushoverAlerterEndpoint {
|
||||
/** The pushover URL including application and user tokens in parameters. */
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** Trigger a refresh of the cached latest hash and message. */
|
||||
export interface RefreshBuildCache {
|
||||
/** Id or name */
|
||||
|
||||
@@ -106,6 +106,6 @@ pub struct RenameRepo {
|
||||
#[error(serror::Error)]
|
||||
pub struct DeleteRepo {
|
||||
pub name: String,
|
||||
/// Clears
|
||||
/// Clears
|
||||
pub is_build: bool,
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ KOMODO_DB_PASSWORD=admin
|
||||
## Configure a secure passkey to authenticate between Core / Periphery.
|
||||
KOMODO_PASSKEY=a_random_passkey
|
||||
|
||||
## Set your time zone for schedules
|
||||
## https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
TZ=Etc/UTC
|
||||
|
||||
#=-------------------------=#
|
||||
#= Komodo Core Environment =#
|
||||
#=-------------------------=#
|
||||
@@ -122,19 +126,13 @@ PERIPHERY_PASSKEYS=${KOMODO_PASSKEY}
|
||||
## Specify the root directory used by Periphery agent.
|
||||
PERIPHERY_ROOT_DIRECTORY=/etc/komodo
|
||||
|
||||
PERIPHERY_REPO_DIR=$PERIPHERY_ROOT_DIRECTORY/stacks
|
||||
PERIPHERY_STACK_DIR=$PERIPHERY_ROOT_DIRECTORY/repos
|
||||
PERIPHERY_BUILD_DIR=$PERIPHERY_ROOT_DIRECTORY/builds
|
||||
|
||||
## Enable SSL using self signed certificates.
|
||||
## Connect to Periphery at https://address:8120.
|
||||
PERIPHERY_SSL_ENABLED=true
|
||||
PERIPHERY_SSL_KEY_FILE=$PERIPHERY_ROOT_DIRECTORY/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE=$PERIPHERY_ROOT_DIRECTORY/ssl/cert.pem
|
||||
|
||||
## If the disk size is overreporting, can use one of these to
|
||||
## whitelist / blacklist the disks to filter them, whichever is easier.
|
||||
## Accepts comma separated list of paths.
|
||||
## Usually whitelisting just /etc/hostname gives correct size.
|
||||
PERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostname
|
||||
# PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos
|
||||
# PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos
|
||||
|
||||
@@ -15,11 +15,7 @@ services:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
## https://komo.do/docs/connect-servers#configuration
|
||||
environment:
|
||||
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
|
||||
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
|
||||
PERIPHERY_BUILD_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/builds
|
||||
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
|
||||
PERIPHERY_ROOT_DIRECTORY: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
## Pass the same passkey as used by the Komodo Core connecting to this Periphery agent.
|
||||
PERIPHERY_PASSKEYS: abc123
|
||||
## If the disk size is overreporting, can use one of these to
|
||||
|
||||
@@ -341,12 +341,18 @@ webhook_base_url = ""
|
||||
## Default: info
|
||||
logging.level = "info"
|
||||
|
||||
## Specify the logging format for stdout / stderr.
|
||||
## Specify the logging format.
|
||||
## Env: KOMODO_LOGGING_STDIO
|
||||
## Options: standard, json, none
|
||||
## Default: standard
|
||||
logging.stdio = "standard"
|
||||
|
||||
## Specify whether logging is more human readable.
|
||||
## Note. Single logs will span multiple lines.
|
||||
## Env: KOMODO_LOGGING_PRETTY
|
||||
## Default: false
|
||||
logging.pretty = false
|
||||
|
||||
## Optionally specify a opentelemetry otlp endpoint to send traces to.
|
||||
## Example: http://localhost:4317
|
||||
## Env: KOMODO_LOGGING_OTLP_ENDPOINT
|
||||
|
||||
@@ -24,23 +24,29 @@ port = 8120
|
||||
## Default: [::]
|
||||
bind_ip = "[::]"
|
||||
|
||||
## The directory periphery will use to manage repos.
|
||||
## The directory periphery will use as the default base for the directories it uses.
|
||||
## The periphery user must have write access to this directory.
|
||||
## Env: PERIPHERY_ROOT_DIRECTORY
|
||||
## Default: /etc/komodo
|
||||
root_directory = "/etc/komodo"
|
||||
|
||||
## Optional. Override the directory periphery will use to manage repos.
|
||||
## The periphery user must have write access to this directory.
|
||||
## Env: PERIPHERY_REPO_DIR
|
||||
## Default: /etc/komodo/repos
|
||||
repo_dir = "/etc/komodo/repos"
|
||||
## Default: ${root_directory}/repos
|
||||
# repo_dir = "/etc/komodo/repos"
|
||||
|
||||
## The directory periphery will use to manage stacks.
|
||||
## Optional. Override the directory periphery will use to manage stacks.
|
||||
## The periphery user must have write access to this directory.
|
||||
## Env: PERIPHERY_STACK_DIR
|
||||
## Default: /etc/komodo/stacks
|
||||
stack_dir = "/etc/komodo/stacks"
|
||||
## Default: ${root_directory}/stacks
|
||||
# stack_dir = "/etc/komodo/stacks"
|
||||
|
||||
## The directory periphery will use to manage builds.
|
||||
## Optional. Override the directory periphery will use to manage builds.
|
||||
## The periphery user must have write access to this directory.
|
||||
## Env: PERIPHERY_BUILD_DIR
|
||||
## Default: /etc/komodo/builds
|
||||
build_dir = "/etc/komodo/builds"
|
||||
## Default: ${root_directory}/builds
|
||||
# build_dir = "/etc/komodo/builds"
|
||||
|
||||
## How often Periphery polls the host for system stats,
|
||||
## like CPU / memory usage.
|
||||
@@ -116,6 +122,12 @@ logging.level = "info"
|
||||
## Default: standard
|
||||
logging.stdio = "standard"
|
||||
|
||||
## Specify whether logging is more human readable.
|
||||
## Note. Single logs will span multiple lines.
|
||||
## Env: PERIPHERY_LOGGING_PRETTY
|
||||
## Default: false
|
||||
logging.pretty = false
|
||||
|
||||
## Specify a opentelemetry otlp endpoint to send traces to.
|
||||
## Example: http://localhost:4317.
|
||||
## Env: PERIPHERY_LOGGING_OTLP_ENDPOINT
|
||||
|
||||
@@ -19,5 +19,5 @@ FROM scratch
|
||||
COPY --from=builder /builder/frontend/dist /frontend
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo
|
||||
LABEL org.opencontainers.image.description="Komodo Periphery"
|
||||
LABEL org.opencontainers.image.description="Komodo Frontend"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
27
frontend/public/client/lib.d.ts
vendored
27
frontend/public/client/lib.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { AuthResponses, ExecuteResponses, ReadResponses, UserResponses, WriteResponses } from "./responses.js";
|
||||
import { AuthRequest, ExecuteRequest, ReadRequest, UpdateListItem, UserRequest, WriteRequest } from "./types.js";
|
||||
import { AuthRequest, ExecuteRequest, ReadRequest, Update, UpdateListItem, UserRequest, WriteRequest } from "./types.js";
|
||||
export * as Types from "./types.js";
|
||||
type InitOptions = {
|
||||
type: "jwt";
|
||||
@@ -86,11 +86,36 @@ export declare function KomodoClient(url: string, options: InitOptions): {
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* NOTE. These calls return immediately when the update is created, NOT when the execution task finishes.
|
||||
* To have the call only return when the task finishes, use [execute_and_poll_until_complete].
|
||||
*
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
|
||||
*/
|
||||
execute: <T extends ExecuteRequest["type"], Req extends Extract<ExecuteRequest, {
|
||||
type: T;
|
||||
}>>(type: T, params: Req["params"]) => Promise<ExecuteResponses[Req["type"]]>;
|
||||
/**
|
||||
* Call the `/execute` api, and poll the update until the task has completed.
|
||||
*
|
||||
* ```
|
||||
* const update = await komodo.execute_and_poll("DeployStack", {
|
||||
* stack: "my-stack"
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
|
||||
*/
|
||||
execute_and_poll: <T extends ExecuteRequest["type"], Req extends Extract<ExecuteRequest, {
|
||||
type: T;
|
||||
}>>(type: T, params: Req["params"]) => Promise<Update | (Update | {
|
||||
status: "Err";
|
||||
data: import("./types.js").BatchExecutionResponseItemErr;
|
||||
})[]>;
|
||||
/**
|
||||
* Poll an Update (returned by the `execute` calls) until the `status` is `Complete`.
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status.
|
||||
*/
|
||||
poll_update_until_complete: (update_id: string) => Promise<Update>;
|
||||
/** Returns the version of Komodo Core the client is calling to. */
|
||||
core_version: () => Promise<string>;
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UpdateStatus, } from "./types.js";
|
||||
export * as Types from "./types.js";
|
||||
export class CancelToken {
|
||||
cancelled;
|
||||
@@ -74,6 +75,33 @@ export function KomodoClient(url, options) {
|
||||
const read = async (type, params) => await request("/read", { type, params });
|
||||
const write = async (type, params) => await request("/write", { type, params });
|
||||
const execute = async (type, params) => await request("/execute", { type, params });
|
||||
const execute_and_poll = async (type, params) => {
|
||||
const res = await execute(type, params);
|
||||
// Check if its a batch of updates or a single update;
|
||||
if (Array.isArray(res)) {
|
||||
const batch = res;
|
||||
return await Promise.all(batch.map(async (item) => {
|
||||
if (item.status === "Err") {
|
||||
return item;
|
||||
}
|
||||
return await poll_update_until_complete(item.data._id?.$oid);
|
||||
}));
|
||||
}
|
||||
else {
|
||||
// it is a single update
|
||||
const update = res;
|
||||
return await poll_update_until_complete(update._id?.$oid);
|
||||
}
|
||||
};
|
||||
const poll_update_until_complete = async (update_id) => {
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const update = await read("GetUpdate", { id: update_id });
|
||||
if (update.status === UpdateStatus.Complete) {
|
||||
return update;
|
||||
}
|
||||
}
|
||||
};
|
||||
const core_version = () => read("GetVersion", {}).then((res) => res.version);
|
||||
const subscribe_to_update_websocket = async ({ on_update, on_login, on_close, retry_timeout_ms = 5_000, cancel = new CancelToken(), on_cancel, }) => {
|
||||
while (true) {
|
||||
@@ -117,7 +145,7 @@ export function KomodoClient(url, options) {
|
||||
// Sleep for a bit before checking for websocket closed
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
// Sleep for a bit before retrying to avoid spam.
|
||||
// Sleep for a bit before retrying connection to avoid spam.
|
||||
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
|
||||
}
|
||||
catch (error) {
|
||||
@@ -186,9 +214,29 @@ export function KomodoClient(url, options) {
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* NOTE. These calls return immediately when the update is created, NOT when the execution task finishes.
|
||||
* To have the call only return when the task finishes, use [execute_and_poll_until_complete].
|
||||
*
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
|
||||
*/
|
||||
execute,
|
||||
/**
|
||||
* Call the `/execute` api, and poll the update until the task has completed.
|
||||
*
|
||||
* ```
|
||||
* const update = await komodo.execute_and_poll("DeployStack", {
|
||||
* stack: "my-stack"
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
|
||||
*/
|
||||
execute_and_poll,
|
||||
/**
|
||||
* Poll an Update (returned by the `execute` calls) until the `status` is `Complete`.
|
||||
* https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status.
|
||||
*/
|
||||
poll_update_until_complete,
|
||||
/** Returns the version of Komodo Core the client is calling to. */
|
||||
core_version,
|
||||
/**
|
||||
|
||||
151
frontend/public/client/types.d.ts
vendored
151
frontend/public/client/types.d.ts
vendored
@@ -42,12 +42,46 @@ export interface Resource<Config, Info> {
|
||||
*/
|
||||
base_permission?: PermissionLevel;
|
||||
}
|
||||
export declare enum ScheduleFormat {
|
||||
English = "English",
|
||||
Cron = "Cron"
|
||||
}
|
||||
export interface ActionConfig {
|
||||
/** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */
|
||||
schedule_format?: ScheduleFormat;
|
||||
/**
|
||||
* Typescript file contents using pre-initialized `komodo` client.
|
||||
* Supports variable / secret interpolation.
|
||||
* Optionally provide a schedule for the procedure to run on.
|
||||
*
|
||||
* There are 2 ways to specify a schedule:
|
||||
*
|
||||
* 1. Regular CRON expression:
|
||||
*
|
||||
* (second, minute, hour, day, month, day-of-week)
|
||||
* ```
|
||||
* 0 0 0 1,15 * ?
|
||||
* ```
|
||||
*
|
||||
* 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
|
||||
*
|
||||
* ```
|
||||
* at midnight on the 1st and 15th of the month
|
||||
* ```
|
||||
*/
|
||||
file_contents?: string;
|
||||
schedule?: string;
|
||||
/**
|
||||
* Whether schedule is enabled if one is provided.
|
||||
* Can be used to temporarily disable the schedule.
|
||||
*/
|
||||
schedule_enabled: boolean;
|
||||
/**
|
||||
* Optional. A TZ Identifier. If not provided, will use Core local timezone.
|
||||
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
|
||||
*/
|
||||
schedule_timezone?: string;
|
||||
/** Whether to send alerts when the schedule was run. */
|
||||
schedule_alert: boolean;
|
||||
/** Whether to send alerts when this action fails. */
|
||||
failure_alert: boolean;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
@@ -55,6 +89,11 @@ export interface ActionConfig {
|
||||
* If its an empty string, use the default secret from the config.
|
||||
*/
|
||||
webhook_secret?: string;
|
||||
/**
|
||||
* Typescript file contents using pre-initialized `komodo` client.
|
||||
* Supports variable / secret interpolation.
|
||||
*/
|
||||
file_contents?: string;
|
||||
}
|
||||
export interface ActionInfo {
|
||||
/** When action was last run */
|
||||
@@ -88,6 +127,16 @@ export interface ActionListItemInfo {
|
||||
last_run_at: I64;
|
||||
/** Whether last action run successful */
|
||||
state: ActionState;
|
||||
/**
|
||||
* If the procedure has schedule enabled, this is the
|
||||
* next scheduled run time in unix ms.
|
||||
*/
|
||||
next_scheduled_run?: I64;
|
||||
/**
|
||||
* If there is an error parsing schedule expression,
|
||||
* it will be given here.
|
||||
*/
|
||||
schedule_error?: string;
|
||||
}
|
||||
export type ActionListItem = ResourceListItem<ActionListItemInfo>;
|
||||
export declare enum TagBehavior {
|
||||
@@ -127,6 +176,11 @@ export type AlerterEndpoint =
|
||||
| {
|
||||
type: "Ntfy";
|
||||
params: NtfyAlerterEndpoint;
|
||||
}
|
||||
/** Send alert to Pushover */
|
||||
| {
|
||||
type: "Pushover";
|
||||
params: PushoverAlerterEndpoint;
|
||||
};
|
||||
/** Used to reference a specific resource across all resource types */
|
||||
export type ResourceTarget = {
|
||||
@@ -668,6 +722,41 @@ export interface ProcedureStage {
|
||||
export interface ProcedureConfig {
|
||||
/** The stages to be run by the procedure. */
|
||||
stages?: ProcedureStage[];
|
||||
/** Choose whether to specify schedule as regular CRON, or using the english to CRON parser. */
|
||||
schedule_format?: ScheduleFormat;
|
||||
/**
|
||||
* Optionally provide a schedule for the procedure to run on.
|
||||
*
|
||||
* There are 2 ways to specify a schedule:
|
||||
*
|
||||
* 1. Regular CRON expression:
|
||||
*
|
||||
* (second, minute, hour, day, month, day-of-week)
|
||||
* ```
|
||||
* 0 0 0 1,15 * ?
|
||||
* ```
|
||||
*
|
||||
* 2. "English" expression via [english-to-cron](https://crates.io/crates/english-to-cron):
|
||||
*
|
||||
* ```
|
||||
* at midnight on the 1st and 15th of the month
|
||||
* ```
|
||||
*/
|
||||
schedule?: string;
|
||||
/**
|
||||
* Whether schedule is enabled if one is provided.
|
||||
* Can be used to temporarily disable the schedule.
|
||||
*/
|
||||
schedule_enabled: boolean;
|
||||
/**
|
||||
* Optional. A TZ Identifier. If not provided, will use Core local timezone.
|
||||
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
|
||||
*/
|
||||
schedule_timezone?: string;
|
||||
/** Whether to send alerts when the schedule was run. */
|
||||
schedule_alert: boolean;
|
||||
/** Whether to send alerts when this procedure fails. */
|
||||
failure_alert: boolean;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
@@ -1284,6 +1373,38 @@ export type AlertData =
|
||||
/** The name of the repo */
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
/** A procedure has failed */
|
||||
| {
|
||||
type: "ProcedureFailed";
|
||||
data: {
|
||||
/** The id of the procedure */
|
||||
id: string;
|
||||
/** The name of the procedure */
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
/** An action has failed */
|
||||
| {
|
||||
type: "ActionFailed";
|
||||
data: {
|
||||
/** The id of the action */
|
||||
id: string;
|
||||
/** The name of the action */
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
/** A schedule was run */
|
||||
| {
|
||||
type: "ScheduleRun";
|
||||
data: {
|
||||
/** Procedure or Action */
|
||||
resource_type: ResourceTarget["type"];
|
||||
/** The resource id */
|
||||
id: string;
|
||||
/** The resource name */
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
/** Representation of an alert in the system. */
|
||||
export interface Alert {
|
||||
@@ -1532,6 +1653,11 @@ export interface ResourceSyncConfig {
|
||||
include_variables?: boolean;
|
||||
/** Whether sync should include user groups. */
|
||||
include_user_groups?: boolean;
|
||||
/**
|
||||
* Whether sync should send alert when it enters Pending state.
|
||||
* Default: true
|
||||
*/
|
||||
pending_alert: boolean;
|
||||
/** Manage the file contents in the UI. */
|
||||
file_contents?: string;
|
||||
}
|
||||
@@ -2283,6 +2409,10 @@ export interface Update {
|
||||
commit_hash?: string;
|
||||
/** Some unstructured, operation specific data. Not for general usage. */
|
||||
other_data?: string;
|
||||
/** If the update is for resource config update, give the previous toml contents */
|
||||
prev_toml?: string;
|
||||
/** If the update is for resource config update, give the current (at time of Update) toml contents */
|
||||
current_toml?: string;
|
||||
}
|
||||
export type GetUpdateResponse = Update;
|
||||
/**
|
||||
@@ -3259,6 +3389,16 @@ export interface ProcedureListItemInfo {
|
||||
stages: I64;
|
||||
/** Reflect whether last run successful / currently running. */
|
||||
state: ProcedureState;
|
||||
/**
|
||||
* If the procedure has schedule enabled, this is the
|
||||
* next scheduled run time in unix ms.
|
||||
*/
|
||||
next_scheduled_run?: I64;
|
||||
/**
|
||||
* If there is an error parsing schedule expression,
|
||||
* it will be given here.
|
||||
*/
|
||||
schedule_error?: string;
|
||||
}
|
||||
export type ProcedureListItem = ResourceListItem<ProcedureListItemInfo>;
|
||||
export type ListProceduresResponse = ProcedureListItem[];
|
||||
@@ -6225,6 +6365,11 @@ export interface PushRecentlyViewed {
|
||||
/** The target to push. */
|
||||
resource: ResourceTarget;
|
||||
}
|
||||
/** Configuration for a Pushover alerter. */
|
||||
export interface PushoverAlerterEndpoint {
|
||||
/** The pushover URL including application and user tokens in parameters. */
|
||||
url: string;
|
||||
}
|
||||
/** Trigger a refresh of the cached latest hash and message. */
|
||||
export interface RefreshBuildCache {
|
||||
/** Id or name */
|
||||
|
||||
@@ -13,6 +13,11 @@ export var PermissionLevel;
|
||||
/** Can update the resource configuration */
|
||||
PermissionLevel["Write"] = "Write";
|
||||
})(PermissionLevel || (PermissionLevel = {}));
|
||||
export var ScheduleFormat;
|
||||
(function (ScheduleFormat) {
|
||||
ScheduleFormat["English"] = "English";
|
||||
ScheduleFormat["Cron"] = "Cron";
|
||||
})(ScheduleFormat || (ScheduleFormat = {}));
|
||||
export var ActionState;
|
||||
(function (ActionState) {
|
||||
/** Unknown case */
|
||||
|
||||
@@ -93,7 +93,7 @@ export type PrimitiveConfigArgs = {
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
boldLabel?: boolean;
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
};
|
||||
|
||||
export type ConfigComponent<T> = {
|
||||
|
||||
@@ -119,7 +119,7 @@ export const ConfigInput = ({
|
||||
label: string;
|
||||
boldLabel?: boolean;
|
||||
value: string | number | undefined;
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
onChange?: (value: string) => void;
|
||||
@@ -168,7 +168,7 @@ export const ConfigSwitch = ({
|
||||
label: string;
|
||||
boldLabel?: boolean;
|
||||
value: boolean | undefined;
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
disabled: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
}) => (
|
||||
@@ -180,7 +180,7 @@ export const ConfigSwitch = ({
|
||||
>
|
||||
<div
|
||||
className="py-2 flex flex-row gap-4 items-center text-sm cursor-pointer"
|
||||
onClick={() => onChange(!checked)}
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
>
|
||||
{/* <div
|
||||
className={cn(
|
||||
@@ -205,83 +205,6 @@ export const ConfigSwitch = ({
|
||||
</ConfigItem>
|
||||
);
|
||||
|
||||
export const DoubleInput = <
|
||||
T extends object,
|
||||
K extends keyof T,
|
||||
L extends T[K] extends string | number | undefined ? K : never,
|
||||
R extends T[K] extends string | number | undefined ? K : never,
|
||||
>({
|
||||
disabled,
|
||||
values,
|
||||
leftval,
|
||||
leftpl,
|
||||
rightval,
|
||||
rightpl,
|
||||
// addName,
|
||||
onLeftChange,
|
||||
onRightChange,
|
||||
// onAdd,
|
||||
onRemove,
|
||||
containerClassName,
|
||||
inputClassName,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
values: T[] | undefined;
|
||||
leftval: L;
|
||||
leftpl: string;
|
||||
rightval: R;
|
||||
rightpl: string;
|
||||
// addName: string;
|
||||
onLeftChange: (value: T[L], i: number) => void;
|
||||
onRightChange: (value: T[R], i: number) => void;
|
||||
// onAdd: () => void;
|
||||
onRemove: (i: number) => void;
|
||||
containerClassName?: string;
|
||||
inputClassName?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-4", containerClassName)}>
|
||||
{values?.map((value, i) => (
|
||||
<div
|
||||
className="flex items-center justify-between gap-4 flex-wrap"
|
||||
key={i}
|
||||
>
|
||||
<Input
|
||||
className={inputClassName}
|
||||
value={value[leftval] as any}
|
||||
placeholder={leftpl}
|
||||
onChange={(e) => onLeftChange(e.target.value as T[L], i)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
:
|
||||
<Input
|
||||
className={inputClassName}
|
||||
value={value[rightval] as any}
|
||||
placeholder={rightpl}
|
||||
onChange={(e) => onRightChange(e.target.value as T[R], i)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button variant="secondary" onClick={() => onRemove(i)}>
|
||||
<MinusCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* {!disabled && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2 w-[200px] place-self-end"
|
||||
onClick={onAdd}
|
||||
>
|
||||
<PlusCircle className="w-4 h-4" />
|
||||
Add {addName}
|
||||
</Button>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProviderSelector = ({
|
||||
disabled,
|
||||
account_type,
|
||||
|
||||
@@ -10,7 +10,6 @@ import * as pluginYaml from "prettier/plugins/yaml";
|
||||
import { useWindowDimensions } from "@lib/hooks";
|
||||
|
||||
const MIN_EDITOR_HEIGHT = 56;
|
||||
// const MAX_EDITOR_HEIGHT = 500;
|
||||
|
||||
export type MonacoLanguage =
|
||||
| "yaml"
|
||||
@@ -102,13 +101,9 @@ export const MonacoEditor = ({
|
||||
const contentHeight = line_count * 18 + 30;
|
||||
const containerNode = editor.getContainerDomNode();
|
||||
|
||||
// containerNode.style.height = `${Math.max(
|
||||
// Math.ceil(contentHeight),
|
||||
// minHeight ?? MIN_EDITOR_HEIGHT
|
||||
// )}px`;
|
||||
containerNode.style.height = `${Math.min(
|
||||
Math.max(Math.ceil(contentHeight), minHeight ?? MIN_EDITOR_HEIGHT),
|
||||
Math.floor(dimensions.height * (3 / 5))
|
||||
containerNode.style.height = `${Math.max(
|
||||
Math.min(contentHeight, Math.floor(dimensions.height * 0.5)),
|
||||
minHeight ?? MIN_EDITOR_HEIGHT
|
||||
)}px`;
|
||||
}, [editor, line_count]);
|
||||
|
||||
@@ -182,8 +177,9 @@ export const MonacoDiffEditor = ({
|
||||
if (!editor) return;
|
||||
const contentHeight = line_count * 18 + 30;
|
||||
const node = editor.getContainerDomNode();
|
||||
|
||||
node.style.height = `${Math.max(
|
||||
Math.min(Math.ceil(contentHeight), MAX_DIFF_HEIGHT),
|
||||
Math.min(contentHeight, MAX_DIFF_HEIGHT),
|
||||
MIN_DIFF_HEIGHT
|
||||
)}px`;
|
||||
}, [editor, line_count]);
|
||||
|
||||
@@ -10,12 +10,23 @@ import { Config } from "@components/config";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { SecretsSearch } from "@components/config/env_vars";
|
||||
import { Button } from "@ui/button";
|
||||
import { ConfigItem, WebhookBuilder } from "@components/config/util";
|
||||
import {
|
||||
ConfigItem,
|
||||
ConfigSwitch,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { Input } from "@ui/input";
|
||||
import { useState } from "react";
|
||||
import { CopyWebhook } from "../common";
|
||||
import { ActionInfo } from "./info";
|
||||
import { Switch } from "@ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
|
||||
const ACTION_GIT_PROVIDER = "Action";
|
||||
|
||||
@@ -45,7 +56,6 @@ export const ActionConfig = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<Config
|
||||
disabled={disabled}
|
||||
disableSidebar
|
||||
original={config}
|
||||
update={update}
|
||||
set={set}
|
||||
@@ -57,13 +67,6 @@ export const ActionConfig = ({ id }: { id: string }) => {
|
||||
{
|
||||
label: "Action File",
|
||||
description: "Manage the action file contents here.",
|
||||
// actions: (
|
||||
// <ShowHideButton
|
||||
// show={show.file}
|
||||
// setShow={(file) => setShow({ ...show, file })}
|
||||
// />
|
||||
// ),
|
||||
// contentHidden: !show.file,
|
||||
components: {
|
||||
file_contents: (file_contents, set) => {
|
||||
return (
|
||||
@@ -103,6 +106,101 @@ export const ActionConfig = ({ id }: { id: string }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Alert",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
failure_alert: {
|
||||
boldLabel: true,
|
||||
description: "Send an alert any time the Procedure fails",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Schedule",
|
||||
description:
|
||||
"Configure the Procedure to run at defined times using English or CRON.",
|
||||
components: {
|
||||
schedule_enabled: (schedule_enabled, set) => (
|
||||
<ConfigSwitch
|
||||
label="Enabled"
|
||||
value={
|
||||
(update.schedule ?? config.schedule)
|
||||
? schedule_enabled
|
||||
: false
|
||||
}
|
||||
disabled={disabled || !(update.schedule ?? config.schedule)}
|
||||
onChange={(schedule_enabled) => set({ schedule_enabled })}
|
||||
/>
|
||||
),
|
||||
schedule_format: (schedule_format, set) => (
|
||||
<ConfigItem
|
||||
label="Format"
|
||||
description="Choose whether to provide English or CRON schedule expression"
|
||||
>
|
||||
<Select
|
||||
value={schedule_format}
|
||||
onValueChange={(schedule_format) =>
|
||||
set({
|
||||
schedule_format:
|
||||
schedule_format as Types.ScheduleFormat,
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]" disabled={disabled}>
|
||||
<SelectValue placeholder="Select Format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(Types.ScheduleFormat).map((mode) => (
|
||||
<SelectItem
|
||||
key={mode}
|
||||
value={mode!}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{mode}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ConfigItem>
|
||||
),
|
||||
schedule: {
|
||||
label: "Expression",
|
||||
description:
|
||||
(update.schedule_format ?? config.schedule_format) ===
|
||||
"Cron" ? (
|
||||
<div className="pt-1 flex flex-col gap-1">
|
||||
<code>
|
||||
second - minute - hour - day - month - day-of-week
|
||||
</code>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-1 flex flex-col gap-1">
|
||||
<code>Examples:</code>
|
||||
<code>- Run every day at 4:00 pm</code>
|
||||
<code>
|
||||
- Run at 21:00 on the 1st and 15th of the month
|
||||
</code>
|
||||
<code>- Every Sunday at midnight</code>
|
||||
</div>
|
||||
),
|
||||
placeholder:
|
||||
(update.schedule_format ?? config.schedule_format) === "Cron"
|
||||
? "0 0 0 ? * SUN"
|
||||
: "Enter English expression",
|
||||
},
|
||||
schedule_timezone: {
|
||||
label: "Timezone",
|
||||
description:
|
||||
"Optional. Enter specific IANA timezone for schedule expression. If not provided, uses the Core timezone.",
|
||||
placeholder: "Enter IANA timezone",
|
||||
},
|
||||
schedule_alert: {
|
||||
description: "Send an alert when the scheduled run occurs",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Webhook",
|
||||
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@components/util";
|
||||
import { useExecute, useRead } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Clapperboard } from "lucide-react";
|
||||
import { Clapperboard, Clock } from "lucide-react";
|
||||
import { ActionConfig } from "./config";
|
||||
import { ActionTable } from "./table";
|
||||
import { DeleteResource, NewResource } from "../common";
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
action_state_intention,
|
||||
stroke_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { cn } from "@lib/utils";
|
||||
import { cn, updateLogToHtml } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { Card } from "@ui/card";
|
||||
|
||||
const useAction = (id?: string) =>
|
||||
useRead("ListActions", {}).data?.find((d) => d.id === id);
|
||||
@@ -78,7 +80,47 @@ export const ActionComponents: RequiredResourceComponents = {
|
||||
|
||||
Status: {},
|
||||
|
||||
Info: {},
|
||||
Info: {
|
||||
Schedule: ({ id }) => {
|
||||
const next_scheduled_run = useAction(id)?.info.next_scheduled_run;
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Clock className="w-4 h-4" />
|
||||
Next Run:
|
||||
<div className="font-bold">
|
||||
{next_scheduled_run
|
||||
? new Date(next_scheduled_run).toLocaleString()
|
||||
: "Not Scheduled"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
ScheduleErrors: ({ id }) => {
|
||||
const error = useAction(id)?.info.schedule_error;
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Card className="px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer">
|
||||
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
|
||||
Schedule Error
|
||||
</div>
|
||||
</Card>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-[400px]">
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: updateLogToHtml(error),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Actions: {
|
||||
RunAction: ({ id }) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ const ENDPOINT_TYPES: Types.AlerterEndpoint["type"][] = [
|
||||
"Discord",
|
||||
"Slack",
|
||||
"Ntfy",
|
||||
"Pushover",
|
||||
];
|
||||
|
||||
export const EndpointConfig = ({
|
||||
@@ -70,5 +71,7 @@ const default_url = (type: Types.AlerterEndpoint["type"]) => {
|
||||
? "https://discord.com/api/webhooks/XXXXXXXXXXXX/XXXX-XXXXXXXXXX"
|
||||
: type === "Ntfy"
|
||||
? "https://ntfy.sh/komodo"
|
||||
: type === "Pushover"
|
||||
? "https://api.pushover.net/1/messages.json?token=XXXXXXXXXXXXX&user=XXXXXXXXXXXXX"
|
||||
: "";
|
||||
};
|
||||
|
||||
@@ -90,103 +90,4 @@ export const TerminationTimeout = ({
|
||||
</div>
|
||||
</ConfigItem>
|
||||
);
|
||||
};
|
||||
|
||||
// export const TermSignalLabels = ({
|
||||
// args,
|
||||
// set,
|
||||
// disabled,
|
||||
// }: {
|
||||
// args: Types.TerminationSignalLabel[];
|
||||
// set: (input: Partial<Types.DeploymentConfig>) => void;
|
||||
// disabled: boolean;
|
||||
// }) => {
|
||||
// const signals = Object.values(Types.TerminationSignal)
|
||||
// .filter((signal) => args.every((arg) => arg.signal !== signal))
|
||||
// .reverse();
|
||||
// return (
|
||||
// <ConfigItem label="Signal Labels" className="items-start">
|
||||
// <div className="grid gap-2">
|
||||
// {args.map((label, i) => (
|
||||
// <div key={label.signal} className="flex gap-4 items-center w-full">
|
||||
// <Input
|
||||
// placeholder="Label this termination signal"
|
||||
// value={label.label}
|
||||
// onChange={(e) =>
|
||||
// set({
|
||||
// term_signal_labels: args.map((item, index) =>
|
||||
// index === i ? { ...item, label: e.target.value } : item
|
||||
// ),
|
||||
// })
|
||||
// }
|
||||
// disabled={disabled}
|
||||
// />
|
||||
|
||||
// <Select
|
||||
// value={label.signal}
|
||||
// onValueChange={(value) =>
|
||||
// set({
|
||||
// term_signal_labels: args.map((item, index) =>
|
||||
// index === i
|
||||
// ? { ...item, signal: value as Types.TerminationSignal }
|
||||
// : item
|
||||
// ),
|
||||
// })
|
||||
// }
|
||||
// disabled={disabled}
|
||||
// >
|
||||
// <SelectTrigger className="w-[200px]" disabled={disabled}>
|
||||
// {label.signal}
|
||||
// </SelectTrigger>
|
||||
// <SelectContent>
|
||||
// <SelectGroup>
|
||||
// {signals.map((term_signal) => (
|
||||
// <SelectItem
|
||||
// key={term_signal}
|
||||
// value={term_signal}
|
||||
// className="cursor-pointer"
|
||||
// >
|
||||
// {term_signal}
|
||||
// </SelectItem>
|
||||
// ))}
|
||||
// </SelectGroup>
|
||||
// </SelectContent>
|
||||
// </Select>
|
||||
|
||||
// {!disabled && (
|
||||
// <Button
|
||||
// variant="outline"
|
||||
// size="icon"
|
||||
// onClick={() =>
|
||||
// set({
|
||||
// term_signal_labels: args.filter((_, index) => i !== index),
|
||||
// })
|
||||
// }
|
||||
// className="p-2"
|
||||
// >
|
||||
// <MinusCircle className="w-4 h-4" />
|
||||
// </Button>
|
||||
// )}
|
||||
// </div>
|
||||
// ))}
|
||||
// {!disabled && signals.length > 0 && (
|
||||
// <Button
|
||||
// className="justify-self-end p-2"
|
||||
// variant="outline"
|
||||
// size="icon"
|
||||
// onClick={() =>
|
||||
// set({
|
||||
// term_signal_labels: [
|
||||
// ...args,
|
||||
// { label: "", signal: signals[0] },
|
||||
// ],
|
||||
// })
|
||||
// }
|
||||
// >
|
||||
// <PlusCircle className="w-4 h-4" />
|
||||
// </Button>
|
||||
// )}
|
||||
// </div>
|
||||
// </ConfigItem>
|
||||
// );
|
||||
// };
|
||||
};
|
||||
@@ -365,7 +365,7 @@ export const DeploymentConfig = ({
|
||||
description="Choose between multiple signals when stopping"
|
||||
>
|
||||
<MonacoEditor
|
||||
value={value || " # SIGTERM: your label\n"}
|
||||
value={value || DEFAULT_TERM_SIGNAL_LABELS}
|
||||
language="key_value"
|
||||
onValueChange={(term_signal_labels) =>
|
||||
set({ term_signal_labels })
|
||||
@@ -381,3 +381,9 @@ export const DeploymentConfig = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DEFAULT_TERM_SIGNAL_LABELS = ` # SIGTERM: sigterm label
|
||||
# SIGQUIT: sigquit label
|
||||
# SIGINT: sigint label
|
||||
# SIGHUP: sighup label
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
ConfigInput,
|
||||
ConfigItem,
|
||||
ConfigSwitch,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { Section } from "@components/layouts";
|
||||
import {
|
||||
useLocalStorage,
|
||||
useRead,
|
||||
@@ -13,31 +6,33 @@ import {
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { Config } from "@components/config";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/card";
|
||||
ConfigItem,
|
||||
ConfigSwitch,
|
||||
WebhookBuilder,
|
||||
} from "@components/config/util";
|
||||
import { Input } from "@ui/input";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CopyWebhook, ResourceSelector } from "../common";
|
||||
import { ConfigLayout } from "@components/config";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { Button } from "@ui/button";
|
||||
import { Switch } from "@ui/switch";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ChevronsUpDown,
|
||||
Info,
|
||||
Minus,
|
||||
MinusCircle,
|
||||
Plus,
|
||||
PlusCircle,
|
||||
SearchX,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { TextUpdateMenuMonaco } from "@components/util";
|
||||
import { Card } from "@ui/card";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { fmt_upper_camelcase } from "@lib/formatting";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -46,8 +41,6 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@ui/command";
|
||||
import { Switch } from "@ui/switch";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -56,180 +49,304 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/dropdown-menu";
|
||||
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";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
|
||||
export const ProcedureConfig = ({ id }: { id: string }) => {
|
||||
const procedure = useRead("GetProcedure", { procedure: id }).data;
|
||||
if (!procedure) return null;
|
||||
return <ProcedureConfigInner procedure={procedure} />;
|
||||
type ExecutionType = Types.Execution["type"];
|
||||
|
||||
type ExecutionConfigComponent<
|
||||
T extends ExecutionType,
|
||||
P = Extract<Types.Execution, { type: T }>["params"],
|
||||
> = React.FC<{
|
||||
params: P;
|
||||
setParams: React.Dispatch<React.SetStateAction<P>>;
|
||||
disabled: boolean;
|
||||
}>;
|
||||
|
||||
type MinExecutionType = Exclude<
|
||||
ExecutionType,
|
||||
| "StartContainer"
|
||||
| "RestartContainer"
|
||||
| "PauseContainer"
|
||||
| "UnpauseContainer"
|
||||
| "StopContainer"
|
||||
| "DestroyContainer"
|
||||
| "DeleteNetwork"
|
||||
| "DeleteImage"
|
||||
| "DeleteVolume"
|
||||
| "TestAlerter"
|
||||
>;
|
||||
|
||||
type ExecutionConfigParams<T extends MinExecutionType> = Extract<
|
||||
Types.Execution,
|
||||
{ type: T }
|
||||
>["params"];
|
||||
|
||||
type ExecutionConfigs = {
|
||||
[ExType in MinExecutionType]: {
|
||||
Component: ExecutionConfigComponent<ExType>;
|
||||
params: ExecutionConfigParams<ExType>;
|
||||
};
|
||||
};
|
||||
|
||||
const PROCEDURE_GIT_PROVIDER = "Procedure";
|
||||
|
||||
const ProcedureConfigInner = ({
|
||||
procedure,
|
||||
}: {
|
||||
procedure: Types.Procedure;
|
||||
}) => {
|
||||
const new_stage = (next_index: number) => ({
|
||||
name: `Stage ${next_index}`,
|
||||
enabled: true,
|
||||
executions: [default_enabled_execution()],
|
||||
});
|
||||
|
||||
const default_enabled_execution: () => Types.EnabledExecution = () => ({
|
||||
enabled: true,
|
||||
execution: {
|
||||
type: "None",
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const ProcedureConfig = ({ id }: { id: string }) => {
|
||||
const [branch, setBranch] = useState("main");
|
||||
const [config, setConfig] = useLocalStorage<Partial<Types.ProcedureConfig>>(
|
||||
`procedure-${procedure._id?.$oid}-update-v1`,
|
||||
{}
|
||||
);
|
||||
const perms = useRead("GetPermissionLevel", {
|
||||
target: { type: "Procedure", id: procedure._id?.$oid! },
|
||||
target: { type: "Procedure", id },
|
||||
}).data;
|
||||
const procedure = useRead("GetProcedure", { procedure: id }).data;
|
||||
const config = procedure?.config;
|
||||
const name = procedure?.name;
|
||||
const global_disabled =
|
||||
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
|
||||
const [update, set] = useLocalStorage<Partial<Types.ProcedureConfig>>(
|
||||
`procedure-${id}-update-v1`,
|
||||
{}
|
||||
);
|
||||
const { mutateAsync } = useWrite("UpdateProcedure");
|
||||
const { integrations } = useWebhookIntegrations();
|
||||
const [id_or_name] = useWebhookIdOrName();
|
||||
const webhook_integration = integrations[PROCEDURE_GIT_PROVIDER] ?? "Github";
|
||||
|
||||
const stages = config.stages || procedure.config?.stages || [];
|
||||
if (!config) return null;
|
||||
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
const webhook_integration = integrations[PROCEDURE_GIT_PROVIDER] ?? "Github";
|
||||
const stages = update.stages || procedure.config?.stages || [];
|
||||
|
||||
const add_stage = () =>
|
||||
setConfig((config) => ({
|
||||
set((config) => ({
|
||||
...config,
|
||||
stages: [...stages, new_stage()],
|
||||
stages: [...stages, new_stage(stages.length + 1)],
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<ConfigLayout
|
||||
original={procedure.config}
|
||||
titleOther={
|
||||
<div className="flex items-center gap-4 text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="w-4 h-4" />
|
||||
<h2 className="text-xl">Config</h2>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Info className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
The executions in a stage are all run in parallel. The stages
|
||||
themselves are run sequentially.
|
||||
<Config
|
||||
disabled={disabled}
|
||||
original={config}
|
||||
update={update}
|
||||
set={set}
|
||||
onSave={async () => {
|
||||
await mutateAsync({ id, config: update });
|
||||
}}
|
||||
components={{
|
||||
"": [
|
||||
{
|
||||
label: "Stages",
|
||||
description:
|
||||
"The executions in a stage are all run in parallel. The stages themselves are run sequentially.",
|
||||
components: {
|
||||
stages: (stages, set) => (
|
||||
<div className="flex flex-col gap-4">
|
||||
{stages &&
|
||||
stages.map((stage, index) => (
|
||||
<Stage
|
||||
stage={stage}
|
||||
setStage={(stage) =>
|
||||
set({
|
||||
stages: stages.map((s, i) =>
|
||||
index === i ? stage : s
|
||||
),
|
||||
})
|
||||
}
|
||||
removeStage={() =>
|
||||
set({
|
||||
stages: stages.filter((_, i) => index !== i),
|
||||
})
|
||||
}
|
||||
moveUp={
|
||||
index === 0
|
||||
? undefined
|
||||
: () =>
|
||||
set({
|
||||
stages: stages.map((stage, i) => {
|
||||
// Make sure its not the first row
|
||||
if (i === index && index !== 0) {
|
||||
return stages[index - 1];
|
||||
} else if (i === index - 1) {
|
||||
// Reverse the entry, moving this row "Up"
|
||||
return stages[index];
|
||||
} else {
|
||||
return stage;
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
moveDown={
|
||||
index === stages.length - 1
|
||||
? undefined
|
||||
: () =>
|
||||
set({
|
||||
stages: stages.map((stage, i) => {
|
||||
// The index also cannot be the last index, which cannot be moved down
|
||||
if (
|
||||
i === index &&
|
||||
index !== stages.length - 1
|
||||
) {
|
||||
return stages[index + 1];
|
||||
} else if (i === index + 1) {
|
||||
// Move the row "Down"
|
||||
return stages[index];
|
||||
} else {
|
||||
return stage;
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
insertAbove={() =>
|
||||
set({
|
||||
stages: [
|
||||
...stages.slice(0, index),
|
||||
new_stage(index + 1),
|
||||
...stages.slice(index),
|
||||
],
|
||||
})
|
||||
}
|
||||
insertBelow={() =>
|
||||
set({
|
||||
stages: [
|
||||
...stages.slice(0, index + 1),
|
||||
new_stage(index + 2),
|
||||
...stages.slice(index + 1),
|
||||
],
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={add_stage}
|
||||
className="w-fit"
|
||||
disabled={disabled}
|
||||
>
|
||||
Add Stage
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
disabled={disabled}
|
||||
update={config}
|
||||
onConfirm={async () => {
|
||||
await mutateAsync({ id: procedure._id!.$oid, config });
|
||||
setConfig({});
|
||||
}}
|
||||
onReset={() => setConfig({})}
|
||||
>
|
||||
{stages.map((stage, index) => (
|
||||
<Stage
|
||||
stage={stage}
|
||||
setStage={(stage) =>
|
||||
setConfig((config) => ({
|
||||
...config,
|
||||
stages: stages.map((s, i) => (index === i ? stage : s)),
|
||||
}))
|
||||
}
|
||||
removeStage={() =>
|
||||
setConfig((config) => ({
|
||||
...config,
|
||||
stages: stages.filter((_, i) => index !== i),
|
||||
}))
|
||||
}
|
||||
moveUp={
|
||||
index === 0
|
||||
? undefined
|
||||
: () =>
|
||||
setConfig((config) => ({
|
||||
...config,
|
||||
stages: stages.map((stage, i) => {
|
||||
// Make sure its not the first row
|
||||
if (i === index && index !== 0) {
|
||||
return stages[index - 1];
|
||||
} else if (i === index - 1) {
|
||||
// Reverse the entry, moving this row "Up"
|
||||
return stages[index];
|
||||
} else {
|
||||
return stage;
|
||||
}
|
||||
}),
|
||||
}))
|
||||
}
|
||||
moveDown={
|
||||
index === stages.length - 1
|
||||
? undefined
|
||||
: () =>
|
||||
setConfig((config) => ({
|
||||
...config,
|
||||
stages: stages.map((stage, i) => {
|
||||
// The index also cannot be the last index, which cannot be moved down
|
||||
if (i === index && index !== stages.length - 1) {
|
||||
return stages[index + 1];
|
||||
} else if (i === index + 1) {
|
||||
// Move the row "Down"
|
||||
return stages[index];
|
||||
} else {
|
||||
return stage;
|
||||
}
|
||||
}),
|
||||
}))
|
||||
}
|
||||
insertAbove={() =>
|
||||
setConfig((config) => ({
|
||||
...config,
|
||||
stages: [
|
||||
...stages.slice(0, index),
|
||||
new_stage(),
|
||||
...stages.slice(index),
|
||||
],
|
||||
}))
|
||||
}
|
||||
insertBelow={() =>
|
||||
setConfig((config) => ({
|
||||
...config,
|
||||
stages: [
|
||||
...stages.slice(0, index + 1),
|
||||
new_stage(),
|
||||
...stages.slice(index + 1),
|
||||
],
|
||||
}))
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={add_stage}
|
||||
className="w-fit"
|
||||
disabled={disabled}
|
||||
>
|
||||
Add Stage
|
||||
</Button>
|
||||
</ConfigLayout>
|
||||
<Section>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Webhook</CardTitle>
|
||||
<CardDescription>
|
||||
Copy the webhook given here, and configure your
|
||||
{webhook_integration}-style repo provider to send webhooks to
|
||||
Komodo
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ConfigItem>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Alert",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
failure_alert: {
|
||||
boldLabel: true,
|
||||
description: "Send an alert any time the Procedure fails",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Schedule",
|
||||
description:
|
||||
"Configure the Procedure to run at defined times using English or CRON.",
|
||||
components: {
|
||||
schedule_enabled: (schedule_enabled, set) => (
|
||||
<ConfigSwitch
|
||||
label="Enabled"
|
||||
value={
|
||||
(update.schedule ?? config.schedule)
|
||||
? schedule_enabled
|
||||
: false
|
||||
}
|
||||
disabled={disabled || !(update.schedule ?? config.schedule)}
|
||||
onChange={(schedule_enabled) => set({ schedule_enabled })}
|
||||
/>
|
||||
),
|
||||
schedule_format: (schedule_format, set) => (
|
||||
<ConfigItem
|
||||
label="Format"
|
||||
description="Choose whether to provide English or CRON schedule expression"
|
||||
>
|
||||
<Select
|
||||
value={schedule_format}
|
||||
onValueChange={(schedule_format) =>
|
||||
set({
|
||||
schedule_format:
|
||||
schedule_format as Types.ScheduleFormat,
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]" disabled={disabled}>
|
||||
<SelectValue placeholder="Select Format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(Types.ScheduleFormat).map((mode) => (
|
||||
<SelectItem
|
||||
key={mode}
|
||||
value={mode!}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{mode}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ConfigItem>
|
||||
),
|
||||
schedule: {
|
||||
label: "Expression",
|
||||
description:
|
||||
(update.schedule_format ?? config.schedule_format) ===
|
||||
"Cron" ? (
|
||||
<div className="pt-1 flex flex-col gap-1">
|
||||
<code>
|
||||
second - minute - hour - day - month - day-of-week
|
||||
</code>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-1 flex flex-col gap-1">
|
||||
<code>Examples:</code>
|
||||
<code>- Run every day at 4:00 pm</code>
|
||||
<code>
|
||||
- Run at 21:00 on the 1st and 15th of the month
|
||||
</code>
|
||||
<code>- Every Sunday at midnight</code>
|
||||
</div>
|
||||
),
|
||||
placeholder:
|
||||
(update.schedule_format ?? config.schedule_format) === "Cron"
|
||||
? "0 0 0 ? * SUN"
|
||||
: "Enter English expression",
|
||||
},
|
||||
schedule_timezone: {
|
||||
label: "Timezone",
|
||||
description:
|
||||
"Optional. Enter specific IANA timezone for schedule expression. If not provided, uses the Core timezone.",
|
||||
placeholder: "Enter IANA timezone",
|
||||
},
|
||||
schedule_alert: {
|
||||
description: "Send an alert when the scheduled run occurs",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Webhook",
|
||||
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
|
||||
components: {
|
||||
["Builder" as any]: () => (
|
||||
<WebhookBuilder git_provider={PROCEDURE_GIT_PROVIDER}>
|
||||
<div className="text-nowrap text-muted-foreground text-sm">
|
||||
Listen on branch:
|
||||
@@ -259,40 +376,26 @@ const ProcedureConfigInner = ({
|
||||
</div>
|
||||
</div>
|
||||
</WebhookBuilder>
|
||||
</ConfigItem>
|
||||
<ConfigItem label="Webhook Url - Run">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/procedure/${id_or_name === "Id" ? procedure._id?.$oid! : procedure.name}/${branch}`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
<ConfigSwitch
|
||||
label="Webhook Enabled"
|
||||
value={
|
||||
config.webhook_enabled ?? procedure.config?.webhook_enabled
|
||||
}
|
||||
disabled={disabled}
|
||||
onChange={(webhook_enabled) =>
|
||||
setConfig({ ...config, webhook_enabled })
|
||||
}
|
||||
/>
|
||||
<ConfigInput
|
||||
label="Custom Secret"
|
||||
description="Provide a custom webhook secret for this resource, or use the global default."
|
||||
placeholder="Input custom secret"
|
||||
value={
|
||||
config.webhook_secret ?? procedure.config?.webhook_secret
|
||||
}
|
||||
disabled={disabled}
|
||||
onChange={(webhook_secret) =>
|
||||
setConfig({ ...config, webhook_secret })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
</div>
|
||||
),
|
||||
["run" as any]: () => (
|
||||
<ConfigItem label="Webhook Url - Run">
|
||||
<CopyWebhook
|
||||
integration={webhook_integration}
|
||||
path={`/procedure/${id_or_name === "Id" ? id : name}/${branch}`}
|
||||
/>
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: true,
|
||||
webhook_secret: {
|
||||
description:
|
||||
"Provide a custom webhook secret for this resource, or use the global default.",
|
||||
placeholder: "Input custom secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -542,20 +645,6 @@ const Stage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const new_stage = () => ({
|
||||
name: "Stage",
|
||||
enabled: true,
|
||||
executions: [default_enabled_execution()],
|
||||
});
|
||||
|
||||
const default_enabled_execution: () => Types.EnabledExecution = () => ({
|
||||
enabled: true,
|
||||
execution: {
|
||||
type: "None",
|
||||
params: {},
|
||||
},
|
||||
});
|
||||
|
||||
const ExecutionTypeSelector = ({
|
||||
type,
|
||||
onSelect,
|
||||
@@ -612,43 +701,6 @@ const ExecutionTypeSelector = ({
|
||||
);
|
||||
};
|
||||
|
||||
type ExecutionType = Types.Execution["type"];
|
||||
|
||||
type ExecutionConfigComponent<
|
||||
T extends ExecutionType,
|
||||
P = Extract<Types.Execution, { type: T }>["params"],
|
||||
> = React.FC<{
|
||||
params: P;
|
||||
setParams: React.Dispatch<React.SetStateAction<P>>;
|
||||
disabled: boolean;
|
||||
}>;
|
||||
|
||||
type MinExecutionType = Exclude<
|
||||
ExecutionType,
|
||||
| "StartContainer"
|
||||
| "RestartContainer"
|
||||
| "PauseContainer"
|
||||
| "UnpauseContainer"
|
||||
| "StopContainer"
|
||||
| "DestroyContainer"
|
||||
| "DeleteNetwork"
|
||||
| "DeleteImage"
|
||||
| "DeleteVolume"
|
||||
| "TestAlerter"
|
||||
>;
|
||||
|
||||
type ExecutionConfigParams<T extends MinExecutionType> = Extract<
|
||||
Types.Execution,
|
||||
{ type: T }
|
||||
>["params"];
|
||||
|
||||
type ExecutionConfigs = {
|
||||
[ExType in MinExecutionType]: {
|
||||
Component: ExecutionConfigComponent<ExType>;
|
||||
params: ExecutionConfigParams<ExType>;
|
||||
};
|
||||
};
|
||||
|
||||
const TARGET_COMPONENTS: ExecutionConfigs = {
|
||||
None: {
|
||||
params: {},
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@components/util";
|
||||
import { useExecute, useRead } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Route } from "lucide-react";
|
||||
import { Clock, Route } from "lucide-react";
|
||||
import { ProcedureConfig } from "./config";
|
||||
import { ProcedureTable } from "./table";
|
||||
import { DeleteResource, NewResource } from "../common";
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
procedure_state_intention,
|
||||
stroke_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { cn } from "@lib/utils";
|
||||
import { cn, updateLogToHtml } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@pages/home/dashboard";
|
||||
import { RenameResource } from "@components/config/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { Card } from "@ui/card";
|
||||
|
||||
const useProcedure = (id?: string) =>
|
||||
useRead("ListProcedures", {}).data?.find((d) => d.id === id);
|
||||
@@ -64,7 +66,9 @@ export const ProcedureComponents: RequiredResourceComponents = {
|
||||
|
||||
New: () => <NewResource type="Procedure" />,
|
||||
|
||||
GroupActions: () => <GroupActions type="Procedure" actions={["RunProcedure"]} />,
|
||||
GroupActions: () => (
|
||||
<GroupActions type="Procedure" actions={["RunProcedure"]} />
|
||||
),
|
||||
|
||||
Table: ({ resources }) => (
|
||||
<ProcedureTable procedures={resources as Types.ProcedureListItem[]} />
|
||||
@@ -83,7 +87,45 @@ export const ProcedureComponents: RequiredResourceComponents = {
|
||||
Status: {},
|
||||
|
||||
Info: {
|
||||
Stages: ({ id }) => <div>Stages: {useProcedure(id)?.info.stages}</div>,
|
||||
Schedule: ({ id }) => {
|
||||
const next_scheduled_run = useProcedure(id)?.info.next_scheduled_run;
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Clock className="w-4 h-4" />
|
||||
Next Run:
|
||||
<div className="font-bold">
|
||||
{next_scheduled_run
|
||||
? new Date(next_scheduled_run).toLocaleString()
|
||||
: "Not Scheduled"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
ScheduleErrors: ({ id }) => {
|
||||
const error = useProcedure(id)?.info.schedule_error;
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Card className="px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer">
|
||||
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
|
||||
Schedule Error
|
||||
</div>
|
||||
</Card>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-[400px]">
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: updateLogToHtml(error),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Actions: {
|
||||
@@ -129,7 +171,7 @@ export const ProcedureComponents: RequiredResourceComponents = {
|
||||
icon={<ProcedureIcon id={id} size={8} />}
|
||||
name={procedure?.name}
|
||||
state={procedure?.info.state}
|
||||
status={procedure?.info.stages.toString() + "Stages"}
|
||||
status={`${procedure?.info.stages} Stage${procedure?.info.stages === 1 ? "" : "s"}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -188,6 +188,11 @@ export const ResourceSyncConfig = ({
|
||||
description:
|
||||
"Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.",
|
||||
},
|
||||
pending_alert: {
|
||||
label: "Pending Alerts",
|
||||
description:
|
||||
"Send a message to your Alerters when the Sync has Pending Changes",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import { UsableResource } from "@types";
|
||||
import { UserAvatar } from "@components/util";
|
||||
import { ResourceName } from "@components/resources/common";
|
||||
import { useWebsocketMessages } from "@lib/socket";
|
||||
import { MonacoDiffEditor } from "@components/monaco";
|
||||
|
||||
export const UpdateUser = ({
|
||||
user_id,
|
||||
@@ -203,6 +204,21 @@ const UpdateDetailsContent = ({
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="grid gap-2 max-h-[calc(85vh-110px)] overflow-y-auto">
|
||||
{update.prev_toml && update.current_toml && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Changes made</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<MonacoDiffEditor
|
||||
original={update.prev_toml}
|
||||
modified={update.current_toml}
|
||||
language="toml"
|
||||
readOnly
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{update.logs?.map((log, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="flex-col">
|
||||
|
||||
@@ -16,19 +16,38 @@ pub fn init(config: &LogConfig) -> anyhow::Result<()> {
|
||||
|
||||
let use_otel = !config.otlp_endpoint.is_empty();
|
||||
|
||||
match (config.stdio, use_otel) {
|
||||
(StdioLogMode::Standard, true) => {
|
||||
match (config.stdio, use_otel, config.pretty) {
|
||||
(StdioLogMode::Standard, true, true) => {
|
||||
let tracer = otel::tracer(
|
||||
&config.otlp_endpoint,
|
||||
config.opentelemetry_service_name.clone(),
|
||||
);
|
||||
registry
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.pretty()
|
||||
.with_file(false)
|
||||
.with_line_number(false),
|
||||
)
|
||||
.with(OpenTelemetryLayer::new(tracer))
|
||||
.try_init()
|
||||
}
|
||||
(StdioLogMode::Standard, true, false) => {
|
||||
let tracer = otel::tracer(
|
||||
&config.otlp_endpoint,
|
||||
config.opentelemetry_service_name.clone(),
|
||||
);
|
||||
registry
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_file(false)
|
||||
.with_line_number(false),
|
||||
)
|
||||
.with(OpenTelemetryLayer::new(tracer))
|
||||
.try_init()
|
||||
}
|
||||
|
||||
(StdioLogMode::Json, true) => {
|
||||
(StdioLogMode::Json, true, _) => {
|
||||
let tracer = otel::tracer(
|
||||
&config.otlp_endpoint,
|
||||
config.opentelemetry_service_name.clone(),
|
||||
@@ -39,23 +58,34 @@ pub fn init(config: &LogConfig) -> anyhow::Result<()> {
|
||||
.try_init()
|
||||
}
|
||||
|
||||
(StdioLogMode::None, true) => {
|
||||
(StdioLogMode::Standard, false, true) => registry
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.pretty()
|
||||
.with_file(false)
|
||||
.with_line_number(false),
|
||||
)
|
||||
.try_init(),
|
||||
(StdioLogMode::Standard, false, false) => registry
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_file(false)
|
||||
.with_line_number(false),
|
||||
)
|
||||
.try_init(),
|
||||
|
||||
(StdioLogMode::Json, false, _) => registry
|
||||
.with(tracing_subscriber::fmt::layer().json())
|
||||
.try_init(),
|
||||
|
||||
(StdioLogMode::None, true, _) => {
|
||||
let tracer = otel::tracer(
|
||||
&config.otlp_endpoint,
|
||||
config.opentelemetry_service_name.clone(),
|
||||
);
|
||||
registry.with(OpenTelemetryLayer::new(tracer)).try_init()
|
||||
}
|
||||
|
||||
(StdioLogMode::Standard, false) => {
|
||||
registry.with(tracing_subscriber::fmt::layer()).try_init()
|
||||
}
|
||||
|
||||
(StdioLogMode::Json, false) => registry
|
||||
.with(tracing_subscriber::fmt::layer().json())
|
||||
.try_init(),
|
||||
|
||||
(StdioLogMode::None, false) => Ok(()),
|
||||
(StdioLogMode::None, false, _) => Ok(()),
|
||||
}
|
||||
.context("failed to init logger")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user