forked from github-starred/komodo
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b43e2918da | ||
|
|
f45205011e | ||
|
|
5b211fb8f0 | ||
|
|
3f04614303 | ||
|
|
4c3210f70a |
Generated
+625
-509
File diff suppressed because it is too large
Load Diff
+14
-8
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.17.0"
|
||||
version = "1.17.2"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -30,7 +30,7 @@ git = { path = "lib/git" }
|
||||
# MOGH
|
||||
run_command = { version = "0.0.6", features = ["async_tokio"] }
|
||||
serror = { version = "0.5.0", default-features = false }
|
||||
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
merge_config_files = "0.1.5"
|
||||
@@ -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
|
||||
@@ -80,7 +80,7 @@ opentelemetry = "0.29.0"
|
||||
tracing = "0.1.41"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.32", features = ["derive"] }
|
||||
clap = { version = "4.5.36", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
@@ -92,7 +92,7 @@ urlencoding = "2.1.3"
|
||||
nom_pem = "4.0.0"
|
||||
bcrypt = "0.17.0"
|
||||
base64 = "0.22.1"
|
||||
rustls = "0.23.25"
|
||||
rustls = "0.23.26"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
rand = "0.9.0"
|
||||
@@ -100,13 +100,19 @@ hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
bollard = "0.18.1"
|
||||
sysinfo = "0.33.1"
|
||||
sysinfo = "0.34.2"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.6.0"
|
||||
aws-sdk-ec2 = "1.118.1"
|
||||
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
|
||||
+5
-1
@@ -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,10 +189,52 @@ 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() {
|
||||
send_message(url, &content).await?;
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
// interpolate variables and secrets into the url
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut url_interpolated,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
send_message(&url_interpolated, &content)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let replacers =
|
||||
secret_replacers.into_iter().collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {}",
|
||||
sanitized_error
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ use crate::helpers::query::get_variables_and_secrets;
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
mod discord;
|
||||
mod ntfy;
|
||||
mod pushover;
|
||||
mod slack;
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
@@ -128,6 +130,22 @@ 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
|
||||
)
|
||||
})
|
||||
}
|
||||
AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => {
|
||||
pushover::send_alert(url, alert).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send alert to Pushover Alerter {}",
|
||||
alerter.name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
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 {} failed\nversion: v{}\n{link}",
|
||||
name, version,
|
||||
)
|
||||
}
|
||||
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<()> {
|
||||
let response = http_client()
|
||||
.post(url)
|
||||
.header("Title", "ntfy Alert")
|
||||
.body(content)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send message")?;
|
||||
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
debug!("ntfy alert sent successfully: {}", status);
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.with_context(|| {
|
||||
format!(
|
||||
"Failed to send message to ntfy | {} | failed to get response text",
|
||||
status
|
||||
)
|
||||
})?;
|
||||
Err(anyhow!(
|
||||
"Failed to send message to ntfy | {} | {}",
|
||||
status,
|
||||
text
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
@@ -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,11 +393,69 @@ 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() {
|
||||
let slack = ::slack::Client::new(url);
|
||||
slack.send_message(text, blocks).await?;
|
||||
let vars_and_secrets = get_variables_and_secrets().await?;
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
let mut url_interpolated = url.to_string();
|
||||
|
||||
// interpolate variables and secrets into the url
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut url_interpolated,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
let slack = ::slack::Client::new(url_interpolated);
|
||||
slack.send_message(text, blocks).await.map_err(|e| {
|
||||
let replacers =
|
||||
secret_replacers.into_iter().collect::<Vec<_>>();
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {}",
|
||||
sanitized_error
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,13 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
PermissionLevel::Execute,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut vars_and_secrets = get_variables_and_secrets().await?;
|
||||
// Add the $VERSION to variables. Use with [[$VERSION]]
|
||||
vars_and_secrets.variables.insert(
|
||||
String::from("$VERSION"),
|
||||
build.config.version.to_string(),
|
||||
);
|
||||
|
||||
if build.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("Must attach builder to RunBuild").into());
|
||||
@@ -110,14 +116,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
// Add the $VERSION to variables. Use with [[$VERSION]]
|
||||
if !vars_and_secrets.variables.contains_key("$VERSION") {
|
||||
vars_and_secrets.variables.insert(
|
||||
String::from("$VERSION"),
|
||||
build.config.version.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let git_token = git_token(
|
||||
&build.config.git_provider,
|
||||
&build.config.git_account,
|
||||
@@ -177,7 +175,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
});
|
||||
|
||||
// GET BUILDER PERIPHERY
|
||||
|
||||
let (periphery, cleanup_data) = match get_builder_periphery(
|
||||
build.name.clone(),
|
||||
Some(build.config.version),
|
||||
@@ -203,9 +200,8 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
}
|
||||
};
|
||||
|
||||
// CLONE REPO
|
||||
// INTERPOLATE VARIABLES
|
||||
let secret_replacers = if !build.config.skip_secret_interp {
|
||||
// Interpolate variables / secrets into pre build command
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
@@ -216,6 +212,34 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.build_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.secret_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.dockerfile,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_extra_args(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.extra_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
add_interp_update_log(
|
||||
&mut update,
|
||||
&global_replacers,
|
||||
@@ -227,84 +251,57 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&build).into(),
|
||||
git_token,
|
||||
environment: Default::default(),
|
||||
env_file_path: Default::default(),
|
||||
skip_secret_interp: Default::default(),
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
}) => res,
|
||||
_ = cancel.cancelled() => {
|
||||
debug!("build cancelled during clone, cleaning up builder");
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
},
|
||||
};
|
||||
|
||||
let commit_message = match res {
|
||||
Ok(res) => {
|
||||
debug!("finished repo clone");
|
||||
update.logs.extend(res.logs);
|
||||
update.commit_hash =
|
||||
res.commit_hash.unwrap_or_default().to_string();
|
||||
res.commit_message.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed build at clone repo | {e:#}");
|
||||
update.push_error_log(
|
||||
"clone repo",
|
||||
format_serror(&e.context("failed to clone repo").into()),
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
let secret_replacers = if !build.config.skip_secret_interp {
|
||||
// Interpolate variables / secrets into build args
|
||||
let mut global_replacers = HashSet::new();
|
||||
let mut secret_replacers = HashSet::new();
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.build_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_string(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.secret_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
interpolate_variables_secrets_into_extra_args(
|
||||
&vars_and_secrets,
|
||||
&mut build.config.extra_args,
|
||||
&mut global_replacers,
|
||||
&mut secret_replacers,
|
||||
)?;
|
||||
|
||||
add_interp_update_log(
|
||||
&mut update,
|
||||
&global_replacers,
|
||||
&secret_replacers,
|
||||
);
|
||||
|
||||
secret_replacers
|
||||
} else {
|
||||
Default::default()
|
||||
let commit_message = if !build.config.files_on_host
|
||||
&& !build.config.repo.is_empty()
|
||||
{
|
||||
// CLONE REPO
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::git::CloneRepo {
|
||||
args: (&build).into(),
|
||||
git_token,
|
||||
environment: Default::default(),
|
||||
env_file_path: Default::default(),
|
||||
skip_secret_interp: Default::default(),
|
||||
replacers: Default::default(),
|
||||
}) => res,
|
||||
_ = cancel.cancelled() => {
|
||||
debug!("build cancelled during clone, cleaning up builder");
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
|
||||
cleanup_builder_instance(cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
},
|
||||
};
|
||||
|
||||
let commit_message = match res {
|
||||
Ok(res) => {
|
||||
debug!("finished repo clone");
|
||||
update.logs.extend(res.logs);
|
||||
update.commit_hash =
|
||||
res.commit_hash.unwrap_or_default().to_string();
|
||||
res.commit_message.unwrap_or_default()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed build at clone repo | {e:#}");
|
||||
update.push_error_log(
|
||||
"clone repo",
|
||||
format_serror(&e.context("failed to clone repo").into()),
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
Some(commit_message)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if all_logs_success(&update.logs) {
|
||||
// RUN BUILD
|
||||
let res = tokio::select! {
|
||||
res = periphery
|
||||
.request(api::build::Build {
|
||||
@@ -321,7 +318,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
_ = cancel.cancelled() => {
|
||||
info!("build cancelled during build, cleaning up builder");
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
cleanup_builder_instance(cleanup_data, &mut update)
|
||||
.await;
|
||||
return handle_early_return(update, build.id, build.name, true).await
|
||||
},
|
||||
@@ -365,8 +362,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
// stop the cancel listening task from going forever
|
||||
cancel.cancel();
|
||||
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
// If building on temporary cloud server (AWS),
|
||||
// this will terminate the server.
|
||||
cleanup_builder_instance(cleanup_data, &mut update).await;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
_ = cancel.cancelled() => {
|
||||
debug!("build cancelled during clone, cleaning up builder");
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
cleanup_builder_instance(cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
return handle_builder_early_return(update, repo.id, repo.name, true).await
|
||||
@@ -506,8 +506,9 @@ impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
// stop the cancel listening task from going forever
|
||||
cancel.cancel();
|
||||
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
// If building on temporary cloud server (AWS),
|
||||
// this will terminate the server.
|
||||
cleanup_builder_instance(cleanup_data, &mut update).await;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
|
||||
@@ -116,6 +116,7 @@ enum ReadRequest {
|
||||
InspectDockerImage(InspectDockerImage),
|
||||
ListDockerImageHistory(ListDockerImageHistory),
|
||||
InspectDockerVolume(InspectDockerVolume),
|
||||
GetDockerContainersSummary(GetDockerContainersSummary),
|
||||
ListAllDockerContainers(ListAllDockerContainers),
|
||||
ListDockerContainers(ListDockerContainers),
|
||||
ListDockerNetworks(ListDockerNetworks),
|
||||
|
||||
@@ -14,7 +14,9 @@ use komodo_client::{
|
||||
ResourceTarget,
|
||||
deployment::Deployment,
|
||||
docker::{
|
||||
container::{Container, ContainerListItem},
|
||||
container::{
|
||||
Container, ContainerListItem, ContainerStateStatusEnum,
|
||||
},
|
||||
image::{Image, ImageHistoryResponseItem},
|
||||
network::Network,
|
||||
volume::Volume,
|
||||
@@ -413,6 +415,45 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for GetDockerContainersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetDockerContainersSummaryResponse> {
|
||||
let servers = resource::list_full_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to get servers from db")?;
|
||||
|
||||
let mut res = GetDockerContainersSummaryResponse::default();
|
||||
|
||||
for server in servers {
|
||||
let cache = server_status_cache()
|
||||
.get_or_insert_default(&server.id)
|
||||
.await;
|
||||
|
||||
if let Some(containers) = &cache.containers {
|
||||
for container in containers {
|
||||
res.total += 1;
|
||||
match container.state {
|
||||
ContainerStateStatusEnum::Created
|
||||
| ContainerStateStatusEnum::Paused
|
||||
| ContainerStateStatusEnum::Exited => res.stopped += 1,
|
||||
ContainerStateStatusEnum::Running => res.running += 1,
|
||||
ContainerStateStatusEnum::Empty => res.unknown += 1,
|
||||
_ => res.unhealthy += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectDockerContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
|
||||
+337
-37
@@ -1,12 +1,17 @@
|
||||
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use git::GitRes;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
CloneArgs, NoData,
|
||||
CloneArgs, FileContents, NoData, Operation, all_logs_success,
|
||||
build::{Build, BuildInfo, PartialBuildConfig},
|
||||
builder::{Builder, BuilderConfig},
|
||||
config::core::CoreConfig,
|
||||
permission::PermissionLevel,
|
||||
server::ServerState,
|
||||
update::Update,
|
||||
},
|
||||
};
|
||||
@@ -15,11 +20,22 @@ use mungos::mongodb::bson::to_document;
|
||||
use octorust::types::{
|
||||
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
|
||||
};
|
||||
use periphery_client::{
|
||||
PeripheryClient,
|
||||
api::build::{
|
||||
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::git_token,
|
||||
helpers::{
|
||||
git_token, periphery_client,
|
||||
query::get_server_with_state,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
resource,
|
||||
state::{db_client, github_client},
|
||||
};
|
||||
@@ -88,6 +104,184 @@ impl Resolve<WriteArgs> for RenameBuild {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for WriteBuildFileContents {
|
||||
#[instrument(name = "WriteBuildFileContents", skip(args))]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
let build = resource::get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
&args.user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !build.config.files_on_host && build.config.repo.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Build is not configured to use Files on Host or Git Repo, can't write dockerfile contents"
|
||||
).into());
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&build, Operation::WriteDockerfile, &args.user);
|
||||
|
||||
update.push_simple_log("Dockerfile to write", &self.contents);
|
||||
|
||||
if build.config.files_on_host {
|
||||
match get_on_host_periphery(&build)
|
||||
.await?
|
||||
.request(WriteDockerfileContentsToHost {
|
||||
name: build.name,
|
||||
build_path: build.config.build_path,
|
||||
dockerfile_path: build.config.dockerfile_path,
|
||||
contents: self.contents,
|
||||
})
|
||||
.await
|
||||
.context("Failed to write dockerfile contents to host")
|
||||
{
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
}
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"Write Dockerfile Contents",
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if !all_logs_success(&update.logs) {
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
(RefreshBuildCache { build: build.id }).resolve(args).await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh build cache",
|
||||
format_serror(&e.error.into()),
|
||||
);
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
} else {
|
||||
write_dockerfile_contents_git(self, args, build, update).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_dockerfile_contents_git(
|
||||
req: WriteBuildFileContents,
|
||||
args: &WriteArgs,
|
||||
build: Build,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
let WriteBuildFileContents { build: _, contents } = req;
|
||||
|
||||
let mut clone_args: CloneArgs = (&build).into();
|
||||
let root = clone_args.unique_path(&core_config().repo_directory)?;
|
||||
|
||||
let build_path = build
|
||||
.config
|
||||
.build_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid build path")?;
|
||||
let dockerfile_path = build
|
||||
.config
|
||||
.dockerfile_path
|
||||
.parse::<PathBuf>()
|
||||
.context("Invalid dockerfile path")?;
|
||||
|
||||
let full_path = root.join(&build_path).join(&dockerfile_path);
|
||||
|
||||
if let Some(parent) = full_path.parent() {
|
||||
fs::create_dir_all(parent).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to initialize dockerfile parent directory {parent:?}"
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Ensure the folder is initialized as git repo.
|
||||
// This allows a new file to be committed on a branch that may not exist.
|
||||
if !root.join(".git").exists() {
|
||||
let access_token = if let Some(account) = &clone_args.account {
|
||||
git_token(&clone_args.provider, account, |https| clone_args.https = https)
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
git::init_folder_as_repo(
|
||||
&root,
|
||||
&clone_args,
|
||||
access_token.as_deref(),
|
||||
&mut update.logs,
|
||||
)
|
||||
.await;
|
||||
|
||||
if !all_logs_success(&update.logs) {
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
return Ok(update);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
fs::write(&full_path, &contents).await.with_context(|| {
|
||||
format!("Failed to write dockerfile contents to {full_path:?}")
|
||||
})
|
||||
{
|
||||
update
|
||||
.push_error_log("Write Dockerfile", format_serror(&e.into()));
|
||||
} else {
|
||||
update.push_simple_log(
|
||||
"Write Dockerfile",
|
||||
format!("File written to {full_path:?}"),
|
||||
);
|
||||
};
|
||||
|
||||
if !all_logs_success(&update.logs) {
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
let commit_res = git::commit_file(
|
||||
&format!("{}: Commit Dockerfile", args.user.username),
|
||||
&root,
|
||||
&build_path.join(&dockerfile_path),
|
||||
&build.config.branch,
|
||||
)
|
||||
.await;
|
||||
|
||||
update.logs.extend(commit_res.logs);
|
||||
|
||||
if let Err(e) = (RefreshBuildCache { build: build.name })
|
||||
.resolve(args)
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh build cache",
|
||||
format_serror(&e.error.into()),
|
||||
);
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for RefreshBuildCache {
|
||||
#[instrument(
|
||||
name = "RefreshBuildCache",
|
||||
@@ -107,55 +301,104 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if build.config.repo.is_empty()
|
||||
|| build.config.git_provider.is_empty()
|
||||
{
|
||||
// Nothing to do here
|
||||
return Ok(NoData {});
|
||||
}
|
||||
let (
|
||||
remote_path,
|
||||
remote_contents,
|
||||
remote_error,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
) = if build.config.files_on_host {
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
match get_on_host_dockerfile(&build).await {
|
||||
Ok(FileContents { path, contents }) => {
|
||||
(Some(path), Some(contents), None, None, None)
|
||||
}
|
||||
Err(e) => {
|
||||
(None, None, Some(format_serror(&e.into())), None, None)
|
||||
}
|
||||
}
|
||||
} else if !build.config.repo.is_empty() {
|
||||
// ================
|
||||
// REPO BASED BUILD
|
||||
// ================
|
||||
if build.config.git_provider.is_empty() {
|
||||
// Nothing to do here
|
||||
return Ok(NoData {});
|
||||
}
|
||||
let config = core_config();
|
||||
|
||||
let config = core_config();
|
||||
let mut clone_args: CloneArgs = (&build).into();
|
||||
let repo_path =
|
||||
clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
// Don't want to run these on core.
|
||||
clone_args.on_clone = None;
|
||||
clone_args.on_pull = None;
|
||||
|
||||
let mut clone_args: CloneArgs = (&build).into();
|
||||
let repo_path =
|
||||
clone_args.unique_path(&core_config().repo_directory)?;
|
||||
clone_args.destination = Some(repo_path.display().to_string());
|
||||
// Don't want to run these on core.
|
||||
clone_args.on_clone = None;
|
||||
clone_args.on_pull = None;
|
||||
|
||||
let access_token = if let Some(username) = &clone_args.account {
|
||||
git_token(&clone_args.provider, username, |https| {
|
||||
let access_token = if let Some(username) = &clone_args.account {
|
||||
git_token(&clone_args.provider, username, |https| {
|
||||
clone_args.https = https
|
||||
})
|
||||
.await
|
||||
.with_context(
|
||||
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let GitRes {
|
||||
hash: latest_hash,
|
||||
message: latest_message,
|
||||
..
|
||||
} = git::pull_or_clone(
|
||||
clone_args,
|
||||
&config.repo_directory,
|
||||
access_token,
|
||||
&[],
|
||||
"",
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to clone build repo")?;
|
||||
let GitRes { hash, message, .. } = git::pull_or_clone(
|
||||
clone_args,
|
||||
&config.repo_directory,
|
||||
access_token,
|
||||
&[],
|
||||
"",
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to clone build repo")?;
|
||||
|
||||
let relative_path = PathBuf::from_str(&build.config.build_path)
|
||||
.context("Invalid build path")?
|
||||
.join(&build.config.dockerfile_path);
|
||||
|
||||
let full_path = repo_path.join(&relative_path);
|
||||
let (contents, error) = match fs::read_to_string(&full_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to read dockerfile contents at {full_path:?}"
|
||||
)
|
||||
}) {
|
||||
Ok(contents) => (Some(contents), None),
|
||||
Err(e) => (None, Some(format_serror(&e.into()))),
|
||||
};
|
||||
|
||||
(
|
||||
Some(relative_path.display().to_string()),
|
||||
contents,
|
||||
error,
|
||||
hash,
|
||||
message,
|
||||
)
|
||||
} else {
|
||||
// =============
|
||||
// UI BASED FILE
|
||||
// =============
|
||||
(None, None, None, None, None)
|
||||
};
|
||||
|
||||
let info = BuildInfo {
|
||||
last_built_at: build.info.last_built_at,
|
||||
built_hash: build.info.built_hash,
|
||||
built_message: build.info.built_message,
|
||||
built_contents: build.info.built_contents,
|
||||
remote_path,
|
||||
remote_contents,
|
||||
remote_error,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
};
|
||||
@@ -176,6 +419,63 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_on_host_periphery(
|
||||
build: &Build,
|
||||
) -> anyhow::Result<PeripheryClient> {
|
||||
if build.config.builder_id.is_empty() {
|
||||
return Err(anyhow!("No builder associated with build"));
|
||||
}
|
||||
|
||||
let builder = resource::get::<Builder>(&build.config.builder_id)
|
||||
.await
|
||||
.context("Failed to get builder")?;
|
||||
|
||||
match builder.config {
|
||||
BuilderConfig::Aws(_) => {
|
||||
Err(anyhow!("Files on host doesn't work with AWS builder"))
|
||||
}
|
||||
BuilderConfig::Url(config) => {
|
||||
let periphery = PeripheryClient::new(
|
||||
config.address,
|
||||
config.passkey,
|
||||
Duration::from_secs(3),
|
||||
);
|
||||
periphery.health_check().await?;
|
||||
Ok(periphery)
|
||||
}
|
||||
BuilderConfig::Server(config) => {
|
||||
if config.server_id.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Builder is type server, but has no server attached"
|
||||
));
|
||||
}
|
||||
let (server, state) =
|
||||
get_server_with_state(&config.server_id).await?;
|
||||
if state != ServerState::Ok {
|
||||
return Err(anyhow!(
|
||||
"Builder server is disabled or not reachable"
|
||||
));
|
||||
};
|
||||
periphery_client(&server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The successful case will be included as Some(remote_contents).
|
||||
/// The error case will be included as Some(remote_error)
|
||||
async fn get_on_host_dockerfile(
|
||||
build: &Build,
|
||||
) -> anyhow::Result<FileContents> {
|
||||
get_on_host_periphery(build)
|
||||
.await?
|
||||
.request(GetDockerfileContentsOnHost {
|
||||
name: build.name.clone(),
|
||||
build_path: build.config.build_path.clone(),
|
||||
dockerfile_path: build.config.dockerfile_path.clone(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CreateBuildWebhook {
|
||||
#[instrument(name = "CreateBuildWebhook", skip(args))]
|
||||
async fn resolve(
|
||||
|
||||
@@ -96,6 +96,7 @@ pub enum WriteRequest {
|
||||
DeleteBuild(DeleteBuild),
|
||||
UpdateBuild(UpdateBuild),
|
||||
RenameBuild(RenameBuild),
|
||||
WriteBuildFileContents(WriteBuildFileContents),
|
||||
RefreshBuildCache(RefreshBuildCache),
|
||||
CreateBuildWebhook(CreateBuildWebhook),
|
||||
DeleteBuildWebhook(DeleteBuildWebhook),
|
||||
|
||||
@@ -211,7 +211,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
add_update(update.clone()).await?;
|
||||
update.id = add_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,38 @@ impl Resolve<WriteArgs> for RenameResourceSync {
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for WriteSyncFileContents {
|
||||
#[instrument(name = "WriteSyncFileContents", skip(args))]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
&args.user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !sync.config.files_on_host && sync.config.repo.is_empty() {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"This method is only for 'files on host' or 'repo' based syncs."
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&sync, Operation::WriteSyncContents, &args.user);
|
||||
|
||||
update.push_simple_log("File contents", &self.contents);
|
||||
|
||||
if sync.config.files_on_host {
|
||||
write_sync_file_contents_on_host(self, args, sync, update).await
|
||||
} else {
|
||||
write_sync_file_contents_git(self, args, sync, update).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_sync_file_contents_on_host(
|
||||
req: WriteSyncFileContents,
|
||||
args: &WriteArgs,
|
||||
@@ -298,7 +330,7 @@ async fn write_sync_file_contents_git(
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh failed",
|
||||
"Refresh sync pending",
|
||||
format_serror(&e.error.into()),
|
||||
);
|
||||
}
|
||||
@@ -309,37 +341,6 @@ async fn write_sync_file_contents_git(
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for WriteSyncFileContents {
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
let sync = resource::get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
&args.user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !sync.config.files_on_host && sync.config.repo.is_empty() {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"This method is only for 'files on host' or 'repo' based syncs."
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut update =
|
||||
make_update(&sync, Operation::WriteSyncContents, &args.user);
|
||||
|
||||
update.push_simple_log("File contents", &self.contents);
|
||||
|
||||
if sync.config.files_on_host {
|
||||
write_sync_file_contents_on_host(self, args, sync, update).await
|
||||
} else {
|
||||
write_sync_file_contents_git(self, args, sync, update).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<WriteArgs> for CommitSync {
|
||||
#[instrument(name = "CommitSync", skip(args))]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
@@ -828,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) => {
|
||||
|
||||
@@ -5,6 +5,8 @@ pub mod hetzner;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BuildCleanupData {
|
||||
Server { repo_name: String },
|
||||
/// Nothing to clean up
|
||||
Server,
|
||||
/// Clean up AWS instance
|
||||
Aws { instance_id: String, region: String },
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
title: env.komodo_title.unwrap_or(config.title),
|
||||
host: env.komodo_host.unwrap_or(config.host),
|
||||
port: env.komodo_port.unwrap_or(config.port),
|
||||
bind_ip: env.komodo_bind_ip.unwrap_or(config.bind_ip),
|
||||
first_server: env.komodo_first_server.unwrap_or(config.first_server),
|
||||
frontend_path: env.komodo_frontend_path.unwrap_or(config.frontend_path),
|
||||
jwt_ttl: env
|
||||
@@ -193,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),
|
||||
|
||||
@@ -61,12 +61,7 @@ pub async fn get_builder_periphery(
|
||||
.health_check()
|
||||
.await
|
||||
.context("Url Builder failed health check")?;
|
||||
Ok((
|
||||
periphery,
|
||||
BuildCleanupData::Server {
|
||||
repo_name: resource_name,
|
||||
},
|
||||
))
|
||||
Ok((periphery, BuildCleanupData::Server))
|
||||
}
|
||||
BuilderConfig::Server(config) => {
|
||||
if config.server_id.is_empty() {
|
||||
@@ -74,12 +69,7 @@ pub async fn get_builder_periphery(
|
||||
}
|
||||
let server = resource::get::<Server>(&config.server_id).await?;
|
||||
let periphery = periphery_client(&server)?;
|
||||
Ok((
|
||||
periphery,
|
||||
BuildCleanupData::Server {
|
||||
repo_name: resource_name,
|
||||
},
|
||||
))
|
||||
Ok((periphery, BuildCleanupData::Server))
|
||||
}
|
||||
BuilderConfig::Aws(config) => {
|
||||
get_aws_builder(&resource_name, version, config, update).await
|
||||
@@ -179,17 +169,14 @@ async fn get_aws_builder(
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip(periphery, update))]
|
||||
#[instrument(skip(update))]
|
||||
pub async fn cleanup_builder_instance(
|
||||
periphery: PeripheryClient,
|
||||
cleanup_data: BuildCleanupData,
|
||||
update: &mut Update,
|
||||
) {
|
||||
match cleanup_data {
|
||||
BuildCleanupData::Server { repo_name } => {
|
||||
let _ = periphery
|
||||
.request(api::git::DeleteRepo { name: repo_name })
|
||||
.await;
|
||||
BuildCleanupData::Server => {
|
||||
// Nothing to clean up
|
||||
}
|
||||
BuildCleanupData::Aws {
|
||||
instance_id,
|
||||
|
||||
@@ -78,6 +78,9 @@ pub async fn git_token(
|
||||
account_username: &str,
|
||||
mut on_https_found: impl FnMut(bool),
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
if provider_domain.is_empty() || account_username.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let db_provider = db_client()
|
||||
.git_accounts
|
||||
.find_one(doc! { "domain": provider_domain, "username": account_username })
|
||||
@@ -143,7 +146,11 @@ pub fn periphery_client(
|
||||
|
||||
let client = PeripheryClient::new(
|
||||
&server.config.address,
|
||||
&core_config().passkey,
|
||||
if server.config.passkey.is_empty() {
|
||||
&core_config().passkey
|
||||
} else {
|
||||
&server.config.passkey
|
||||
},
|
||||
Duration::from_secs(server.config.timeout_seconds as u64),
|
||||
);
|
||||
|
||||
@@ -352,4 +359,4 @@ pub async fn ensure_first_server_and_builder() {
|
||||
e.error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 @@ async fn app() -> anyhow::Result<()> {
|
||||
)
|
||||
.into_make_service();
|
||||
|
||||
let socket_addr =
|
||||
SocketAddr::from_str(&format!("0.0.0.0:{}", core_config().port))
|
||||
.context("failed to parse socket 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 {
|
||||
info!("🔒 Core 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
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use komodo_client::entities::{
|
||||
Operation, ResourceTargetVariant,
|
||||
build::{
|
||||
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
|
||||
BuildListItemInfo, BuildQuerySpecifics, BuildState,
|
||||
PartialBuildConfig,
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::RefreshBuildCache,
|
||||
entities::{
|
||||
Operation, ResourceTarget, ResourceTargetVariant,
|
||||
build::{
|
||||
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
|
||||
BuildListItemInfo, BuildQuerySpecifics, BuildState,
|
||||
PartialBuildConfig,
|
||||
},
|
||||
builder::Builder,
|
||||
environment_vars_from_str, optional_string,
|
||||
permission::PermissionLevel,
|
||||
resource::Resource,
|
||||
update::Update,
|
||||
user::{User, build_user},
|
||||
},
|
||||
builder::Builder,
|
||||
environment_vars_from_str,
|
||||
permission::PermissionLevel,
|
||||
resource::Resource,
|
||||
update::Update,
|
||||
user::User,
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::{Collection, bson::doc, options::FindOptions},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs,
|
||||
config::core_config,
|
||||
helpers::{empty_or_only_spaces, query::get_latest_update},
|
||||
state::{action_states, build_state_cache, db_client},
|
||||
@@ -38,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
|
||||
@@ -56,10 +66,13 @@ impl super::KomodoResource for Build {
|
||||
last_built_at: build.info.last_built_at,
|
||||
version: build.config.version,
|
||||
builder_id: build.config.builder_id,
|
||||
git_provider: build.config.git_provider,
|
||||
image_registry_domain: build.config.image_registry.domain,
|
||||
repo: build.config.repo,
|
||||
branch: build.config.branch,
|
||||
files_on_host: build.config.files_on_host,
|
||||
git_provider: optional_string(build.config.git_provider),
|
||||
repo: optional_string(build.config.repo),
|
||||
branch: optional_string(build.config.branch),
|
||||
image_registry_domain: optional_string(
|
||||
build.config.image_registry.domain,
|
||||
),
|
||||
built_hash: build.info.built_hash,
|
||||
latest_hash: build.info.latest_hash,
|
||||
state,
|
||||
@@ -96,10 +109,23 @@ impl super::KomodoResource for Build {
|
||||
}
|
||||
|
||||
async fn post_create(
|
||||
_created: &Resource<Self::Config, Self::Info>,
|
||||
_update: &mut Update,
|
||||
created: &Resource<Self::Config, Self::Info>,
|
||||
update: &mut Update,
|
||||
) -> anyhow::Result<()> {
|
||||
refresh_build_state_cache().await;
|
||||
if let Err(e) = (RefreshBuildCache {
|
||||
build: created.name.clone(),
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: build_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh build cache",
|
||||
format_serror(&e.error.context("The build cache has failed to refresh. This may be due to a misconfiguration of the Build").into())
|
||||
);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -170,6 +174,7 @@ impl super::KomodoResource for Repo {
|
||||
} else {
|
||||
repo.config.path.clone()
|
||||
},
|
||||
is_build: false,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -162,7 +166,7 @@ impl super::KomodoResource for Stack {
|
||||
{
|
||||
update.push_error_log(
|
||||
"Refresh stack cache",
|
||||
format_serror(&e.error.context("The stack cache has failed to refresh. This is likely due to a misconfiguration of the Stack").into())
|
||||
format_serror(&e.error.context("The stack cache has failed to refresh. This may be due to a misconfiguration of the Stack").into())
|
||||
);
|
||||
};
|
||||
if created.config.server_id.is_empty() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub async fn get_repo_compose_contents(
|
||||
let (repo_path, _logs, hash, message) =
|
||||
ensure_remote_repo(clone_args)
|
||||
.await
|
||||
.context("failed to clone stack repo")?;
|
||||
.context("Failed to clone stack repo")?;
|
||||
|
||||
let run_directory = repo_path.join(&stack.config.run_directory);
|
||||
// This will remove any intermediate '/./' which can be a problem for some OS.
|
||||
@@ -44,7 +44,7 @@ pub async fn get_repo_compose_contents(
|
||||
}
|
||||
// If file does not exist, will show up in err case so the log is handled
|
||||
match fs::read_to_string(&file_path).with_context(|| {
|
||||
format!("failed to read file contents from {file_path:?}")
|
||||
format!("Failed to read file contents from {file_path:?}")
|
||||
}) {
|
||||
Ok(contents) => successful.push(FileContents {
|
||||
path: path.to_string(),
|
||||
@@ -102,6 +102,6 @@ pub async fn ensure_remote_repo(
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.context("failed to clone stack repo")
|
||||
.context("Failed to clone stack repo")
|
||||
.map(|res| (repo_path, res.logs, res.hash, res.message))
|
||||
}
|
||||
|
||||
@@ -8,18 +8,15 @@ pub fn extract_services_from_stack(
|
||||
stack: &Stack,
|
||||
) -> Vec<StackServiceNames> {
|
||||
if let Some(mut services) = stack.info.deployed_services.clone() {
|
||||
if services.iter().any(|service| service.image.is_empty()) {
|
||||
for service in
|
||||
services.iter_mut().filter(|s| s.image.is_empty())
|
||||
{
|
||||
service.image = stack
|
||||
.info
|
||||
.latest_services
|
||||
.iter()
|
||||
.find(|s| s.service_name == service.service_name)
|
||||
.map(|s| s.image.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
for service in services.iter_mut().filter(|s| s.image.is_empty())
|
||||
{
|
||||
service.image = stack
|
||||
.info
|
||||
.latest_services
|
||||
.iter()
|
||||
.find(|s| s.service_name == service.service_name)
|
||||
.map(|s| s.image.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
services
|
||||
} else {
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
+177
-13
@@ -1,8 +1,12 @@
|
||||
use std::{fmt::Write, path::Path};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::{
|
||||
run_komodo_command, run_komodo_command_with_interpolation,
|
||||
run_komodo_command, run_komodo_command_multiline,
|
||||
run_komodo_command_with_interpolation,
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
@@ -16,9 +20,12 @@ use komodo_client::{
|
||||
parsers::QUOTE_PATTERN,
|
||||
};
|
||||
use periphery_client::api::build::{
|
||||
self, PruneBuilders, PruneBuildx,
|
||||
self, GetDockerfileContentsOnHost,
|
||||
GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx,
|
||||
WriteDockerfileContentsToHost,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
@@ -26,6 +33,88 @@ use crate::{
|
||||
helpers::{parse_extra_args, parse_labels},
|
||||
};
|
||||
|
||||
impl Resolve<super::Args> for GetDockerfileContentsOnHost {
|
||||
#[instrument(name = "GetDockerfileContentsOnHost", level = "debug")]
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> serror::Result<GetDockerfileContentsOnHostResponse> {
|
||||
let GetDockerfileContentsOnHost {
|
||||
name,
|
||||
build_path,
|
||||
dockerfile_path,
|
||||
} = self;
|
||||
|
||||
let root =
|
||||
periphery_config().build_dir().join(to_komodo_name(&name));
|
||||
let build_dir =
|
||||
root.join(&build_path).components().collect::<PathBuf>();
|
||||
|
||||
if !build_dir.exists() {
|
||||
fs::create_dir_all(&build_dir)
|
||||
.await
|
||||
.context("Failed to initialize build directory")?;
|
||||
}
|
||||
|
||||
let full_path = build_dir
|
||||
.join(&dockerfile_path)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
|
||||
let contents =
|
||||
fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!("Failed to read dockerfile contents at {full_path:?}")
|
||||
})?;
|
||||
|
||||
Ok(GetDockerfileContentsOnHostResponse {
|
||||
contents,
|
||||
path: full_path.display().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for WriteDockerfileContentsToHost {
|
||||
#[instrument(
|
||||
name = "WriteDockerfileContentsToHost",
|
||||
skip_all,
|
||||
fields(
|
||||
stack = &self.name,
|
||||
build_path = &self.build_path,
|
||||
dockerfile_path = &self.dockerfile_path,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
|
||||
let WriteDockerfileContentsToHost {
|
||||
name,
|
||||
build_path,
|
||||
dockerfile_path,
|
||||
contents,
|
||||
} = self;
|
||||
let full_path = periphery_config()
|
||||
.build_dir()
|
||||
.join(to_komodo_name(&name))
|
||||
.join(&build_path)
|
||||
.join(dockerfile_path)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = full_path.parent() {
|
||||
if !parent.exists() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
|
||||
}
|
||||
}
|
||||
fs::write(&full_path, contents).await.with_context(|| {
|
||||
format!("Failed to write dockerfile contents to {full_path:?}")
|
||||
})?;
|
||||
Ok(Log::simple(
|
||||
"Write dockerfile to host",
|
||||
format!("dockerfile contents written to {full_path:?}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<super::Args> for build::Build {
|
||||
#[instrument(name = "Build", skip_all, fields(build = self.build.name.to_string()))]
|
||||
async fn resolve(
|
||||
@@ -36,7 +125,7 @@ impl Resolve<super::Args> for build::Build {
|
||||
build,
|
||||
registry_token,
|
||||
additional_tags,
|
||||
replacers: core_replacers,
|
||||
replacers: mut core_replacers,
|
||||
} = self;
|
||||
let Build {
|
||||
name,
|
||||
@@ -53,11 +142,19 @@ impl Resolve<super::Args> for build::Build {
|
||||
extra_args,
|
||||
use_buildx,
|
||||
image_registry,
|
||||
repo,
|
||||
files_on_host,
|
||||
dockerfile,
|
||||
pre_build,
|
||||
..
|
||||
},
|
||||
..
|
||||
} = &build;
|
||||
|
||||
if !*files_on_host && repo.is_empty() && dockerfile.is_empty() {
|
||||
return Err(anyhow!("Build must be files on host mode, have a repo attached, or have dockerfile contents set to build").into());
|
||||
}
|
||||
|
||||
let mut logs = Vec::new();
|
||||
|
||||
// Maybe docker login
|
||||
@@ -82,14 +179,81 @@ impl Resolve<super::Args> for build::Build {
|
||||
|
||||
let name = to_komodo_name(name);
|
||||
|
||||
// Get paths
|
||||
let build_dir =
|
||||
periphery_config().repo_dir.join(&name).join(build_path);
|
||||
let dockerfile_path = match optional_string(dockerfile_path) {
|
||||
Some(dockerfile_path) => dockerfile_path.to_owned(),
|
||||
None => "Dockerfile".to_owned(),
|
||||
let build_path =
|
||||
periphery_config().build_dir().join(&name).join(build_path);
|
||||
let dockerfile_path = optional_string(dockerfile_path)
|
||||
.unwrap_or("Dockerfile".to_owned());
|
||||
|
||||
// Write UI defined Dockerfile to host
|
||||
if !*files_on_host && repo.is_empty() && !dockerfile.is_empty() {
|
||||
let dockerfile = if *skip_secret_interp {
|
||||
dockerfile.to_string()
|
||||
} else {
|
||||
let (dockerfile, replacers) = svi::interpolate_variables(
|
||||
dockerfile,
|
||||
&periphery_config().secrets,
|
||||
svi::Interpolator::DoubleBrackets,
|
||||
true,
|
||||
).context("Failed to interpolate variables into UI defined dockerfile")?;
|
||||
core_replacers.extend(replacers);
|
||||
dockerfile
|
||||
};
|
||||
|
||||
let full_path = build_path
|
||||
.join(&dockerfile_path)
|
||||
.components()
|
||||
.collect::<PathBuf>();
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = full_path.parent() {
|
||||
if !parent.exists() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(&full_path, dockerfile).await.with_context(|| {
|
||||
format!(
|
||||
"Failed to write dockerfile contents to {full_path:?}"
|
||||
)
|
||||
})?;
|
||||
|
||||
logs.push(Log::simple(
|
||||
"Write Dockerfile",
|
||||
format!("Dockerfile contents written to {full_path:?}"),
|
||||
));
|
||||
};
|
||||
|
||||
// Pre Build
|
||||
if !pre_build.is_none() {
|
||||
let pre_build_path = build_path.join(&pre_build.path);
|
||||
if let Some(log) = if !skip_secret_interp {
|
||||
run_komodo_command_with_interpolation(
|
||||
"Pre Build",
|
||||
Some(pre_build_path.as_path()),
|
||||
&pre_build.command,
|
||||
true,
|
||||
&periphery_config().secrets,
|
||||
&core_replacers,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
run_komodo_command_multiline(
|
||||
"Pre Build",
|
||||
Some(pre_build_path.as_path()),
|
||||
&pre_build.command,
|
||||
)
|
||||
.await
|
||||
} {
|
||||
let success = log.success;
|
||||
logs.push(log);
|
||||
if success {
|
||||
return Ok(logs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Get command parts
|
||||
let image_name =
|
||||
get_image_name(&build).context("failed to make image name")?;
|
||||
@@ -109,7 +273,7 @@ impl Resolve<super::Args> for build::Build {
|
||||
.context("Invalid secret_args")?;
|
||||
let command_secret_args = parse_secret_args(
|
||||
&secret_args,
|
||||
&build_dir,
|
||||
&build_path,
|
||||
*skip_secret_interp,
|
||||
)
|
||||
.await?;
|
||||
@@ -131,14 +295,14 @@ impl Resolve<super::Args> for build::Build {
|
||||
if *skip_secret_interp {
|
||||
let build_log = run_komodo_command(
|
||||
"Docker Build",
|
||||
build_dir.as_ref(),
|
||||
build_path.as_ref(),
|
||||
command,
|
||||
)
|
||||
.await;
|
||||
logs.push(build_log);
|
||||
} else if let Some(log) = run_komodo_command_with_interpolation(
|
||||
"Docker Build",
|
||||
build_dir.as_ref(),
|
||||
build_path.as_ref(),
|
||||
command,
|
||||
false,
|
||||
&periphery_config().secrets,
|
||||
|
||||
@@ -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)
|
||||
@@ -206,7 +206,7 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
|
||||
if let Some(parent) = file_path.parent() {
|
||||
fs::create_dir_all(&parent)
|
||||
.await
|
||||
.with_context(|| format!("Failed to initialize environment file parent directory {parent:?}"))?;
|
||||
.with_context(|| format!("Failed to initialize compose file parent directory {parent:?}"))?;
|
||||
}
|
||||
fs::write(&file_path, contents).await.with_context(|| {
|
||||
format!(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use formatting::format_serror;
|
||||
use git::GitRes;
|
||||
use komodo_client::entities::{CloneArgs, LatestCommit, update::Log};
|
||||
use periphery_client::api::git::{
|
||||
@@ -12,14 +13,14 @@ use tokio::fs;
|
||||
use crate::config::periphery_config;
|
||||
|
||||
impl Resolve<super::Args> for GetLatestCommit {
|
||||
#[instrument(name = "CloneRepo", level = "debug")]
|
||||
#[instrument(name = "GetLatestCommit", level = "debug")]
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> 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(
|
||||
@@ -68,9 +69,14 @@ impl Resolve<super::Args> for CloneRepo {
|
||||
)?,
|
||||
),
|
||||
};
|
||||
let parent_dir = if args.is_build {
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
git::clone(
|
||||
args,
|
||||
&periphery_config().repo_dir,
|
||||
&parent_dir,
|
||||
token,
|
||||
&environment,
|
||||
&env_file_path,
|
||||
@@ -133,9 +139,14 @@ impl Resolve<super::Args> for PullRepo {
|
||||
)?,
|
||||
),
|
||||
};
|
||||
let parent_dir = if args.is_build {
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
git::pull(
|
||||
args,
|
||||
&periphery_config().repo_dir,
|
||||
&parent_dir,
|
||||
token,
|
||||
&environment,
|
||||
&env_file_path,
|
||||
@@ -198,9 +209,14 @@ impl Resolve<super::Args> for PullOrCloneRepo {
|
||||
)?,
|
||||
),
|
||||
};
|
||||
let parent_dir = if args.is_build {
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
git::pull_or_clone(
|
||||
args,
|
||||
&periphery_config().repo_dir,
|
||||
&parent_dir,
|
||||
token,
|
||||
&environment,
|
||||
&env_file_path,
|
||||
@@ -236,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"),
|
||||
@@ -254,16 +269,25 @@ impl Resolve<super::Args> for RenameRepo {
|
||||
impl Resolve<super::Args> for DeleteRepo {
|
||||
#[instrument(name = "DeleteRepo")]
|
||||
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
|
||||
let DeleteRepo { name } = self;
|
||||
let DeleteRepo { name, is_build } = self;
|
||||
// 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 deleted =
|
||||
fs::remove_dir_all(periphery_config().repo_dir.join(&name))
|
||||
.await;
|
||||
let msg = match deleted {
|
||||
Ok(_) => format!("Deleted Repo {name}"),
|
||||
Err(_) => format!("No Repo at {name} to delete"),
|
||||
let root = if is_build {
|
||||
periphery_config().build_dir()
|
||||
} else {
|
||||
periphery_config().repo_dir()
|
||||
};
|
||||
Ok(Log::simple("Delete Repo on Host", msg))
|
||||
let full_path = root.join(&name);
|
||||
let deleted =
|
||||
fs::remove_dir_all(&full_path).await.with_context(|| {
|
||||
format!("Failed to delete repo at {full_path:?}")
|
||||
});
|
||||
let log = match deleted {
|
||||
Ok(_) => {
|
||||
Log::simple("Delete repo", format!("Deleted Repo {name}"))
|
||||
}
|
||||
Err(e) => Log::error("Delete repo", format_serror(&e.into())),
|
||||
};
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ pub enum PeripheryRequest {
|
||||
DeleteRepo(DeleteRepo),
|
||||
|
||||
// Build
|
||||
GetDockerfileContentsOnHost(GetDockerfileContentsOnHost),
|
||||
WriteDockerfileContentsToHost(WriteDockerfileContentsToHost),
|
||||
Build(Build),
|
||||
PruneBuilders(PruneBuilders),
|
||||
PruneBuildx(PruneBuildx),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -35,8 +35,13 @@ pub fn periphery_config() -> &'static PeripheryConfig {
|
||||
|
||||
PeripheryConfig {
|
||||
port: env.periphery_port.unwrap_or(config.port),
|
||||
repo_dir: env.periphery_repo_dir.unwrap_or(config.repo_dir),
|
||||
stack_dir: env.periphery_stack_dir.unwrap_or(config.stack_dir),
|
||||
bind_ip: env.periphery_bind_ip.unwrap_or(config.bind_ip),
|
||||
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),
|
||||
@@ -52,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),
|
||||
@@ -78,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,
|
||||
|
||||
@@ -945,7 +945,7 @@ pub async fn docker_login(
|
||||
None => crate::helpers::registry_token(domain, account)?,
|
||||
};
|
||||
let log = async_run_command(&format!(
|
||||
"echo {registry_token} | docker login {domain} --username {account} --password-stdin",
|
||||
"echo {registry_token} | docker login {domain} --username '{account}' --password-stdin",
|
||||
))
|
||||
.await;
|
||||
if log.success() {
|
||||
|
||||
@@ -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,9 +26,14 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
stats::spawn_system_stats_polling_thread();
|
||||
|
||||
let socket_addr =
|
||||
SocketAddr::from_str(&format!("0.0.0.0:{}", config.port))
|
||||
.context("failed to parse socket addr")?;
|
||||
let addr = format!(
|
||||
"{}:{}",
|
||||
config::periphery_config().bind_ip,
|
||||
config::periphery_config().port
|
||||
);
|
||||
|
||||
let socket_addr = SocketAddr::from_str(&addr)
|
||||
.context("failed to parse listen address")?;
|
||||
|
||||
let app = router::router()
|
||||
.into_make_service_with_connect_info::<SocketAddr>();
|
||||
@@ -41,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\""
|
||||
|
||||
@@ -194,7 +194,7 @@ fn get_system_information(
|
||||
os: System::long_os_version(),
|
||||
kernel: System::kernel_version(),
|
||||
host_name: System::host_name(),
|
||||
core_count: sys.physical_core_count().map(|c| c as u32),
|
||||
core_count: System::physical_core_count().map(|c| c as u32),
|
||||
cpu_brand: sys
|
||||
.cpus()
|
||||
.iter()
|
||||
|
||||
@@ -294,6 +294,35 @@ pub type ListAllDockerContainersResponse = Vec<ContainerListItem>;
|
||||
|
||||
//
|
||||
|
||||
/// Gets a summary of data relating to all containers.
|
||||
/// Response: [GetDockerContainersSummaryResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(GetDockerContainersSummaryResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct GetDockerContainersSummary {}
|
||||
|
||||
/// Response for [GetDockerContainersSummary]
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct GetDockerContainersSummaryResponse {
|
||||
/// The total number of Containers
|
||||
pub total: u32,
|
||||
/// The number of Containers with Running state
|
||||
pub running: u32,
|
||||
/// The number of Containers with Stopped or Paused or Created state
|
||||
pub stopped: u32,
|
||||
/// The number of Containers with Restarting or Dead state
|
||||
pub unhealthy: u32,
|
||||
/// The number of Containers with Unknown state
|
||||
pub unknown: u32,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Inspect a docker container on the server. Response: [Container].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
|
||||
@@ -98,7 +98,6 @@ pub struct UpdateAction {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameAction {
|
||||
/// The id or name of the Action to rename.
|
||||
pub id: String,
|
||||
|
||||
@@ -95,7 +95,6 @@ pub struct UpdateAlerter {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameAlerter {
|
||||
/// The id or name of the Alerter to rename.
|
||||
pub id: String,
|
||||
|
||||
@@ -98,7 +98,6 @@ pub struct UpdateBuild {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameBuild {
|
||||
/// The id or name of the Build to rename.
|
||||
pub id: String,
|
||||
@@ -108,6 +107,24 @@ pub struct RenameBuild {
|
||||
|
||||
//
|
||||
|
||||
/// Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
pub struct WriteBuildFileContents {
|
||||
/// The name or id of the target Build.
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub build: String,
|
||||
/// The dockerfile contents to write.
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Trigger a refresh of the cached latest hash and message.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
|
||||
@@ -97,7 +97,6 @@ pub struct UpdateBuilder {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameBuilder {
|
||||
/// The id or name of the Builder to rename.
|
||||
pub id: String,
|
||||
|
||||
@@ -122,7 +122,6 @@ pub struct UpdateDeployment {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameDeployment {
|
||||
/// The id of the deployment to rename.
|
||||
pub id: String,
|
||||
|
||||
@@ -109,7 +109,6 @@ pub type UpdateProcedureResponse = Procedure;
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameProcedure {
|
||||
/// The id or name of the Procedure to rename.
|
||||
pub id: String,
|
||||
|
||||
@@ -101,7 +101,6 @@ pub struct UpdateRepo {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameRepo {
|
||||
/// The id or name of the Repo to rename.
|
||||
pub id: String,
|
||||
|
||||
@@ -79,7 +79,6 @@ pub struct UpdateServer {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameServer {
|
||||
/// The id or name of the Server to rename.
|
||||
pub id: String,
|
||||
@@ -100,7 +99,6 @@ pub struct RenameServer {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct CreateNetwork {
|
||||
/// Server Id or name
|
||||
pub server: String,
|
||||
|
||||
@@ -97,7 +97,6 @@ pub struct UpdateServerTemplate {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameServerTemplate {
|
||||
/// The id or name of the ServerTemplate to rename.
|
||||
pub id: String,
|
||||
|
||||
@@ -100,7 +100,6 @@ pub struct UpdateStack {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameStack {
|
||||
/// The id of the stack to rename.
|
||||
pub id: String,
|
||||
@@ -118,7 +117,6 @@ pub struct RenameStack {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct WriteStackFileContents {
|
||||
/// The name or id of the target Stack.
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
|
||||
@@ -99,7 +99,6 @@ pub struct UpdateResourceSync {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct RenameResourceSync {
|
||||
/// The id or name of the ResourceSync to rename.
|
||||
pub id: String,
|
||||
@@ -133,7 +132,6 @@ pub struct RefreshResourceSyncPending {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct WriteSyncFileContents {
|
||||
/// The name or id of the target Sync.
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
@@ -166,7 +164,6 @@ pub struct WriteSyncFileContents {
|
||||
#[empty_traits(KomodoWriteRequest)]
|
||||
#[response(Update)]
|
||||
#[error(serror::Error)]
|
||||
#[error(serror::Error)]
|
||||
pub struct CommitSync {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
|
||||
@@ -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,14 +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.
|
||||
#[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")]
|
||||
@@ -82,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 {
|
||||
@@ -97,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 {
|
||||
|
||||
@@ -111,6 +111,12 @@ pub enum AlerterEndpoint {
|
||||
|
||||
/// Send alert to a Discord app
|
||||
Discord(DiscordAlerterEndpoint),
|
||||
|
||||
/// Send alert to Ntfy
|
||||
Ntfy(NtfyAlerterEndpoint),
|
||||
|
||||
/// Send alert to Pushover
|
||||
Pushover(PushoverAlerterEndpoint),
|
||||
}
|
||||
|
||||
impl Default for AlerterEndpoint {
|
||||
@@ -195,8 +201,57 @@ fn default_discord_url() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
// QUERY
|
||||
/// Configuration for a Ntfy alerter.
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Serialize, Deserialize, Builder,
|
||||
)]
|
||||
pub struct NtfyAlerterEndpoint {
|
||||
/// The ntfy topic URL
|
||||
#[serde(default = "default_ntfy_url")]
|
||||
#[builder(default = "default_ntfy_url()")]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl Default for NtfyAlerterEndpoint {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: default_ntfy_url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
|
||||
|
||||
@@ -32,26 +32,31 @@ pub type BuildListItem = ResourceListItem<BuildListItemInfo>;
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuildListItemInfo {
|
||||
/// State of the build. Reflects whether most recent build successful.
|
||||
pub state: BuildState,
|
||||
/// Unix timestamp in milliseconds of last build
|
||||
pub last_built_at: I64,
|
||||
/// The current version of the build
|
||||
pub version: Version,
|
||||
/// The builder attached to build.
|
||||
pub builder_id: String,
|
||||
|
||||
/// Whether build is in files on host mode.
|
||||
pub files_on_host: bool,
|
||||
|
||||
/// The git provider domain
|
||||
pub git_provider: String,
|
||||
/// The image registry domain
|
||||
pub image_registry_domain: String,
|
||||
pub git_provider: Option<String>,
|
||||
/// The repo used as the source of the build
|
||||
pub repo: String,
|
||||
pub repo: Option<String>,
|
||||
/// The branch of the repo
|
||||
pub branch: String,
|
||||
/// State of the build. Reflects whether most recent build successful.
|
||||
pub state: BuildState,
|
||||
pub branch: Option<String>,
|
||||
/// Latest built short commit hash, or null.
|
||||
pub built_hash: Option<String>,
|
||||
/// Latest short commit hash, or null. Only for repo based stacks
|
||||
pub latest_hash: Option<String>,
|
||||
|
||||
/// The image registry domain
|
||||
pub image_registry_domain: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -73,11 +78,26 @@ pub enum BuildState {
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct BuildInfo {
|
||||
/// The timestamp build was last built.
|
||||
pub last_built_at: I64,
|
||||
|
||||
/// Latest built short commit hash, or null.
|
||||
pub built_hash: Option<String>,
|
||||
/// Latest built commit message, or null. Only for repo based stacks
|
||||
pub built_message: Option<String>,
|
||||
/// The last built dockerfile contents.
|
||||
/// This is updated whenever Komodo successfully runs the build.
|
||||
pub built_contents: Option<String>,
|
||||
|
||||
/// The absolute path to the file
|
||||
pub remote_path: Option<String>,
|
||||
/// The remote dockerfile contents, whether on host or in repo.
|
||||
/// This is updated whenever Komodo refreshes the build cache.
|
||||
/// It will be empty if the dockerfile is defined directly in the build config.
|
||||
pub remote_contents: Option<String>,
|
||||
/// If there was an error in getting the remote contents, it will be here.
|
||||
pub remote_error: Option<String>,
|
||||
|
||||
/// Latest remote short commit hash, or null.
|
||||
pub latest_hash: Option<String>,
|
||||
/// Latest remote commit message, or null
|
||||
@@ -192,10 +212,13 @@ pub struct BuildConfig {
|
||||
#[builder(default)]
|
||||
pub webhook_secret: String,
|
||||
|
||||
/// The optional command run after repo clone and before docker build.
|
||||
/// If this is checked, the build will source the files on the host.
|
||||
/// Use `build_path` and `dockerfile_path` to specify the path on the host.
|
||||
/// This is useful for those who wish to setup their files on the host,
|
||||
/// rather than defining the contents in UI or in a git repo.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub pre_build: SystemCommand,
|
||||
pub files_on_host: bool,
|
||||
|
||||
/// The path of the docker build context relative to the root of the repo.
|
||||
/// Default: "." (the root of the repo).
|
||||
@@ -238,6 +261,17 @@ pub struct BuildConfig {
|
||||
#[builder(default)]
|
||||
pub extra_args: Vec<String>,
|
||||
|
||||
/// The optional command run after repo clone and before docker build.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub pre_build: SystemCommand,
|
||||
|
||||
/// UI defined dockerfile contents.
|
||||
/// Supports variable / secret interpolation.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub dockerfile: String,
|
||||
|
||||
/// Docker build arguments.
|
||||
///
|
||||
/// These values are visible in the final image by running `docker inspect`.
|
||||
@@ -338,6 +372,8 @@ impl Default for BuildConfig {
|
||||
image_registry: Default::default(),
|
||||
webhook_enabled: default_webhook_enabled(),
|
||||
webhook_secret: Default::default(),
|
||||
dockerfile: Default::default(),
|
||||
files_on_host: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ pub struct Env {
|
||||
pub komodo_host: Option<String>,
|
||||
/// Override `port`
|
||||
pub komodo_port: Option<u16>,
|
||||
/// Override `bind_ip`
|
||||
pub komodo_bind_ip: Option<String>,
|
||||
/// Override `passkey`
|
||||
pub komodo_passkey: Option<String>,
|
||||
/// Override `passkey` with file
|
||||
@@ -83,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`
|
||||
@@ -256,6 +260,11 @@ pub struct CoreConfig {
|
||||
#[serde(default = "default_core_port")]
|
||||
pub port: u16,
|
||||
|
||||
/// IP address the core server binds to.
|
||||
/// Default: [::].
|
||||
#[serde(default = "default_core_bind_ip")]
|
||||
pub bind_ip: String,
|
||||
|
||||
/// Sent in auth header with req to periphery.
|
||||
/// Should be some secure hash, maybe 20-40 chars.
|
||||
pub passkey: String,
|
||||
@@ -521,6 +530,10 @@ fn default_core_port() -> u16 {
|
||||
9120
|
||||
}
|
||||
|
||||
fn default_core_bind_ip() -> String {
|
||||
"[::]".to_string()
|
||||
}
|
||||
|
||||
fn default_frontend_path() -> String {
|
||||
"/app/frontend".to_string()
|
||||
}
|
||||
@@ -571,6 +584,7 @@ impl CoreConfig {
|
||||
title: config.title,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
bind_ip: config.bind_ip,
|
||||
passkey: empty_or_redacted(&config.passkey),
|
||||
first_server: config.first_server,
|
||||
frontend_path: config.frontend_path,
|
||||
|
||||
@@ -114,10 +114,16 @@ pub struct Env {
|
||||
|
||||
/// Override `port`
|
||||
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`
|
||||
pub periphery_stack_dir: Option<PathBuf>,
|
||||
/// Override `build_dir`
|
||||
pub periphery_build_dir: Option<PathBuf>,
|
||||
/// Override `stats_polling_rate`
|
||||
pub periphery_stats_polling_rate: Option<Timelength>,
|
||||
/// Override `legacy_compose_cli`
|
||||
@@ -128,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`
|
||||
@@ -162,15 +170,38 @@ pub struct PeripheryConfig {
|
||||
#[serde(default = "default_periphery_port")]
|
||||
pub port: u16,
|
||||
|
||||
/// IP address the periphery server binds to.
|
||||
/// Default: [::].
|
||||
#[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.
|
||||
/// 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`
|
||||
@@ -230,26 +261,24 @@ 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 {
|
||||
8120
|
||||
}
|
||||
|
||||
fn default_repo_dir() -> PathBuf {
|
||||
"/etc/komodo/repos".parse().unwrap()
|
||||
fn default_periphery_bind_ip() -> String {
|
||||
"[::]".to_string()
|
||||
}
|
||||
|
||||
fn default_stack_dir() -> PathBuf {
|
||||
"/etc/komodo/stacks".parse().unwrap()
|
||||
fn default_root_directory() -> PathBuf {
|
||||
"/etc/komodo".parse().unwrap()
|
||||
}
|
||||
|
||||
fn default_stats_polling_rate() -> Timelength {
|
||||
@@ -260,20 +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(),
|
||||
repo_dir: default_repo_dir(),
|
||||
stack_dir: default_stack_dir(),
|
||||
bind_ip: default_periphery_bind_ip(),
|
||||
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(),
|
||||
@@ -285,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,8 +319,11 @@ impl PeripheryConfig {
|
||||
pub fn sanitized(&self) -> 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(),
|
||||
stats_polling_rate: self.stats_polling_rate,
|
||||
legacy_compose_cli: self.legacy_compose_cli,
|
||||
logging: self.logging.clone(),
|
||||
@@ -352,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(
|
||||
),
|
||||
|
||||
@@ -118,11 +118,12 @@ pub fn all_logs_success(logs: &[update::Log]) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn optional_string(string: &str) -> Option<String> {
|
||||
pub fn optional_string(string: impl Into<String>) -> Option<String> {
|
||||
let string = string.into();
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(string.to_string())
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +395,8 @@ pub struct CloneArgs {
|
||||
pub branch: String,
|
||||
/// Specific commit hash. Optional
|
||||
pub commit: Option<String>,
|
||||
/// Use PERIPHERY_BUILD_DIR as the parent folder for the clone.
|
||||
pub is_build: bool,
|
||||
/// The clone destination path
|
||||
pub destination: Option<String>,
|
||||
/// Command to run after the repo has been cloned
|
||||
@@ -459,8 +462,9 @@ impl From<&self::build::Build> for CloneArgs {
|
||||
branch: optional_string(&build.config.branch)
|
||||
.unwrap_or_else(|| String::from("main")),
|
||||
commit: optional_string(&build.config.commit),
|
||||
is_build: true,
|
||||
destination: None,
|
||||
on_clone: build.config.pre_build.clone().into_option(),
|
||||
on_clone: None,
|
||||
on_pull: None,
|
||||
https: build.config.git_https,
|
||||
account: optional_string(&build.config.git_account),
|
||||
@@ -478,6 +482,7 @@ impl From<&self::repo::Repo> for CloneArgs {
|
||||
branch: optional_string(&repo.config.branch)
|
||||
.unwrap_or_else(|| String::from("main")),
|
||||
commit: optional_string(&repo.config.commit),
|
||||
is_build: false,
|
||||
destination: optional_string(&repo.config.path),
|
||||
on_clone: repo.config.on_clone.clone().into_option(),
|
||||
on_pull: repo.config.on_pull.clone().into_option(),
|
||||
@@ -497,6 +502,7 @@ impl From<&self::sync::ResourceSync> for CloneArgs {
|
||||
branch: optional_string(&sync.config.branch)
|
||||
.unwrap_or_else(|| String::from("main")),
|
||||
commit: optional_string(&sync.config.commit),
|
||||
is_build: false,
|
||||
destination: None,
|
||||
on_clone: None,
|
||||
on_pull: None,
|
||||
@@ -516,6 +522,7 @@ impl From<&self::stack::Stack> for CloneArgs {
|
||||
branch: optional_string(&stack.config.branch)
|
||||
.unwrap_or_else(|| String::from("main")),
|
||||
commit: optional_string(&stack.config.commit),
|
||||
is_build: false,
|
||||
destination: None,
|
||||
on_clone: None,
|
||||
on_pull: None,
|
||||
@@ -713,6 +720,7 @@ pub enum Operation {
|
||||
DeleteBuild,
|
||||
RunBuild,
|
||||
CancelBuild,
|
||||
WriteDockerfile,
|
||||
|
||||
// repo
|
||||
CreateRepo,
|
||||
@@ -991,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(),
|
||||
}
|
||||
|
||||
@@ -62,6 +62,21 @@ pub struct Resource<Config: Default, Info: Default = ()> {
|
||||
pub base_permission: PermissionLevel,
|
||||
}
|
||||
|
||||
impl<C: Default, I: Default> Default for Resource<C, I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
name: String::from("temp-resource"),
|
||||
description: String::new(),
|
||||
updated_at: 0,
|
||||
tags: Vec::new(),
|
||||
info: I::default(),
|
||||
config: C::default(),
|
||||
base_permission: PermissionLevel::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResourceListItem<Info> {
|
||||
|
||||
@@ -77,6 +77,13 @@ pub struct ServerConfig {
|
||||
#[partial_default(default_timeout_seconds())]
|
||||
pub timeout_seconds: I64,
|
||||
|
||||
/// An optional override passkey to use
|
||||
/// to authenticate with periphery agent.
|
||||
/// If this is empty, will use passkey in core config.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub passkey: String,
|
||||
|
||||
/// Sometimes the system stats reports a mount path that is not desired.
|
||||
/// Use this field to filter it out from the report.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
@@ -240,6 +247,7 @@ impl Default for ServerConfig {
|
||||
send_mem_alerts: default_send_alerts(),
|
||||
send_disk_alerts: default_send_alerts(),
|
||||
region: Default::default(),
|
||||
passkey: Default::default(),
|
||||
cpu_warning: default_cpu_warning(),
|
||||
cpu_critical: default_cpu_critical(),
|
||||
mem_warning: default_mem_warning(),
|
||||
|
||||
@@ -269,47 +269,6 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
/// If this is checked, the stack will source the files on the host.
|
||||
/// Use `run_directory` and `file_paths` to specify the path on the host.
|
||||
/// This is useful for those who wish to setup their files on the host using SSH or similar,
|
||||
/// rather than defining the contents in UI or in a git repo.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub files_on_host: bool,
|
||||
|
||||
/// Directory to change to (`cd`) before running `docker compose up -d`.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub run_directory: String,
|
||||
|
||||
/// Add paths to compose files, relative to the run path.
|
||||
/// If this is empty, will use file `compose.yaml`.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_string_list_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub file_paths: Vec<String>,
|
||||
|
||||
/// The name of the written environment file before `docker compose up`.
|
||||
/// Relative to the run directory root.
|
||||
/// Default: .env
|
||||
#[serde(default = "default_env_file_path")]
|
||||
#[builder(default = "default_env_file_path()")]
|
||||
#[partial_default(default_env_file_path())]
|
||||
pub env_file_path: String,
|
||||
|
||||
/// Add additional env files to attach with `--env-file`.
|
||||
/// Relative to the run directory root.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_string_list_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub additional_env_files: Vec<String>,
|
||||
|
||||
/// The git provider domain. Default: github.com
|
||||
#[serde(default = "default_git_provider")]
|
||||
#[builder(default = "default_git_provider()")]
|
||||
@@ -373,6 +332,47 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub webhook_force_deploy: bool,
|
||||
|
||||
/// If this is checked, the stack will source the files on the host.
|
||||
/// Use `run_directory` and `file_paths` to specify the path on the host.
|
||||
/// This is useful for those who wish to setup their files on the host,
|
||||
/// rather than defining the contents in UI or in a git repo.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub files_on_host: bool,
|
||||
|
||||
/// Directory to change to (`cd`) before running `docker compose up -d`.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub run_directory: String,
|
||||
|
||||
/// Add paths to compose files, relative to the run path.
|
||||
/// If this is empty, will use file `compose.yaml`.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_string_list_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub file_paths: Vec<String>,
|
||||
|
||||
/// The name of the written environment file before `docker compose up`.
|
||||
/// Relative to the run directory root.
|
||||
/// Default: .env
|
||||
#[serde(default = "default_env_file_path")]
|
||||
#[builder(default = "default_env_file_path()")]
|
||||
#[partial_default(default_env_file_path())]
|
||||
pub env_file_path: String,
|
||||
|
||||
/// Add additional env files to attach with `--env-file`.
|
||||
/// Relative to the run directory root.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_string_list_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub additional_env_files: Vec<String>,
|
||||
|
||||
/// Whether to send StackStateChange alerts for this stack.
|
||||
#[serde(default = "default_send_alerts")]
|
||||
#[builder(default = "default_send_alerts()")]
|
||||
@@ -434,6 +434,7 @@ pub struct StackConfig {
|
||||
/// The contents of the file directly, for management in the UI.
|
||||
/// If this is empty, it will fall back to checking git config for
|
||||
/// repo based compose file.
|
||||
/// Supports variable / secret interpolation.
|
||||
#[serde(default, deserialize_with = "file_contents_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { KomodoClient, Types } from "komodo_client";
|
||||
const komodo = KomodoClient("https://demo.komo.do", {
|
||||
type: "api-key",
|
||||
params: {
|
||||
api_key: "your_key",
|
||||
key: "your_key",
|
||||
secret: "your secret",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "1.17.0",
|
||||
"version": "1.17.2",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -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 ({
|
||||
@@ -220,6 +259,9 @@ export function KomodoClient(url: string, options: InitOptions) {
|
||||
// Sleep for a bit before checking for websocket closed
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
// Sleep for a bit before retrying connection to avoid spam.
|
||||
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Sleep for a bit before retrying, maybe Komodo Core is down temporarily.
|
||||
@@ -287,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,
|
||||
/**
|
||||
|
||||
@@ -61,6 +61,7 @@ export type ReadResponses = {
|
||||
GetServer: Types.GetServerResponse;
|
||||
GetServerState: Types.GetServerStateResponse;
|
||||
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
|
||||
GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;
|
||||
ListDockerContainers: Types.ListDockerContainersResponse;
|
||||
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
|
||||
InspectDockerContainer: Types.InspectDockerContainerResponse;
|
||||
@@ -227,6 +228,7 @@ export type WriteResponses = {
|
||||
DeleteBuild: Types.Build;
|
||||
UpdateBuild: Types.Build;
|
||||
RenameBuild: Types.Update;
|
||||
WriteBuildFileContents: Types.Update;
|
||||
RefreshBuildCache: Types.NoData;
|
||||
CreateBuildWebhook: Types.CreateBuildWebhookResponse;
|
||||
DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;
|
||||
|
||||
+259
-42
@@ -51,9 +51,47 @@ export interface Resource<Config, Info> {
|
||||
base_permission?: PermissionLevel;
|
||||
}
|
||||
|
||||
export enum ScheduleFormat {
|
||||
English = "English",
|
||||
Cron = "Cron",
|
||||
}
|
||||
|
||||
export interface ActionConfig {
|
||||
/** Typescript file contents using pre-initialized `komodo` client. */
|
||||
file_contents?: string;
|
||||
/** 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 action fails. */
|
||||
failure_alert: boolean;
|
||||
/** Whether incoming webhooks actually trigger action. */
|
||||
webhook_enabled: boolean;
|
||||
/**
|
||||
@@ -61,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 {
|
||||
@@ -99,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>;
|
||||
@@ -130,7 +183,11 @@ export type AlerterEndpoint =
|
||||
/** Send alert to a Slack app */
|
||||
| { type: "Slack", params: SlackAlerterEndpoint }
|
||||
/** Send alert to a Discord app */
|
||||
| { type: "Discord", params: DiscordAlerterEndpoint };
|
||||
| { type: "Discord", params: DiscordAlerterEndpoint }
|
||||
/** Send alert to Ntfy */
|
||||
| { 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 =
|
||||
@@ -210,11 +267,6 @@ export interface Version {
|
||||
patch: number;
|
||||
}
|
||||
|
||||
export interface SystemCommand {
|
||||
path?: string;
|
||||
command?: string;
|
||||
}
|
||||
|
||||
/** Configuration for an image registry */
|
||||
export interface ImageRegistryConfig {
|
||||
/**
|
||||
@@ -231,6 +283,11 @@ export interface ImageRegistryConfig {
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
export interface SystemCommand {
|
||||
path?: string;
|
||||
command?: string;
|
||||
}
|
||||
|
||||
/** The build configuration. */
|
||||
export interface BuildConfig {
|
||||
/** Which builder is used to build the image. */
|
||||
@@ -292,8 +349,13 @@ export interface BuildConfig {
|
||||
* If its an empty string, use the default secret from the config.
|
||||
*/
|
||||
webhook_secret?: string;
|
||||
/** The optional command run after repo clone and before docker build. */
|
||||
pre_build?: SystemCommand;
|
||||
/**
|
||||
* If this is checked, the build will source the files on the host.
|
||||
* Use `build_path` and `dockerfile_path` to specify the path on the host.
|
||||
* This is useful for those who wish to setup their files on the host,
|
||||
* rather than defining the contents in UI or in a git repo.
|
||||
*/
|
||||
files_on_host?: boolean;
|
||||
/**
|
||||
* The path of the docker build context relative to the root of the repo.
|
||||
* Default: "." (the root of the repo).
|
||||
@@ -309,6 +371,13 @@ export interface BuildConfig {
|
||||
use_buildx?: boolean;
|
||||
/** Any extra docker cli arguments to be included in the build command */
|
||||
extra_args?: string[];
|
||||
/** The optional command run after repo clone and before docker build. */
|
||||
pre_build?: SystemCommand;
|
||||
/**
|
||||
* UI defined dockerfile contents.
|
||||
* Supports variable / secret interpolation.
|
||||
*/
|
||||
dockerfile?: string;
|
||||
/**
|
||||
* Docker build arguments.
|
||||
*
|
||||
@@ -333,11 +402,27 @@ export interface BuildConfig {
|
||||
}
|
||||
|
||||
export interface BuildInfo {
|
||||
/** The timestamp build was last built. */
|
||||
last_built_at: I64;
|
||||
/** Latest built short commit hash, or null. */
|
||||
built_hash?: string;
|
||||
/** Latest built commit message, or null. Only for repo based stacks */
|
||||
built_message?: string;
|
||||
/**
|
||||
* The last built dockerfile contents.
|
||||
* This is updated whenever Komodo successfully runs the build.
|
||||
*/
|
||||
built_contents?: string;
|
||||
/** The absolute path to the file */
|
||||
remote_path?: string;
|
||||
/**
|
||||
* The remote dockerfile contents, whether on host or in repo.
|
||||
* This is updated whenever Komodo refreshes the build cache.
|
||||
* It will be empty if the dockerfile is defined directly in the build config.
|
||||
*/
|
||||
remote_contents?: string;
|
||||
/** If there was an error in getting the remote contents, it will be here. */
|
||||
remote_error?: string;
|
||||
/** Latest remote short commit hash, or null. */
|
||||
latest_hash?: string;
|
||||
/** Latest remote commit message, or null */
|
||||
@@ -358,26 +443,28 @@ export enum BuildState {
|
||||
}
|
||||
|
||||
export interface BuildListItemInfo {
|
||||
/** State of the build. Reflects whether most recent build successful. */
|
||||
state: BuildState;
|
||||
/** Unix timestamp in milliseconds of last build */
|
||||
last_built_at: I64;
|
||||
/** The current version of the build */
|
||||
version: Version;
|
||||
/** The builder attached to build. */
|
||||
builder_id: string;
|
||||
/** Whether build is in files on host mode. */
|
||||
files_on_host: boolean;
|
||||
/** The git provider domain */
|
||||
git_provider: string;
|
||||
/** The image registry domain */
|
||||
image_registry_domain: string;
|
||||
git_provider?: string;
|
||||
/** The repo used as the source of the build */
|
||||
repo: string;
|
||||
repo?: string;
|
||||
/** The branch of the repo */
|
||||
branch: string;
|
||||
/** State of the build. Reflects whether most recent build successful. */
|
||||
state: BuildState;
|
||||
branch?: string;
|
||||
/** Latest built short commit hash, or null. */
|
||||
built_hash?: string;
|
||||
/** Latest short commit hash, or null. Only for repo based stacks */
|
||||
latest_hash?: string;
|
||||
/** The image registry domain */
|
||||
image_registry_domain?: string;
|
||||
}
|
||||
|
||||
export type BuildListItem = ResourceListItem<BuildListItemInfo>;
|
||||
@@ -509,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;
|
||||
/**
|
||||
@@ -1111,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. */
|
||||
@@ -1389,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;
|
||||
}
|
||||
@@ -1520,6 +1670,12 @@ export interface ServerConfig {
|
||||
* default: 2
|
||||
*/
|
||||
timeout_seconds: I64;
|
||||
/**
|
||||
* An optional override passkey to use
|
||||
* to authenticate with periphery agent.
|
||||
* If this is empty, will use passkey in core config.
|
||||
*/
|
||||
passkey?: string;
|
||||
/**
|
||||
* Sometimes the system stats reports a mount path that is not desired.
|
||||
* Use this field to filter it out from the report.
|
||||
@@ -1633,31 +1789,6 @@ export interface StackConfig {
|
||||
destroy_before_deploy?: boolean;
|
||||
/** Whether to skip secret interpolation into the stack environment variables. */
|
||||
skip_secret_interp?: boolean;
|
||||
/**
|
||||
* If this is checked, the stack will source the files on the host.
|
||||
* Use `run_directory` and `file_paths` to specify the path on the host.
|
||||
* This is useful for those who wish to setup their files on the host using SSH or similar,
|
||||
* rather than defining the contents in UI or in a git repo.
|
||||
*/
|
||||
files_on_host?: boolean;
|
||||
/** Directory to change to (`cd`) before running `docker compose up -d`. */
|
||||
run_directory?: string;
|
||||
/**
|
||||
* Add paths to compose files, relative to the run path.
|
||||
* If this is empty, will use file `compose.yaml`.
|
||||
*/
|
||||
file_paths?: string[];
|
||||
/**
|
||||
* The name of the written environment file before `docker compose up`.
|
||||
* Relative to the run directory root.
|
||||
* Default: .env
|
||||
*/
|
||||
env_file_path: string;
|
||||
/**
|
||||
* Add additional env files to attach with `--env-file`.
|
||||
* Relative to the run directory root.
|
||||
*/
|
||||
additional_env_files?: string[];
|
||||
/** The git provider domain. Default: github.com */
|
||||
git_provider: string;
|
||||
/**
|
||||
@@ -1697,6 +1828,31 @@ export interface StackConfig {
|
||||
* If this option is enabled, will always run `DeployStack` without diffing.
|
||||
*/
|
||||
webhook_force_deploy?: boolean;
|
||||
/**
|
||||
* If this is checked, the stack will source the files on the host.
|
||||
* Use `run_directory` and `file_paths` to specify the path on the host.
|
||||
* This is useful for those who wish to setup their files on the host,
|
||||
* rather than defining the contents in UI or in a git repo.
|
||||
*/
|
||||
files_on_host?: boolean;
|
||||
/** Directory to change to (`cd`) before running `docker compose up -d`. */
|
||||
run_directory?: string;
|
||||
/**
|
||||
* Add paths to compose files, relative to the run path.
|
||||
* If this is empty, will use file `compose.yaml`.
|
||||
*/
|
||||
file_paths?: string[];
|
||||
/**
|
||||
* The name of the written environment file before `docker compose up`.
|
||||
* Relative to the run directory root.
|
||||
* Default: .env
|
||||
*/
|
||||
env_file_path: string;
|
||||
/**
|
||||
* Add additional env files to attach with `--env-file`.
|
||||
* Relative to the run directory root.
|
||||
*/
|
||||
additional_env_files?: string[];
|
||||
/** Whether to send StackStateChange alerts for this stack. */
|
||||
send_alerts: boolean;
|
||||
/** Used with `registry_account` to login to a registry before docker compose up. */
|
||||
@@ -1728,6 +1884,7 @@ export interface StackConfig {
|
||||
* The contents of the file directly, for management in the UI.
|
||||
* If this is empty, it will fall back to checking git config for
|
||||
* repo based compose file.
|
||||
* Supports variable / secret interpolation.
|
||||
*/
|
||||
file_contents?: string;
|
||||
/**
|
||||
@@ -2053,6 +2210,7 @@ export enum Operation {
|
||||
DeleteBuild = "DeleteBuild",
|
||||
RunBuild = "RunBuild",
|
||||
CancelBuild = "CancelBuild",
|
||||
WriteDockerfile = "WriteDockerfile",
|
||||
CreateRepo = "CreateRepo",
|
||||
UpdateRepo = "UpdateRepo",
|
||||
RenameRepo = "RenameRepo",
|
||||
@@ -2155,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;
|
||||
@@ -3243,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>;
|
||||
@@ -4007,6 +4179,8 @@ export interface CloneArgs {
|
||||
branch: string;
|
||||
/** Specific commit hash. Optional */
|
||||
commit?: string;
|
||||
/** Use PERIPHERY_BUILD_DIR as the parent folder for the clone. */
|
||||
is_build: boolean;
|
||||
/** The clone destination path */
|
||||
destination?: string;
|
||||
/** Command to run after the repo has been cloned */
|
||||
@@ -5169,6 +5343,27 @@ export interface GetDeploymentsSummaryResponse {
|
||||
unknown: I64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a summary of data relating to all containers.
|
||||
* Response: [GetDockerContainersSummaryResponse].
|
||||
*/
|
||||
export interface GetDockerContainersSummary {
|
||||
}
|
||||
|
||||
/** Response for [GetDockerContainersSummary] */
|
||||
export interface GetDockerContainersSummaryResponse {
|
||||
/** The total number of Containers */
|
||||
total: number;
|
||||
/** The number of Containers with Running state */
|
||||
running: number;
|
||||
/** The number of Containers with Stopped or Paused or Created state */
|
||||
stopped: number;
|
||||
/** The number of Containers with Restarting or Dead state */
|
||||
unhealthy: number;
|
||||
/** The number of Containers with Unknown state */
|
||||
unknown: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific docker registry account.
|
||||
* Response: [GetDockerRegistryAccountResponse].
|
||||
@@ -6354,6 +6549,12 @@ export interface NameAndId {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Configuration for a Ntfy alerter. */
|
||||
export interface NtfyAlerterEndpoint {
|
||||
/** The ntfy topic URL */
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** Pauses all containers on the target server. Response: [Update] */
|
||||
export interface PauseAllContainers {
|
||||
/** Name or id */
|
||||
@@ -6537,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 */
|
||||
@@ -7498,6 +7705,14 @@ export interface UrlBuilderConfig {
|
||||
passkey?: string;
|
||||
}
|
||||
|
||||
/** Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update]. */
|
||||
export interface WriteBuildFileContents {
|
||||
/** The name or id of the target Build. */
|
||||
build: string;
|
||||
/** The dockerfile contents to write. */
|
||||
contents: string;
|
||||
}
|
||||
|
||||
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
|
||||
export interface WriteStackFileContents {
|
||||
/** The name or id of the target Stack. */
|
||||
@@ -7648,6 +7863,7 @@ export type ReadRequest =
|
||||
| { type: "InspectDockerImage", params: InspectDockerImage }
|
||||
| { type: "ListDockerImageHistory", params: ListDockerImageHistory }
|
||||
| { type: "InspectDockerVolume", params: InspectDockerVolume }
|
||||
| { type: "GetDockerContainersSummary", params: GetDockerContainersSummary }
|
||||
| { type: "ListAllDockerContainers", params: ListAllDockerContainers }
|
||||
| { type: "ListDockerContainers", params: ListDockerContainers }
|
||||
| { type: "ListDockerNetworks", params: ListDockerNetworks }
|
||||
@@ -7763,6 +7979,7 @@ export type WriteRequest =
|
||||
| { type: "DeleteBuild", params: DeleteBuild }
|
||||
| { type: "UpdateBuild", params: UpdateBuild }
|
||||
| { type: "RenameBuild", params: RenameBuild }
|
||||
| { type: "WriteBuildFileContents", params: WriteBuildFileContents }
|
||||
| { type: "RefreshBuildCache", params: RefreshBuildCache }
|
||||
| { type: "CreateBuildWebhook", params: CreateBuildWebhook }
|
||||
| { type: "DeleteBuildWebhook", params: DeleteBuildWebhook }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use komodo_client::entities::update::Log;
|
||||
use komodo_client::entities::{FileContents, update::Log};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -21,6 +21,42 @@ pub type BuildResponse = Vec<Log>;
|
||||
|
||||
//
|
||||
|
||||
/// Get the dockerfile contents on the host, for builds using
|
||||
/// `files_on_host`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(GetDockerfileContentsOnHostResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct GetDockerfileContentsOnHost {
|
||||
/// The name of the build
|
||||
pub name: String,
|
||||
/// The build path for the build.
|
||||
pub build_path: String,
|
||||
/// The dockerfile path for the build, relative to the build_path
|
||||
pub dockerfile_path: String,
|
||||
}
|
||||
|
||||
pub type GetDockerfileContentsOnHostResponse = FileContents;
|
||||
|
||||
//
|
||||
|
||||
/// Write the dockerfile contents to the file on the host, for build using
|
||||
/// `files_on_host`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(serror::Error)]
|
||||
pub struct WriteDockerfileContentsToHost {
|
||||
/// The name of the build
|
||||
pub name: String,
|
||||
/// The build path for the build.
|
||||
pub build_path: String,
|
||||
/// The dockerfile path for the build, relative to the build_path
|
||||
pub dockerfile_path: String,
|
||||
/// The contents to write.
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[response(Log)]
|
||||
#[error(serror::Error)]
|
||||
|
||||
@@ -106,4 +106,6 @@ pub struct RenameRepo {
|
||||
#[error(serror::Error)]
|
||||
pub struct DeleteRepo {
|
||||
pub name: String,
|
||||
/// Clears
|
||||
pub is_build: bool,
|
||||
}
|
||||
|
||||
+5
-1
@@ -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 =#
|
||||
#=-------------------------=#
|
||||
@@ -131,4 +135,4 @@ PERIPHERY_SSL_ENABLED=true
|
||||
## 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
|
||||
|
||||
@@ -64,11 +64,6 @@ services:
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
|
||||
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
|
||||
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
|
||||
volumes:
|
||||
## Mount external docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
@@ -15,10 +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_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
|
||||
@@ -36,4 +33,10 @@ services:
|
||||
## Must be the same inside and outside the container,
|
||||
## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180.
|
||||
## Default: /etc/komodo.
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
|
||||
## If periphery is being run remote from the core server, ports need
|
||||
## to be exposed on the host.
|
||||
# ports:
|
||||
# - 8120:8120
|
||||
## If you want to use a custom periphery config file, use command to pass it to periphery.
|
||||
# command: periphery --config-path ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/periphery.config.toml
|
||||
@@ -75,11 +75,6 @@ services:
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
|
||||
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
|
||||
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
|
||||
volumes:
|
||||
## Mount external docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
@@ -59,11 +59,6 @@ services:
|
||||
logging:
|
||||
driver: ${COMPOSE_LOGGING_DRIVER:-local}
|
||||
env_file: ./compose.env
|
||||
environment:
|
||||
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
|
||||
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
|
||||
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
|
||||
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
|
||||
volumes:
|
||||
## Mount external docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
+13
-1
@@ -33,6 +33,12 @@ host = "https://demo.komo.do"
|
||||
## Default: 9120
|
||||
port = 9120
|
||||
|
||||
## The IP address the core server will bind to.
|
||||
## The default will allow it to accept external IPv4 and IPv6 connections.
|
||||
## Env: KOMODO_BIND_IP
|
||||
## Default: [::]
|
||||
bind_ip = "[::]"
|
||||
|
||||
## This is the token used to authenticate core requests to periphery.
|
||||
## Ensure this matches a passkey in the connected periphery configs.
|
||||
## If the periphery servers don't have passkeys configured, this doesn't need to be changed.
|
||||
@@ -335,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
|
||||
|
||||
@@ -18,17 +18,35 @@
|
||||
## Default: 8120
|
||||
port = 8120
|
||||
|
||||
## The directory periphery will use to manage repos.
|
||||
## The IP address the periphery server will bind to.
|
||||
## The default will allow it to accept external IPv4 and IPv6 connections.
|
||||
## Env: PERIPHERY_BIND_IP
|
||||
## Default: [::]
|
||||
bind_ip = "[::]"
|
||||
|
||||
## 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"
|
||||
|
||||
## 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: ${root_directory}/builds
|
||||
# build_dir = "/etc/komodo/builds"
|
||||
|
||||
## How often Periphery polls the host for system stats,
|
||||
## like CPU / memory usage.
|
||||
@@ -104,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
|
||||
|
||||
@@ -77,7 +77,7 @@ Ensure that the user which periphery is run as has access to the docker group wi
|
||||
```
|
||||
periphery \
|
||||
--config-path /path/to/periphery.config.base.toml \
|
||||
--config-path /other_path/to/overide-periphery-config-directory \
|
||||
--config-path /other_path/to/override-periphery-config-directory \
|
||||
--config-keyword periphery \
|
||||
--config-keyword config \
|
||||
--merge-nested-config true
|
||||
@@ -87,21 +87,26 @@ periphery \
|
||||
You can run `periphery --help` to see the manual.
|
||||
:::
|
||||
|
||||
When running periphery in docker, use [command](https://docs.docker.com/reference/compose-file/services/#command) to pass in additional arguments.
|
||||
```
|
||||
command: periphery --config-path /path/in/container/to/periphery.config.base.toml
|
||||
```
|
||||
|
||||
### Passing config files
|
||||
|
||||
Either file paths or directory paths can be passed to `--config-path`.
|
||||
|
||||
When using directories, the file entries can be filtered by name with the `--config-keyword` argument, which can be passed multiple times to add more keywords. If passed, then only config files with file names that contain all keywords will be merged.
|
||||
|
||||
When passing multiple config files, later --config-path given in the command will always overide previous ones. Directory config files are merged in alphabetical order by name, so `config_b.toml` will overide `config_a.toml`.
|
||||
When passing multiple config files, later --config-path given in the command will always override previous ones. Directory config files are merged in alphabetical order by name, so `config_b.toml` will override `config_a.toml`.
|
||||
|
||||
There are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the overide config. So if you pass `allowed_ips = []` in your overide config, the final allowed_ips will be an empty list as well.
|
||||
There are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the override config. So if you pass `allowed_ips = []` in your override config, the final allowed_ips will be an empty list as well.
|
||||
|
||||
`--merge-nested-config true` will merge config fields recursively and extend config array fields.
|
||||
|
||||
For example, with `--merge-nested-config true` you can specify an allowed ip in the base config, and another in the overide config, they will both be present in the final config.
|
||||
For example, with `--merge-nested-config true` you can specify an allowed ip in the base config, and another in the override config, they will both be present in the final config.
|
||||
|
||||
Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the overide config.
|
||||
Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the override config.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Other Resources
|
||||
|
||||
### Posts and Guides
|
||||
- [Migrating to Komodo](https://blog.foxxmd.dev/posts/migrating-to-komodo) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [FAQ, Tips, and Tricks](https://blog.foxxmd.dev/posts/komodo-tips-tricks) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [Compose Environments Explained](https://blog.foxxmd.dev/posts/compose-envs-explained) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [How To: Automate version updates for your self-hosted Docker containers with Gitea, Renovate, and Komodo](https://nickcunningh.am/blog/how-to-automate-version-updates-for-your-self-hosted-docker-containers-with-gitea-renovate-and-komodo) by [TheNickOfTime](https://github.com/TheNickOfTime)
|
||||
|
||||
### Community Alerters
|
||||
These provide alerting implementations which can be used with the `Custom` Alerter type.
|
||||
- [Discord](https://github.com/FoxxMD/deploy-discord-alerter) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [Telegram](https://github.com/mattsmallman/komodo-alert-to-telgram) by [mattsmallman](https://github.com/mattsmallman)
|
||||
- [Ntfy](https://github.com/FoxxMD/deploy-ntfy-alerter) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [Gotify](https://github.com/FoxxMD/deploy-gotify-alerter) by [FoxxMD](https://github.com/FoxxMD)
|
||||
- [Apprise](https://github.com/FoxxMD/deploy-apprise-alerter) by [FoxxMD](https://github.com/FoxxMD)
|
||||
@@ -1,7 +1,7 @@
|
||||
# Sqlite
|
||||
|
||||
:::info
|
||||
FerritDB + Sqlite authentication support is [still experimental](https://docs.ferretdb.io/security/authentication/#experimental-authentication-mode),
|
||||
FerretDB + Sqlite authentication support is [still experimental](https://docs.ferretdb.io/security/authentication/#experimental-authentication-mode),
|
||||
and is not enabled in this database configuration. Because of this, the values for `DB_USERNAME` and `DB_PASSWORD`
|
||||
are ignored when using Sqlite, and the database port is not exposed publicly by default.
|
||||
:::
|
||||
@@ -22,4 +22,4 @@ docker compose -p komodo -f komodo/sqlite.compose.yaml --env-file komodo/compose
|
||||
import ComposeAndEnv from "@site/src/components/ComposeAndEnv";
|
||||
|
||||
<ComposeAndEnv file_name="sqlite.compose.yaml" />
|
||||
```
|
||||
```
|
||||
|
||||
+2
-1
@@ -65,7 +65,8 @@ const sidebars: SidebarsConfig = {
|
||||
"webhooks",
|
||||
"version-upgrades",
|
||||
"api",
|
||||
"development"
|
||||
"development",
|
||||
"other-resources"
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user