mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-21 22:00:36 -05:00
* add some network stuff to container summary * improve settings tables UI * periphery build supports additional tags * fix variable container sizing * alert types newline wrap * plumbing for Stack resource * plumbing for Stack resource * mount stack api * stack resource sync * get remote compose file * support image_name and image_tag * add server config placeholders. default server config address * configure image name and image tag * deployment work with build image_name and image_tag * stack UI * fe builds * configure registry provider and account * implement periphery stack api * stack poll interval * add UI provider management * deploy stacks * build push commit hash tag. * Destroy stack * update default core port to 9120 * remove git_account alias * finish stack (and container) api * frontend builds * cant cancel server based builds * fix * use git pull -f * 9120 * start UI updates (#15) * fix From<Stack> for CloneArgs * remove unused imports * UI Updates (#16) * cleanup dashboard charts for resources * bring back solid scrollbars * enable sidebar scrolling * remove alerts from all resources * pass jwt secret * stacks dont delete the target * parse services from yaml * stacks deploy * close * looking good * closer * destroy stack when file missing. onboard stacks * figure out stack container name matching * get stack state correct * work with service views * UI Updates - Sidebar, Topbar Alerts, and All Resources page (#17) * move sidebar to use fixed positioning instead of sticky * add alert details dialog to topbar alerts * cleanup all resources page layout * ensure resource links don't propagate clicks * periphery support passing env with --env-file * StackServicePage * default run_directory to ./ for clarify * add stack webhook listeners * add default compose name of stack name * stacks controlled with project name * migrate to dotenvy * add stack to dashboard * remove deploying / destroying stack services * update config files * fix getting service logs * git / docker provider management api * implement passing git / registry token from db * rename system user Github to Git Webhook * seperate deployed and latest services on stack info * add stack service level operations * UI Updates - Update Shadcn/UI components, prevent navbar menu layout shift (#20) * add dashboard pie for resource syncs * dashboard items same height * update shadcn components * ensure centered following sheet update * cleanup layout, prevent navbar menu layout shifts * add manual filter, fix toast call * guard webhooks * remove deployed_message, latest_message from StackListItemInfo * stop all containers on server correctly * support multiple compose files * cache all containers networks images projects * remove project missing from db cache * work on sync deploy stuff * rework deployment sync deploy to support stacks. they can depend on each other. * UI Updates - Remove topbar transparency, pretty status badges, tidy resource page layout with a 'back' button (#21) * remove topbar transparency * cleanup unused * responsive dashboard * better mobile header * dont need to calc 64px less since header is using position fixed * add status badge component * update status badges * further simplify layout * allow undefined status as prop * use new status badges for alerts * update status badges for all resources * undo layout change * tidy up resource page layout, add back button * no need for button wrapper * remove unused * build cancel log * update ts types * fix fe type changes * fe tweaks * remove on build logs * core refresh cache immediately on startup * jwt_ttl * canonicalize run directory on host * update canonicalize error message * core use docker-compose * fix incorrect project missing, add status string to stack info * remove entries in "after" that aren't deploying * fix dockerfiel * build custom tag postfix * sync fixes * ensure UpdateGitProviderAccount doesn't change id * ensure UpdateDockerRegistryAccount doesn't change id * configure providers in the UI * add // comment support to env, conversions * add updates for provider deletes * improve sync pending deploy log * add more deployment actions * add backward compat with v1.12 for clone repo * stack deploy format * fe * alert menus clone when click resource link * rename stacks * don't close on click * snake case stack state, in line with deployment state * sync redeploy stack if newer hash (optional behind resource field 'latest_hash') * remove nav to tree * RefreshStack/Sync debug instruments * improve inline UI docs * implement resource base_permission backend * plumbing for Repo build * build repos * write env file repos * add latest hash / message to build info * add optional hash to update * keep built_hash updated * add backend for build / repo latest hash management * remove unused resources * clean up repo dirs after cache update * fix repo info deser error * add build / repo git status * fix page layouts * improve layout responsive * most config incline docs * add descriptions for all resource types * default local auth false * fix omnibar arrow keys issue * add compose file to example config * image registry * dashboard display no resources messge * update deps. * show when no config * resource sync use config git_provider * fix networks * fix deploy error due to after * update lots of docs * fix server stat charts not working * update screenshots * update changelog * add a disclaimer * remove file paths docs stuff * build repo * v1.13 - Komodo * update docs for cli * fill out the compose example more --------- Co-authored-by: Karamvir Singh <67458484+karamvirsingh98@users.noreply.github.com>
480 lines
13 KiB
Rust
480 lines
13 KiB
Rust
use anyhow::{anyhow, Context};
|
|
use derive_variants::ExtractVariant;
|
|
use futures::future::join_all;
|
|
use monitor_client::entities::{
|
|
alert::{Alert, AlertData},
|
|
alerter::*,
|
|
deployment::DeploymentState,
|
|
server::stats::SeverityLevel,
|
|
stack::StackState,
|
|
update::ResourceTargetVariant,
|
|
};
|
|
use mungos::{find::find_collect, mongodb::bson::doc};
|
|
use slack::types::Block;
|
|
|
|
use crate::{config::core_config, state::db_client};
|
|
|
|
#[instrument]
|
|
pub async fn send_alerts(alerts: &[Alert]) {
|
|
if alerts.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let Ok(alerters) = find_collect(
|
|
&db_client().await.alerters,
|
|
doc! { "config.enabled": true },
|
|
None,
|
|
)
|
|
.await
|
|
.inspect_err(|e| {
|
|
error!(
|
|
"ERROR sending alerts | failed to get alerters from db | {e:#}"
|
|
)
|
|
}) else {
|
|
return;
|
|
};
|
|
|
|
let handles =
|
|
alerts.iter().map(|alert| send_alert(&alerters, alert));
|
|
|
|
join_all(handles).await;
|
|
}
|
|
|
|
#[instrument(level = "debug")]
|
|
async fn send_alert(alerters: &[Alerter], alert: &Alert) {
|
|
if alerters.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let alert_type = alert.data.extract_variant();
|
|
|
|
let handles = alerters.iter().map(|alerter| async {
|
|
// Don't send if not enabled
|
|
if !alerter.config.enabled {
|
|
return Ok(());
|
|
}
|
|
|
|
// Don't send if alert type not configured on the alerter
|
|
if !alerter.config.alert_types.is_empty()
|
|
&& !alerter.config.alert_types.contains(&alert_type)
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
// Don't send if resource is in the blacklist
|
|
if alerter.config.except_resources.contains(&alert.target) {
|
|
return Ok(());
|
|
}
|
|
|
|
// Don't send if whitelist configured and target is not included
|
|
if !alerter.config.resources.is_empty()
|
|
&& !alerter.config.resources.contains(&alert.target)
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
match &alerter.config.endpoint {
|
|
AlerterEndpoint::Slack(SlackAlerterEndpoint { url }) => {
|
|
send_slack_alert(url, alert).await.with_context(|| {
|
|
format!(
|
|
"failed to send alert to slack alerter {}",
|
|
alerter.name
|
|
)
|
|
})
|
|
}
|
|
AlerterEndpoint::Custom(CustomAlerterEndpoint { url }) => {
|
|
send_custom_alert(url, alert).await.with_context(|| {
|
|
format!(
|
|
"failed to send alert to custom alerter {}",
|
|
alerter.name
|
|
)
|
|
})
|
|
}
|
|
}
|
|
});
|
|
|
|
join_all(handles)
|
|
.await
|
|
.into_iter()
|
|
.filter_map(|res| res.err())
|
|
.for_each(|e| error!("{e:#}"));
|
|
}
|
|
|
|
#[instrument(level = "debug")]
|
|
async fn send_custom_alert(
|
|
url: &str,
|
|
alert: &Alert,
|
|
) -> anyhow::Result<()> {
|
|
let res = reqwest::Client::new()
|
|
.post(url)
|
|
.json(alert)
|
|
.send()
|
|
.await
|
|
.context("failed at post request to alerter")?;
|
|
let status = res.status();
|
|
if !status.is_success() {
|
|
let text = res
|
|
.text()
|
|
.await
|
|
.context("failed to get response text on alerter response")?;
|
|
return Err(anyhow!(
|
|
"post to alerter failed | {status} | {text}"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[instrument(level = "debug")]
|
|
async fn send_slack_alert(
|
|
url: &str,
|
|
alert: &Alert,
|
|
) -> anyhow::Result<()> {
|
|
let level = fmt_level(alert.level);
|
|
let (text, blocks): (_, Option<_>) = match &alert.data {
|
|
AlertData::ServerUnreachable {
|
|
id,
|
|
name,
|
|
region,
|
|
err,
|
|
} => {
|
|
let region = fmt_region(region);
|
|
match alert.level {
|
|
SeverityLevel::Ok => {
|
|
let text =
|
|
format!("{level} | *{name}*{region} is now *reachable*");
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} is now *reachable*"
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
SeverityLevel::Critical => {
|
|
let text =
|
|
format!("{level} | *{name}*{region} is *unreachable* ❌");
|
|
let err = err
|
|
.as_ref()
|
|
.map(|e| format!("\nerror: {e:#?}"))
|
|
.unwrap_or_default();
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} is *unreachable* ❌{err}"
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Server,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
AlertData::ServerCpu {
|
|
id,
|
|
name,
|
|
region,
|
|
percentage,
|
|
} => {
|
|
let region = fmt_region(region);
|
|
match alert.level {
|
|
SeverityLevel::Ok => {
|
|
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%*");
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} cpu usage at *{percentage:.1}%*"
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Server,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
_ => {
|
|
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈");
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} cpu usage at *{percentage:.1}%* 📈"
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Server,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
}
|
|
}
|
|
AlertData::ServerMem {
|
|
id,
|
|
name,
|
|
region,
|
|
used_gb,
|
|
total_gb,
|
|
} => {
|
|
let region = fmt_region(region);
|
|
let percentage = 100.0 * used_gb / total_gb;
|
|
match alert.level {
|
|
SeverityLevel::Ok => {
|
|
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
|
)),
|
|
Block::section(format!(
|
|
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Server,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
_ => {
|
|
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
|
)),
|
|
Block::section(format!(
|
|
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Server,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
}
|
|
}
|
|
AlertData::ServerDisk {
|
|
id,
|
|
name,
|
|
region,
|
|
path,
|
|
used_gb,
|
|
total_gb,
|
|
} => {
|
|
let region = fmt_region(region);
|
|
let percentage = 100.0 * used_gb / total_gb;
|
|
match alert.level {
|
|
SeverityLevel::Ok => {
|
|
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
|
|
)),
|
|
Block::section(format!(
|
|
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
|
)),
|
|
Block::section(resource_link(ResourceTargetVariant::Server, id)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
_ => {
|
|
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
|
|
let blocks = vec![
|
|
Block::header(level),
|
|
Block::section(format!(
|
|
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
|
|
)),
|
|
Block::section(format!(
|
|
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
|
)),
|
|
Block::section(resource_link(ResourceTargetVariant::Server, id)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
}
|
|
}
|
|
AlertData::ContainerStateChange {
|
|
name,
|
|
server_name,
|
|
from,
|
|
to,
|
|
id,
|
|
..
|
|
} => {
|
|
let to = fmt_docker_container_state(to);
|
|
let text = format!("📦 Container *{name}* is now {to}");
|
|
let blocks = vec![
|
|
Block::header(text.clone()),
|
|
Block::section(format!(
|
|
"server: {server_name}\nprevious: {from}",
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Deployment,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
AlertData::StackStateChange {
|
|
name,
|
|
server_name,
|
|
from,
|
|
to,
|
|
id,
|
|
..
|
|
} => {
|
|
let to = fmt_stack_state(to);
|
|
let text = format!("🥞 Stack *{name}* is now {to}");
|
|
let blocks = vec![
|
|
Block::header(text.clone()),
|
|
Block::section(format!(
|
|
"server: {server_name}\nprevious: {from}",
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Stack,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
AlertData::AwsBuilderTerminationFailed {
|
|
instance_id,
|
|
message,
|
|
} => {
|
|
let text = format!(
|
|
"{level} | Failed to terminated AWS builder instance "
|
|
);
|
|
let blocks = vec![
|
|
Block::header(text.clone()),
|
|
Block::section(format!(
|
|
"instance id: *{instance_id}*\n{message}"
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
AlertData::ResourceSyncPendingUpdates { id, name } => {
|
|
let text =
|
|
format!("{level} | Pending resource sync updates on {name}");
|
|
let blocks = vec![
|
|
Block::header(text.clone()),
|
|
Block::section(format!(
|
|
"sync id: *{id}*\nsync name: *{name}*",
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::ResourceSync,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
AlertData::BuildFailed { id, name, version } => {
|
|
let text = format!("{level} | Build {name} has failed");
|
|
let blocks = vec![
|
|
Block::header(text.clone()),
|
|
Block::section(format!(
|
|
"build id: *{id}*\nbuild name: *{name}*\nversion: v{version}",
|
|
)),
|
|
Block::section(resource_link(ResourceTargetVariant::Build, id))
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
AlertData::RepoBuildFailed { id, name } => {
|
|
let text =
|
|
format!("{level} | Repo build for {name} has failed");
|
|
let blocks = vec![
|
|
Block::header(text.clone()),
|
|
Block::section(format!(
|
|
"repo id: *{id}*\nrepo name: *{name}*",
|
|
)),
|
|
Block::section(resource_link(
|
|
ResourceTargetVariant::Repo,
|
|
id,
|
|
)),
|
|
];
|
|
(text, blocks.into())
|
|
}
|
|
AlertData::None {} => Default::default(),
|
|
};
|
|
if !text.is_empty() {
|
|
let slack = slack::Client::new(url);
|
|
slack.send_message(text, blocks).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn fmt_region(region: &Option<String>) -> String {
|
|
match region {
|
|
Some(region) => format!(" ({region})"),
|
|
None => String::new(),
|
|
}
|
|
}
|
|
|
|
fn fmt_docker_container_state(state: &DeploymentState) -> String {
|
|
match state {
|
|
DeploymentState::Running => String::from("Running ▶️"),
|
|
DeploymentState::Exited => String::from("Exited 🛑"),
|
|
DeploymentState::Restarting => String::from("Restarting 🔄"),
|
|
DeploymentState::NotDeployed => String::from("Not Deployed"),
|
|
_ => state.to_string(),
|
|
}
|
|
}
|
|
|
|
fn fmt_stack_state(state: &StackState) -> String {
|
|
match state {
|
|
StackState::Running => String::from("Running ▶️"),
|
|
StackState::Stopped => String::from("Stopped 🛑"),
|
|
StackState::Restarting => String::from("Restarting 🔄"),
|
|
StackState::Down => String::from("Down ⬇️"),
|
|
_ => state.to_string(),
|
|
}
|
|
}
|
|
|
|
fn fmt_level(level: SeverityLevel) -> &'static str {
|
|
match level {
|
|
SeverityLevel::Critical => "CRITICAL 🚨",
|
|
SeverityLevel::Warning => "WARNING ‼️",
|
|
SeverityLevel::Ok => "OK ✅",
|
|
}
|
|
}
|
|
|
|
fn resource_link(
|
|
resource_type: ResourceTargetVariant,
|
|
id: &str,
|
|
) -> String {
|
|
let path = match resource_type {
|
|
ResourceTargetVariant::System => unreachable!(),
|
|
ResourceTargetVariant::Build => format!("/builds/{id}"),
|
|
ResourceTargetVariant::Builder => {
|
|
format!("/builders/{id}")
|
|
}
|
|
ResourceTargetVariant::Deployment => {
|
|
format!("/deployments/{id}")
|
|
}
|
|
ResourceTargetVariant::Stack => {
|
|
format!("/stacks/{id}")
|
|
}
|
|
ResourceTargetVariant::Server => {
|
|
format!("/servers/{id}")
|
|
}
|
|
ResourceTargetVariant::Repo => format!("/repos/{id}"),
|
|
ResourceTargetVariant::Alerter => {
|
|
format!("/alerters/{id}")
|
|
}
|
|
ResourceTargetVariant::Procedure => {
|
|
format!("/procedures/{id}")
|
|
}
|
|
ResourceTargetVariant::ServerTemplate => {
|
|
format!("/server-templates/{id}")
|
|
}
|
|
ResourceTargetVariant::ResourceSync => {
|
|
format!("/resource-syncs/{id}")
|
|
}
|
|
};
|
|
|
|
format!("{}{path}", core_config().host)
|
|
}
|