Compare commits

..
58 Commits
Author SHA1 Message Date
mbecker20 0aa9513dd0 1.8.0 2024-06-16 15:36:51 -07:00
mbecker20 26b216b478 add resources page 2024-06-16 15:33:31 -07:00
mbecker20 166299bb57 sync docs 2024-06-16 14:35:09 -07:00
mbecker20 03c47eb3dc remove cli sync 2024-06-16 01:41:54 -07:00
mbecker20 1fcb4ad085 move / update changelog 2024-06-16 01:41:15 -07:00
mbecker20 f51af8fbe1 docs 2024-06-16 01:34:08 -07:00
mbecker20 4a975e1b92 update resource sync docs 2024-06-16 01:33:05 -07:00
mbecker20 ba556e3284 fix doc link 2024-06-16 00:31:23 -07:00
mbecker20 299a326942 log build has new version 2024-06-16 00:20:22 -07:00
mbecker20 a5d4b9aefb add cached results reasons 2024-06-16 00:04:05 -07:00
mbecker20 40b820ae42 add reason to deploy logs 2024-06-15 22:01:14 -07:00
mbecker20 7028bf2996 remove termination_signal for tokio signal 2024-06-15 21:48:54 -07:00
mbecker20 75ebd0e6c0 fix fe cancel logic error 2024-06-15 21:36:26 -07:00
mbecker20 426153df66 try improve toml parse error message 2024-06-15 21:33:53 -07:00
mbecker20 5bd423a6a6 sync deploy new build 2024-06-15 21:15:17 -07:00
mbecker20 c24131d383 nested propogate read resources error 2024-06-15 20:37:29 -07:00
mbecker20 9f54b6c26a 1.8.0. improve env config UI, add sync deploy state management 2024-06-15 20:15:33 -07:00
mbecker20 ab8ae51ece slight more colors 2024-06-15 20:14:25 -07:00
mbecker20 ef2a83ff16 add colors to procedure logs 2024-06-15 20:06:34 -07:00
mbecker20 7872771aee clean up sync log 2024-06-15 19:45:53 -07:00
mbecker20 b12cf858d8 sync deploy logs need \n 2024-06-15 19:36:46 -07:00
mbecker20 38dba91c3a sync deploy accounts for any dependencies in 'after' need deploy 2024-06-15 19:20:45 -07:00
mbecker20 ea8136aa57 add sync deployment state log 2024-06-15 17:31:49 -07:00
mbecker20 f956e12e28 move formatting to shared lib 2024-06-15 17:15:05 -07:00
mbecker20 207ea52b95 add finished log 2024-06-15 17:12:02 -07:00
mbecker20 caf28d3a26 sync deploy 2024-06-15 17:03:16 -07:00
mbecker20 8fff45649d implement sync deployment get updates for view with deploy action 2024-06-15 15:50:10 -07:00
mbecker20 de5df70e11 invert search FE 2024-06-15 00:58:03 -07:00
mbecker20 3df010ac2a read req error debug 2024-06-15 00:54:11 -07:00
mbecker20 2d3beb708e invert logs 2024-06-15 00:28:04 -07:00
mbecker20 1dc22d01c4 improve execute instrumentation 2024-06-15 00:20:28 -07:00
mbecker20 eb029d0408 clone repo to specific directory on host 2024-06-14 23:43:47 -07:00
mbecker20 f926932181 build / deployment env variable / secret selectors 2024-06-14 23:28:08 -07:00
mbecker20 cc96d80c6a string deser filter empty lines 2024-06-14 22:20:39 -07:00
mbecker20 144b49495c string deser can handle empty string 2024-06-14 22:15:02 -07:00
mbecker20 de9354bdc7 frontend manage env with string 2024-06-14 22:10:07 -07:00
mbecker20 38bfee84d7 read resources propogate error 2024-06-14 21:53:13 -07:00
mbecker20 ec33d9fb9e trim incoming value env var string, conversion string, before deserialize 2024-06-14 21:42:59 -07:00
mbecker20 0a66937b1d fix unused liniting 2024-06-14 21:30:10 -07:00
mbecker20 43cc0c3bc1 remove @ in format date 2024-06-14 14:48:22 -07:00
mbecker20 c14b395c70 quick copy variable value 2024-06-12 12:15:29 -07:00
mbecker20 7b8529a7c6 tweak colors 2024-06-12 11:55:06 -07:00
mbecker20 547c089581 update colors 2024-06-12 11:53:39 -07:00
mbecker20 4fe5e461b3 use stroke for icons 2024-06-12 03:48:47 -07:00
mbecker20 edfb873f7c improve error logs 2024-06-12 03:22:51 -07:00
mbecker20 5ef5294c44 remove onkeydown causing redundant create 2024-06-12 03:15:07 -07:00
mbecker20 5d3c50e04f reorder procedure config table 2024-06-12 02:47:41 -07:00
mbecker20 f10efbb5ba add bg to body 2024-06-12 02:39:26 -07:00
mbecker20 39ce98161b add the colors, always plz 2024-06-12 02:21:49 -07:00
mbecker20 cff6e79eee fix omnibar all resource types 2024-06-12 01:46:30 -07:00
mbecker20 dedf22ede8 continue on disabled stage 2024-06-12 01:25:10 -07:00
mbecker20 6955b92a99 add same colors in update 2024-06-12 01:15:39 -07:00
mbecker20 5c63eeab02 better sync coloring 2024-06-12 01:13:33 -07:00
mbecker20 4c14a4ae20 create variable log skip description line if it's empty 2024-06-12 00:39:23 -07:00
mbecker20 29fd856a2d deal with deployment build version 2024-06-11 03:07:56 -07:00
mbecker20 195bdbd94a fix " to \" 2024-06-11 02:14:57 -07:00
mbecker20 298ccd945c improve export dialog sizing 2024-06-11 01:42:06 -07:00
mbecker20 436e4e79e9 toml include ResourceSync 2024-06-11 01:09:37 -07:00
94 changed files with 2383 additions and 1128 deletions
Generated
+21 -54
View File
@@ -32,7 +32,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"axum 0.7.5",
@@ -41,7 +41,6 @@ dependencies = [
"logger",
"monitor_client",
"serde",
"termination_signal",
"tokio",
"tracing",
]
@@ -923,7 +922,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"monitor_client",
"run_command",
@@ -1299,6 +1298,10 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "formatting"
version = "1.8.0"
[[package]]
name = "funty"
version = "2.0.0"
@@ -1423,10 +1426,11 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "git"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"command",
"formatting",
"monitor_client",
"run_command",
"tracing",
@@ -1977,7 +1981,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "logger"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"monitor_client",
@@ -2046,7 +2050,7 @@ dependencies = [
[[package]]
name = "migrator"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"chrono",
@@ -2056,7 +2060,6 @@ dependencies = [
"monitor_client",
"mungos",
"serde",
"termination_signal",
"tokio",
"tracing",
]
@@ -2169,7 +2172,7 @@ dependencies = [
[[package]]
name = "monitor_cli"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"clap",
@@ -2189,7 +2192,7 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2221,7 +2224,7 @@ dependencies = [
[[package]]
name = "monitor_core"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2234,6 +2237,7 @@ dependencies = [
"derive_variants",
"dotenv",
"envy",
"formatting",
"futures",
"git",
"hex",
@@ -2256,8 +2260,8 @@ dependencies = [
"serror",
"sha2",
"slack_client_rs",
"strum 0.26.2",
"svi",
"termination_signal",
"tokio",
"tokio-util",
"toml",
@@ -2272,7 +2276,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2296,7 +2300,6 @@ dependencies = [
"serror",
"svi",
"sysinfo",
"termination_signal",
"tokio",
"tracing",
"uuid",
@@ -2608,7 +2611,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"monitor_client",
@@ -3291,16 +3294,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@@ -3310,18 +3303,6 @@ dependencies = [
"libc",
]
[[package]]
name = "signal-hook-tokio"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e"
dependencies = [
"futures-core",
"libc",
"signal-hook",
"tokio",
]
[[package]]
name = "slab"
version = "0.4.9"
@@ -3570,22 +3551,9 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "termination_signal"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e2993a3469a2732b1c07df31a0227f2354500779c175f42af4c21693f1ad8f"
dependencies = [
"anyhow",
"futures",
"signal-hook",
"signal-hook-tokio",
"tokio",
]
[[package]]
name = "tests"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"dotenv",
@@ -3822,9 +3790,9 @@ dependencies = [
[[package]]
name = "toml_pretty"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "100b9fea6caaa91d9e7837591cbda2958b6ff16318b13c8ba92254fdda13db47"
checksum = "dd412f79539d72f97ac0ec9905b0178be1be4f255ddc2ae6f1ff3f7b234bb870"
dependencies = [
"ordered_hash_map",
"serde",
@@ -4170,12 +4138,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.7.3"
version = "1.8.0"
dependencies = [
"anyhow",
"logger",
"monitor_client",
"termination_signal",
"tokio",
"tracing",
]
+4 -4
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
[workspace.package]
version = "1.7.3"
version = "1.8.0"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -15,8 +15,9 @@ monitor_client = { path = "client/core/rs" }
[workspace.dependencies]
# LOCAL
monitor_client = "1.7.3"
monitor_client = "1.8.0"
periphery_client = { path = "client/periphery/rs" }
formatting = { path = "lib/formatting" }
command = { path = "lib/command" }
logger = { path = "lib/logger" }
git = { path = "lib/git" }
@@ -28,13 +29,12 @@ slack = { version = "0.1.0", package = "slack_client_rs" }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
merge_config_files = "0.1.5"
termination_signal = "0.1.3"
async_timing_util = "0.1.14"
partial_derive2 = "0.4.3"
derive_variants = "1.0.0"
mongo_indexed = "0.3.0"
resolver_api = "1.1.0"
toml_pretty = "1.1.1"
toml_pretty = "1.1.2"
parse_csl = "0.1.0"
mungos = "0.5.6"
svi = "1.0.1"
-2
View File
@@ -13,8 +13,6 @@ repository.workspace = true
# local
monitor_client.workspace = true
logger.workspace = true
# mogh
termination_signal.workspace = true
# external
tokio.workspace = true
tracing.workspace = true
+5 -4
View File
@@ -9,7 +9,6 @@ use monitor_client::entities::{
alert::Alert, server::stats::SeverityLevel,
};
use serde::Deserialize;
use termination_signal::tokio::immediate_term_handle;
#[derive(Deserialize)]
struct Env {
@@ -57,13 +56,15 @@ async fn app() -> anyhow::Result<()> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let term_signal = immediate_term_handle()?;
let mut term_signal = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
)?;
let app = tokio::spawn(app());
tokio::select! {
res = app => return res?,
_ = term_signal => {},
res = app => return res?,
_ = term_signal.recv() => {},
}
Ok(())
+19 -9
View File
@@ -80,15 +80,25 @@ pub fn get_updates(
to_update.push(item);
}
None => {
println!(
"\n{}: variable: {}\n{}: {}\n{}: {}",
"CREATE".green(),
variable.name.bold().green(),
"description".dimmed(),
variable.description,
"value".dimmed(),
variable.value,
);
if variable.description.is_empty() {
println!(
"\n{}: variable: {}\n{}: {}",
"CREATE".green(),
variable.name.bold().green(),
"value".dimmed(),
variable.value,
);
} else {
println!(
"\n{}: variable: {}\n{}: {}\n{}: {}",
"CREATE".green(),
variable.name.bold().green(),
"description".dimmed(),
variable.description,
"value".dimmed(),
variable.value,
);
}
to_create.push(variable)
}
}
+2 -1
View File
@@ -17,12 +17,12 @@ path = "src/main.rs"
# local
monitor_client = { workspace = true, features = ["mongo"] }
periphery_client.workspace = true
formatting.workspace = true
logger.workspace = true
git.workspace = true
# mogh
serror = { workspace = true, features = ["axum"] }
merge_config_files.workspace = true
termination_signal.workspace = true
async_timing_util.workspace = true
partial_derive2.workspace = true
derive_variants.workspace = true
@@ -53,6 +53,7 @@ base64.workspace = true
tokio.workspace = true
tower.workspace = true
serde.workspace = true
strum.workspace = true
axum.workspace = true
toml.workspace = true
uuid.workspace = true
+17 -5
View File
@@ -1,6 +1,7 @@
use std::{collections::HashSet, time::Duration};
use anyhow::{anyhow, Context};
use formatting::muted;
use futures::future::join_all;
use monitor_client::{
api::execute::{
@@ -59,7 +60,7 @@ use crate::helpers::update::init_execution_update;
use super::ExecuteRequest;
impl Resolve<RunBuild, (User, Update)> for State {
#[instrument(name = "RunBuild", skip(self, user))]
#[instrument(name = "RunBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunBuild { build }: RunBuild,
@@ -102,10 +103,9 @@ impl Resolve<RunBuild, (User, Update)> for State {
id = cancel_recv.recv() => id?
};
if incoming_build_id == build_id {
info!("build cancel acknowledged");
update.push_simple_log(
"cancel acknowledged",
"the build cancellation has been queud, it may still take some time",
"the build cancellation has been queued, it may still take some time",
);
update.finalize();
let id = update.id.clone();
@@ -338,6 +338,7 @@ async fn handle_early_return(
Ok(update)
}
#[instrument(skip_all)]
pub async fn validate_cancel_build(
request: &ExecuteRequest,
) -> anyhow::Result<()> {
@@ -381,7 +382,7 @@ pub async fn validate_cancel_build(
}
impl Resolve<CancelBuild, (User, Update)> for State {
#[instrument(name = "CancelBuild", skip(self, user))]
#[instrument(name = "CancelBuild", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
CancelBuild { build }: CancelBuild,
@@ -672,7 +673,18 @@ fn start_aws_builder_log(
let readable_sec_group_ids = security_group_ids.join(", ");
format!("instance id: {instance_id}\nip: {ip}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}\nassign public ip: {assign_public_ip}\nuse public ip: {use_public_ip}")
[
format!("{}: {instance_id}", muted("instance id")),
format!("{}: {ip}", muted("ip")),
format!("{}: {ami_id}", muted("ami id")),
format!("{}: {instance_type}", muted("instance type")),
format!("{}: {volume_gb} GB", muted("volume size")),
format!("{}: {subnet_id}", muted("subnet id")),
format!("{}: {readable_sec_group_ids}", muted("security groups")),
format!("{}: {assign_public_ip}", muted("assign public ip")),
format!("{}: {use_public_ip}", muted("use public ip")),
]
.join("\n")
}
/// This will make sure that a build with non-none image registry has an account attached,
+5 -5
View File
@@ -35,7 +35,7 @@ use crate::{
use crate::helpers::update::init_execution_update;
impl Resolve<Deploy, (User, Update)> for State {
#[instrument(name = "Deploy", skip(self, user))]
#[instrument(name = "Deploy", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
Deploy {
@@ -203,7 +203,7 @@ impl Resolve<Deploy, (User, Update)> for State {
}
impl Resolve<StartContainer, (User, Update)> for State {
#[instrument(name = "StartContainer", skip(self, user))]
#[instrument(name = "StartContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StartContainer { deployment }: StartContainer,
@@ -263,7 +263,7 @@ impl Resolve<StartContainer, (User, Update)> for State {
}
impl Resolve<StopContainer, (User, Update)> for State {
#[instrument(name = "StopContainer", skip(self, user))]
#[instrument(name = "StopContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StopContainer {
@@ -333,7 +333,7 @@ impl Resolve<StopContainer, (User, Update)> for State {
}
impl Resolve<StopAllContainers, (User, Update)> for State {
#[instrument(name = "StopAllContainers", skip(self, user))]
#[instrument(name = "StopAllContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
StopAllContainers { server }: StopAllContainers,
@@ -419,7 +419,7 @@ impl Resolve<StopAllContainers, (User, Update)> for State {
}
impl Resolve<RemoveContainer, (User, Update)> for State {
#[instrument(name = "RemoveContainer", skip(self, user))]
#[instrument(name = "RemoveContainer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RemoveContainer {
+2 -2
View File
@@ -121,7 +121,7 @@ async fn handler(
Ok(Json(update))
}
#[instrument(name = "ExecuteRequest", skip(user, update))]
#[instrument(name = "ExecuteRequest", skip(user, update), fields(user_id = user.id, update_id = update.id))]
async fn task(
req_id: Uuid,
request: ExecuteRequest,
@@ -149,7 +149,7 @@ async fn task(
}
let elapsed = timer.elapsed();
info!("/execute request {req_id} | resolve time: {elapsed:?}");
debug!("/execute request {req_id} | resolve time: {elapsed:?}");
res
}
+17 -4
View File
@@ -1,5 +1,6 @@
use std::pin::Pin;
use formatting::{bold, colored, muted, Color};
use monitor_client::{
api::execute::RunProcedure,
entities::{
@@ -19,7 +20,7 @@ use crate::{
};
impl Resolve<RunProcedure, (User, Update)> for State {
#[instrument(name = "RunProcedure", skip(self, user))]
#[instrument(name = "RunProcedure", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunProcedure { procedure }: RunProcedure,
@@ -51,7 +52,11 @@ fn resolve_inner(
// and will panic otherwise.
update.push_simple_log(
"execute_procedure",
format!("executing procedure {}", procedure.name),
format!(
"{}: executing procedure '{}'",
muted("INFO"),
bold(&procedure.name)
),
);
// get the action state for the procedure (or insert default).
@@ -75,12 +80,20 @@ fn resolve_inner(
Ok(_) => {
update.push_simple_log(
"execution ok",
"the procedure has completed with no errors",
format!(
"{}: the procedure has {} with no errors",
muted("INFO"),
colored("completed", Color::Green)
),
);
}
Err(e) => update.push_error_log(
"execution error",
serialize_error_pretty(&e),
format!(
"{}: {}",
colored("ERROR", Color::Red),
serialize_error_pretty(&e)
),
),
}
+2 -2
View File
@@ -26,7 +26,7 @@ use crate::{
};
impl Resolve<CloneRepo, (User, Update)> for State {
#[instrument(name = "CloneRepo", skip(self, user))]
#[instrument(name = "CloneRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
CloneRepo { repo }: CloneRepo,
@@ -87,7 +87,7 @@ impl Resolve<CloneRepo, (User, Update)> for State {
}
impl Resolve<PullRepo, (User, Update)> for State {
#[instrument(name = "PullRepo", skip(self, user))]
#[instrument(name = "PullRepo", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PullRepo { repo }: PullRepo,
+3 -3
View File
@@ -20,7 +20,7 @@ use crate::{
};
impl Resolve<PruneContainers, (User, Update)> for State {
#[instrument(name = "PruneContainers", skip(self, user))]
#[instrument(name = "PruneContainers", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneContainers { server }: PruneContainers,
@@ -71,7 +71,7 @@ impl Resolve<PruneContainers, (User, Update)> for State {
}
impl Resolve<PruneNetworks, (User, Update)> for State {
#[instrument(name = "PruneNetworks", skip(self, user))]
#[instrument(name = "PruneNetworks", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneNetworks { server }: PruneNetworks,
@@ -122,7 +122,7 @@ impl Resolve<PruneNetworks, (User, Update)> for State {
}
impl Resolve<PruneImages, (User, Update)> for State {
#[instrument(name = "PruneImages", skip(self, user))]
#[instrument(name = "PruneImages", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
PruneImages { server }: PruneImages,
+1 -1
View File
@@ -21,7 +21,7 @@ use crate::{
};
impl Resolve<LaunchServer, (User, Update)> for State {
#[instrument(name = "LaunchServer", skip(self, user))]
#[instrument(name = "LaunchServer", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
LaunchServer {
+17 -13
View File
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Context};
use formatting::{colored, Color};
use mongo_indexed::doc;
use monitor_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
@@ -7,7 +8,6 @@ use monitor_client::{
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
monitor_timestamp,
permission::PermissionLevel,
procedure::Procedure,
@@ -26,7 +26,7 @@ use crate::{
helpers::{
query::get_id_to_tags,
sync::{
colored,
deployment,
resource::{
get_updates_for_execution, AllResourcesById, ResourceSync,
},
@@ -38,6 +38,7 @@ use crate::{
};
impl Resolve<RunSync, (User, Update)> for State {
#[instrument(name = "RunSync", skip(self, user, update), fields(user_id = user.id, update_id = update.id))]
async fn resolve(
&self,
RunSync { sync }: RunSync,
@@ -77,7 +78,7 @@ impl Resolve<RunSync, (User, Update)> for State {
deployments_to_create,
deployments_to_update,
deployments_to_delete,
) = get_updates_for_execution::<Deployment>(
) = deployment::get_updates_for_execution(
resources.deployments,
sync.config.delete,
&all_resources,
@@ -205,7 +206,10 @@ impl Resolve<RunSync, (User, Update)> for State {
{
update.push_simple_log(
"No Changes",
format!("{}. exiting.", colored("nothing to do", "green")),
format!(
"{}. exiting.",
colored("nothing to do", Color::Green)
),
);
update.finalize();
update_update(update.clone()).await?;
@@ -302,15 +306,15 @@ impl Resolve<RunSync, (User, Update)> for State {
);
// Dependant on server / build
maybe_extend(
&mut update.logs,
Deployment::run_updates(
deployments_to_create,
deployments_to_update,
deployments_to_delete,
)
.await,
);
if let Some(res) = deployment::run_updates(
deployments_to_create,
deployments_to_update,
deployments_to_delete,
)
.await
{
update.logs.extend(res);
}
// Dependant on everything
maybe_extend(
+5 -1
View File
@@ -201,6 +201,7 @@ impl Resolve<GetBuildVersions, User> for State {
major,
minor,
patch,
limit,
}: GetBuildVersions,
user: User,
) -> anyhow::Result<Vec<BuildVersionResponseItem>> {
@@ -233,7 +234,10 @@ impl Resolve<GetBuildVersions, User> for State {
let versions = find_collect(
&db_client().await.updates,
filter,
FindOptions::builder().sort(doc! { "_id": -1 }).build(),
FindOptions::builder()
.sort(doc! { "_id": -1 })
.limit(limit)
.build(),
)
.await
.context("failed to pull versions from mongo")?
+2
View File
@@ -121,6 +121,7 @@ impl Resolve<SearchLog, User> for State {
deployment,
terms,
combinator,
invert,
}: SearchLog,
user: User,
) -> anyhow::Result<Log> {
@@ -143,6 +144,7 @@ impl Resolve<SearchLog, User> for State {
name,
terms,
combinator,
invert,
})
.await
.context("failed at call to periphery")
+3 -6
View File
@@ -172,17 +172,14 @@ pub fn router() -> Router {
.layer(middleware::from_fn(auth_request))
}
#[instrument(name = "ReadHandler", level = "debug", skip(user))]
#[instrument(name = "ReadHandler", level = "debug", skip(user), fields(user_id = user.id))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<ReadRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!(
"/read request {req_id} | user: {} ({})",
user.username, user.id
);
debug!("/read request | user: {}", user.username);
let res =
State
.resolve_request(request, user)
@@ -194,7 +191,7 @@ async fn handler(
resolver_api::Error::Inner(e) => e,
});
if let Err(e) = &res {
warn!("/read request {req_id} error: {e:#}");
debug!("/read request {req_id} error: {e:#}");
}
let elapsed = timer.elapsed();
debug!("/read request {req_id} | resolve time: {elapsed:?}");
+31 -1
View File
@@ -118,13 +118,22 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
);
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(tags).build(),
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::ServerTemplate(resource.id)),
);
targets.extend(
resource::list_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags).build(),
&user,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::ResourceSync(resource.id)),
);
let user_groups = if user.admin {
find_collect(&db_client().await.user_groups, None, None)
@@ -533,6 +542,8 @@ fn convert_resource<R: MonitorResource>(
.filter_map(|t| tag_names.get(t).cloned())
.collect(),
description: resource.description,
deploy: false,
after: Default::default(),
config,
}
}
@@ -570,6 +581,25 @@ fn serialize_resources_toml(
.context("deployment has no config?")?
.as_object_mut()
.context("config is not object?")?;
if let Some(DeploymentImage::Build { version, .. }) =
&deployment.config.image
{
let image = config
.get_mut("image")
.context("deployment has no image")?
.get_mut("params")
.context("deployment image has no params")?
.as_object_mut()
.context("deployment image params is not object")?;
if version.is_none() {
image.remove("version");
} else {
image.insert(
"version".to_string(),
Value::String(version.to_string()),
);
}
}
if let Some(term_signal_labels) =
&deployment.config.term_signal_labels
{
+3 -6
View File
@@ -149,16 +149,13 @@ async fn handler(
Ok((TypedHeader(ContentType::json()), res??))
}
#[instrument(name = "WriteRequest", skip(user))]
#[instrument(name = "WriteRequest", skip(user), fields(user_id = user.id))]
async fn task(
req_id: Uuid,
request: WriteRequest,
user: User,
) -> anyhow::Result<String> {
info!(
"/write request {req_id} | user: {} ({})",
user.username, user.id
);
info!("/write request | user: {}", user.username);
let timer = Instant::now();
@@ -178,7 +175,7 @@ async fn task(
}
let elapsed = timer.elapsed();
info!("/write request {req_id} | resolve time: {elapsed:?}");
debug!("/write request {req_id} | resolve time: {elapsed:?}");
res
}
+5 -3
View File
@@ -7,7 +7,6 @@ use monitor_client::{
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
monitor_timestamp,
permission::PermissionLevel,
procedure::Procedure,
@@ -33,7 +32,10 @@ use serror::serialize_error_pretty;
use crate::{
helpers::{
query::get_id_to_tags,
sync::resource::{get_updates_for_view, AllResourcesById},
sync::{
deployment,
resource::{get_updates_for_view, AllResourcesById},
},
},
resource,
state::{db_client, State},
@@ -127,7 +129,7 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
)
.await
.context("failed to get server updates")?,
deployment_updates: get_updates_for_view::<Deployment>(
deployment_updates: deployment::get_updates_for_view(
resources.deployments,
sync.config.delete,
&all_resources,
+3 -3
View File
@@ -216,8 +216,8 @@ impl GithubOauthClient {
#[derive(Deserialize)]
pub struct AccessTokenResponse {
pub access_token: String,
pub scope: String,
pub token_type: String,
// pub scope: String,
// pub token_type: String,
}
#[derive(Deserialize)]
@@ -225,5 +225,5 @@ pub struct GithubUserResponse {
pub login: String,
pub id: u128,
pub avatar_url: String,
pub email: Option<String>,
// pub email: Option<String>,
}
+3 -3
View File
@@ -185,10 +185,10 @@ impl GoogleOauthClient {
#[derive(Deserialize)]
pub struct AccessTokenResponse {
pub access_token: String,
// pub access_token: String,
pub id_token: String,
pub scope: String,
pub token_type: String,
// pub scope: String,
// pub token_type: String,
}
#[derive(Deserialize, Clone)]
+2
View File
@@ -1,4 +1,6 @@
pub mod aws;
#[allow(unused)]
pub mod hetzner;
#[derive(Debug)]
+31 -11
View File
@@ -1,6 +1,7 @@
use std::time::{Duration, Instant};
use anyhow::{anyhow, Context, Ok};
use formatting::{bold, colored, muted, Color};
use futures::future::join_all;
use monitor_client::{
api::execute::Execution,
@@ -21,9 +22,16 @@ pub async fn execute_procedure(
update: &Mutex<Update>,
) -> anyhow::Result<()> {
for stage in &procedure.config.stages {
if !stage.enabled {
continue;
}
add_line_to_update(
update,
&format!("executing stage: {}", stage.name),
&format!(
"{}: executing stage: '{}'",
muted("INFO"),
bold(&stage.name)
),
)
.await;
let timer = Instant::now();
@@ -41,16 +49,19 @@ pub async fn execute_procedure(
.await
.with_context(|| {
format!(
"failed stage '{}' execution after {:?}",
stage.name,
"{}: failed stage '{}' execution after {:?}",
colored("ERROR", Color::Red),
bold(&stage.name),
timer.elapsed(),
)
})?;
add_line_to_update(
update,
&format!(
"finished stage '{}' execution in {:?}",
stage.name,
"{}: {} stage '{}' execution in {:?}",
muted("INFO"),
colored("finished", Color::Green),
bold(&stage.name),
timer.elapsed()
),
)
@@ -69,9 +80,15 @@ async fn execute_stage(
) -> anyhow::Result<()> {
let futures = executions.into_iter().map(|execution| async move {
let now = Instant::now();
add_line_to_update(update, &format!("executing: {execution:?}"))
.await;
let fail_log = format!("failed on {execution:?}");
add_line_to_update(
update,
&format!("{}: executing: {execution:?}", muted("INFO")),
)
.await;
let fail_log = format!(
"{}: failed on {execution:?}",
colored("ERROR", Color::Red)
);
let res =
execute_execution(execution.clone(), parent_id, parent_name)
.await
@@ -79,7 +96,9 @@ async fn execute_stage(
add_line_to_update(
update,
&format!(
"finished execution in {:?}: {execution:?}",
"{}: {} execution in {:?}: {execution:?}",
muted("INFO"),
colored("finished", Color::Green),
now.elapsed()
),
)
@@ -263,8 +282,9 @@ async fn execute_execution(
Ok(())
} else {
Err(anyhow!(
"execution not successful. see update {}",
update.id
"{}: execution not successful. see update '{}'",
colored("ERROR", Color::Red),
bold(&update.id),
))
}
}
+858
View File
@@ -0,0 +1,858 @@
use std::{collections::HashMap, time::Duration};
use anyhow::Context;
use formatting::{bold, colored, muted, Color};
use futures::future::join_all;
use monitor_client::{
api::{execute::Deploy, read::GetBuildVersions},
entities::{
deployment::{
Deployment, DeploymentConfig, DeploymentImage, DeploymentState,
PartialDeploymentConfig,
},
sync::SyncUpdate,
tag::Tag,
toml::ResourceToml,
update::{Log, ResourceTarget},
user::sync_user,
},
};
use mungos::find::find_collect;
use partial_derive2::{Diff, FieldDiff, MaybeNone, PartialDiff};
use resolver_api::Resolve;
use crate::{
api::execute::ExecuteRequest,
helpers::update::init_execution_update,
resource::MonitorResource,
state::{deployment_status_cache, State},
};
use super::resource::{
run_update_description, run_update_tags, AllResourcesById,
ResourceSync,
};
pub type ToUpdate = Vec<ToUpdateItem>;
pub type ToCreate = Vec<ResourceToml<PartialDeploymentConfig>>;
/// Vec of resource names
pub type ToDelete = Vec<String>;
type UpdatesResult = (ToCreate, ToUpdate, ToDelete);
pub struct ToUpdateItem {
pub id: String,
pub resource: ResourceToml<PartialDeploymentConfig>,
pub update_description: bool,
pub update_tags: bool,
pub deploy: bool,
}
/// Turns all the diffs into a readable string
pub async fn get_updates_for_view(
resources: Vec<ResourceToml<PartialDeploymentConfig>>,
delete: bool,
all_resources: &AllResourcesById,
id_to_tags: &HashMap<String, Tag>,
) -> anyhow::Result<Option<SyncUpdate>> {
let map = find_collect(Deployment::coll().await, None, None)
.await
.context("failed to get deployments from db")?
.into_iter()
.map(|r| (r.name.clone(), r))
.collect::<HashMap<_, _>>();
let mut update = SyncUpdate {
log: format!("{} Updates", Deployment::resource_type()),
..Default::default()
};
let mut to_delete = Vec::<String>::new();
if delete {
for resource in map.values() {
if !resources.iter().any(|r| r.name == resource.name) {
update.to_delete += 1;
to_delete.push(resource.name.clone())
}
}
}
let mut to_deploy_cache = HashMap::<String, bool>::new();
let mut to_deploy_build_cache = HashMap::<String, String>::new();
for mut resource in resources.clone() {
match map.get(&resource.name) {
Some(original) => {
// First merge toml resource config (partial) onto default resource config.
// Makes sure things that aren't defined in toml (come through as None) actually get removed.
let config: DeploymentConfig = resource.config.into();
resource.config = config.into();
Deployment::validate_partial_config(&mut resource.config);
let mut diff = Deployment::get_diff(
original.config.clone(),
resource.config,
all_resources,
)?;
Deployment::validate_diff(&mut diff);
let original_tags = original
.tags
.iter()
.filter_map(|id| id_to_tags.get(id).map(|t| t.name.clone()))
.collect::<Vec<_>>();
let (to_deploy, state, reason) = extract_to_deploy_and_state(
all_resources,
&map,
&resources,
resource.name.clone(),
&mut to_deploy_cache,
&mut to_deploy_build_cache,
)
.await?;
// Only proceed if there are any fields to update,
// or a change to tags / description
if diff.is_none()
&& !to_deploy
&& resource.description == original.description
&& resource.tags == original_tags
{
if state == DeploymentState::Unknown {
update.log.push_str(&format!(
"\n\n{}: {}: '{}'\nDeployment sync actions could not be computed due to Unknown deployment state\n-------------------",
colored("ERROR", Color::Red),
Deployment::resource_type(),
bold(&resource.name)
));
}
continue;
}
update.to_update += 1;
update.log.push_str(&format!(
"\n\n{}: {}: '{}'\n-------------------",
colored("UPDATE", Color::Blue),
Deployment::resource_type(),
bold(&resource.name)
));
let mut lines = Vec::<String>::new();
if resource.description != original.description {
lines.push(format!(
"{}: 'description'\n{}: {}\n{}: {}",
muted("field"),
muted("from"),
colored(&original.description, Color::Red),
muted("to"),
colored(&resource.description, Color::Green)
));
}
if resource.tags != original_tags {
let from =
colored(&format!("{:?}", original_tags), Color::Red);
let to =
colored(&format!("{:?}", resource.tags), Color::Green);
lines.push(format!(
"{}: 'tags'\n{}: {from}\n{}: {to}",
muted("field"),
muted("from"),
muted("to"),
));
}
lines.extend(diff.iter_field_diffs().map(
|FieldDiff { field, from, to }| {
format!(
"{}: '{field}'\n{}: {}\n{}: {}",
muted("field"),
muted("from"),
colored(from, Color::Red),
muted("to"),
colored(to, Color::Green)
)
},
));
if state == DeploymentState::Unknown {
lines.push(format!(
"{}: Deployment sync actions {} due to Unknown deployment state",
colored("ERROR", Color::Red),
bold("could not be computed")
));
} else if to_deploy {
let mut line = if state == DeploymentState::Running {
format!(
"{}: {reason}, {}",
muted("deploy"),
bold("sync will trigger deploy")
)
} else {
format!(
"{}: deployment is currently in {} state, {}",
muted("deploy"),
colored(&state.to_string(), Color::Red),
bold("sync will trigger deploy")
)
};
if !resource.after.is_empty() {
line.push_str(&format!(
"\n{}: {:?}",
muted("deploy after"),
resource.after
));
}
lines.push(line);
}
update.log.push('\n');
update.log.push_str(&lines.join("\n-------------------\n"));
}
None => {
update.to_create += 1;
let mut lines = vec![
format!(
"{}: {}",
muted("description"),
resource.description,
),
format!("{}: {:?}", muted("tags"), resource.tags,),
format!(
"{}: {}",
muted("config"),
serde_json::to_string_pretty(&resource.config)
.context("failed to serialize config to json")?
),
];
if resource.deploy {
lines.push(format!(
"{}: {}",
muted("will deploy"),
colored("true", Color::Green)
));
if !resource.after.is_empty() {
lines.push(format!(
"{}: {:?}",
muted("deploy after"),
resource.after
));
}
}
update.log.push_str(&format!(
"\n\n{}: {}: {}\n{}",
colored("CREATE", Color::Green),
Deployment::resource_type(),
bold(&resource.name),
lines.join("\n")
))
}
}
}
for name in to_delete {
update.log.push_str(&format!(
"\n\n{}: {}: '{}'\n-------------------",
colored("DELETE", Color::Red),
Deployment::resource_type(),
bold(&name)
));
}
let any_change = update.to_create > 0
|| update.to_update > 0
|| update.to_delete > 0;
Ok(any_change.then_some(update))
}
/// Gets all the resources to update. For use in sync execution.
pub async fn get_updates_for_execution(
resources: Vec<ResourceToml<PartialDeploymentConfig>>,
delete: bool,
all_resources: &AllResourcesById,
id_to_tags: &HashMap<String, Tag>,
) -> anyhow::Result<UpdatesResult> {
let map = find_collect(Deployment::coll().await, None, None)
.await
.context("failed to get deployments from db")?
.into_iter()
.map(|r| (r.name.clone(), r))
.collect::<HashMap<_, _>>();
let mut to_create = ToCreate::new();
let mut to_update = ToUpdate::new();
let mut to_delete = ToDelete::new();
if delete {
for resource in map.values() {
if !resources.iter().any(|r| r.name == resource.name) {
to_delete.push(resource.name.clone());
}
}
}
let mut to_deploy_cache = HashMap::<String, bool>::new();
let mut to_deploy_build_cache = HashMap::<String, String>::new();
for mut resource in resources.clone() {
match map.get(&resource.name) {
Some(original) => {
// First merge toml resource config (partial) onto default resource config.
// Makes sure things that aren't defined in toml (come through as None) actually get removed.
let config: DeploymentConfig = resource.config.into();
resource.config = config.into();
Deployment::validate_partial_config(&mut resource.config);
let mut diff = Deployment::get_diff(
original.config.clone(),
resource.config,
all_resources,
)?;
Deployment::validate_diff(&mut diff);
let original_tags = original
.tags
.iter()
.filter_map(|id| id_to_tags.get(id).map(|t| t.name.clone()))
.collect::<Vec<_>>();
let (to_deploy, _state, _reason) =
extract_to_deploy_and_state(
all_resources,
&map,
&resources,
resource.name.clone(),
&mut to_deploy_cache,
&mut to_deploy_build_cache,
)
.await?;
// Only proceed if there are any fields to update,
// or a change to tags / description
if diff.is_none()
&& !to_deploy
&& resource.description == original.description
&& resource.tags == original_tags
{
continue;
}
// Minimizes updates through diffing.
resource.config = diff.into();
let update = ToUpdateItem {
id: original.id.clone(),
update_description: resource.description
!= original.description,
update_tags: resource.tags != original_tags,
resource,
deploy: to_deploy,
};
to_update.push(update);
}
None => to_create.push(resource),
}
}
Ok((to_create, to_update, to_delete))
}
type Res<'a> = std::pin::Pin<
Box<
dyn std::future::Future<
Output = anyhow::Result<(bool, DeploymentState, String)>,
> + Send
+ 'a,
>,
>;
fn extract_to_deploy_and_state<'a>(
all_resources: &'a AllResourcesById,
map: &'a HashMap<String, Deployment>,
resources: &'a [ResourceToml<PartialDeploymentConfig>],
name: String,
// name to 'to_deploy'
cache: &'a mut HashMap<String, bool>,
// build id to latest built version string
build_cache: &'a mut HashMap<String, String>,
) -> Res<'a> {
Box::pin(async move {
let mut reason = String::new();
let Some(deployment) = resources.iter().find(|r| r.name == name)
else {
// this case should be unreachable, the names come off of a loop over resources
cache.insert(name, false);
return Ok((false, DeploymentState::Unknown, reason));
};
if deployment.deploy {
let Some(original) = map.get(&name) else {
// not created, definitely deploy
cache.insert(name, true);
// Don't need reason here, will be populated automatically
return Ok((true, DeploymentState::NotDeployed, reason));
};
// First merge toml resource config (partial) onto default resource config.
// Makes sure things that aren't defined in toml (come through as None) actually get removed.
let config: DeploymentConfig = deployment.config.clone().into();
let mut config: PartialDeploymentConfig = config.into();
Deployment::validate_partial_config(&mut config);
let mut diff = Deployment::get_diff(
original.config.clone(),
config,
all_resources,
)?;
Deployment::validate_diff(&mut diff);
let status = &deployment_status_cache()
.get_or_insert_default(&original.id)
.await
.curr;
let state = status.state;
let mut to_deploy = match state {
DeploymentState::Unknown => false,
DeploymentState::Running => {
// Needs to only check config fields that affect docker run
let changed = diff.server_id.is_some()
|| diff.image.is_some()
|| diff.image_registry.is_some()
|| diff.skip_secret_interp.is_some()
|| diff.network.is_some()
|| diff.restart.is_some()
|| diff.command.is_some()
|| diff.extra_args.is_some()
|| diff.ports.is_some()
|| diff.volumes.is_some()
|| diff.environment.is_some()
|| diff.labels.is_some();
if changed {
reason = String::from("deployment config has changed")
}
changed
}
// All other cases will require Deploy to enter Running state.
// Don't need reason here as this case is handled outside, using returned state.
_ => true,
};
// Check if build attached, version latest, and there is a new build.
if !to_deploy {
// only need to check original, if diff.image was Some, to_deploy would be true.
if let DeploymentImage::Build { build_id, version } =
&original.config.image
{
// check if version is none, ie use latest build
if version.is_none() {
let deployed_version = status
.container
.as_ref()
.and_then(|c| c.image.split(':').last())
.unwrap_or("0.0.0");
match build_cache.get(build_id) {
Some(version) if deployed_version != version => {
to_deploy = true;
reason = format!(
"attached build has new version ({version})"
);
}
Some(_) => {}
None => {
let Some(version) = State
.resolve(
GetBuildVersions {
build: build_id.to_string(),
limit: Some(1),
..Default::default()
},
sync_user().to_owned(),
)
.await
.context("failed to get build versions")?
.pop()
else {
// this case shouldn't ever happen, how would deployment be deployed if build was never built?
return Ok((
false,
DeploymentState::NotDeployed,
reason,
));
};
let version = version.version.to_string();
build_cache
.insert(build_id.to_string(), version.clone());
if deployed_version != version {
to_deploy = true;
reason = format!(
"attached build has new version ({version})"
);
}
}
};
}
}
}
// Still need to check 'after' if they need deploy
if !to_deploy {
for name in &deployment.after {
match cache.get(name) {
Some(will_deploy) if *will_deploy => {
to_deploy = true;
reason = format!(
"parent dependency '{}' is deploying",
bold(name)
);
break;
}
Some(_) => {}
None => {
let (will_deploy, _, _) = extract_to_deploy_and_state(
all_resources,
map,
resources,
name.to_string(),
cache,
build_cache,
)
.await?;
if will_deploy {
to_deploy = true;
reason = format!(
"parent dependency '{}' is deploying",
bold(name)
);
break;
}
}
}
}
}
cache.insert(name, to_deploy);
Ok((to_deploy, state, reason))
} else {
// The state in this case doesn't matter and won't be read (as long as it isn't 'Unknown' which will log in all cases)
cache.insert(name, false);
Ok((false, DeploymentState::NotDeployed, reason))
}
})
}
pub async fn run_updates(
to_create: ToCreate,
to_update: ToUpdate,
to_delete: ToDelete,
) -> Option<Vec<Log>> {
if to_create.is_empty()
&& to_update.is_empty()
&& to_delete.is_empty()
{
return None;
}
let mut has_error = false;
let mut log = String::new();
// Collect all the deployment names that need to be deployed
// and their 'after' dependencies
let mut to_deploy = Vec::<(String, Vec<String>)>::new();
for resource in to_create {
let name = resource.name.clone();
let tags = resource.tags.clone();
let description = resource.description.clone();
let id = match crate::resource::create::<Deployment>(
&resource.name,
resource.config,
sync_user(),
)
.await
{
Ok(resource) => resource.id,
Err(e) => {
has_error = true;
log.push_str(&format!(
"\n{}: failed to create {} '{}' | {e:#}",
colored("ERROR", Color::Red),
Deployment::resource_type(),
bold(&name)
));
continue;
}
};
run_update_tags::<Deployment>(
id.clone(),
&name,
tags,
&mut log,
&mut has_error,
)
.await;
run_update_description::<Deployment>(
id,
&name,
description,
&mut log,
&mut has_error,
)
.await;
log.push_str(&format!(
"\n{}: {} {} '{}'",
muted("INFO"),
colored("created", Color::Green),
Deployment::resource_type(),
bold(&name)
));
if resource.deploy {
to_deploy.push((resource.name, resource.after));
}
}
for ToUpdateItem {
id,
resource,
update_description,
update_tags,
deploy,
} in to_update
{
// Update resource
let name = resource.name.clone();
let tags = resource.tags.clone();
let description = resource.description.clone();
if update_description {
run_update_description::<Deployment>(
id.clone(),
&name,
description,
&mut log,
&mut has_error,
)
.await;
}
if update_tags {
run_update_tags::<Deployment>(
id.clone(),
&name,
tags,
&mut log,
&mut has_error,
)
.await;
}
let mut config_update_error = false;
if !resource.config.is_none() {
if let Err(e) = crate::resource::update::<Deployment>(
&id,
resource.config,
sync_user(),
)
.await
{
has_error = true;
config_update_error = true;
log.push_str(&format!(
"\n{}: failed to update config on {} '{}' | {e:#}",
colored("ERROR", Color::Red),
Deployment::resource_type(),
bold(&name),
))
} else {
log.push_str(&format!(
"\n{}: {} {} '{}' configuration",
muted("INFO"),
colored("updated", Color::Blue),
Deployment::resource_type(),
bold(&name)
));
}
}
if !config_update_error && deploy {
to_deploy.push((resource.name, resource.after));
}
}
for resource in to_delete {
if let Err(e) =
crate::resource::delete::<Deployment>(&resource, sync_user())
.await
{
has_error = true;
log.push_str(&format!(
"\n{}: failed to delete {} '{}' | {e:#}",
colored("ERROR", Color::Red),
Deployment::resource_type(),
bold(&resource),
))
} else {
log.push_str(&format!(
"\n{}: {} {} '{}'",
muted("INFO"),
colored("deleted", Color::Red),
Deployment::resource_type(),
bold(&resource)
));
}
}
let mut logs = Vec::with_capacity(1);
let stage = format!("Update {}s", Deployment::resource_type());
if has_error {
let log = format!(
"running updates on {}s{log}",
Deployment::resource_type()
);
logs.push(Log::error(&stage, log));
return Some(logs);
} else if !log.is_empty() {
let log = format!(
"running updates on {}s{log}",
Deployment::resource_type()
);
logs.push(Log::simple(&stage, log));
}
if to_deploy.is_empty() {
return Some(logs);
}
let mut log = format!(
"{}: running executions to sync deployment state",
muted("INFO")
);
let mut round = 1;
while !to_deploy.is_empty() {
// Collect all waiting deployments without waiting dependencies.
let good_to_deploy = to_deploy
.iter()
.filter(|(_, after)| {
to_deploy.iter().all(|(name, _)| !after.contains(name))
})
.map(|(name, _)| name.clone())
.collect::<Vec<_>>();
// Deploy the ones ready for deployment
let res =
join_all(good_to_deploy.iter().map(|name| async move {
let res = async {
let req = ExecuteRequest::Deploy(Deploy {
deployment: name.to_string(),
stop_signal: None,
stop_time: None,
});
let user = sync_user();
let update = init_execution_update(&req, user).await?;
let ExecuteRequest::Deploy(req) = req else {
unreachable!()
};
State.resolve(req, (user.to_owned(), update)).await
}
.await;
(name, res)
}))
.await;
// Log results of deploy
for (name, res) in res {
if let Err(e) = res {
has_error = true;
log.push_str(&format!(
"\n{}: failed to deploy '{}' in round {} | {e:#}",
colored("ERROR", Color::Red),
bold(name),
bold(round)
));
} else {
log.push_str(&format!(
"\n{}: deployed '{}' in round {}",
muted("INFO"),
bold(name),
bold(round)
));
}
}
// Early exit if any deploy has errors
if has_error {
log.push_str(&format!(
"\n{}: exited in round {} {}",
muted("INFO"),
bold(round),
colored("with errors", Color::Red)
));
logs.push(Log::error("Sync Deployment State", log));
return Some(logs);
}
// Remove the deployed ones from 'to_deploy'
to_deploy.retain(|(name, _)| !good_to_deploy.contains(name));
// If there must be another round, these are dependent on the first round.
// Sleep for 1s to allow for first round to startup
if !to_deploy.is_empty() {
// Increment the round
round += 1;
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
log.push_str(&format!(
"\n{}: finished after {} round{}",
muted("INFO"),
bold(round),
(round > 1).then_some("s").unwrap_or_default()
));
logs.push(Log::simple("Sync Deployment State", log));
Some(logs)
}
impl ResourceSync for Deployment {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Deployment(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
resources: &AllResourcesById,
) -> anyhow::Result<Self::ConfigDiff> {
// need to replace the server id with name
original.server_id = resources
.servers
.get(&original.server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
// need to replace the build id with name
if let DeploymentImage::Build { build_id, version } =
&original.image
{
original.image = DeploymentImage::Build {
build_id: resources
.builds
.get(build_id)
.map(|b| b.name.clone())
.unwrap_or_default(),
version: version.clone(),
};
}
Ok(original.partial_diff(update))
}
}
+17 -23
View File
@@ -1,17 +1,19 @@
use std::{fs, path::Path};
use anyhow::{anyhow, Context};
use formatting::{colored, muted, Color};
use monitor_client::entities::{toml::ResourcesToml, update::Log};
use serde::de::DeserializeOwned;
use super::{colored, muted};
pub fn read_resources(
path: &Path,
) -> anyhow::Result<(ResourcesToml, Log)> {
let mut res = ResourcesToml::default();
let mut log = format!("reading resources from {path:?}");
read_resources_recursive(path, &mut res, &mut log)?;
let mut log =
format!("{}: reading resources from {path:?}", muted("INFO"));
read_resources_recursive(path, &mut res, &mut log).with_context(
|| format!("failed to read resources from {path:?}"),
)?;
Ok((res, Log::simple("read remote resources", log)))
}
@@ -30,20 +32,15 @@ fn read_resources_recursive(
{
return Ok(());
}
let more = match parse_toml_file::<ResourcesToml>(path) {
Ok(res) => res,
Err(e) => {
warn!("failed to parse {:?}. skipping file | {e:#}", path);
return Ok(());
}
};
let more = parse_toml_file::<ResourcesToml>(path)
.context("failed to parse resource file")?;
log.push('\n');
log.push_str(&format!(
"{}: {} from {}",
muted("INFO"),
colored("adding resources", "green"),
colored(&path.display().to_string(), "blue")
colored("adding resources", Color::Green),
colored(path.display(), Color::Blue)
));
resources.servers.extend(more.servers);
@@ -63,15 +60,9 @@ fn read_resources_recursive(
.context("failed to read directory contents")?;
for entry in directory.into_iter().flatten() {
let path = entry.path();
if let Err(e) = read_resources_recursive(&path, resources, log)
{
log.push('\n');
log.push_str(&format!(
"{}: failed to read additional resources from {} | {e:#}",
colored("ERROR", "red"),
colored(&path.display().to_string(), "blue")
));
}
read_resources_recursive(&path, resources, log).with_context(
|| format!("failed to read resources from {path:?}"),
)?;
}
Ok(())
} else {
@@ -84,5 +75,8 @@ fn parse_toml_file<T: DeserializeOwned>(
) -> anyhow::Result<T> {
let contents = std::fs::read_to_string(path)
.context("failed to read file contents")?;
toml::from_str(&contents).context("failed to parse toml contents")
toml::from_str(&contents)
// the error without this comes through with multiple lines (\n) and looks bad
.map_err(|e| anyhow!("{e:#}"))
.context("failed to parse toml contents")
}
+1 -12
View File
@@ -7,6 +7,7 @@ use resolver_api::Resolve;
use crate::state::{db_client, State};
pub mod deployment;
pub mod remote;
pub mod resource;
pub mod user_groups;
@@ -41,15 +42,3 @@ pub fn spawn_sync_refresh_loop() {
}
});
}
fn muted(content: &str) -> String {
format!("<span class=\"text-muted-foreground\">{content}</span>")
}
fn bold(content: &str) -> String {
format!("<span class=\"font-bold\">{content}</span>")
}
pub fn colored(content: &str, color: &str) -> String {
format!("<span class=\"text-{color}-500\">{content}</span>")
}
+22 -21
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use anyhow::Context;
use formatting::{bold, colored, muted, Color};
use monitor_client::{
api::write::{UpdateDescription, UpdateTagsOnResource},
entities::{
@@ -26,8 +27,6 @@ use resolver_api::Resolve;
use crate::{resource::MonitorResource, state::State};
use super::{bold, colored, muted};
pub type ToUpdate<T> = Vec<ToUpdateItem<T>>;
pub type ToCreate<T> = Vec<ResourceToml<T>>;
/// Vec of resource names
@@ -93,7 +92,7 @@ pub trait ResourceSync: MonitorResource + Sized {
has_error = true;
log.push_str(&format!(
"\n{}: failed to create {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Self::resource_type(),
bold(&name)
));
@@ -119,7 +118,7 @@ pub trait ResourceSync: MonitorResource + Sized {
log.push_str(&format!(
"\n{}: {} {} '{}'",
muted("INFO"),
colored("created", "green"),
colored("created", Color::Green),
Self::resource_type(),
bold(&name)
));
@@ -170,7 +169,7 @@ pub trait ResourceSync: MonitorResource + Sized {
has_error = true;
log.push_str(&format!(
"\n{}: failed to update config on {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Self::resource_type(),
bold(&name),
))
@@ -178,7 +177,7 @@ pub trait ResourceSync: MonitorResource + Sized {
log.push_str(&format!(
"\n{}: {} {} '{}' configuration",
muted("INFO"),
colored("updated", "blue"),
colored("updated", Color::Blue),
Self::resource_type(),
bold(&name)
));
@@ -193,7 +192,7 @@ pub trait ResourceSync: MonitorResource + Sized {
has_error = true;
log.push_str(&format!(
"\n{}: failed to delete {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Self::resource_type(),
bold(&resource),
))
@@ -201,7 +200,7 @@ pub trait ResourceSync: MonitorResource + Sized {
log.push_str(&format!(
"\n{}: {} {} '{}'",
muted("INFO"),
colored("deleted", "red"),
colored("deleted", Color::Red),
Self::resource_type(),
bold(&resource)
));
@@ -283,7 +282,7 @@ pub async fn get_updates_for_view<Resource: ResourceSync>(
update.log.push_str(&format!(
"\n\n{}: {}: '{}'\n-------------------",
colored("UPDATE", "blue"),
colored("UPDATE", Color::Blue),
Resource::resource_type(),
bold(&resource.name)
));
@@ -294,14 +293,16 @@ pub async fn get_updates_for_view<Resource: ResourceSync>(
"{}: 'description'\n{}: {}\n{}: {}",
muted("field"),
muted("from"),
colored(&original.description, "red"),
colored(&original.description, Color::Red),
muted("to"),
colored(&resource.description, "green")
colored(&resource.description, Color::Green)
));
}
if resource.tags != original_tags {
let from = colored(&format!("{:?}", original_tags), "red");
let to = colored(&format!("{:?}", resource.tags), "green");
let from =
colored(&format!("{:?}", original_tags), Color::Red);
let to =
colored(&format!("{:?}", resource.tags), Color::Green);
lines.push(format!(
"{}: 'tags'\n{}: {from}\n{}: {to}",
muted("field"),
@@ -315,9 +316,9 @@ pub async fn get_updates_for_view<Resource: ResourceSync>(
"{}: '{field}'\n{}: {}\n{}: {}",
muted("field"),
muted("from"),
colored(&from, "red"),
colored(from, Color::Red),
muted("to"),
colored(&to, "green")
colored(to, Color::Green)
)
},
));
@@ -328,7 +329,7 @@ pub async fn get_updates_for_view<Resource: ResourceSync>(
update.to_create += 1;
update.log.push_str(&format!(
"\n\n{}: {}: {}\n{}: {}\n{}: {:?}\n{}: {}",
colored("CREATE", "green"),
colored("CREATE", Color::Green),
Resource::resource_type(),
bold(&resource.name),
muted("description"),
@@ -346,7 +347,7 @@ pub async fn get_updates_for_view<Resource: ResourceSync>(
for name in to_delete {
update.log.push_str(&format!(
"\n\n{}: {}: '{}'\n-------------------",
colored("DELETE", "red"),
colored("DELETE", Color::Red),
Resource::resource_type(),
bold(&name)
));
@@ -459,7 +460,7 @@ pub async fn run_update_tags<Resource: ResourceSync>(
*has_error = true;
log.push_str(&format!(
"\n{}: failed to update tags on {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Resource::resource_type(),
bold(name),
))
@@ -467,7 +468,7 @@ pub async fn run_update_tags<Resource: ResourceSync>(
log.push_str(&format!(
"\n{}: {} {} '{}' tags",
muted("INFO"),
colored("updated", "blue"),
colored("updated", Color::Blue),
Resource::resource_type(),
bold(name)
));
@@ -494,7 +495,7 @@ pub async fn run_update_description<Resource: ResourceSync>(
*has_error = true;
log.push_str(&format!(
"\n{}: failed to update description on {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Resource::resource_type(),
bold(name),
))
@@ -502,7 +503,7 @@ pub async fn run_update_description<Resource: ResourceSync>(
log.push_str(&format!(
"\n{}: {} {} '{}' description",
muted("INFO"),
colored("updated", "blue"),
colored("updated", Color::Blue),
Resource::resource_type(),
bold(name)
));
+9 -47
View File
@@ -1,3 +1,4 @@
use formatting::{bold, colored, muted, Color};
use monitor_client::{
api::execute::Execution,
entities::{
@@ -5,7 +6,6 @@ use monitor_client::{
alerter::Alerter,
build::Build,
builder::{Builder, BuilderConfig},
deployment::{Deployment, DeploymentImage},
procedure::Procedure,
repo::Repo,
server::Server,
@@ -17,12 +17,9 @@ use monitor_client::{
use partial_derive2::{MaybeNone, PartialDiff};
use crate::{
helpers::sync::{
bold, colored, muted,
resource::{
run_update_description, run_update_tags, ResourceSync,
ToUpdateItem,
},
helpers::sync::resource::{
run_update_description, run_update_tags, ResourceSync,
ToUpdateItem,
},
resource::MonitorResource,
};
@@ -73,41 +70,6 @@ impl ResourceSync for Build {
}
}
impl ResourceSync for Deployment {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Deployment(id)
}
fn get_diff(
mut original: Self::Config,
update: Self::PartialConfig,
resources: &AllResourcesById,
) -> anyhow::Result<Self::ConfigDiff> {
// need to replace the server id with name
original.server_id = resources
.servers
.get(&original.server_id)
.map(|s| s.name.clone())
.unwrap_or_default();
// need to replace the build id with name
if let DeploymentImage::Build { build_id, version } =
&original.image
{
original.image = DeploymentImage::Build {
build_id: resources
.builds
.get(build_id)
.map(|b| b.name.clone())
.unwrap_or_default(),
version: version.clone(),
};
}
Ok(original.partial_diff(update))
}
}
impl ResourceSync for Repo {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Repo(id)
@@ -329,7 +291,7 @@ impl ResourceSync for Procedure {
has_error = true;
log.push_str(&format!(
"{}: failed to delete {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Self::resource_type(),
bold(&name),
))
@@ -337,7 +299,7 @@ impl ResourceSync for Procedure {
log.push_str(&format!(
"{}: {} {} '{}'",
muted("INFO"),
colored("deleted", "red"),
colored("deleted", Color::Red),
Self::resource_type(),
bold(&name)
));
@@ -398,7 +360,7 @@ impl ResourceSync for Procedure {
has_error = true;
log.push_str(&format!(
"\n{}: failed to update {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Self::resource_type(),
bold(&name)
));
@@ -437,7 +399,7 @@ impl ResourceSync for Procedure {
has_error = true;
log.push_str(&format!(
"\n{}: failed to create {} '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
Self::resource_type(),
bold(&name)
));
@@ -464,7 +426,7 @@ impl ResourceSync for Procedure {
log.push_str(&format!(
"\n{}: {} {} '{}'",
muted("INFO"),
colored("created", "green"),
colored("created", Color::Green),
Self::resource_type(),
bold(&name)
));
+18 -17
View File
@@ -1,6 +1,7 @@
use std::{cmp::Ordering, collections::HashMap};
use anyhow::Context;
use formatting::{bold, colored, muted, Color};
use monitor_client::{
api::{
read::ListUserTargetPermissions,
@@ -22,7 +23,7 @@ use resolver_api::Resolve;
use crate::state::{db_client, State};
use super::{bold, colored, muted, resource::AllResourcesById};
use super::resource::AllResourcesById;
pub struct UpdateItem {
user_group: UserGroupToml,
@@ -77,8 +78,8 @@ pub async fn get_updates_for_view(
update.to_create += 1;
update.log.push_str(&format!(
"\n\n{}: user group: {}\n{}: {:?}\n{}: {:?}",
colored("CREATE", "green"),
colored(&user_group.name, "green"),
colored("CREATE", Color::Green),
colored(&user_group.name, Color::Green),
muted("users"),
user_group.users,
muted("permissions"),
@@ -196,7 +197,7 @@ pub async fn get_updates_for_view(
update.to_update += 1;
update.log.push_str(&format!(
"\n\n{}: user group: '{}'\n-------------------",
colored("UPDATE", "blue"),
colored("UPDATE", Color::Blue),
bold(&user_group.name),
));
let mut lines = Vec::<String>::new();
@@ -210,7 +211,7 @@ pub async fn get_updates_for_view(
let adding = if adding.is_empty() {
String::from("None")
} else {
colored(&adding.join(", "), "green")
colored(&adding.join(", "), Color::Green)
};
let removing = original_users
.iter()
@@ -220,7 +221,7 @@ pub async fn get_updates_for_view(
let removing = if removing.is_empty() {
String::from("None")
} else {
colored(&removing.join(", "), "red")
colored(&removing.join(", "), Color::Red)
};
lines.push(format!(
"{}: 'users'\n{}: {removing}\n{}: {adding}",
@@ -241,7 +242,7 @@ pub async fn get_updates_for_view(
let adding = if adding.is_empty() {
String::from("None")
} else {
colored(&adding.join(", "), "green")
colored(&adding.join(", "), Color::Green)
};
let removing = original_permissions
.iter()
@@ -253,7 +254,7 @@ pub async fn get_updates_for_view(
let removing = if removing.is_empty() {
String::from("None")
} else {
colored(&removing.join(", "), "red")
colored(&removing.join(", "), Color::Red)
};
lines.push(format!(
"{}: 'permissions'\n{}: {removing}\n{}: {adding}",
@@ -270,7 +271,7 @@ pub async fn get_updates_for_view(
for name in &to_delete {
update.log.push_str(&format!(
"\n\n{}: user group: '{}'\n-------------------",
colored("DELETE", "red"),
colored("DELETE", Color::Red),
bold(name),
));
}
@@ -495,7 +496,7 @@ pub async fn run_updates(
has_error = true;
log.push_str(&format!(
"\n{}: failed to create user group '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&user_group.name)
));
continue;
@@ -503,7 +504,7 @@ pub async fn run_updates(
log.push_str(&format!(
"\n{}: {} user group '{}'",
muted("INFO"),
colored("created", "green"),
colored("created", Color::Green),
bold(&user_group.name)
))
};
@@ -562,14 +563,14 @@ pub async fn run_updates(
has_error = true;
log.push_str(&format!(
"\n{}: failed to delete user group '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&user_group.name)
))
} else {
log.push_str(&format!(
"\n{}: {} user group '{}'",
muted("INFO"),
colored("deleted", "red"),
colored("deleted", Color::Red),
bold(&user_group.name)
))
}
@@ -602,14 +603,14 @@ async fn set_users(
*has_error = true;
log.push_str(&format!(
"\n{}: failed to set users in group {} | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&user_group)
))
} else {
log.push_str(&format!(
"\n{}: {} user group '{}' users",
muted("INFO"),
colored("updated", "blue"),
colored("updated", Color::Blue),
bold(&user_group)
))
}
@@ -636,14 +637,14 @@ async fn run_update_permissions(
*has_error = true;
log.push_str(&format!(
"\n{}: failed to set permssion in group {} | target: {target:?} | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&user_group)
))
} else {
log.push_str(&format!(
"\n{}: {} user group '{}' permissions",
muted("INFO"),
colored("updated", "blue"),
colored("updated", Color::Blue),
bold(&user_group)
))
}
+34 -25
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use anyhow::Context;
use formatting::{bold, colored, muted, Color};
use monitor_client::{
api::write::{
CreateVariable, DeleteVariable, UpdateVariableDescription,
@@ -16,8 +17,6 @@ use resolver_api::Resolve;
use crate::state::{db_client, State};
use super::{bold, colored, muted};
pub struct ToUpdateItem {
pub variable: Variable,
pub update_value: bool,
@@ -66,7 +65,7 @@ pub async fn get_updates_for_view(
update.to_update += 1;
update.log.push_str(&format!(
"\n\n{}: variable: '{}'\n-------------------",
colored("UPDATE", "blue"),
colored("UPDATE", Color::Blue),
bold(&item.variable.name),
));
@@ -77,9 +76,9 @@ pub async fn get_updates_for_view(
"{}: 'value'\n{}: {}\n{}: {}",
muted("field"),
muted("from"),
colored(&original.value, "red"),
colored(&original.value, Color::Red),
muted("to"),
colored(&item.variable.value, "green")
colored(&item.variable.value, Color::Green)
))
}
@@ -88,9 +87,9 @@ pub async fn get_updates_for_view(
"{}: 'description'\n{}: {}\n{}: {}",
muted("field"),
muted("from"),
colored(&original.description, "red"),
colored(&original.description, Color::Red),
muted("to"),
colored(&item.variable.description, "green")
colored(&item.variable.description, Color::Green)
))
}
@@ -99,15 +98,25 @@ pub async fn get_updates_for_view(
}
None => {
update.to_create += 1;
update.log.push_str(&format!(
"\n\n{}: variable: {}\n{}: {}\n{}: {}",
colored("CREATE", "green"),
colored(&variable.name, "green"),
muted("description"),
variable.description,
muted("value"),
variable.value,
));
if variable.description.is_empty() {
update.log.push_str(&format!(
"\n\n{}: variable: {}\n{}: {}",
colored("CREATE", Color::Green),
colored(&variable.name, Color::Green),
muted("value"),
variable.value,
));
} else {
update.log.push_str(&format!(
"\n\n{}: variable: {}\n{}: {}\n{}: {}",
colored("CREATE", Color::Green),
colored(&variable.name, Color::Green),
muted("description"),
variable.description,
muted("value"),
variable.value,
));
}
}
}
}
@@ -115,7 +124,7 @@ pub async fn get_updates_for_view(
for name in &to_delete {
update.log.push_str(&format!(
"\n\n{}: variable: '{}'\n-------------------",
colored("DELETE", "red"),
colored("DELETE", Color::Red),
bold(name),
));
}
@@ -202,14 +211,14 @@ pub async fn run_updates(
has_error = true;
log.push_str(&format!(
"\n{}: failed to create variable '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&variable.name)
));
} else {
log.push_str(&format!(
"\n{}: {} variable '{}'",
muted("INFO"),
colored("created", "green"),
colored("created", Color::Green),
bold(&variable.name)
))
};
@@ -235,14 +244,14 @@ pub async fn run_updates(
has_error = true;
log.push_str(&format!(
"\n{}: failed to update variable value for '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&variable.name)
))
} else {
log.push_str(&format!(
"\n{}: {} variable '{}' value",
muted("INFO"),
colored("updated", "blue"),
colored("updated", Color::Blue),
bold(&variable.name)
))
};
@@ -261,14 +270,14 @@ pub async fn run_updates(
has_error = true;
log.push_str(&format!(
"\n{}: failed to update variable description for '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&variable.name)
))
} else {
log.push_str(&format!(
"\n{}: {} variable '{}' description",
muted("INFO"),
colored("updated", "blue"),
colored("updated", Color::Blue),
bold(&variable.name)
))
};
@@ -288,14 +297,14 @@ pub async fn run_updates(
has_error = true;
log.push_str(&format!(
"\n{}: failed to delete variable '{}' | {e:#}",
colored("ERROR", "red"),
colored("ERROR", Color::Red),
bold(&variable)
))
} else {
log.push_str(&format!(
"\n{}: {} variable '{}'",
muted("INFO"),
colored("deleted", "red"),
colored("deleted", Color::Red),
bold(&variable)
))
}
+4 -3
View File
@@ -5,7 +5,6 @@ use std::{net::SocketAddr, str::FromStr};
use anyhow::Context;
use axum::Router;
use termination_signal::tokio::immediate_term_handle;
use tower_http::{
cors::{Any, CorsLayer},
services::{ServeDir, ServeFile},
@@ -77,13 +76,15 @@ async fn app() -> anyhow::Result<()> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let term_signal = immediate_term_handle()?;
let mut term_signal = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
)?;
let app = tokio::spawn(app());
tokio::select! {
res = app => return res?,
_ = term_signal => {},
_ = term_signal.recv() => {},
}
Ok(())
-1
View File
@@ -52,7 +52,6 @@ pub async fn insert_repos_status_unknown(repos: Vec<Repo>) {
.insert(
repo.id.clone(),
CachedRepoStatus {
id: repo.id,
latest_hash: None,
latest_message: None,
}
+1 -3
View File
@@ -52,7 +52,6 @@ pub struct CachedDeploymentStatus {
#[derive(Default, Clone, Debug)]
pub struct CachedRepoStatus {
pub id: String,
pub latest_hash: Option<String>,
pub latest_message: Option<String>,
}
@@ -229,9 +228,8 @@ pub async fn update_cache_for_server(server: &Server) {
.unzip();
status_cache
.insert(
repo.id.clone(),
repo.id,
CachedRepoStatus {
id: repo.id,
latest_hash,
latest_message,
}
+1 -1
View File
@@ -410,7 +410,7 @@ pub async fn update<T: MonitorResource>(
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-500\">{from}</span>\n<span class=\"text-muted-foreground\">to</span>: <span class=\"text-green-500\">{to}</span>",
"\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>",
));
}
-1
View File
@@ -13,7 +13,6 @@ repository.workspace = true
monitor_client.workspace = true
logger.workspace = true
#
termination_signal.workspace = true
mungos.workspace = true
#
tokio.workspace = true
+1
View File
@@ -1,2 +1,3 @@
#[allow(unused)]
pub mod v0;
pub mod v1_6;
-1
View File
@@ -25,7 +25,6 @@ serror = { workspace = true, features = ["axum"] }
async_timing_util.workspace = true
merge_config_files.workspace = true
parse_csl.workspace = true
termination_signal.workspace = true
run_command.workspace = true
svi.workspace = true
resolver_api.workspace = true
+7 -4
View File
@@ -51,14 +51,17 @@ impl Resolve<GetContainerLogSearch> for State {
)]
async fn resolve(
&self,
req: GetContainerLogSearch,
GetContainerLogSearch {
name,
terms,
combinator,
invert,
}: GetContainerLogSearch,
_: (),
) -> anyhow::Result<Log> {
Ok(
docker::container::container_log_search(
&req.name,
&req.terms,
req.combinator,
&name, &terms, combinator, invert,
)
.await,
)
+7 -2
View File
@@ -27,13 +27,18 @@ pub async fn container_log_search(
container_name: &str,
terms: &[String],
combinator: SearchCombinator,
invert: bool,
) -> Log {
let maybe_invert = invert.then_some(" -v").unwrap_or_default();
let grep = match combinator {
SearchCombinator::Or => {
format!("grep -E '{}'", terms.join("|"))
format!("grep{maybe_invert} -E '{}'", terms.join("|"))
}
SearchCombinator::And => {
format!("grep -P '^(?=.*{})'", terms.join(")(?=.*"))
format!(
"grep{maybe_invert} -P '^(?=.*{})'",
terms.join(")(?=.*")
)
}
};
let command =
+4 -3
View File
@@ -5,7 +5,6 @@ use std::{net::SocketAddr, str::FromStr};
use anyhow::Context;
use axum::{middleware, routing::post, Router};
use termination_signal::tokio::immediate_term_handle;
mod api;
mod config;
@@ -52,13 +51,15 @@ async fn app() -> anyhow::Result<()> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let term_signal = immediate_term_handle()?;
let mut term_signal = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
)?;
let app = tokio::spawn(app());
tokio::select! {
res = app => return res?,
_ = term_signal => {},
_ = term_signal.recv() => {},
}
Ok(())
-2
View File
@@ -13,8 +13,6 @@ repository.workspace = true
# local
monitor_client.workspace = true
logger.workspace = true
# mogh
termination_signal.workspace = true
# external
tokio.workspace = true
tracing.workspace = true
+5 -4
View File
@@ -2,7 +2,6 @@
extern crate tracing;
use monitor_client::MonitorClient;
use termination_signal::tokio::immediate_term_handle;
async fn app() -> anyhow::Result<()> {
logger::init(&Default::default())?;
@@ -27,13 +26,15 @@ async fn app() -> anyhow::Result<()> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let term_signal = immediate_term_handle()?;
let mut term_signal = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
)?;
let app = tokio::spawn(app());
tokio::select! {
res = app => return res?,
_ = term_signal => {},
res = app => return res?,
_ = term_signal.recv() => {},
}
Ok(())
+76
View File
@@ -0,0 +1,76 @@
# Changelog
## <ins>v1 (Spring 2024)</ins>
- **New resource types**:
- **Repo**: Clone / pull configured repositories on desired Server. Run shell commands in the repo on every clone / pull to acheive automation. Listen for pushes to a particular branch to automatically pull the repo and run the command.
- **Procedure**: Combine multiple *executions* (Deploy, RunBuild) and run them in sequence or in parallel. *RunProcedure* is an execution type, meaning procedures can run *other procedures*.
- **Builder**: Ephemeral builder configuration has moved to being an API / UI managed entity for greater observability and ease of management.
- **Alerter**: Define multiple alerting endpoints and manage them via the API / UI.
- Slack support continues with the *Slack* Alerter variant.
- Send JSON serialized alert data to any HTTP endpoint with the *Custom* Alerter variant.
- **Template**: Define a template for your cloud provider's VM configuration
- Launch VMs based on the template and automatically add them as Monitor servers.
- Supports AWS EC2 and Hetzner Cloud.
- **Sync**: Sync resources declared in toml files in Github repos.
- Manage resources declaratively, with git history for configuration rollbacks.
- See the actions which will be performed in the UI, and execute them upon manual confirmation.
- Use a Github webhook to automatically execute syncs on git push.
- **Resource Tagging**
- Attach multiple *tags* to resources, which can be used to group related resources together. These can be used to filter resources in the UI.
- For example, resources can be given tags for *environment*, like `Prod`, `Uat`, or `Dev`. This can be combined with tags for the larger system the resource is a part of, such as `Authentication`, `Logging`, or `Messaging`.
- Proper tagging will make it easy to find resources and visualize system components, even as the number of resources grows large.
- **Variables**
- Manage global, non-secret key-value pairs using the API / UI.
- These values can be interpolated into deployment `environments` and build `build_args`
- **Core Accounts and Secrets**
- Docker / Github accounts and Secrets can now be configured in the Core configuration file.
- They can still be added to the Periphery configuration as before. Accounts / Secrets defined in the Core configuration will be preferentially used over like ones defined in Periphery configuration.
- **User Groups**
- Admins can now create User Groups and assign permissions to them as if they were a user.
- Multiple users can then be added to the group, and a user can be added to multiple groups.
- Users in the group inherit the group's permissions.
- **Builds**
- Build log displays the **latest commit hash and message**.
- In-progress builds are able to be **cancelled before completion**.
- Specify a specific commit hash to build from.
- **Deployments**
- Filter log lines using multiple search terms.
- **Alerting**
- The alerting system has been redesigned with a stateful model.
- Alerts can be in an Open or a Resolved state, and alerts are only sent on state changes.
- For example, say a server has just crossed 80% memory usage, the configured memory threshold. An alert will be created in the Open state and the alert data will be sent out. Later, it has dropped down to 70%. The alert will be changed to the Resolved state and the alert data will again be sent.
- In addition to server usage alerts, Monitor now supports deployment state change alerts. These are sent when a deployment's state changes without being caused by a Monitor action. For example, if a deployment goes from the Running state to the Exited state unexpectedly, say from a crash, an alert will be sent.
- Current and past alerts can be retrieved using the API and viewed on the UI.
- **New UI**:
- The Monitor UI has been revamped to support the new features and improve the user experience.
## <ins>v0 (Winter 2022)</ins>
- Move Core and Periphery implementation to Rust.
- Add AWS Ec2 ephemeral build instance support.
- Configuration added to core configuration file.
- Automatic build versioning system, supporting image rollback.
- Realtime and historical system stats - CPU, memory, disk.
- Simple stats alerting based on out-of-bounds values for system stats.
- Support sending alerts to Slack.
## <ins>Pre-versioned releases</ins>
- Defined main resource types:
- Server
- Deployment
- Build
- Basics of Monitor:
- Build docker images from Github repos.
- Manage image deployment on connected servers, see container status, get container logs.
- Add account credentials in Periphery configuration.
- Core and Periphery implemented in Typescript.
+2
View File
@@ -196,6 +196,8 @@ pub struct GetBuildVersions {
pub minor: Option<i32>,
/// Filter to only include versions matching this patch version.
pub patch: Option<i32>,
/// Limit the number of included results. Default is no limit.
pub limit: Option<I64>,
}
#[typeshare]
@@ -153,6 +153,9 @@ pub struct SearchLog {
/// - `OR`: Include lines that have one or more matches in the terms.
#[serde(default)]
pub combinator: SearchCombinator,
/// Invert the results, ie return all lines that DON'T match the terms / combinator.
#[serde(default)]
pub invert: bool,
}
#[typeshare]
+12 -4
View File
@@ -299,11 +299,15 @@ pub fn conversions_to_string(conversions: &[Conversion]) -> String {
pub fn conversions_from_str(
value: &str,
) -> anyhow::Result<Vec<Conversion>> {
let res = value
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(Vec::new());
}
let res = trimmed
.split('\n')
.map(|line| line.trim())
.enumerate()
.filter(|(_, line)| !line.starts_with('#'))
.filter(|(_, line)| !line.is_empty() && !line.starts_with('#'))
.map(|(i, line)| {
let mut split = line.split('=');
let local = split
@@ -604,11 +608,15 @@ pub fn term_signal_labels_to_string(
pub fn term_signal_labels_from_str(
value: &str,
) -> anyhow::Result<Vec<TerminationSignalLabel>> {
let res = value
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(Vec::new());
}
let res = trimmed
.split('\n')
.map(|line| line.trim())
.enumerate()
.filter(|(_, line)| !line.starts_with('#'))
.filter(|(_, line)| !line.is_empty() && !line.starts_with('#'))
.map(|(i, line)| {
let mut split = line.split('=');
let signal = split
+11 -3
View File
@@ -44,7 +44,7 @@ pub mod resource;
pub mod server;
/// Subtypes of [ServerTemplate][server_template::ServerTemplate].
pub mod server_template;
/// Subtypes of [mod@Sync][sync::Sync]
/// Subtypes of [ResourceSync][sync::ResourceSync]
pub mod sync;
/// Subtypes of [Tag][tag::Tag].
pub mod tag;
@@ -347,11 +347,15 @@ pub fn environment_vars_to_string(vars: &[EnvironmentVar]) -> String {
pub fn environment_vars_from_str(
value: &str,
) -> anyhow::Result<Vec<EnvironmentVar>> {
let res = value
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(Vec::new());
}
let res = trimmed
.split('\n')
.map(|line| line.trim())
.enumerate()
.filter(|(_, line)| !line.starts_with('#'))
.filter(|(_, line)| !line.is_empty() && !line.starts_with('#'))
.map(|(i, line)| {
let mut split = line.split('=');
let variable = split
@@ -496,6 +500,7 @@ pub struct CloneArgs {
pub repo: Option<String>,
pub branch: Option<String>,
pub commit: Option<String>,
pub destination: Option<String>,
pub on_clone: Option<SystemCommand>,
pub on_pull: Option<SystemCommand>,
pub github_account: Option<String>,
@@ -508,6 +513,7 @@ impl From<&self::build::Build> for CloneArgs {
repo: optional_string(&build.config.repo),
branch: optional_string(&build.config.branch),
commit: optional_string(&build.config.commit),
destination: None,
on_clone: build.config.pre_build.clone().into_option(),
on_pull: None,
github_account: optional_string(&build.config.github_account),
@@ -522,6 +528,7 @@ impl From<&self::repo::Repo> for CloneArgs {
repo: optional_string(&repo.config.repo),
branch: optional_string(&repo.config.branch),
commit: optional_string(&repo.config.commit),
destination: optional_string(&repo.config.path),
on_clone: repo.config.on_clone.clone().into_option(),
on_pull: repo.config.on_pull.clone().into_option(),
github_account: optional_string(&repo.config.github_account),
@@ -537,6 +544,7 @@ impl From<&self::sync::ResourceSync> for CloneArgs {
branch: optional_string(&sync.config.branch),
commit: optional_string(&sync.config.commit),
github_account: optional_string(&sync.config.github_account),
destination: None,
on_clone: None,
on_pull: None,
}
+6
View File
@@ -99,6 +99,11 @@ pub struct RepoConfig {
#[builder(default)]
pub github_account: String,
/// Explicitly specificy the folder to clone the repo in.
#[serde(default)]
#[builder(default)]
pub path: String,
/// Command to be run after the repo is cloned.
/// The path is relative to the root of the repo.
#[serde(default)]
@@ -140,6 +145,7 @@ impl Default for RepoConfig {
branch: default_branch(),
commit: Default::default(),
github_account: Default::default(),
path: Default::default(),
on_clone: Default::default(),
on_pull: Default::default(),
webhook_enabled: default_webhook_enabled(),
+23 -5
View File
@@ -97,19 +97,37 @@ pub struct ResourceToml<PartialConfig: Default> {
/// The resource name. Required
pub name: String,
/// The resource description.
#[serde(default)]
/// The resource description. Optional.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
/// Tag ids or names
#[serde(default)]
/// Tag ids or names. Optional
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
/// Resource specific configuration
/// Optional. Only relevant for deployments.
///
/// Will ensure deployment is running with the latest configuration.
/// Deploy actions to achieve this will be included in the sync.
#[serde(default, skip_serializing_if = "is_false")]
pub deploy: bool,
/// Optional. Only relevant for deployments using the 'deploy' sync feature.
///
/// Specify other deployments as dependencies.
/// The sync will ensure the deployment will only be deployed 'after' its dependencies.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub after: Vec<String>,
/// Resource specific configuration.
#[serde(default)]
pub config: PartialConfig,
}
fn is_false(b: &bool) -> bool {
!b
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserGroupToml {
/// User group name
+4 -1
View File
@@ -25,9 +25,12 @@ function fix_types() {
const contents = readFileSync(types_path);
const fixed = contents
.toString()
// Replace Variants
.replaceAll("ResourceTargetVariant", 'ResourceTarget["type"]')
.replaceAll("AlerterEndpointVariant", 'AlerterEndpoint["type"]')
.replaceAll("AlertDataVariant", 'AlertData["type"]')
.replaceAll("ServerTemplateConfigVariant", 'ServerTemplateConfig["type"]');
.replaceAll("ServerTemplateConfigVariant", 'ServerTemplateConfig["type"]')
// Add '| string' to env vars
.replaceAll("EnvironmentVar[]", "EnvironmentVar[] | string");
writeFileSync(types_path, fixed);
}
+69 -62
View File
@@ -236,7 +236,7 @@ export interface Resource<Config, Info> {
/** Resource-specific information (not user configurable). */
info?: Info;
/** Resource-specific configuration. */
config: Config;
config?: Config;
}
export type AlerterEndpoint =
@@ -248,6 +248,12 @@ export type AlerterEndpoint =
export interface AlerterConfig {
/** Whether the alerter is enabled */
enabled: boolean;
/**
* Where to route the alert messages.
*
* Default: Custom endpoint `http://localhost:7000`
*/
endpoint?: AlerterEndpoint;
/**
* Only send specific alert types.
* If empty, will send all alert types.
@@ -258,12 +264,6 @@ export interface AlerterConfig {
* If empty, will send alerts for all resources.
*/
resources?: ResourceTarget[];
/**
* Where to route the alert messages.
*
* Default: Custom endpoint `http://localhost:7000`
*/
endpoint?: AlerterEndpoint;
}
export type Alerter = Resource<AlerterConfig, undefined>;
@@ -307,11 +307,6 @@ export interface SystemCommand {
command?: string;
}
export interface EnvironmentVar {
variable: string;
value: string;
}
/** Configuration for the registry to push the built image to. */
export type ImageRegistry =
/** Don't push the image to any registry */
@@ -328,12 +323,15 @@ export type ImageRegistry =
/** Todo. Will point to a custom "Registry" resource by id */
| { type: "Custom", params: string };
export interface EnvironmentVar {
variable: string;
value: string;
}
/** The build configuration. */
export interface BuildConfig {
/** Which builder is used to build the image. */
builder_id?: string;
/** Whether to skip secret interpolation in the build_args. */
skip_secret_interp?: boolean;
/** The current version of the build. */
version?: Version;
/** The Github repo used as the source of the build. */
@@ -349,6 +347,8 @@ export interface BuildConfig {
github_account?: string;
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/** Configuration for the registry to push the built image to. */
image_registry?: ImageRegistry;
/**
* The path of the docker build context relative to the root of the repo.
* Default: "." (the root of the repo).
@@ -356,18 +356,18 @@ export interface BuildConfig {
build_path: string;
/** The path of the dockerfile relative to the build path. */
dockerfile_path: string;
/** Docker build arguments */
build_args?: EnvironmentVar[];
/** Docker labels */
labels?: EnvironmentVar[];
/** Any extra docker cli arguments to be included in the build command */
extra_args?: string[];
/** Whether to skip secret interpolation in the build_args. */
skip_secret_interp?: boolean;
/** Whether to use buildx to build (eg `docker buildx build ...`) */
use_buildx?: boolean;
/** Configuration for the registry to push the built image to. */
image_registry?: ImageRegistry;
/** Whether incoming webhooks actually trigger action. */
webhook_enabled: boolean;
/** Any extra docker cli arguments to be included in the build command */
extra_args?: string[];
/** Docker build arguments */
build_args?: EnvironmentVar[] | string;
/** Docker labels */
labels?: EnvironmentVar[] | string;
}
export interface BuildInfo {
@@ -465,6 +465,13 @@ export type DeploymentImage =
version?: Version;
}};
export enum RestartMode {
NoRestart = "no",
OnFailure = "on-failure",
Always = "always",
UnlessStopped = "unless-stopped",
}
export enum TerminationSignal {
SigHup = "SIGHUP",
SigInt = "SIGINT",
@@ -484,18 +491,9 @@ export interface Conversion {
container: string;
}
export enum RestartMode {
NoRestart = "no",
OnFailure = "on-failure",
Always = "always",
UnlessStopped = "unless-stopped",
}
export interface DeploymentConfig {
/** The id of server the deployment is deployed on. */
server_id?: string;
/** Whether to send ContainerStateChange alerts for this deployment. */
send_alerts: boolean;
/**
* The image which the deployment deploys.
* Can either be a user inputted image, or a Monitor build.
@@ -515,30 +513,8 @@ export interface DeploymentConfig {
skip_secret_interp?: boolean;
/** Whether to redeploy the deployment whenever the attached build finishes. */
redeploy_on_build?: boolean;
/**
* Labels attached to various termination signal options.
* Used to specify different shutdown functionality depending on the termination signal.
*/
term_signal_labels: TerminationSignalLabel[];
/** The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). */
termination_signal?: TerminationSignal;
/** The termination timeout. */
termination_timeout: number;
/**
* The container port mapping.
* Irrelevant if container network is `host`.
* Maps ports on host to ports on container.
*/
ports?: Conversion[];
/**
* The container volume mapping.
* Maps files / folders on host to files / folders in container.
*/
volumes?: Conversion[];
/** The environment variables passed to the container. */
environment?: EnvironmentVar[];
/** The docker labels given to the container. */
labels?: EnvironmentVar[];
/** Whether to send ContainerStateChange alerts for this deployment. */
send_alerts: boolean;
/**
* The network attached to the container.
* Default is `host`.
@@ -553,11 +529,35 @@ export interface DeploymentConfig {
* Empty is no command.
*/
command?: string;
/** The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal). */
termination_signal?: TerminationSignal;
/** The termination timeout. */
termination_timeout: number;
/**
* Extra args which are interpolated into the `docker run` command,
* and affect the container configuration.
*/
extra_args?: string[];
/**
* Labels attached to various termination signal options.
* Used to specify different shutdown functionality depending on the termination signal.
*/
term_signal_labels: TerminationSignalLabel[];
/**
* The container port mapping.
* Irrelevant if container network is `host`.
* Maps ports on host to ports on container.
*/
ports?: Conversion[];
/**
* The container volume mapping.
* Maps files / folders on host to files / folders in container.
*/
volumes?: Conversion[];
/** The environment variables passed to the container. */
environment?: EnvironmentVar[] | string;
/** The docker labels given to the container. */
labels?: EnvironmentVar[] | string;
}
export type Deployment = Resource<DeploymentConfig, undefined>;
@@ -782,6 +782,8 @@ export interface RepoConfig {
* It must be available in the server's periphery config.
*/
github_account?: string;
/** Explicitly specificy the folder to clone the repo in. */
path?: string;
/**
* Command to be run after the repo is cloned.
* The path is relative to the root of the repo.
@@ -857,6 +859,8 @@ export interface ServerConfig {
* Example: http://localhost:8120
*/
address: string;
/** An optional region label */
region?: string;
/**
* Whether a server is enabled.
* If a server is disabled,
@@ -882,8 +886,6 @@ export interface ServerConfig {
send_mem_alerts: boolean;
/** Whether to send alerts about the servers DISK status */
send_disk_alerts: boolean;
/** An optional region label */
region?: string;
/** The percentage threshhold which triggers WARNING state for CPU. */
cpu_warning: number;
/** The percentage threshhold which triggers CRITICAL state for CPU. */
@@ -2039,6 +2041,8 @@ export interface GetBuildVersions {
minor?: number;
/** Filter to only include versions matching this patch version. */
patch?: number;
/** Limit the number of included results. Default is no limit. */
limit?: I64;
}
/**
@@ -2193,6 +2197,8 @@ export interface SearchLog {
* - `OR`: Include lines that have one or more matches in the terms.
*/
combinator?: SearchCombinator;
/** Invert the results, ie return all lines that DON'T match the terms / combinator. */
invert?: boolean;
}
/**
@@ -3630,11 +3636,6 @@ export interface AwsBuilderConfig {
ami_id: string;
/** The subnet id to create the instance in. */
subnet_id: string;
/**
* The security group ids to attach to the instance.
* This should include a security group to allow core inbound access to the periphery port.
*/
security_group_ids: string[];
/** The key pair name to attach to the instance */
key_pair_name: string;
/**
@@ -3647,6 +3648,11 @@ export interface AwsBuilderConfig {
* If false, core will communicate with the instance using the private IP.
*/
use_public_ip: boolean;
/**
* The security group ids to attach to the instance.
* This should include a security group to allow core inbound access to the periphery port.
*/
security_group_ids: string[];
/** Which github accounts (usernames) are available on the AMI */
github_accounts?: string[];
/** Which dockerhub accounts (usernames) are available on the AMI */
@@ -3658,6 +3664,7 @@ export interface CloneArgs {
repo?: string;
branch?: string;
commit?: string;
destination?: string;
on_clone?: SystemCommand;
on_pull?: SystemCommand;
github_account?: string;
+2
View File
@@ -36,6 +36,8 @@ pub struct GetContainerLogSearch {
pub terms: Vec<String>,
#[serde(default)]
pub combinator: SearchCombinator,
#[serde(default)]
pub invert: bool,
}
//
+26 -2
View File
@@ -4,7 +4,31 @@ To run Monitor Core, you will need:
- A valid configuration file.
- An instance of MongoDB to which Core can connect.
- Docker must be installed on the host.
- Docker must be installed on the host. See [the install docs](https://docs.docker.com/engine/install/).
## Mongo
Mongo can be run locally using the docker cli:
```sh
docker run --name monitor-mongo \
--network host \
-v /local/storage/path:/data/db \
-e MONGO_INITDB_ROOT_USERNAME="admin" \
-e MONGO_INITDB_ROOT_PASSWORD="admin" \
mongo:latest
```
You should replace the username and password with your own.
See [the image docs](https://hub.docker.com/_/mongo) for more details.
:::note
The disk space requirements of Monitor are dominated by the storage of system stats.
This depends on the number of connected servers (more system stats being produces / stored), stats collection frequency, and your stats pruning configuration.
If you need to save on space, you can configure these fields in your core config:
- Stats poll frequency can be reduced using, for example, `monitoring_interval = "15-sec"`
- Pruning can be tuned more aggresively using, for example, `keep_stats_for_days = 7`.
:::
## 1. Create the configuration file
@@ -29,8 +53,8 @@ Monitor core is distributed via Github Container Registry under the package [mbe
```sh
docker run -d --name monitor-core \
--network host \
-v $HOME/.monitor/core.config.toml:/config/config.toml \
-p 9000:9000 \
ghcr.io/mbecker20/monitor_core
```
+5 -5
View File
@@ -8,11 +8,11 @@ Monitor is a web app to provide structure for managing your servers, builds, dep
With Monitor you can:
1. Build application source into auto-versioned images.
2. Create, start, stop, and restart Docker containers, and view their status and logs.
3. Manage repositories on remote servers.
4. Keep a record of all the actions that are performed and by whom.
5. View realtime and historical system resource usage, and alert for out of bounds values.
1. Connect all of your remote servers (across clouds + on premise) to be viewable from a unified 'monitor' (hence the name).
2. Build application source into auto-versioned Docker images.
3. Create, start, stop, and restart Docker containers on the connected servers, and view their status and logs.
4. Manage repositories on connected servers.
5. Keep a record of all the actions that are performed and by whom.
## Docker
View File
+27
View File
@@ -0,0 +1,27 @@
# Resources
Entities like `Server`, `Deployment`, and `Build` are abstracted under a `Resource`. A server is a type of resource, a build is a type of resource, and so on.
All resources have common traits, such as a globally unique `name` and `id` amongst all other resources of the same resource type.
All resources can also be assigned `tags`, which can be used to group related resources.
For example, they can be grouped by environment (using `dev` or `prod` tags), or by function (`auth` or `ingress`),
or really by any way that fits best for your infra (they are all user defined).
Here is a list of the resources and their description:
- `Server`: Represents a connected server.
- Holds server config, like the address.
- `Deployment`: Represents a docker container on a server, whether it is actually deployed or not.
- Holds deployment config, like the server it should deploy on, and the image / build to deploy.
- `Build`: Represents a docker image.
- Holds build config, like the source repo, Dockerfile location, and version
- `Repo`: Represents a repo on a server, whether it is cloned or not.
- Holds repo config, like the source repo, and the `on_clone` and `on_pull` commands, which run after the repo is cloned / pulled
- `Procedure`: Configure higher level actions by composing lower level actions.
- Holds the actions to execute, like `RunBuild build_1` and `Deploy deployment_1`, and the order to execute them
- `Alerter`: Route the various alerts produced by monitor to alerting endpoints
- Holds the alerting endpoint (Slack channel or Custom http POST), the alerting types to forward (eg. `ServerUnreachable` or `ContainerStateChange`).
- `Builder`: Represents a server used as the "builder" for builds. Can be connected server or ephemeral AWS server.
- Holds builder config, like the AWS ami-id and security groups to allow for builder reachability.
- `ServerTemplate`: Configure cloud server templates (currently AWS and Hetzner) to easily launch more instances and auto connect them to Monitor
- Holds the cloud server config
- `ResourceSync`: Declare Monitor resources in TOML files, push them to a Github Repo, and sync Monitor config from them.
- Holds config for the source repo containing the files. Will display the computed diff and wait for user to execute.
+156 -2
View File
@@ -1,4 +1,158 @@
# Sync Resources
The [Montior CLI](https://crates.io/crates/monitor_cli) is able to sync Monitor resources declared in TOML files by diffing them against the resources
returned by the API.
Monitor is able to sync resources declared in TOML files by diffing them against the existing resources,
and apply updates based on the diffs. Simply use a Github repo to manage the files and create a `ResourceSync` pointing to the repo,
and the core backend will poll for any updates (you can also manually trigger an update poll / execution in the UI).
File detection is additive and recursive, so you can spread out your resource declarations across any number of files
and use any nesting of folders to organize resources inside a root folder. Additionally, you can create multiple `ResourceSyncs`
and each sync will be handled independently. This allows different syncs to manage resources on a "per-project" basis.
The UI will display the computed sync actions and only execute them upon manual confirmation.
Or the sync execution Github webhook may be configured on the Github repo to
automatically execute syncs upon pushes to the configured branch.
## Example file:
```toml
[[server]] # Declare a new server
name = "server-01"
description = "the main mogh server"
tags = ["monitor"]
config.address = "http://localhost:8120"
config.region = "AshburnDc1"
config.enabled = true
##
[[build]]
name = "test_logger"
description = "Logs randomly at INFO, WARN, ERROR levels to test logging setups"
tags = ["test"]
config.builder_id = "builder-01"
config.repo = "mbecker20/test_logger"
config.branch = "master"
config.github_account = "mbecker20"
config.image_registry.type = "Ghcr"
config.image_registry.params.account = "your_github_account"
# Set docker labels
config.labels = """
org.opencontainers.image.source = https://github.com/mbecker20/test_logger
org.opencontainers.image.description = Logs randomly at INFO, WARN, ERROR levels to test logging setups
org.opencontainers.image.licenses = GPL-3.0"""
##
[[variable]] # Declare variables
name = "OTLP_ENDPOINT"
value = "http://localhost:4317"
##
[[deployment]] # Declare a deployment
name = "test-logger-01"
description = "test logger deployment 1"
tags = ["test"]
# sync will deploy the container:
# - if it is not running.
# - has relevant config updates.
# - the attached build has new version.
deploy = true
config.server_id = "server-01"
config.image.type = "Build"
config.image.params.build = "test_logger"
# set the volumes / bind mounts
config.volumes = """
/data/logs = /etc/logs
/data/config = /etc/config"""
# Set the environment variables
config.environment = """
OTLP_ENDPOINT = [[OTLP_ENDPOINT]] # interpolate variables into the envs. (they also support comments using '#')
VARIABLE_1 = value_1
VARIABLE_2 = value_2"""
# Set Docker labels
config.labels = "deployment.type = logger"
##
[[deployment]]
name = "test-logger-02"
description = "test logger deployment 2"
tags = ["test"]
deploy = true
# Create a dependency on test-logger-01. This deployment will only be deployed after test-logger-01 is deployed.
# Additionally, any sync deploy of test-logger-01 will also trigger sync deploy of this deployment.
after = ["test-logger-01"]
config.server_id = "server-01"
config.image.type = "Build"
config.image.params.build = "test_logger"
config.volumes = """
/data/logs = /etc/logs
/data/config = /etc/config"""
config.environment = """
VARIABLE_1 = value_1
VARIABLE_2 = value_2"""
# Set Docker labels
config.labels = "deployment.type = logger"
##
[[repo]]
name = "monitor-periphery"
description = "Builds new versions of the periphery binary. Requires Rust installed on the host."
tags = ["monitor"]
config.server_id = "server-01"
config.repo = "mbecker20/monitor"
# Run an action after the repo is pulled
config.on_pull.path = "."
config.on_pull.command = "/root/.cargo/bin/cargo build -p monitor_periphery --release && cp ./target/release/periphery /root/periphery"
##
[[procedure]]
name = "test-procedure"
description = "Do some things in a specific order"
tags = ["test"]
# Each stage will be executed one after the other (in sequence)
[[procedure.config.stage]]
name = "Build stuff"
enabled = true
# The executions within a stage will be run in parallel. The stage completes when all executions finish.
executions = [
{ execution.type = "RunBuild", execution.params.build = "test_logger", enabled = true },
{ execution.type = "PullRepo", execution.params.repo = "monitor-periphery", enabled = true },
]
[[procedure.config.stage]]
name = "Deploy test logger 1"
enabled = true
executions = [
{ execution.type = "Deploy", execution.params.deployment = "test-logger-01", enabled = true }
]
[[procedure.config.stage]]
name = "Deploy test logger 2"
enabled = true
executions = [
{ execution.type = "Deploy", execution.params.deployment = "test-logger-02", enabled = true }
]
##
[[builder]] # Declare a builder
name = "builder-01"
tags = []
config.type = "Aws"
config.params.region = "us-east-2"
config.params.ami_id = "ami-0e9bd154667944680"
# These things come from your specific setup
config.params.subnet_id = "subnet-xxxxxxxxxxxxxxxxxx"
config.params.key_pair_name = "xxxxxxxx"
config.params.assign_public_ip = true
config.params.use_public_ip = true
config.params.security_group_ids = [
"sg-xxxxxxxxxxxxxxxxxx",
"sg-xxxxxxxxxxxxxxxxxx"
]
```
+213 -210
View File
@@ -8,20 +8,20 @@
"name": "docsite",
"version": "0.0.0",
"dependencies": {
"@docusaurus/core": "^3.3.2",
"@docusaurus/preset-classic": "^3.3.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"@docusaurus/core": "^3.4.0",
"@docusaurus/preset-classic": "^3.4.0",
"@mdx-js/react": "^3.0.1",
"clsx": "^2.1.1",
"prism-react-renderer": "^2.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.3.2",
"@docusaurus/tsconfig": "^3.3.2",
"@docusaurus/types": "^3.3.2",
"@docusaurus/module-type-aliases": "^3.4.0",
"@docusaurus/tsconfig": "^3.4.0",
"@docusaurus/types": "^3.4.0",
"dotenv": "^16.4.5",
"typescript": "~5.2.2"
"typescript": "^5.4.5"
},
"engines": {
"node": ">=18.0"
@@ -486,9 +486,9 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz",
"integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==",
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz",
"integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==",
"engines": {
"node": ">=6.9.0"
}
@@ -1578,11 +1578,11 @@
}
},
"node_modules/@babel/plugin-transform-react-constant-elements": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.1.tgz",
"integrity": "sha512-QXp1U9x0R7tkiGB0FOk8o74jhnap0FlZ5gNkRIWdG3eP+SvMFg118e1zaWewDzgABb106QSKpVsD3Wgd8t6ifA==",
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.7.tgz",
"integrity": "sha512-7LidzZfUXyfZ8/buRW6qIIHBY8wAZ1OrY9c/wTr8YhZ6vMPo+Uc/CVFLYY1spZrEQlD4w5u8wjqk5NQ3OVqQKA==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.24.0"
"@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -2135,9 +2135,9 @@
}
},
"node_modules/@docusaurus/core": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.3.2.tgz",
"integrity": "sha512-PzKMydKI3IU1LmeZQDi+ut5RSuilbXnA8QdowGeJEgU8EJjmx3rBHNT1LxQxOVqNEwpWi/csLwd9bn7rUjggPA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz",
"integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==",
"dependencies": {
"@babel/core": "^7.23.3",
"@babel/generator": "^7.23.3",
@@ -2149,12 +2149,12 @@
"@babel/runtime": "^7.22.6",
"@babel/runtime-corejs3": "^7.22.6",
"@babel/traverse": "^7.22.8",
"@docusaurus/cssnano-preset": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/cssnano-preset": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.3",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -2220,9 +2220,9 @@
}
},
"node_modules/@docusaurus/cssnano-preset": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.3.2.tgz",
"integrity": "sha512-+5+epLk/Rp4vFML4zmyTATNc3Is+buMAL6dNjrMWahdJCJlMWMPd/8YfU+2PA57t8mlSbhLJ7vAZVy54cd1vRQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz",
"integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==",
"dependencies": {
"cssnano-preset-advanced": "^6.1.2",
"postcss": "^8.4.38",
@@ -2234,9 +2234,9 @@
}
},
"node_modules/@docusaurus/logger": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.3.2.tgz",
"integrity": "sha512-Ldu38GJ4P8g4guN7d7pyCOJ7qQugG7RVyaxrK8OnxuTlaImvQw33aDRwaX2eNmX8YK6v+//Z502F4sOZbHHCHQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz",
"integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==",
"dependencies": {
"chalk": "^4.1.2",
"tslib": "^2.6.0"
@@ -2246,13 +2246,13 @@
}
},
"node_modules/@docusaurus/mdx-loader": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.3.2.tgz",
"integrity": "sha512-AFRxj/aOk3/mfYDPxE3wTbrjeayVRvNSZP7mgMuUlrb2UlPRbSVAFX1k2RbgAJrnTSwMgb92m2BhJgYRfptN3g==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz",
"integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==",
"dependencies": {
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
@@ -2284,11 +2284,11 @@
}
},
"node_modules/@docusaurus/module-type-aliases": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.3.2.tgz",
"integrity": "sha512-b/XB0TBJah5yKb4LYuJT4buFvL0MGAb0+vJDrJtlYMguRtsEBkf2nWl5xP7h4Dlw6ol0hsHrCYzJ50kNIOEclw==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz",
"integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==",
"dependencies": {
"@docusaurus/types": "3.3.2",
"@docusaurus/types": "3.4.0",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2302,17 +2302,17 @@
}
},
"node_modules/@docusaurus/plugin-content-blog": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.3.2.tgz",
"integrity": "sha512-fJU+dmqp231LnwDJv+BHVWft8pcUS2xVPZdeYH6/ibH1s2wQ/sLcmUrGWyIv/Gq9Ptj8XWjRPMghlxghuPPoxg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz",
"integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
@@ -2333,18 +2333,18 @@
}
},
"node_modules/@docusaurus/plugin-content-docs": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.3.2.tgz",
"integrity": "sha512-Dm1ri2VlGATTN3VGk1ZRqdRXWa1UlFubjaEL6JaxaK7IIFqN/Esjpl+Xw10R33loHcRww/H76VdEeYayaL76eg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz",
"integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",
@@ -2363,15 +2363,15 @@
}
},
"node_modules/@docusaurus/plugin-content-pages": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.3.2.tgz",
"integrity": "sha512-EKc9fQn5H2+OcGER8x1aR+7URtAGWySUgULfqE/M14+rIisdrBstuEZ4lUPDRrSIexOVClML82h2fDS+GSb8Ew==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz",
"integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
@@ -2385,13 +2385,13 @@
}
},
"node_modules/@docusaurus/plugin-debug": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.3.2.tgz",
"integrity": "sha512-oBIBmwtaB+YS0XlmZ3gCO+cMbsGvIYuAKkAopoCh0arVjtlyPbejzPrHuCoRHB9G7abjNZw7zoONOR8+8LM5+Q==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz",
"integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"fs-extra": "^11.1.1",
"react-json-view-lite": "^1.2.0",
"tslib": "^2.6.0"
@@ -2405,13 +2405,13 @@
}
},
"node_modules/@docusaurus/plugin-google-analytics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.3.2.tgz",
"integrity": "sha512-jXhrEIhYPSClMBK6/IA8qf1/FBoxqGXZvg7EuBax9HaK9+kL3L0TJIlatd8jQJOMtds8mKw806TOCc3rtEad1A==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz",
"integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"tslib": "^2.6.0"
},
"engines": {
@@ -2423,13 +2423,13 @@
}
},
"node_modules/@docusaurus/plugin-google-gtag": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.3.2.tgz",
"integrity": "sha512-vcrKOHGbIDjVnNMrfbNpRQR1x6Jvcrb48kVzpBAOsKbj9rXZm/idjVAXRaewwobHdOrJkfWS/UJoxzK8wyLRBQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz",
"integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@types/gtag.js": "^0.0.12",
"tslib": "^2.6.0"
},
@@ -2442,13 +2442,13 @@
}
},
"node_modules/@docusaurus/plugin-google-tag-manager": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.3.2.tgz",
"integrity": "sha512-ldkR58Fdeks0vC+HQ+L+bGFSJsotQsipXD+iKXQFvkOfmPIV6QbHRd7IIcm5b6UtwOiK33PylNS++gjyLUmaGw==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz",
"integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"tslib": "^2.6.0"
},
"engines": {
@@ -2460,16 +2460,16 @@
}
},
"node_modules/@docusaurus/plugin-sitemap": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.3.2.tgz",
"integrity": "sha512-/ZI1+bwZBhAgC30inBsHe3qY9LOZS+79fRGkNdTcGHRMcdAp6Vw2pCd1gzlxd/xU+HXsNP6cLmTOrggmRp3Ujg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz",
"integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"fs-extra": "^11.1.1",
"sitemap": "^7.1.1",
"tslib": "^2.6.0"
@@ -2483,23 +2483,23 @@
}
},
"node_modules/@docusaurus/preset-classic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.3.2.tgz",
"integrity": "sha512-1SDS7YIUN1Pg3BmD6TOTjhB7RSBHJRpgIRKx9TpxqyDrJ92sqtZhomDc6UYoMMLQNF2wHFZZVGFjxJhw2VpL+Q==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz",
"integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/plugin-debug": "3.3.2",
"@docusaurus/plugin-google-analytics": "3.3.2",
"@docusaurus/plugin-google-gtag": "3.3.2",
"@docusaurus/plugin-google-tag-manager": "3.3.2",
"@docusaurus/plugin-sitemap": "3.3.2",
"@docusaurus/theme-classic": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-search-algolia": "3.3.2",
"@docusaurus/types": "3.3.2"
"@docusaurus/core": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/plugin-debug": "3.4.0",
"@docusaurus/plugin-google-analytics": "3.4.0",
"@docusaurus/plugin-google-gtag": "3.4.0",
"@docusaurus/plugin-google-tag-manager": "3.4.0",
"@docusaurus/plugin-sitemap": "3.4.0",
"@docusaurus/theme-classic": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-search-algolia": "3.4.0",
"@docusaurus/types": "3.4.0"
},
"engines": {
"node": ">=18.0"
@@ -2510,22 +2510,22 @@
}
},
"node_modules/@docusaurus/theme-classic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.3.2.tgz",
"integrity": "sha512-gepHFcsluIkPb4Im9ukkiO4lXrai671wzS3cKQkY9BXQgdVwsdPf/KS0Vs4Xlb0F10fTz+T3gNjkxNEgSN9M0A==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz",
"integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0",
@@ -2549,17 +2549,17 @@
}
},
"node_modules/@docusaurus/theme-common": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.3.2.tgz",
"integrity": "sha512-kXqSaL/sQqo4uAMQ4fHnvRZrH45Xz2OdJ3ABXDS7YVGPSDTBC8cLebFrRR4YF9EowUHto1UC/EIklJZQMG/usA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz",
"integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==",
"dependencies": {
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2578,18 +2578,18 @@
}
},
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.3.2.tgz",
"integrity": "sha512-qLkfCl29VNBnF1MWiL9IyOQaHxUvicZp69hISyq/xMsNvFKHFOaOfk9xezYod2Q9xx3xxUh9t/QPigIei2tX4w==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz",
"integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==",
"dependencies": {
"@docsearch/react": "^3.5.2",
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"algoliasearch": "^4.18.0",
"algoliasearch-helper": "^3.13.3",
"clsx": "^2.0.0",
@@ -2608,9 +2608,9 @@
}
},
"node_modules/@docusaurus/theme-translations": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.3.2.tgz",
"integrity": "sha512-bPuiUG7Z8sNpGuTdGnmKl/oIPeTwKr0AXLGu9KaP6+UFfRZiyWbWE87ti97RrevB2ffojEdvchNujparR3jEZQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz",
"integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==",
"dependencies": {
"fs-extra": "^11.1.1",
"tslib": "^2.6.0"
@@ -2620,15 +2620,15 @@
}
},
"node_modules/@docusaurus/tsconfig": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.3.2.tgz",
"integrity": "sha512-2MQXkLoWqgOSiqFojNEq8iPtFBHGQqd1b/SQMoe+v3GgHmk/L6YTTO/hMcHhWb1hTFmbkei++IajSfD3RlZKvw==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.4.0.tgz",
"integrity": "sha512-0qENiJ+TRaeTzcg4olrnh0BQ7eCxTgbYWBnWUeQDc84UYkt/T3pDNnm3SiQkqPb+YQ1qtYFlC0RriAElclo8Dg==",
"dev": true
},
"node_modules/@docusaurus/types": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.3.2.tgz",
"integrity": "sha512-5p201S7AZhliRxTU7uMKtSsoC8mgPA9bs9b5NQg1IRdRxJfflursXNVsgc3PcMqiUTul/v1s3k3rXXFlRE890w==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz",
"integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==",
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
"@types/history": "^4.7.11",
@@ -2646,12 +2646,12 @@
}
},
"node_modules/@docusaurus/utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.3.2.tgz",
"integrity": "sha512-f4YMnBVymtkSxONv4Y8js3Gez9IgHX+Lcg6YRMOjVbq8sgCcdYK1lf6SObAuz5qB/mxiSK7tW0M9aaiIaUSUJg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz",
"integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==",
"dependencies": {
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@svgr/webpack": "^8.1.0",
"escape-string-regexp": "^4.0.0",
"file-loader": "^6.2.0",
@@ -2668,6 +2668,7 @@
"shelljs": "^0.8.5",
"tslib": "^2.6.0",
"url-loader": "^4.1.1",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"engines": {
@@ -2683,9 +2684,9 @@
}
},
"node_modules/@docusaurus/utils-common": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.3.2.tgz",
"integrity": "sha512-QWFTLEkPYsejJsLStgtmetMFIA3pM8EPexcZ4WZ7b++gO5jGVH7zsipREnCHzk6+eDgeaXfkR6UPaTt86bp8Og==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz",
"integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==",
"dependencies": {
"tslib": "^2.6.0"
},
@@ -2702,15 +2703,17 @@
}
},
"node_modules/@docusaurus/utils-validation": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.3.2.tgz",
"integrity": "sha512-itDgFs5+cbW9REuC7NdXals4V6++KifgVMzoGOOOSIifBQw+8ULhy86u5e1lnptVL0sv8oAjq2alO7I40GR7pA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz",
"integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==",
"dependencies": {
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"fs-extra": "^11.2.0",
"joi": "^17.9.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"tslib": "^2.6.0"
},
"engines": {
@@ -3415,9 +3418,9 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"node_modules/@types/mdast": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz",
"integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"dependencies": {
"@types/unist": "*"
}
@@ -3887,9 +3890,9 @@
}
},
"node_modules/algoliasearch-helper": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.19.0.tgz",
"integrity": "sha512-AaSb5DZDMZmDQyIy6lf4aL0OZGgyIdqvLIIvSuVQOIOqfhrYSY7TvotIFI2x0Q3cP3xUpTd7lI1astUC4aXBJw==",
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.21.0.tgz",
"integrity": "sha512-hjVOrL15I3Y3K8xG0icwG1/tWE+MocqBrhW6uVBWpU+/kVEMK0BnM2xdssj6mZM61eJ4iRxHR0djEI3ENOpR8w==",
"dependencies": {
"@algolia/events": "^4.0.1"
},
@@ -4238,11 +4241,11 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -6227,9 +6230,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -8145,9 +8148,9 @@
}
},
"node_modules/mdast-util-from-markdown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz",
"integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz",
"integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==",
"dependencies": {
"@types/mdast": "^4.0.0",
"@types/unist": "^3.0.0",
@@ -8427,9 +8430,9 @@
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz",
"integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==",
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
@@ -12507,9 +12510,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sax": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
},
"node_modules/scheduler": {
"version": "0.23.2",
@@ -12538,9 +12541,9 @@
}
},
"node_modules/search-insights": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz",
"integrity": "sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.14.0.tgz",
"integrity": "sha512-OLN6MsPMCghDOqlCtsIsYgtsC0pnwVTyT9Mu6A3ewOj1DxvzZF6COrn2g86E/c05xbktB0XN04m/t1Z+n+fTGw==",
"peer": true
},
"node_modules/section-matter": {
@@ -12898,9 +12901,9 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
},
"node_modules/sitemap": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.1.tgz",
"integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz",
"integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==",
"dependencies": {
"@types/node": "^17.0.5",
"@types/sax": "^1.2.1",
@@ -13524,9 +13527,9 @@
}
},
"node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
+11 -11
View File
@@ -15,20 +15,20 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.3.2",
"@docusaurus/preset-classic": "^3.3.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"@docusaurus/core": "^3.4.0",
"@docusaurus/preset-classic": "^3.4.0",
"@mdx-js/react": "^3.0.1",
"clsx": "^2.1.1",
"prism-react-renderer": "^2.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.3.2",
"@docusaurus/tsconfig": "^3.3.2",
"@docusaurus/types": "^3.3.2",
"@docusaurus/module-type-aliases": "^3.4.0",
"@docusaurus/tsconfig": "^3.4.0",
"@docusaurus/types": "^3.4.0",
"dotenv": "^16.4.5",
"typescript": "~5.2.2"
"typescript": "^5.4.5"
},
"browserslist": {
"production": [
+1
View File
@@ -13,6 +13,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
const sidebars: SidebarsConfig = {
docs: [
"intro",
"resources",
"core-setup",
{
type: "category",
+1 -1
View File
@@ -13,7 +13,7 @@
<title>Monitor</title>
</head>
<body class="min-h-screen">
<body class="min-h-screen bg-background">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
+65
View File
@@ -639,3 +639,68 @@ const default_registry_config = (
return { type, params: "" };
}
};
export const SecretSelector = ({
keys,
onSelect,
type,
disabled,
}: {
keys: string[];
onSelect: (key: string) => void;
type: "Variable" | "Secret";
disabled: boolean;
}) => {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const filtered = filterBySplit(keys, search, (item) => item).sort((a, b) => {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
});
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" className="flex gap-2" disabled={disabled}>
<PlusCircle className="w-4 h-4" />
<div>Add {type}</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] max-h-[300px] p-0" align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder={`Search ${type}s`}
className="h-9"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center pt-2">
{`No ${type}s Found`}
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
{filtered.map((key) => (
<CommandItem
key={key}
onSelect={() => {
onSelect(key);
setOpen(false);
}}
className="flex items-center justify-between cursor-pointer"
>
<div className="p-1">{key}</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
+1 -1
View File
@@ -32,7 +32,7 @@ export const ExportButton = ({
<FileDown className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent className="min-w-[600px]">
<DialogContent className="w-[900px] max-w-[95vw]">
<DialogHeader>
<DialogTitle>Export to Toml</DialogTitle>
</DialogHeader>
+45 -6
View File
@@ -22,6 +22,7 @@ import { BuilderComponents } from "./resources/builder";
import { AlerterComponents } from "./resources/alerter";
import { ServerTemplateComponents } from "./resources/server-template";
import { Badge } from "@ui/badge";
import { ResourceSyncComponents } from "./resources/resource-sync";
export const OmniSearch = ({
className,
@@ -43,7 +44,10 @@ export const OmniSearch = ({
<Search className="w-4 h-4" />{" "}
<span className="text-muted-foreground hidden md:flex">Search</span>
</div>
<Badge variant="outline" className="text-muted-foreground hidden md:inline-flex">
<Badge
variant="outline"
className="text-muted-foreground hidden md:inline-flex"
>
shift + s
</Badge>
</Button>
@@ -117,6 +121,7 @@ const useOmniItems = (
const builders = useRead("ListBuilders", {}).data;
const alerters = useRead("ListAlerters", {}).data;
const templates = useRead("ListServerTemplates", {}).data;
const syncs = useRead("ListResourceSyncs", {}).data;
const searchTerms = search
.toLowerCase()
.split(" ")
@@ -169,6 +174,11 @@ const useOmniItems = (
icon: <ServerTemplateComponents.Icon />,
onSelect: () => nav("/server-templates"),
},
{
label: "Syncs",
icon: <ResourceSyncComponents.Icon />,
onSelect: () => nav("/resource-syncs"),
},
(user?.admin && {
label: "Users",
icon: <User className="w-4 h-4" />,
@@ -282,7 +292,7 @@ const useOmniItems = (
)
.map((builder) => ({
label: builder.name,
icon: <ProcedureComponents.Icon id={builder.id} />,
icon: <BuilderComponents.Icon id={builder.id} />,
onSelect: () => nav(`/builders/${builder.id}`),
})) || [],
@@ -299,7 +309,7 @@ const useOmniItems = (
)
.map((alerter) => ({
label: alerter.name,
icon: <ProcedureComponents.Icon id={alerter.id} />,
icon: <AlerterComponents.Icon id={alerter.id} />,
onSelect: () => nav(`/alerters/${alerter.id}`),
})) || [],
@@ -316,10 +326,39 @@ const useOmniItems = (
)
.map((template) => ({
label: template.name,
icon: <ProcedureComponents.Icon id={template.id} />,
onSelect: () => nav(`/templates/${template.id}`),
icon: <ServerTemplateComponents.Icon id={template.id} />,
onSelect: () => nav(`/server-templates/${template.id}`),
})) || [],
Syncs:
syncs
?.filter(
(item) =>
searchTerms.length === 0 ||
searchTerms.every(
(term) =>
item.name.toLowerCase().includes(term) ||
"sync".includes(term)
)
)
.map((template) => ({
label: template.name,
icon: <ResourceSyncComponents.Icon id={template.id} />,
onSelect: () => nav(`/resource-syncs/${template.id}`),
})) || [],
}),
[user, servers, deployments, builds, repos, procedures, search]
[
user,
servers,
deployments,
builds,
repos,
procedures,
alerters,
builders,
templates,
syncs,
search,
]
);
};
@@ -38,9 +38,10 @@ export const RunBuild = ({ id }: { id: string }) => {
(u) => u.operation === Types.Operation.CancelBuild
);
const cancelDisabled =
cancelPending || (latestCancel && latestBuild)
cancelPending ||
(latestCancel && latestBuild
? latestCancel!.start_ts > latestBuild!.start_ts
: false;
: false);
if (building) {
return (
@@ -5,17 +5,17 @@ import {
ConfigItem,
ImageRegistryConfig,
InputList,
SecretSelector,
SystemCommand,
} from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { env_to_text, text_to_env } from "@lib/utils";
import { env_to_text } from "@lib/utils";
import { Types } from "@monitor/client";
import { Button } from "@ui/button";
import { Textarea } from "@ui/textarea";
import { PlusCircle } from "lucide-react";
import { ReactNode, RefObject, createRef, useEffect, useState } from "react";
import { ReactNode, RefObject, createRef, useState } from "react";
import { CopyGithubWebhook, LabelsConfig, ResourceSelector } from "../common";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
export const BuildConfig = ({
id,
@@ -209,13 +209,15 @@ export const BuildConfig = ({
<Button
variant="secondary"
onClick={() =>
set((update) => ({
...update,
labels: [
...(update.labels ?? config.labels ?? []),
{ variable: "", value: "" },
],
}))
set((update) => {
return {
...update,
labels: [
...(update.labels ?? config.labels ?? []),
{ variable: "", value: "" },
] as Types.EnvironmentVar[],
};
})
}
className="flex items-center gap-2 w-[200px]"
>
@@ -225,7 +227,11 @@ export const BuildConfig = ({
),
components: {
labels: (l, set) => (
<LabelsConfig labels={l ?? []} set={set} disabled={disabled} />
<LabelsConfig
labels={(l as Types.EnvironmentVar[]) ?? []}
set={set}
disabled={disabled}
/>
),
},
},
@@ -257,9 +263,13 @@ export const BuildConfig = ({
{
label: "Build Args",
components: {
build_args: (vars, set) => (
<BuildArgs vars={vars ?? []} set={set} disabled={disabled} />
),
build_args: (vars, set) => {
const args =
typeof vars === "object" ? env_to_text(vars) : vars;
return (
<BuildArgs args={args ?? ""} set={set} disabled={disabled} />
);
},
skip_secret_interp: true,
},
},
@@ -270,44 +280,16 @@ export const BuildConfig = ({
};
const BuildArgs = ({
vars,
args,
set,
disabled,
}: {
vars: Types.EnvironmentVar[];
args: string;
set: (input: Partial<Types.BuildConfig>) => void;
disabled: boolean;
}) => {
const ref = createRef<HTMLTextAreaElement>();
const [args, setArgs] = useState<string>();
useEffect(() => setArgs(env_to_text(vars)), [vars]);
const update = () => {
if (!args) return;
const parsed = text_to_env(args);
// Diff the vars from old to new
for (const [v, i] of vars.map(
(v, i) => [v, i] as [Types.EnvironmentVar, number]
)) {
const _v = parsed[i];
if (!_v || v.value !== _v.value || v.variable !== _v.variable) {
set({ build_args: parsed });
return;
}
}
// Diff the vars from new to old
for (const [v, i] of parsed.map(
(v, i) => [v, i] as [Types.EnvironmentVar, number]
)) {
const _v = vars[i];
if (!_v || v.value !== _v.value || v.variable !== _v.variable) {
set({ build_args: parsed });
return;
}
}
};
const setArgs = (build_args: string) => set({ build_args });
return (
<ConfigItem className="flex-col gap-4 items-start">
@@ -318,7 +300,6 @@ const BuildArgs = ({
placeholder="VARIABLE=value"
value={args}
onChange={(e) => setArgs(e.target.value)}
onBlur={update}
disabled={disabled}
/>
</ConfigItem>
@@ -343,103 +324,36 @@ const Secrets = ({
if (variables.length === 0 && secrets.length === 0) return;
if (variables.length === 0) {
// ONLY SECRETS
return (
<div className="flex flex-col gap-2 w-full">
<h2 className="text-muted-foreground">Secrets</h2>
<div className="flex gap-4 items-center flex-wrap w-full">
{secrets.map((secret) => (
<Button
variant="secondary"
key={secret}
onClick={() =>
setArgs(
_args.slice(0, argsRef.current?.selectionStart) +
`[[${secret}]]` +
_args.slice(argsRef.current?.selectionStart, undefined)
)
}
>
{secret}
</Button>
))}
</div>
</div>
);
}
if (secrets.length === 0) {
// ONLY VARIABLES
return (
<div className="flex flex-col gap-2 w-full">
<h2 className="text-muted-foreground">Variables</h2>
<div className="flex gap-4 items-center flex-wrap w-full">
{variables.map(({ name }) => (
<Button
variant="secondary"
key={name}
onClick={() =>
setArgs(
_args.slice(0, argsRef.current?.selectionStart) +
`[[${name}]]` +
_args.slice(argsRef.current?.selectionStart, undefined)
)
}
>
{name}
</Button>
))}
</div>
</div>
);
}
return (
<Tabs className="w-full" defaultValue="Variables">
<TabsList>
<TabsTrigger value="Variables">Variables</TabsTrigger>
<TabsTrigger value="Secrets">Secrets</TabsTrigger>
</TabsList>
<TabsContent value="Variables">
<div className="flex gap-4 items-center w-full flex-wrap pt-1">
{variables.map(({ name }) => (
<Button
variant="secondary"
key={name}
onClick={() =>
setArgs(
_args.slice(0, argsRef.current?.selectionStart) +
`[[${name}]]` +
_args.slice(argsRef.current?.selectionStart, undefined)
)
}
>
{name}
</Button>
))}
</div>
</TabsContent>
<TabsContent value="Secrets">
<div className="flex gap-4 items-center w-full flex-wrap pt-1">
{secrets.map((secret) => (
<Button
variant="secondary"
key={secret}
onClick={() =>
setArgs(
_args.slice(0, argsRef.current?.selectionStart) +
`[[${secret}]]` +
_args.slice(argsRef.current?.selectionStart, undefined)
)
}
>
{secret}
</Button>
))}
</div>
</TabsContent>
</Tabs>
<div className="flex items-center gap-2">
{variables.length > 0 && (
<SecretSelector
type="Variable"
keys={variables.map((v) => v.name)}
onSelect={(variable) =>
setArgs(
_args.slice(0, argsRef.current?.selectionStart) +
`[[${variable}]]` +
_args.slice(argsRef.current?.selectionStart, undefined)
)
}
disabled={false}
/>
)}
{secrets.length > 0 && (
<SecretSelector
type="Secret"
keys={secrets}
onSelect={(secret) =>
setArgs(
_args.slice(0, argsRef.current?.selectionStart) +
`[[${secret}]]` +
_args.slice(argsRef.current?.selectionStart, undefined)
)
}
disabled={false}
/>
)}
</div>
);
};
@@ -11,7 +11,7 @@ import { RunBuild } from "./actions";
import {
bg_color_class_by_intention,
build_state_intention,
fill_color_class_by_intention,
stroke_color_class_by_intention,
} from "@lib/color";
import { Card, CardHeader } from "@ui/card";
import { cn } from "@lib/utils";
@@ -25,7 +25,7 @@ const useBuild = (id?: string) =>
const BuildIcon = ({ id, size }: { id?: string; size: number }) => {
const state = useBuild(id)?.info.state;
const color = fill_color_class_by_intention(build_state_intention(state));
const color = stroke_color_class_by_intention(build_state_intention(state));
return <Hammer className={cn(`w-${size} h-${size}`, state && color)} />;
};
@@ -18,7 +18,7 @@ const AwsBuilderConfig = ({ id }: { id: string }) => {
target: { type: "Builder", id },
}).data;
const config = useRead("GetBuilder", { builder: id }).data?.config
.params as Types.AwsBuilderConfig;
?.params as Types.AwsBuilderConfig;
const global_disabled =
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
const [update, set] = useState<Partial<Types.AwsBuilderConfig>>({});
@@ -61,7 +61,7 @@ const AwsBuilderConfig = ({ id }: { id: string }) => {
<Button
variant="secondary"
onClick={() =>
set(update => ({
set((update) => ({
...update,
security_group_ids: [
...(update.security_group_ids ??
@@ -97,7 +97,7 @@ const AwsBuilderConfig = ({ id }: { id: string }) => {
<Button
variant="secondary"
onClick={() =>
set(update => ({
set((update) => ({
...update,
github_accounts: [
...(update.github_accounts ??
+1 -1
View File
@@ -402,4 +402,4 @@ export const ServerSelector = ({
align={align}
/>
</ConfigItem>
);
);
@@ -23,8 +23,8 @@ export const DeployContainer = ({ id }: DeploymentId) => {
const [signal, setSignal] = useState<Types.TerminationSignal>();
useEffect(
() => setSignal(deployment?.config.termination_signal),
[deployment?.config.termination_signal]
() => setSignal(deployment?.config?.termination_signal),
[deployment?.config?.termination_signal]
);
const { mutate: deploy, isPending } = useExecute("Deploy");
@@ -60,9 +60,9 @@ export const DeployContainer = ({ id }: DeploymentId) => {
disabled={pending}
loading={pending}
additional={
deployed && deployment.config.term_signal_labels.length > 1 ? (
deployed && deployment.config!.term_signal_labels.length > 1 ? (
<TermSignalSelector
signals={deployment.config.term_signal_labels}
signals={deployment.config!.term_signal_labels}
signal={signal}
setSignal={setSignal}
/>
@@ -113,8 +113,8 @@ const StopContainer = ({ id }: DeploymentId) => {
const [signal, setSignal] = useState<Types.TerminationSignal>();
useEffect(
() => setSignal(deployment?.config.termination_signal),
[deployment?.config.termination_signal]
() => setSignal(deployment?.config?.termination_signal),
[deployment?.config?.termination_signal]
);
const { mutate, isPending } = useExecute("StopContainer");
@@ -138,9 +138,9 @@ const StopContainer = ({ id }: DeploymentId) => {
disabled={pending}
loading={pending}
additional={
deployment.config.term_signal_labels.length > 1 ? (
deployment.config!.term_signal_labels.length > 1 ? (
<TermSignalSelector
signals={deployment.config.term_signal_labels}
signals={deployment.config!.term_signal_labels}
signal={signal}
setSignal={setSignal}
/>
@@ -181,8 +181,8 @@ export const RemoveContainer = ({ id }: DeploymentId) => {
const [signal, setSignal] = useState<Types.TerminationSignal>();
useEffect(
() => setSignal(deployment?.config.termination_signal),
[deployment?.config.termination_signal]
() => setSignal(deployment?.config?.termination_signal),
[deployment?.config?.termination_signal]
);
const { mutate, isPending } = useExecute("RemoveContainer");
@@ -212,9 +212,9 @@ export const RemoveContainer = ({ id }: DeploymentId) => {
disabled={pending}
loading={pending}
additional={
deployment.config.term_signal_labels.length > 1 ? (
deployment.config!.term_signal_labels.length > 1 ? (
<TermSignalSelector
signals={deployment.config.term_signal_labels}
signals={deployment.config!.term_signal_labels}
signal={signal}
setSignal={setSignal}
/>
@@ -1,57 +1,26 @@
import { ConfigItem } from "@components/config/util";
import { ConfigItem, SecretSelector } from "@components/config/util";
import { useRead } from "@lib/hooks";
import { env_to_text, text_to_env } from "@lib/utils";
import { Types } from "@monitor/client";
import { Button } from "@ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { Textarea } from "@ui/textarea";
import { RefObject, createRef, useEffect, useState } from "react";
import { RefObject, createRef } from "react";
export const EnvVars = ({
vars,
env,
set,
disabled,
server,
}: {
vars: Types.EnvironmentVar[];
env: string;
set: (input: Partial<Types.DeploymentConfig>) => void;
disabled: boolean;
/// eg server id
server?: string;
}) => {
const ref = createRef<HTMLTextAreaElement>();
const [env, setEnv] = useState<string>();
useEffect(() => setEnv(env_to_text(vars)), [vars]);
const update = () => {
if (!env) return;
const parsed = text_to_env(env);
// Diff the vars from old to new
for (const [v, i] of vars.map(
(v, i) => [v, i] as [Types.EnvironmentVar, number]
)) {
const _v = parsed[i];
if (!_v || v.value !== _v.value || v.variable !== _v.variable) {
set({ environment: parsed });
return;
}
}
// Diff the vars from new to old
for (const [v, i] of parsed.map(
(v, i) => [v, i] as [Types.EnvironmentVar, number]
)) {
const _v = vars[i];
if (!_v || v.value !== _v.value || v.variable !== _v.variable) {
set({ environment: parsed });
return;
}
}
};
const setEnv = (environment: string) => set({ environment });
return (
<ConfigItem className="flex-col gap-4 items-start">
<ConfigItem className="flex-col gap-2 items-start">
{!disabled && server && (
<Secrets server={server} env={env} setEnv={setEnv} envRef={ref} />
)}
@@ -61,7 +30,6 @@ export const EnvVars = ({
placeholder="VARIABLE=value"
value={env}
onChange={(e) => setEnv(e.target.value)}
onBlur={update}
disabled={disabled}
/>
</ConfigItem>
@@ -95,102 +63,36 @@ const Secrets = ({
if (variables.length === 0 && secrets.length === 0) return;
if (variables.length === 0) {
// ONLY SECRETS
return (
<div className="flex flex-col gap-2 w-full">
<h2 className="text-muted-foreground">Secrets</h2>
<div className="flex gap-4 items-center flex-wrap w-full">
{secrets.map((secret) => (
<Button
variant="secondary"
key={secret}
onClick={() =>
setEnv(
_env.slice(0, envRef.current?.selectionStart) +
`[[${secret}]]` +
_env.slice(envRef.current?.selectionStart, undefined)
)
}
>
{secret}
</Button>
))}
</div>
</div>
);
}
if (secrets.length === 0) {
// ONLY VARIABLES
return (
<div className="flex flex-col gap-2 w-full">
<h2 className="text-muted-foreground">Variables</h2>
<div className="flex gap-4 items-center flex-wrap w-full">
{variables.map(({ name }) => (
<Button
variant="secondary"
key={name}
onClick={() =>
setEnv(
_env.slice(0, envRef.current?.selectionStart) +
`[[${name}]]` +
_env.slice(envRef.current?.selectionStart, undefined)
)
}
>
{name}
</Button>
))}
</div>
</div>
);
}
return (
<Tabs className="w-full" defaultValue="Variables">
<TabsList>
<TabsTrigger value="Variables">Variables</TabsTrigger>
<TabsTrigger value="Secrets">Secrets</TabsTrigger>
</TabsList>
<TabsContent value="Variables">
<div className="flex gap-4 items-center w-full flex-wrap pt-1">
{variables.map(({ name }) => (
<Button
variant="secondary"
key={name}
onClick={() =>
setEnv(
_env.slice(0, envRef.current?.selectionStart) +
`[[${name}]]` +
_env.slice(envRef.current?.selectionStart, undefined)
)
}
>
{name}
</Button>
))}
</div>
</TabsContent>
<TabsContent value="Secrets">
<div className="flex gap-4 items-center w-full flex-wrap pt-1">
{secrets.map((secret) => (
<Button
variant="secondary"
key={secret}
onClick={() =>
setEnv(
_env.slice(0, envRef.current?.selectionStart) +
`[[${secret}]]` +
_env.slice(envRef.current?.selectionStart, undefined)
)
}
>
{secret}
</Button>
))}
</div>
</TabsContent>
</Tabs>
<div className="flex items-center gap-2">
{variables.length > 0 && (
<SecretSelector
type="Variable"
keys={variables.map((v) => v.name)}
onSelect={(variable) =>
setEnv(
_env.slice(0, envRef.current?.selectionStart) +
`[[${variable}]]` +
_env.slice(envRef.current?.selectionStart, undefined)
)
}
disabled={false}
/>
)}
{secrets.length > 0 && (
<SecretSelector
type="Secret"
keys={secrets}
onSelect={(secret) =>
setEnv(
_env.slice(0, envRef.current?.selectionStart) +
`[[${secret}]]` +
_env.slice(envRef.current?.selectionStart, undefined)
)
}
disabled={false}
/>
)}
</div>
);
};
@@ -23,6 +23,7 @@ import { LabelsConfig, ServerSelector } from "@components/resources/common";
import { TextUpdateMenu } from "@components/util";
import { Button } from "@ui/button";
import { PlusCircle } from "lucide-react";
import { env_to_text } from "@lib/utils";
export const DeploymentConfig = ({
id,
@@ -90,7 +91,7 @@ export const DeploymentConfig = ({
(config.image?.params as any)?.build_id;
const build_registry_type = useRead("GetBuild", {
build: build_id!,
}).data?.config.image_registry?.type;
}).data?.config?.image_registry?.type;
const server_id = update.server_id ?? config.server_id;
return (
<ImageRegistryConfig
@@ -240,7 +241,7 @@ export const DeploymentConfig = ({
labels: [
...(update.labels ?? config.labels ?? []),
{ variable: "", value: "" },
],
] as Types.EnvironmentVar[],
})
}
className="flex items-center gap-2 w-[200px]"
@@ -251,7 +252,11 @@ export const DeploymentConfig = ({
),
components: {
labels: (l, set) => (
<LabelsConfig labels={l ?? []} set={set} disabled={disabled} />
<LabelsConfig
labels={(l as Types.EnvironmentVar[]) ?? []}
set={set}
disabled={disabled}
/>
),
},
},
@@ -268,14 +273,17 @@ export const DeploymentConfig = ({
{
label: "Environment",
components: {
environment: (vars, set) => (
<EnvVars
vars={vars ?? []}
set={set}
server={update.server_id || config.server_id}
disabled={disabled}
/>
),
environment: (vars, set) => {
const env = typeof vars === "object" ? env_to_text(vars) : vars;
return (
<EnvVars
env={env ?? ""}
set={set}
server={update.server_id || config.server_id}
disabled={disabled}
/>
);
},
skip_secret_interp: true,
},
},
@@ -16,7 +16,7 @@ import { snake_case_to_upper_space_case } from "@lib/formatting";
import {
bg_color_class_by_intention,
deployment_state_intention,
fill_color_class_by_intention,
stroke_color_class_by_intention,
} from "@lib/color";
import { DeploymentTable } from "./table";
import { DeploymentsChart } from "./dashboard";
@@ -92,7 +92,7 @@ const ConfigOrLog = ({ id }: { id: string }) => {
const DeploymentIcon = ({ id, size }: { id?: string; size: number }) => {
const state = useDeployment(id)?.info.state;
const color = fill_color_class_by_intention(
const color = stroke_color_class_by_intention(
deployment_state_intention(state)
);
return <Rocket className={cn(`w-${size} h-${size}`, state && color)} />;
@@ -2,12 +2,7 @@ import { Section } from "@components/layouts";
import { useRead } from "@lib/hooks";
import { Types } from "@monitor/client";
import { Button } from "@ui/button";
import {
RefreshCw,
ChevronDown,
X,
AlertOctagon,
} from "lucide-react";
import { RefreshCw, ChevronDown, X, AlertOctagon } from "lucide-react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { useDeployment } from ".";
import {
@@ -22,8 +17,15 @@ import { Input } from "@ui/input";
import { useToast } from "@ui/use-toast";
import { logToHtml } from "@lib/utils";
import { ToggleGroup, ToggleGroupItem } from "@ui/toggle-group";
import { Switch } from "@ui/switch";
export const DeploymentLogs = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
export const DeploymentLogs = ({
id,
titleOther,
}: {
id: string;
titleOther: ReactNode;
}) => {
const state = useDeployment(id)?.info.state;
if (
state === undefined ||
@@ -35,11 +37,18 @@ export const DeploymentLogs = ({ id, titleOther }: { id: string; titleOther: Rea
return <DeploymentLogsInner id={id} titleOther={titleOther} />;
};
const DeploymentLogsInner = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
const DeploymentLogsInner = ({
id,
titleOther,
}: {
id: string;
titleOther: ReactNode;
}) => {
const { toast } = useToast();
const [stream, setStream] = useState("stdout");
const [tail, set] = useState("100");
const [terms, setTerms] = useState<string[]>([]);
const [invert, setInvert] = useState(false);
const [search, setSearch] = useState("");
const addTerm = () => {
@@ -59,14 +68,18 @@ const DeploymentLogsInner = ({ id, titleOther }: { id: string; titleOther: React
};
const { Log, refetch, stderr } = terms.length
? SearchLogs(id, terms)
? SearchLogs(id, terms, invert)
: NoSearchLogs(id, tail, stream);
return (
<Section
titleOther={titleOther}
actions={
<div className="flex gap-2">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="text-muted-foreground">Invert Search </div>
<Switch checked={invert} onCheckedChange={setInvert} />
</div>
{terms.map((term, index) => (
<Button
key={term}
@@ -87,7 +100,7 @@ const DeploymentLogsInner = ({ id, titleOther }: { id: string; titleOther: React
onKeyDown={(e) => {
if (e.key === "Enter") addTerm();
}}
className="w-[300px]"
className="w-[240px]"
/>
<Button
variant="ghost"
@@ -140,11 +153,12 @@ const NoSearchLogs = (id: string, tail: string, stream: string) => {
};
};
const SearchLogs = (id: string, terms: string[]) => {
const SearchLogs = (id: string, terms: string[], invert: boolean) => {
const { data: log, refetch } = useRead("SearchLog", {
deployment: id,
terms,
combinator: Types.SearchCombinator.And,
invert,
});
return {
Log: (
@@ -182,7 +196,11 @@ const Log = ({
className="-scroll-mt-24 pb-[20vh]"
/>
</div>
<Button variant="secondary" className="absolute top-4 right-4" onClick={scroll}>
<Button
variant="secondary"
className="absolute top-4 right-4"
onClick={scroll}
>
<ChevronDown className="h-4 w-4" />
</Button>
</>
@@ -62,7 +62,7 @@ const ProcedureConfigInner = ({
const global_disabled =
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
const { mutateAsync } = useWrite("UpdateProcedure");
const stages = config.stages || procedure.config.stages || [];
const stages = config.stages || procedure.config?.stages || [];
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
@@ -214,7 +214,7 @@ const ProcedureConfigInner = ({
<div className="text-muted-foreground">Enabled:</div>
<Switch
checked={
config.webhook_enabled ?? procedure.config.webhook_enabled
config.webhook_enabled ?? procedure.config?.webhook_enabled
}
onCheckedChange={(webhook_enabled) =>
setConfig({ ...config, webhook_enabled })
@@ -346,30 +346,6 @@ const Stage = ({
</Button>
}
columns={[
{
header: "Enabled",
cell: ({
row: {
original: { enabled },
index,
},
}) => {
return (
<Switch
checked={enabled}
onClick={() =>
setStage({
...stage,
executions: stage.executions!.map((item, i) =>
i === index ? { ...item, enabled: !enabled } : item
),
})
}
disabled={disabled}
/>
);
},
},
{
header: "Execution",
cell: ({ row: { original, index } }) => (
@@ -431,7 +407,7 @@ const Stage = ({
},
},
{
header: "Modify",
header: "Add / Remove",
cell: ({ row: { index } }) => (
<div className="flex items-center gap-2">
<Button
@@ -467,6 +443,30 @@ const Stage = ({
</div>
),
},
{
header: "Enabled",
cell: ({
row: {
original: { enabled },
index,
},
}) => {
return (
<Switch
checked={enabled}
onClick={() =>
setStage({
...stage,
executions: stage.executions!.map((item, i) =>
i === index ? { ...item, enabled: !enabled } : item
),
})
}
disabled={disabled}
/>
);
},
},
]}
/>
</Card>
@@ -595,7 +595,6 @@ const TARGET_COMPONENTS: ExecutionConfigs = {
Deploy: {
params: { deployment: "" },
Component: ({ params, setParams, disabled }) => {
console.log(params.deployment);
return (
<ResourceSelector
type="Deployment"
@@ -9,6 +9,7 @@ import { DeleteResource, NewResource } from "../common";
import {
bg_color_class_by_intention,
procedure_state_intention,
stroke_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
import { ProcedureDashboard } from "./dashboard";
@@ -17,6 +18,12 @@ import { Types } from "@monitor/client";
const useProcedure = (id?: string) =>
useRead("ListProcedures", {}).data?.find((d) => d.id === id);
const ProcedureIcon = ({ id, size }: { id?: string; size: number }) => {
const state = useProcedure(id)?.info.state;
const color = stroke_color_class_by_intention(procedure_state_intention(state));
return <Route className={cn(`w-${size} h-${size}`, state && color)} />;
};
export const ProcedureComponents: RequiredResourceComponents = {
list_item: (id) => useProcedure(id),
@@ -28,8 +35,8 @@ export const ProcedureComponents: RequiredResourceComponents = {
<ProcedureTable procedures={resources as Types.ProcedureListItem[]} />
),
Icon: () => <Route className="w-4" />,
BigIcon: () => <Route className="w-8" />,
Icon: ({ id }) => <ProcedureIcon id={id} size={4} />,
BigIcon: ({ id }) => <ProcedureIcon id={id} size={8} />,
Status: {
State: ({ id }) => {
@@ -52,6 +52,9 @@ export const RepoConfig = ({ id }: { id: string }) => {
repo: { placeholder: "Enter repo" },
branch: { placeholder: "Enter branch" },
commit: { placeholder: "Enter specific commit hash. Optional." },
path: {
placeholder: "Enter a specific clone path. Optional.",
},
github_account: (value, set) => {
const server_id = update.server_id || config.server_id;
return (
@@ -8,8 +8,8 @@ import { DeleteResource, NewResource, ResourceLink } from "../common";
import { RepoTable } from "./table";
import {
bg_color_class_by_intention,
fill_color_class_by_intention,
repo_state_intention,
stroke_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
@@ -24,7 +24,7 @@ export const useRepo = (id?: string) =>
const RepoIcon = ({ id, size }: { id?: string; size: number }) => {
const state = useRepo(id)?.info.state;
const color = fill_color_class_by_intention(repo_state_intention(state));
const color = stroke_color_class_by_intention(repo_state_intention(state));
return <GitBranch className={cn(`w-${size} h-${size}`, state && color)} />;
};
@@ -11,6 +11,7 @@ import { PendingOrConfig } from "./pending-or-config";
import {
bg_color_class_by_intention,
resource_sync_state_intention,
stroke_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
@@ -21,6 +22,14 @@ const useResourceSync = (id?: string) =>
(d) => d.id === id
);
const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => {
const state = useResourceSync(id)?.info.state;
const color = stroke_color_class_by_intention(
resource_sync_state_intention(state)
);
return <FolderSync className={cn(`w-${size} h-${size}`, state && color)} />;
};
export const ResourceSyncComponents: RequiredResourceComponents = {
list_item: (id) => useResourceSync(id),
@@ -49,8 +58,8 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
<ResourceSyncTable syncs={resources as Types.ResourceSyncListItem[]} />
),
Icon: () => <FolderSync className="w-4 h-4" />,
BigIcon: () => <FolderSync className="w-8 h-8" />,
Icon: ({ id }) => <ResourceSyncIcon id={id} size={4} />,
BigIcon: ({ id }) => <ResourceSyncIcon id={id} size={8} />,
Status: {
State: ({ id }) => {
@@ -32,7 +32,7 @@ export const AwsServerTemplateConfig = ({
disabled: boolean;
}) => {
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config.params as Types.AwsServerTemplateConfig;
?.config?.params as Types.AwsServerTemplateConfig;
const [update, set] = useState<Partial<Types.AwsServerTemplateConfig>>({});
const { mutateAsync } = useWrite("UpdateServerTemplate");
if (!config) return null;
@@ -41,7 +41,7 @@ export const HetznerServerTemplateConfig = ({
disabled: boolean;
}) => {
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config.params as Types.HetznerServerTemplateConfig;
?.config?.params as Types.HetznerServerTemplateConfig;
const [update, set] = useState<Partial<Types.HetznerServerTemplateConfig>>(
{}
);
@@ -16,8 +16,8 @@ import { Section } from "@components/layouts";
import { RenameServer } from "./actions";
import {
bg_color_class_by_intention,
fill_color_class_by_intention,
server_state_intention,
stroke_color_class_by_intention,
} from "@lib/color";
import { ServerConfig } from "./config";
import { DeploymentTable } from "../deployment/table";
@@ -44,7 +44,7 @@ const Icon = ({ id, size }: { id?: string; size: number }) => {
<Server
className={cn(
`w-${size} h-${size}`,
id && fill_color_class_by_intention(server_state_intention(state))
state && stroke_color_class_by_intention(server_state_intention(state))
)}
/>
);
-9
View File
@@ -258,15 +258,6 @@ export const AddTags = ({ target }: { target: TargetExcludingSystem }) => {
className="h-9"
value={search}
onValueChange={setSearch}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
// check that no tags still match
all_tags?.every((tag) => !tag.name.includes(search))
) {
create_tag();
}
}}
/>
<CommandList>
<CommandEmpty className="m-1">
+25 -18
View File
@@ -25,31 +25,38 @@ export const hex_color_by_intention = (intention: ColorIntention) => {
}
};
export const color_class_by_intention = (intention: ColorIntention) => {
export const fill_color_class_by_intention = (intention: ColorIntention) => {
switch (intention) {
case "Good":
return "green-500";
return "text-green-400 dark:text-green-700";
case "Neutral":
return "blue-500";
return "text-blue-400 dark:text-blue-700";
case "Warning":
return "orange-500";
return "text-orange-400 dark:text-orange-700";
case "Critical":
return "red-500";
return "text-red-400 dark:text-red-700";
case "Unknown":
return "purple-500";
return "text-purple-400 dark:text-purple-700";
case "None":
return "";
}
};
export const fill_color_class_by_intention = (intention: ColorIntention) => {
if (intention === "None") return "";
return `fill-${color_class_by_intention(intention)}`;
};
export const stroke_color_class_by_intention = (intention: ColorIntention) => {
if (intention === "None") return "";
return `stroke-${color_class_by_intention(intention)}`;
switch (intention) {
case "Good":
return "stroke-green-600 dark:stroke-green-500";
case "Neutral":
return "stroke-blue-600 dark:stroke-blue-500";
case "Warning":
return "stroke-orange-600 dark:stroke-orange-500";
case "Critical":
return "stroke-red-600 dark:stroke-red-500";
case "Unknown":
return "stroke-purple-600 dark:stroke-purple-500";
case "None":
return "";
}
};
export const bg_color_class_by_intention = (intention: ColorIntention) => {
@@ -72,15 +79,15 @@ export const bg_color_class_by_intention = (intention: ColorIntention) => {
export const text_color_class_by_intention = (intention: ColorIntention) => {
switch (intention) {
case "Good":
return "text-green-500";
return "text-green-700 dark:text-green-400";
case "Neutral":
return "text-blue-500";
return "text-blue-700 dark:text-blue-400";
case "Warning":
return "text-orange-500";
return "text-orange-700 dark:text-orange-400";
case "Critical":
return "text-red-500";
return "text-red-700 dark:text-red-400";
case "Unknown":
return "text-purple-500";
return "text-purple-700 dark:text-purple-400";
case "None":
return "";
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { Types } from "@monitor/client";
export const fmt_date = (d: Date) => {
const hours = d.getHours();
const minutes = d.getMinutes();
return `${fmt_month(d.getMonth())} ${d.getDate()} @ ${
return `${fmt_month(d.getMonth())} ${d.getDate()} ${
hours > 9 ? hours : "0" + hours
}:${minutes > 9 ? minutes : "0" + minutes}`;
};
+6 -6
View File
@@ -97,8 +97,8 @@ export const useManageUser = <
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().user({ type, params } as R),
onError: (e, v, c) => {
console.log("useManageUser error:", e);
onError: (e: { response: { data: any } }, v, c) => {
console.log("Auth error:", e.response.data);
toast({
title: `Request ${type} Failed`,
description: "See console for details",
@@ -126,8 +126,8 @@ export const useWrite = <
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().write({ type, params } as R),
onError: (e, v, c) => {
console.log("useWrite error:", e);
onError: (e: { response: { data: any } }, v, c) => {
console.log("Write error:", e.response.data);
toast({
title: `Write request ${type} failed`,
description: "See console for details",
@@ -155,8 +155,8 @@ export const useExecute = <
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().execute({ type, params } as R),
onError: (e, v, c) => {
console.log("useExecute error:", e);
onError: (e: { response: { data: any } }, v, c) => {
console.log("Execute error:", e.response.data);
toast({
title: `Execute request ${type} failed`,
description: "See console for details",
+24 -21
View File
@@ -1,5 +1,5 @@
import { ExportButton } from "@components/export";
import { ConfirmButton, TextUpdateMenu } from "@components/util";
import { ConfirmButton, CopyButton, TextUpdateMenu } from "@components/util";
import {
useInvalidate,
useRead,
@@ -108,26 +108,29 @@ export const Variables = () => {
),
cell: ({ row }) => {
return (
<Card
className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full"
onClick={() => {
setUpdateMenuData({
title: `${row.original.name} - Value`,
value: row.original.value ?? "",
placeholder: "Set value",
onUpdate: (value) => {
if (row.original.value === value) {
return;
}
updateValue({ name: row.original.name, value });
},
});
}}
>
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis w-full text-muted-foreground">
{row.original.value || "Set value"}
</div>
</Card>
<div className="flex items-center gap-2">
<Card
className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full"
onClick={() => {
setUpdateMenuData({
title: `${row.original.name} - Value`,
value: row.original.value ?? "",
placeholder: "Set value",
onUpdate: (value) => {
if (row.original.value === value) {
return;
}
updateValue({ name: row.original.name, value });
},
});
}}
>
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis w-full text-muted-foreground">
{row.original.value || "Set value"}
</div>
</Card>
<CopyButton content={row.original.value} />
</div>
);
},
},
+92 -5
View File
@@ -2,10 +2,97 @@
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
safelist: [
// red
"text-red-400",
"text-red-500",
"text-red-600",
"text-red-700",
"bg-red-400",
"bg-red-500",
"bg-red-600",
"bg-red-700",
"fill-red-400",
"fill-red-500",
"fill-red-600",
"fill-red-700",
"stroke-red-400",
"stroke-red-500",
"stroke-red-600",
"stroke-red-700",
// green
"text-green-400",
"text-green-500",
"text-green-600",
"text-green-700",
"bg-green-400",
"bg-green-500",
"bg-green-600",
"bg-green-700",
"fill-green-400",
"fill-green-500",
"fill-green-600",
"fill-green-700",
"stroke-green-400",
"stroke-green-500",
"stroke-green-600",
"stroke-green-700",
// blue
"text-blue-400",
"text-blue-500",
"text-blue-600",
"text-blue-700",
"bg-blue-400",
"bg-blue-500",
"bg-blue-600",
"bg-blue-700",
"fill-blue-400",
"fill-blue-500",
"fill-blue-600",
"fill-blue-700",
"stroke-blue-400",
"stroke-blue-500",
"stroke-blue-600",
"stroke-blue-700",
// orange
"text-orange-400",
"text-orange-500",
"text-orange-600",
"text-orange-700",
"bg-orange-400",
"bg-orange-500",
"bg-orange-600",
"bg-orange-700",
"fill-orange-400",
"fill-orange-500",
"fill-orange-600",
"fill-orange-700",
"stroke-orange-400",
"stroke-orange-500",
"stroke-orange-600",
"stroke-orange-700",
// purple
"text-purple-400",
"text-purple-500",
"text-purple-600",
"text-purple-700",
"bg-purple-400",
"bg-purple-500",
"bg-purple-600",
"bg-purple-700",
"fill-purple-400",
"fill-purple-500",
"fill-purple-600",
"fill-purple-700",
"stroke-purple-400",
"stroke-purple-500",
"stroke-purple-600",
"stroke-purple-700",
],
prefix: "",
theme: {
@@ -74,4 +161,4 @@ module.exports = {
},
},
plugins: [require("tailwindcss-animate")],
}
};
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "formatting"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
[dependencies]
+32
View File
@@ -0,0 +1,32 @@
pub fn muted(content: impl std::fmt::Display) -> String {
format!("<span class=\"text-muted-foreground\">{content}</span>")
}
pub fn bold(content: impl std::fmt::Display) -> String {
format!("<span class=\"font-bold\">{content}</span>")
}
pub fn colored(
content: impl std::fmt::Display,
color: Color,
) -> String {
format!("<span class=\"{color}\">{content}</span>")
}
pub enum Color {
Red,
Green,
Blue,
}
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Color::Red => f.write_str("text-red-700 dark:text-red-400"),
Color::Green => {
f.write_str("text-green-700 dark:text-green-400")
}
Color::Blue => f.write_str("text-blue-700 dark:text-blue-400"),
}
}
}
+1
View File
@@ -9,6 +9,7 @@ homepage.workspace = true
[dependencies]
monitor_client.workspace = true
formatting.workspace = true
command.workspace = true
#
run_command.workspace = true
+18 -3
View File
@@ -1,7 +1,11 @@
use std::path::Path;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use anyhow::Context;
use command::run_monitor_command;
use formatting::{bold, muted};
use monitor_client::entities::{
all_logs_success, monitor_timestamp, to_monitor_name, update::Log,
CloneArgs, LatestCommit, SystemCommand,
@@ -77,6 +81,7 @@ where
repo,
branch,
commit,
destination,
on_clone,
on_pull,
..
@@ -85,7 +90,11 @@ where
let repo = repo.as_ref().context("build has no repo attached")?;
let name = to_monitor_name(&name);
let repo_dir = repo_dir.join(name);
let repo_dir = match destination {
Some(destination) => PathBuf::from_str(&destination)
.context("destination is not valid path")?,
None => repo_dir.join(name),
};
let mut logs =
clone_inner(repo, &repo_dir, &branch, &commit, github_token)
@@ -244,7 +253,13 @@ pub async fn get_commit_hash_log(
let log = Log {
stage: "latest commit".into(),
command,
stdout: format!("hash: {short}\nmessage: {msg}"),
stdout: format!(
"{} {}\n{} {}",
muted("hash:"),
bold(short),
muted("message:"),
bold(msg),
),
stderr: String::new(),
success: true,
start_ts,
+1 -78
View File
@@ -6,84 +6,7 @@ Docs for periphery setup script can be found in [scripts/readme.md](https://gith
## Changelog
### <ins>v1 (Spring 2024)</ins>
- **New resource types**:
- **Repo**: Clone / pull configured repositories on desired Server. Run shell commands in the repo on every clone / pull to acheive automation. Listen for pushes to a particular branch to automatically pull the repo and run the command.
- **Procedure**: Combine multiple *executions* (Deploy, RunBuild) and run them in sequence or in parallel. *RunProcedure* is an execution type, meaning procedures can run *other procedures*.
- **Builder**: Ephemeral builder configuration has moved to being an API / UI managed entity for greater observability and ease of management.
- **Alerter**: Define multiple alerting endpoints and manage them via the API / UI.
- Slack support continues with the *Slack* Alerter variant.
- Send JSON serialized alert data to any HTTP endpoint with the *Custom* Alerter variant.
- **Template**: Define a template for your cloud provider's VM configuration
- Launch VMs based on the template and automatically add them as Monitor servers.
- Supports AWS EC2 and Hetzner Cloud.
- **Resource Tagging**
- Attach multiple *tags* to resources, which can be used to group related resources together. These can be used to filter resources in the UI.
- For example, resources can be given tags for *environment*, like `Prod`, `Uat`, or `Dev`. This can be combined with tags for the larger system the resource is a part of, such as `Authentication`, `Logging`, or `Messaging`.
- Proper tagging will make it easy to find resources and visualize system components, even as the number of resources grows large.
- **Variables**
- Manage global, non-secret key-value pairs using the API / UI.
- These values can be interpolated into deployment `environments` and build `build_args`
- **Core Accounts and Secrets**
- Docker / Github accounts and Secrets can now be configured in the Core configuration file.
- They can still be added to the Periphery configuration as before. Accounts / Secrets defined in the Core configuration will be preferentially used over like ones defined in Periphery configuration.
- **User Groups**
- Admins can now create User Groups and assign permissions to them as if they were a user.
- Multiple users can then be added to the group, and a user can be added to multiple groups.
- Users in the group inherit the group's permissions.
- **Monitor CLI + Sync**
- Introduces the [monitor cli](https://crates.io/crates/monitor_cli), which can sync resources declared across multiple toml files.
- Implements granular diffing of local and remote resources, producing detailed logs of the changes which will be made before user confirm.
- Resource files can be checked into git and managed via PR, enabling easy scaling even with large numbers of resources.
- All UI resources include button to export the resource to TOML for easy addition to your resource file. There is also a button to export all resources
to make the move to file managed resources.
- **Builds**
- Build log displays the **latest commit hash and message**.
- In-progress builds are able to be **cancelled before completion**.
- Specify a specific commit hash to build from.
- **Deployments**
- Filter log lines using multiple search terms.
- **Alerting**
- The alerting system has been redesigned with a stateful model.
- Alerts can be in an Open or a Resolved state, and alerts are only sent on state changes.
- For example, say a server has just crossed 80% memory usage, the configured memory threshold. An alert will be created in the Open state and the alert data will be sent out. Later, it has dropped down to 70%. The alert will be changed to the Resolved state and the alert data will again be sent.
- In addition to server usage alerts, Monitor now supports deployment state change alerts. These are sent when a deployment's state changes without being caused by a Monitor action. For example, if a deployment goes from the Running state to the Exited state unexpectedly, say from a crash, an alert will be sent.
- Current and past alerts can be retrieved using the API and viewed on the UI.
- **New UI**:
- The Monitor UI has been revamped to support the new features and improve the user experience.
### <ins>v0 (Winter 2022)</ins>
- Move Core and Periphery implementation to Rust.
- Add AWS Ec2 ephemeral build instance support.
- Configuration added to core configuration file.
- Automatic build versioning system, supporting image rollback.
- Realtime and historical system stats - CPU, memory, disk.
- Simple stats alerting based on out-of-bounds values for system stats.
- Support sending alerts to Slack.
### <ins>Pre-versioned releases</ins>
- Defined main resource types:
- Server
- Deployment
- Build
- Basics of Monitor:
- Build docker images from Github repos.
- Manage image deployment on connected servers, see container status, get container logs.
- Add account credentials in Periphery configuration.
- Core and Periphery implemented in Typescript.
View the [changelog](https://github.com/mbecker20/monitor/blob/main/changelog.md)
## Screenshots