Compare commits

..
90 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
mbecker20 8b8c89d976 1.7.3 procedure stage alias 2024-06-11 00:51:16 -07:00
mbecker20 25c8d25636 1.7.2 default resource config parsing 2024-06-11 00:44:41 -07:00
mbecker20 ea242de2e4 default the config if not exists 2024-06-11 00:34:11 -07:00
mbecker20 be03547407 reorder struct fields for improved toml 2024-06-11 00:04:20 -07:00
mbecker20 9c0d28b311 allow inline arrow up to max length 2024-06-10 23:53:23 -07:00
mbecker20 f269deb99c update toml_pretty 2024-06-10 23:30:17 -07:00
mbecker20 3df8163131 improve procedure toml 2024-06-10 23:14:04 -07:00
mbecker20 33a16a9bd2 need 2 \n 2024-06-10 22:36:17 -07:00
mbecker20 215e7d1bdc update toml_pretty 2024-06-10 22:11:40 -07:00
mbecker20 25e0905c0c fix deserializers 2024-06-10 21:31:17 -07:00
mbecker20 1c07ccea85 bump toml for multiline string 2024-06-10 19:26:01 -07:00
mbecker20 405ec1b8cc bump toml_pretty for fix 2024-06-10 18:58:33 -07:00
mbecker20 4f212bd06f update toml_pretty with skip empty strings 2024-06-10 18:43:53 -07:00
mbecker20 074f4ea2db fix toml 2024-06-10 18:07:05 -07:00
mbecker20 c9abccaf02 build use string serialized version 2024-06-10 17:59:03 -07:00
mbecker20 6428fa6de2 1.7.1 2024-06-10 17:37:22 -07:00
mbecker20 883f54431d custom to toml serializer for api 2024-06-10 17:34:56 -07:00
mbecker20 28dc030e2b custom Vec<EnvVar>, Vec<Conversion> deserializers to support config them as string 2024-06-10 14:39:51 -07:00
mbecker20 145d933e63 pt-2 2024-06-10 01:47:46 -07:00
mbecker20 9772ca1a1c add Resource Sync system user 2024-06-10 01:46:26 -07:00
mbecker20 4059b69201 core auto refreshes all syncs every 5 min 2024-06-09 23:49:02 -07:00
mbecker20 8e175ea5a1 add pending sync alert variant 2024-06-09 23:23:40 -07:00
mbecker20 d931b8b4e7 fix deployment when image_type None 2024-06-09 23:15:52 -07:00
mbecker20 0982800ad2 update client to 1.7.0 2024-06-09 22:47:49 -07:00
mbecker20 4382ad0b3b migrate 1.6 to 1.7 2024-06-09 22:46:21 -07:00
mbecker20 e7891f7870 update docs for ghcr 2024-06-09 21:56:01 -07:00
mbecker20 6bada46841 add export variables / user groups 2024-06-09 21:32:53 -07:00
mbecker20 eae6cbd228 label the image 2024-06-09 20:55:09 -07:00
mbecker20 a0ee6180b2 finish 1.7.0 2024-06-09 19:45:46 -07:00
mbecker20 3ce3de8768 configure registry 2024-06-09 19:34:49 -07:00
mbecker20 6c46993b61 New Monitor logo cr. George Weston 2024-06-09 18:38:58 -07:00
mbecker20 fbd9d14aaa change handler loggin 2024-06-09 15:11:18 -07:00
126 changed files with 3772 additions and 1382 deletions
Generated
+75 -57
View File
@@ -32,7 +32,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.7.0"
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.0"
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.0"
version = "1.8.0"
dependencies = [
"anyhow",
"command",
"formatting",
"monitor_client",
"run_command",
"tracing",
@@ -1482,6 +1486,15 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@@ -1968,7 +1981,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "logger"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"monitor_client",
@@ -2037,7 +2050,7 @@ dependencies = [
[[package]]
name = "migrator"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"chrono",
@@ -2047,7 +2060,6 @@ dependencies = [
"monitor_client",
"mungos",
"serde",
"termination_signal",
"tokio",
"tracing",
]
@@ -2160,7 +2172,7 @@ dependencies = [
[[package]]
name = "monitor_cli"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"clap",
@@ -2180,7 +2192,7 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2203,7 +2215,7 @@ dependencies = [
"strum 0.26.2",
"thiserror",
"tokio",
"tokio-tungstenite 0.22.0",
"tokio-tungstenite 0.23.0",
"tokio-util",
"tracing",
"typeshare",
@@ -2212,7 +2224,7 @@ dependencies = [
[[package]]
name = "monitor_core"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2225,6 +2237,7 @@ dependencies = [
"derive_variants",
"dotenv",
"envy",
"formatting",
"futures",
"git",
"hex",
@@ -2235,6 +2248,7 @@ dependencies = [
"mongo_indexed",
"monitor_client",
"mungos",
"ordered_hash_map",
"parse_csl",
"partial_derive2",
"periphery_client",
@@ -2246,11 +2260,12 @@ dependencies = [
"serror",
"sha2",
"slack_client_rs",
"strum 0.26.2",
"svi",
"termination_signal",
"tokio",
"tokio-util",
"toml",
"toml_pretty",
"tower",
"tower-http",
"tracing",
@@ -2261,7 +2276,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2285,7 +2300,6 @@ dependencies = [
"serror",
"svi",
"sysinfo",
"termination_signal",
"tokio",
"tracing",
"uuid",
@@ -2509,6 +2523,16 @@ dependencies = [
"num-traits",
]
[[package]]
name = "ordered_hash_map"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176"
dependencies = [
"hashbrown 0.13.2",
"serde",
]
[[package]]
name = "outref"
version = "0.5.1"
@@ -2587,7 +2611,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"monitor_client",
@@ -3270,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"
@@ -3289,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"
@@ -3549,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.0"
version = "1.8.0"
dependencies = [
"anyhow",
"dotenv",
@@ -3736,19 +3725,19 @@ dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
"tungstenite 0.21.0",
]
[[package]]
name = "tokio-tungstenite"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d46baf930138837d65e25e3b33be49c9228579a6135dbf756b5cb9e4283e7cef"
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
"tungstenite 0.23.0",
]
[[package]]
@@ -3799,6 +3788,18 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_pretty"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd412f79539d72f97ac0ec9905b0178be1be4f255ddc2ae6f1ff3f7b234bb870"
dependencies = [
"ordered_hash_map",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "tonic"
version = "0.11.0"
@@ -4042,6 +4043,24 @@ dependencies = [
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http 1.1.0",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typed-builder"
version = "0.10.0"
@@ -4119,12 +4138,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.7.0"
version = "1.8.0"
dependencies = [
"anyhow",
"logger",
"monitor_client",
"termination_signal",
"tokio",
"tracing",
]
+6 -4
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
[workspace.package]
version = "1.7.0"
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.6.2"
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,12 +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.2"
parse_csl = "0.1.0"
mungos = "0.5.6"
svi = "1.0.1"
@@ -50,9 +51,10 @@ axum = { version = "0.7.5", features = ["ws", "json"] }
axum-extra = { version = "0.9.3", features = ["typed-header"] }
tower = { version = "0.4.13", features = ["timeout"] }
tower-http = { version = "0.5.2", features = ["fs", "cors"] }
tokio-tungstenite = "0.22.0"
tokio-tungstenite = "0.23.0"
# SER/DE
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
serde = { version = "1.0.203", features = ["derive"] }
strum = { version = "0.26.2", features = ["derive"] }
serde_json = "1.0.117"
-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(())
+2 -1
View File
@@ -19,7 +19,7 @@ pub type ToDelete = Vec<String>;
type UpdatesResult<T> = (ToCreate<T>, ToUpdate<T>, ToDelete);
pub struct ToUpdateItem<T> {
pub struct ToUpdateItem<T: Default> {
pub id: String,
pub resource: ResourceToml<T>,
pub update_description: bool,
@@ -37,6 +37,7 @@ pub trait ResourceSync: Sized {
type PartialConfig: std::fmt::Debug
+ Clone
+ Send
+ Default
+ From<Self::Config>
+ From<Self::ConfigDiff>
+ Serialize
+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)
}
}
+4 -1
View File
@@ -17,22 +17,24 @@ 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
mongo_indexed.workspace = true
resolver_api.workspace = true
toml_pretty.workspace = true
parse_csl.workspace = true
mungos.workspace = true
slack.workspace = true
svi.workspace = true
# external
ordered_hash_map.workspace = true
urlencoding.workspace = true
aws-sdk-ec2.workspace = true
aws-config.workspace = true
@@ -51,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
+12
View File
@@ -14,9 +14,21 @@ RUN cd frontend && yarn link @monitor/client && yarn && yarn build
# Final Image
FROM debian:bookworm-slim
# Install Deps
RUN apt update && apt install -y git ca-certificates
# Copy
COPY ./config_example/core.config.example.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /
COPY --from=frontend-builder /builder/frontend/dist /frontend
# Hint at the port
EXPOSE 9000
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/monitor
LABEL org.opencontainers.image.description="A tool to build and deploy software across many servers"
LABEL org.opencontainers.image.licenses=GPL-3.0
CMD ["./core"]
+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:?}");
+260 -3
View File
@@ -14,7 +14,11 @@ use monitor_client::{
alerter::Alerter,
build::Build,
builder::{Builder, BuilderConfig},
deployment::{Deployment, DeploymentImage},
deployment::{
conversions_to_string, term_signal_labels_to_string,
Deployment, DeploymentImage,
},
environment_vars_to_string,
permission::{PermissionLevel, UserTarget},
procedure::Procedure,
repo::Repo,
@@ -30,8 +34,10 @@ use monitor_client::{
},
};
use mungos::find::find_collect;
use ordered_hash_map::OrderedHashMap;
use partial_derive2::PartialDiff;
use resolver_api::Resolve;
use serde_json::Value;
use crate::{
helpers::query::get_user_user_group_ids,
@@ -112,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)
@@ -305,7 +320,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
.context("failed to get variables from db")?;
}
let toml = toml::to_string(&res)
let toml = serialize_resources_toml(&res)
.context("failed to serialize resources to toml")?;
Ok(ExportResourcesToTomlResponse { toml })
@@ -527,6 +542,248 @@ fn convert_resource<R: MonitorResource>(
.filter_map(|t| tag_names.get(t).cloned())
.collect(),
description: resource.description,
deploy: false,
after: Default::default(),
config,
}
}
fn serialize_resources_toml(
resources: &ResourcesToml,
) -> anyhow::Result<String> {
let mut res = String::new();
let options = toml_pretty::Options::default()
.tab(" ")
.skip_empty_string(true)
.max_inline_array_length(30);
for server in &resources.servers {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[server]]\n");
res.push_str(
&toml_pretty::to_string(&server, options)
.context("failed to serialize servers to toml")?,
);
}
for deployment in &resources.deployments {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[deployment]]\n");
let mut parsed: OrderedHashMap<String, Value> =
serde_json::from_str(&serde_json::to_string(&deployment)?)?;
let config = parsed
.get_mut("config")
.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
{
config.insert(
"term_signal_labels".to_string(),
Value::String(term_signal_labels_to_string(
term_signal_labels,
)),
);
}
if let Some(ports) = &deployment.config.ports {
config.insert(
"ports".to_string(),
Value::String(conversions_to_string(ports)),
);
}
if let Some(volumes) = &deployment.config.volumes {
config.insert(
"volumes".to_string(),
Value::String(conversions_to_string(volumes)),
);
}
if let Some(environment) = &deployment.config.environment {
config.insert(
"environment".to_string(),
Value::String(environment_vars_to_string(environment)),
);
}
if let Some(labels) = &deployment.config.labels {
config.insert(
"labels".to_string(),
Value::String(environment_vars_to_string(labels)),
);
}
res.push_str(
&toml_pretty::to_string(&parsed, options)
.context("failed to serialize deployments to toml")?,
);
}
for build in &resources.builds {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
let mut parsed: OrderedHashMap<String, Value> =
serde_json::from_str(&serde_json::to_string(&build)?)?;
let config = parsed
.get_mut("config")
.context("build has no config?")?
.as_object_mut()
.context("config is not object?")?;
if let Some(version) = &build.config.version {
config.insert(
"version".to_string(),
Value::String(version.to_string()),
);
}
if let Some(build_args) = &build.config.build_args {
config.insert(
"build_args".to_string(),
Value::String(environment_vars_to_string(build_args)),
);
}
if let Some(labels) = &build.config.labels {
config.insert(
"labels".to_string(),
Value::String(environment_vars_to_string(labels)),
);
}
res.push_str("[[build]]\n");
res.push_str(
&toml_pretty::to_string(&parsed, options)
.context("failed to serialize builds to toml")?,
);
}
for repo in &resources.repos {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[repo]]\n");
res.push_str(
&toml_pretty::to_string(&repo, options)
.context("failed to serialize repos to toml")?,
);
}
for procedure in &resources.procedures {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
let mut parsed: OrderedHashMap<String, Value> =
serde_json::from_str(&serde_json::to_string(&procedure)?)?;
let config = parsed
.get_mut("config")
.context("procedure has no config?")?
.as_object_mut()
.context("config is not object?")?;
let stages = config
.remove("stages")
.context("procedure config has no stages")?;
let stages = stages.as_array().context("stages is not array")?;
res.push_str("[[procedure]]\n");
res.push_str(
&toml_pretty::to_string(&parsed, options)
.context("failed to serialize procedures to toml")?,
);
for stage in stages {
res.push_str("\n\n[[procedure.config.stage]]\n");
res.push_str(
&toml_pretty::to_string(stage, options)
.context("failed to serialize procedures to toml")?,
);
}
}
for alerter in &resources.alerters {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[alerter]]\n");
res.push_str(
&toml_pretty::to_string(&alerter, options)
.context("failed to serialize alerters to toml")?,
);
}
for builder in &resources.builders {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[builder]]\n");
res.push_str(
&toml_pretty::to_string(&builder, options)
.context("failed to serialize builders to toml")?,
);
}
for server_template in &resources.server_templates {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[server_template]]\n");
res.push_str(
&toml_pretty::to_string(&server_template, options)
.context("failed to serialize server_templates to toml")?,
);
}
for resource_sync in &resources.resource_syncs {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[resource_sync]]\n");
res.push_str(
&toml_pretty::to_string(&resource_sync, options)
.context("failed to serialize resource_syncs to toml")?,
);
}
for variable in &resources.variables {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[variable]]\n");
res.push_str(
&toml_pretty::to_string(&variable, options)
.context("failed to serialize variables to toml")?,
);
}
for user_group in &resources.user_groups {
if !res.is_empty() {
res.push_str("\n\n##\n\n");
}
res.push_str("[[user_group]]\n");
res.push_str(
&toml_pretty::to_string(&user_group, options)
.context("failed to serialize user_groups to toml")?,
);
}
Ok(res)
}
+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)]
+1 -1
View File
@@ -56,7 +56,7 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.jwt_valid_for),
sync_directory: env
.monitor_sync_directory
.map(|dir|
.map(|dir|
dir.parse()
.context("failed to parse env MONITOR_SYNC_DIRECTORY as valid path").unwrap())
.unwrap_or(config.sync_directory),
+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")
}
+35 -10
View File
@@ -1,3 +1,13 @@
use async_timing_util::{wait_until_timelength, Timelength};
use monitor_client::{
api::write::RefreshResourceSyncPending, entities::user::sync_user,
};
use mungos::find::find_collect;
use resolver_api::Resolve;
use crate::state::{db_client, State};
pub mod deployment;
pub mod remote;
pub mod resource;
pub mod user_groups;
@@ -6,14 +16,29 @@ pub mod variables;
mod file;
mod resources;
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>")
pub fn spawn_sync_refresh_loop() {
tokio::spawn(async move {
let db = db_client().await;
let user = sync_user();
loop {
wait_until_timelength(Timelength::FiveMinutes, 0).await;
let Ok(syncs) = find_collect(&db.resource_syncs, None, None)
.await
.inspect_err(|e| warn!("failed to get resource syncs from db in refresh task | {e:#}")) else {
continue;
};
for sync in syncs {
State
.resolve(
RefreshResourceSyncPending { sync: sync.id },
user.clone(),
)
.await
.inspect_err(|e| {
warn!("failed to refresh resource sync in refresh task | sync: {} | {e:#}", sync.name)
})
.ok();
}
}
});
}
+23 -22
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
@@ -35,7 +34,7 @@ pub type ToDelete = Vec<String>;
type UpdatesResult<T> = (ToCreate<T>, ToUpdate<T>, ToDelete);
pub struct ToUpdateItem<T> {
pub struct ToUpdateItem<T: Default> {
pub id: String,
pub resource: ResourceToml<T>,
pub update_description: bool,
@@ -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)
))
}
+5 -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},
@@ -35,6 +34,7 @@ async fn app() -> anyhow::Result<()> {
// Spawn monitoring loops
monitor::spawn_monitor_loop()?;
helpers::prune::spawn_prune_loop();
helpers::sync::spawn_sync_refresh_loop();
resource::spawn_build_state_refresh_loop();
resource::spawn_repo_state_refresh_loop();
resource::spawn_procedure_state_refresh_loop();
@@ -76,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,
}
+5 -2
View File
@@ -78,7 +78,10 @@ pub trait MonitorResource {
+ From<Self::PartialConfig>
+ PartialDiff<Self::PartialConfig, Self::ConfigDiff>
+ 'static;
type PartialConfig: From<Self::Config> + Serialize + MaybeNone;
type PartialConfig: Default
+ From<Self::Config>
+ Serialize
+ MaybeNone;
type ConfigDiff: Into<Self::PartialConfig>
+ Serialize
+ Diff
@@ -407,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;
+24
View File
@@ -1,9 +1,33 @@
use mungos::{init::MongoBuilder, mongodb::Collection};
use serde::{Deserialize, Serialize};
pub mod build;
pub mod deployment;
pub mod resource;
pub struct DbClient {
pub deployments: Collection<deployment::Deployment>,
pub builds: Collection<build::Build>,
}
impl DbClient {
pub async fn new(
legacy_uri: &str,
legacy_db_name: &str,
) -> DbClient {
let client = MongoBuilder::default()
.uri(legacy_uri)
.build()
.await
.expect("failed to init legacy mongo client");
let db = client.database(legacy_db_name);
DbClient {
deployments: db.collection("Deployment"),
builds: db.collection("Build"),
}
}
}
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
)]
+26 -6
View File
@@ -11,8 +11,15 @@ use serde::Deserialize;
mod legacy;
mod migrate;
#[derive(Deserialize)]
enum AppMode {
V0,
V1_6,
}
#[derive(Deserialize)]
struct Env {
app_mode: AppMode,
legacy_uri: String,
legacy_db_name: String,
target_uri: String,
@@ -28,13 +35,26 @@ async fn main() -> anyhow::Result<()> {
let env: Env = envy::from_env()?;
let legacy_db =
legacy::v0::DbClient::new(&env.legacy_uri, &env.legacy_db_name)
match env.app_mode {
AppMode::V0 => {
let legacy_db = legacy::v0::DbClient::new(
&env.legacy_uri,
&env.legacy_db_name,
)
.await;
let target_db =
DbClient::new(&env.target_uri, &env.target_db_name).await?;
migrate::migrate_all(&legacy_db, &target_db).await?;
let target_db =
DbClient::new(&env.target_uri, &env.target_db_name).await?;
migrate::v0::migrate_all(&legacy_db, &target_db).await?
}
AppMode::V1_6 => {
let db = legacy::v1_6::DbClient::new(
&env.target_uri,
&env.target_db_name,
)
.await;
migrate::v1_6::migrate_all_in_place(&db).await?
}
}
info!("finished!");
+2
View File
@@ -0,0 +1,2 @@
pub mod v0;
pub mod v1_6;
+74
View File
@@ -0,0 +1,74 @@
use anyhow::Context;
use monitor_client::entities::{
build::Build, deployment::Deployment,
};
use mungos::{
find::find_collect,
mongodb::bson::{doc, to_document},
};
use crate::legacy::v1_6;
pub async fn migrate_all_in_place(
db: &v1_6::DbClient,
) -> anyhow::Result<()> {
migrate_deployments_in_place(db).await?;
migrate_builds_in_place(db).await?;
Ok(())
}
pub async fn migrate_deployments_in_place(
db: &v1_6::DbClient,
) -> anyhow::Result<()> {
let deployments = find_collect(&db.deployments, None, None)
.await
.context("failed to get deployments")?
.into_iter()
.map(Into::into)
.collect::<Vec<Deployment>>();
info!("migrating {} deployments...", deployments.len());
for deployment in deployments {
db.deployments
.update_one(
doc! { "name": &deployment.name },
doc! { "$set": to_document(&deployment)? },
None,
)
.await
.context("failed to insert deployments on target")?;
}
info!("deployments have been migrated\n");
Ok(())
}
pub async fn migrate_builds_in_place(
db: &v1_6::DbClient,
) -> anyhow::Result<()> {
let builds = find_collect(&db.builds, None, None)
.await
.context("failed to get builds")?
.into_iter()
.map(Into::into)
.collect::<Vec<Build>>();
info!("migrating {} builds...", builds.len());
for build in builds {
db.builds
.update_one(
doc! { "name": &build.name },
doc! { "$set": to_document(&build)? },
None,
)
.await
.context("failed to insert builds on target")?;
}
info!("builds have been migrated\n");
Ok(())
}
-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 =
+1 -4
View File
@@ -40,10 +40,7 @@ pub async fn docker_login(
.await;
Ok(true)
}
ImageRegistry::Ghcr(CloudRegistryConfig {
account,
..
}) => {
ImageRegistry::Ghcr(CloudRegistryConfig { account, .. }) => {
if account.is_empty() {
return Err(anyhow!(
"Must configure account for GithubContainerRegistry"
+1 -2
View File
@@ -29,7 +29,6 @@ async fn task(
req_id: Uuid,
request: crate::api::PeripheryRequest,
) -> anyhow::Result<String> {
info!("request {req_id} | {request:?}");
let timer = Instant::now();
let res =
@@ -48,7 +47,7 @@ async fn task(
}
let elapsed = timer.elapsed();
info!("request {req_id} | resolve time: {elapsed:?}");
debug!("request {req_id} | resolve time: {elapsed:?}");
res
}
+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.
+15 -15
View File
@@ -17,29 +17,29 @@ docker = ["dep:bollard"]
[dependencies]
# mogh
mongo_indexed = { workspace = true, optional = true }
serror.workspace = true
resolver_api.workspace = true
derive_default_builder.workspace = true
derive_empty_traits.workspace = true
async_timing_util.workspace = true
partial_derive2.workspace = true
derive_variants.workspace = true
derive_default_builder.workspace = true
async_timing_util.workspace = true
resolver_api.workspace = true
serror.workspace = true
# external
bollard = { workspace = true, optional = true }
reqwest.workspace = true
anyhow.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
envy.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tokio-tungstenite.workspace = true
futures.workspace = true
typeshare.workspace = true
strum.workspace = true
derive_builder.workspace = true
serde_json.workspace = true
tokio-util.workspace = true
thiserror.workspace = true
typeshare.workspace = true
futures.workspace = true
reqwest.workspace = true
tracing.workspace = true
anyhow.workspace = true
serde.workspace = true
tokio.workspace = true
strum.workspace = true
envy.workspace = true
uuid.workspace = true
clap.workspace = true
bson.workspace = true
+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]
+7 -7
View File
@@ -42,6 +42,13 @@ pub struct AlerterConfig {
#[partial_default(default_enabled())]
pub enabled: bool,
/// Where to route the alert messages.
///
/// Default: Custom endpoint `http://localhost:7000`
#[serde(default)]
#[builder(default)]
pub endpoint: AlerterEndpoint,
/// Only send specific alert types.
/// If empty, will send all alert types.
#[serde(default)]
@@ -53,13 +60,6 @@ pub struct AlerterConfig {
#[serde(default)]
#[builder(default)]
pub resources: Vec<ResourceTarget>,
/// Where to route the alert messages.
///
/// Default: Custom endpoint `http://localhost:7000`
#[serde(default)]
#[builder(default)]
pub endpoint: AlerterEndpoint,
}
impl AlerterConfig {
+36 -22
View File
@@ -71,11 +71,6 @@ pub struct BuildConfig {
#[builder(default)]
pub builder_id: String,
/// Whether to skip secret interpolation in the build_args.
#[serde(default)]
#[builder(default)]
pub skip_secret_interp: bool,
/// The current version of the build.
#[serde(default)]
#[builder(default)]
@@ -108,6 +103,11 @@ pub struct BuildConfig {
#[builder(default)]
pub pre_build: SystemCommand,
/// Configuration for the registry to push the built image to.
#[serde(default)]
#[builder(default)]
pub image_registry: ImageRegistry,
/// The path of the docker build context relative to the root of the repo.
/// Default: "." (the root of the repo).
#[serde(default = "default_build_path")]
@@ -121,36 +121,50 @@ pub struct BuildConfig {
#[partial_default(default_dockerfile_path())]
pub dockerfile_path: String,
/// Docker build arguments
/// Whether to skip secret interpolation in the build_args.
#[serde(default)]
#[builder(default)]
pub build_args: Vec<EnvironmentVar>,
/// Docker labels
#[serde(default)]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
/// Any extra docker cli arguments to be included in the build command
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
pub skip_secret_interp: bool,
/// Whether to use buildx to build (eg `docker buildx build ...`)
#[serde(default)]
#[builder(default)]
pub use_buildx: bool,
/// Configuration for the registry to push the built image to.
#[serde(default)]
#[builder(default)]
pub image_registry: ImageRegistry,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
#[builder(default = "default_webhook_enabled()")]
#[partial_default(default_webhook_enabled())]
pub webhook_enabled: bool,
/// Any extra docker cli arguments to be included in the build command
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
/// Docker build arguments
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub build_args: Vec<EnvironmentVar>,
/// Docker labels
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
}
impl BuildConfig {
+9 -3
View File
@@ -70,6 +70,12 @@ pub enum PartialBuilderConfig {
Aws(_PartialAwsBuilderConfig),
}
impl Default for PartialBuilderConfig {
fn default() -> Self {
Self::Aws(Default::default())
}
}
impl MaybeNone for PartialBuilderConfig {
fn is_none(&self) -> bool {
match self {
@@ -290,9 +296,6 @@ pub struct AwsBuilderConfig {
pub ami_id: String,
/// The subnet id to create the instance in.
pub 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.
pub security_group_ids: Vec<String>,
/// The key pair name to attach to the instance
pub key_pair_name: String,
/// Whether to assign the instance a public IP address.
@@ -301,6 +304,9 @@ pub struct AwsBuilderConfig {
/// Whether core should use the public IP address to communicate with periphery on the builder.
/// If false, core will communicate with the instance using the private IP.
pub use_public_ip: bool,
/// The security group ids to attach to the instance.
/// This should include a security group to allow core inbound access to the periphery port.
pub security_group_ids: Vec<String>,
/// Which github accounts (usernames) are available on the AMI
#[serde(default)]
+399 -49
View File
@@ -1,11 +1,15 @@
use std::collections::HashMap;
use anyhow::Context;
use bson::{doc, Document};
use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use derive_variants::EnumVariants;
use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer, Serialize,
};
use strum::{Display, EnumString};
use typeshare::typeshare;
@@ -42,7 +46,7 @@ pub type _PartialDeploymentConfig = PartialDeploymentConfig;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]
#[partial_derive(Serialize, Deserialize, Debug, Clone)]
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
#[partial(skip_serializing_none, from, diff)]
pub struct DeploymentConfig {
/// The id of server the deployment is deployed on.
@@ -51,12 +55,6 @@ pub struct DeploymentConfig {
#[builder(default)]
pub server_id: String,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
#[partial_default(default_send_alerts())]
pub send_alerts: bool,
/// The image which the deployment deploys.
/// Can either be a user inputted image, or a Monitor build.
#[serde(default)]
@@ -84,46 +82,11 @@ pub struct DeploymentConfig {
#[builder(default)]
pub redeploy_on_build: bool,
/// Labels attached to various termination signal options.
/// Used to specify different shutdown functionality depending on the termination signal.
#[serde(default = "default_term_signal_labels")]
#[builder(default = "default_term_signal_labels()")]
#[partial_default(default_term_signal_labels())]
pub term_signal_labels: Vec<TerminationSignalLabel>,
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
#[serde(default)]
#[builder(default)]
pub termination_signal: TerminationSignal,
/// The termination timeout.
#[serde(default = "default_termination_timeout")]
#[builder(default = "default_termination_timeout()")]
#[partial_default(default_termination_timeout())]
pub termination_timeout: i32,
/// The container port mapping.
/// Irrelevant if container network is `host`.
/// Maps ports on host to ports on container.
#[serde(default)]
#[builder(default)]
pub ports: Vec<Conversion>,
/// The container volume mapping.
/// Maps files / folders on host to files / folders in container.
#[serde(default)]
#[builder(default)]
pub volumes: Vec<Conversion>,
/// The environment variables passed to the container.
#[serde(default)]
#[builder(default)]
pub environment: Vec<EnvironmentVar>,
/// The docker labels given to the container.
#[serde(default)]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
#[partial_default(default_send_alerts())]
pub send_alerts: bool,
/// The network attached to the container.
/// Default is `host`.
@@ -145,11 +108,81 @@ pub struct DeploymentConfig {
#[builder(default)]
pub command: String,
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
#[serde(default)]
#[builder(default)]
pub termination_signal: TerminationSignal,
/// The termination timeout.
#[serde(default = "default_termination_timeout")]
#[builder(default = "default_termination_timeout()")]
#[partial_default(default_termination_timeout())]
pub termination_timeout: i32,
/// Extra args which are interpolated into the `docker run` command,
/// and affect the container configuration.
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
/// Labels attached to various termination signal options.
/// Used to specify different shutdown functionality depending on the termination signal.
#[serde(
default = "default_term_signal_labels",
deserialize_with = "term_labels_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "option_term_labels_deserializer"
))]
#[builder(default = "default_term_signal_labels()")]
#[partial_default(default_term_signal_labels())]
pub term_signal_labels: Vec<TerminationSignalLabel>,
/// The container port mapping.
/// Irrelevant if container network is `host`.
/// Maps ports on host to ports on container.
#[serde(default, deserialize_with = "conversions_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_conversions_deserializer"
))]
#[builder(default)]
pub ports: Vec<Conversion>,
/// The container volume mapping.
/// Maps files / folders on host to files / folders in container.
#[serde(default, deserialize_with = "conversions_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_conversions_deserializer"
))]
#[builder(default)]
pub volumes: Vec<Conversion>,
/// The environment variables passed to the container.
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub environment: Vec<EnvironmentVar>,
/// The docker labels given to the container.
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
}
impl DeploymentConfig {
@@ -244,7 +277,7 @@ impl Default for DeploymentImage {
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Conversion {
/// reference on the server.
@@ -253,6 +286,165 @@ pub struct Conversion {
pub container: String,
}
pub fn conversions_to_string(conversions: &[Conversion]) -> String {
conversions
.iter()
.map(|Conversion { local, container }| {
format!("{local}={container}")
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn conversions_from_str(
value: &str,
) -> anyhow::Result<Vec<Conversion>> {
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.is_empty() && !line.starts_with('#'))
.map(|(i, line)| {
let mut split = line.split('=');
let local = split
.next()
.with_context(|| {
format!("line {i} does not have 'local' key")
})?
.trim()
.to_string();
// remove trailing comments
let mut container_split = split
.next()
.with_context(|| {
format!("line {i} does not have 'container' key")
})?
.split('#');
let container = container_split
.next()
.with_context(|| {
format!("line {i} does not have 'container' key")
})?
.trim()
.to_string();
anyhow::Ok(Conversion { local, container })
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(res)
}
pub fn conversions_deserializer<'de, D>(
deserializer: D,
) -> Result<Vec<Conversion>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ConversionVisitor)
}
pub fn option_conversions_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<Vec<Conversion>>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionConversionVisitor)
}
struct ConversionVisitor;
impl<'de> Visitor<'de> for ConversionVisitor {
type Value = Vec<Conversion>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<Conversion>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
conversions_from_str(v)
.map_err(|e| serde::de::Error::custom(format!("{e:#}")))
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
#[derive(Deserialize)]
struct ConversionInner {
local: String,
container: String,
}
impl From<ConversionInner> for Conversion {
fn from(value: ConversionInner) -> Self {
Self {
local: value.local,
container: value.container,
}
}
}
let res = Vec::<ConversionInner>::deserialize(
SeqAccessDeserializer::new(seq),
)?
.into_iter()
.map(Into::into)
.collect();
Ok(res)
}
}
struct OptionConversionVisitor;
impl<'de> Visitor<'de> for OptionConversionVisitor {
type Value = Option<Vec<Conversion>>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<Conversion>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
ConversionVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
ConversionVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
/// A summary of a docker container on a server.
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -401,6 +593,164 @@ pub struct TerminationSignalLabel {
pub label: String,
}
pub fn term_signal_labels_to_string(
labels: &[TerminationSignalLabel],
) -> String {
labels
.iter()
.map(|TerminationSignalLabel { signal, label }| {
format!("{signal}={label}")
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn term_signal_labels_from_str(
value: &str,
) -> anyhow::Result<Vec<TerminationSignalLabel>> {
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.is_empty() && !line.starts_with('#'))
.map(|(i, line)| {
let mut split = line.split('=');
let signal = split
.next()
.with_context(|| format!("line {i} does not have signal"))?
.trim()
.parse::<TerminationSignal>()
.with_context(|| {
format!("line {i} does not have valid signal")
})?;
// remove trailing comments
let mut label_split = split
.next()
.with_context(|| format!("line {i} does not have label"))?
.split('#');
let label = label_split
.next()
.with_context(|| format!("line {i} does not have label"))?
.trim()
.to_string();
anyhow::Ok(TerminationSignalLabel { signal, label })
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(res)
}
pub fn term_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<Vec<TerminationSignalLabel>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TermSignalLabelVisitor)
}
pub fn option_term_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<Vec<TerminationSignalLabel>>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionTermSignalLabelVisitor)
}
struct TermSignalLabelVisitor;
impl<'de> Visitor<'de> for TermSignalLabelVisitor {
type Value = Vec<TerminationSignalLabel>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<TerminationSignalLabel>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
term_signal_labels_from_str(v)
.map_err(|e| serde::de::Error::custom(format!("{e:#}")))
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
#[derive(Deserialize)]
struct TermSignalLabelInner {
signal: TerminationSignal,
label: String,
}
impl From<TermSignalLabelInner> for TerminationSignalLabel {
fn from(value: TermSignalLabelInner) -> Self {
Self {
signal: value.signal,
label: value.label,
}
}
}
let res = Vec::<TermSignalLabelInner>::deserialize(
SeqAccessDeserializer::new(seq),
)?
.into_iter()
.map(Into::into)
.collect();
Ok(res)
}
}
struct OptionTermSignalLabelVisitor;
impl<'de> Visitor<'de> for OptionTermSignalLabelVisitor {
type Value = Option<Vec<TerminationSignalLabel>>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<TerminationSignalLabel>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
TermSignalLabelVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
TermSignalLabelVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
#[typeshare]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct DeploymentActionState {
+164 -4
View File
@@ -6,8 +6,11 @@ use build::CloudRegistryConfig;
use clap::Parser;
use derive_empty_traits::EmptyTraits;
use serde::{
de::{value::MapAccessDeserializer, Visitor},
Deserialize, Serialize,
de::{
value::{MapAccessDeserializer, SeqAccessDeserializer},
Visitor,
},
Deserialize, Deserializer, Serialize,
};
use serror::Serror;
use strum::{AsRefStr, Display, EnumString};
@@ -41,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;
@@ -324,13 +327,166 @@ impl Version {
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct EnvironmentVar {
pub variable: String,
pub value: String,
}
pub fn environment_vars_to_string(vars: &[EnvironmentVar]) -> String {
vars
.iter()
.map(|EnvironmentVar { variable, value }| {
format!("{variable}={value}")
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn environment_vars_from_str(
value: &str,
) -> anyhow::Result<Vec<EnvironmentVar>> {
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.is_empty() && !line.starts_with('#'))
.map(|(i, line)| {
let mut split = line.split('=');
let variable = split
.next()
.with_context(|| format!("line {i} does not have variable"))?
.trim()
.to_string();
// remove trailing comments
let mut value_split = split
.next()
.with_context(|| format!("line {i} does not have value"))?
.split('#');
let value = value_split
.next()
.with_context(|| format!("line {i} does not have value"))?
.trim()
.to_string();
anyhow::Ok(EnvironmentVar { variable, value })
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(res)
}
pub fn env_vars_deserializer<'de, D>(
deserializer: D,
) -> Result<Vec<EnvironmentVar>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(EnvironmentVarVisitor)
}
pub fn option_env_vars_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<Vec<EnvironmentVar>>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionEnvVarVisitor)
}
struct EnvironmentVarVisitor;
impl<'de> Visitor<'de> for EnvironmentVarVisitor {
type Value = Vec<EnvironmentVar>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
environment_vars_from_str(v)
.map_err(|e| serde::de::Error::custom(format!("{e:#}")))
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
#[derive(Deserialize)]
struct EnvironmentVarInner {
variable: String,
value: String,
}
impl From<EnvironmentVarInner> for EnvironmentVar {
fn from(value: EnvironmentVarInner) -> Self {
Self {
variable: value.variable,
value: value.value,
}
}
}
let res = Vec::<EnvironmentVarInner>::deserialize(
SeqAccessDeserializer::new(seq),
)?
.into_iter()
.map(Into::into)
.collect();
Ok(res)
}
}
struct OptionEnvVarVisitor;
impl<'de> Visitor<'de> for OptionEnvVarVisitor {
type Value = Option<Vec<EnvironmentVar>>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
EnvironmentVarVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
EnvironmentVarVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatestCommit {
pub hash: String,
@@ -344,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>,
@@ -356,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),
@@ -370,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),
@@ -385,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,
}
+2 -1
View File
@@ -56,7 +56,8 @@ pub type _PartialProcedureConfig = PartialProcedureConfig;
#[partial(skip_serializing_none, from, diff)]
pub struct ProcedureConfig {
/// The stages to be run by the procedure.
#[serde(default)]
#[serde(default, alias = "stage")]
#[partial_attr(serde(alias = "stage"))]
#[builder(default)]
pub stages: Vec<ProcedureStage>,
+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(),
+3 -1
View File
@@ -12,7 +12,7 @@ use super::update::ResourceTargetVariant;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
pub struct Resource<Config, Info: Default = ()> {
pub struct Resource<Config: Default, Info: Default = ()> {
/// The Mongo ID of the resource.
/// This field is de/serialized from/to JSON as
/// `{ "_id": { "$oid": "..." }, ...(rest of serialized Resource<T>) }`
@@ -50,6 +50,8 @@ pub struct Resource<Config, Info: Default = ()> {
pub info: Info,
/// Resource-specific configuration.
#[serde(default)]
#[builder(default)]
pub config: Config,
}
+5 -5
View File
@@ -47,6 +47,11 @@ pub struct ServerConfig {
/// Example: http://localhost:8120
pub address: String,
/// An optional region label
#[serde(default)]
#[builder(default)]
pub region: String,
/// Whether a server is enabled.
/// If a server is disabled,
/// you won't be able to perform any actions on it or see deployment's status.
@@ -94,11 +99,6 @@ pub struct ServerConfig {
#[partial_default(default_send_alerts())]
pub send_disk_alerts: bool,
/// An optional region label
#[serde(default)]
#[builder(default)]
pub region: String,
/// The percentage threshhold which triggers WARNING state for CPU.
#[serde(default = "default_cpu_warning")]
#[builder(default = "default_cpu_warning()")]
@@ -78,6 +78,12 @@ pub enum PartialServerTemplateConfig {
Hetzner(hetzner::_PartialHetznerServerTemplateConfig),
}
impl Default for PartialServerTemplateConfig {
fn default() -> Self {
Self::Aws(Default::default())
}
}
impl MaybeNone for PartialServerTemplateConfig {
fn is_none(&self) -> bool {
match self {
+32 -13
View File
@@ -48,13 +48,6 @@ pub struct ResourcesToml {
)]
pub procedures: Vec<ResourceToml<PartialProcedureConfig>>,
#[serde(
default,
rename = "builder",
skip_serializing_if = "Vec::is_empty"
)]
pub builders: Vec<ResourceToml<PartialBuilderConfig>>,
#[serde(
default,
rename = "alerter",
@@ -62,6 +55,13 @@ pub struct ResourcesToml {
)]
pub alerters: Vec<ResourceToml<PartialAlerterConfig>>,
#[serde(
default,
rename = "builder",
skip_serializing_if = "Vec::is_empty"
)]
pub builders: Vec<ResourceToml<PartialBuilderConfig>>,
#[serde(
default,
rename = "server_template",
@@ -93,22 +93,41 @@ pub struct ResourcesToml {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceToml<PartialConfig> {
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,
}
//
+14 -1
View File
@@ -18,7 +18,20 @@ If the build directory is the root of the repository, you pass the build path as
The dockerfile's path is given relative to the build directory. So if your build directory is `build/directory` and the dockerfile is in `build/directory/Dockerfile.example`, you give the dockerfile path simply as `Dockerfile.example`.
Just as with private repos, you will need to select a docker account to use with `docker push`.
### Image registry
Monitor supports pushing to DockerHub and Github Container Registry (ghcr.io).
Any of the Docker / Github accounts that are specified in config, between the core config and builder, will be available to use.
Additionally, allowed organizations for both DockerHub and Github can be specified on the core config and attached to builds.
Doing so will cause the images to be published under the organization's namespace rather than the account's.
When connecting a build to a deployments, the default behavior is for the deployment to inherit the registry configuration from the build.
In cases where that account isn't available to the deployment, another account can be chosen in the deployment config.
:::note
In order to publish to the Github Container Registry, your Github access token must be given the `write:packages` permission.
See the Github docs [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic).
:::
### Adding build args
+28 -4
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
@@ -25,13 +49,13 @@ See [config docs](https://docs.rs/monitor_client/latest/monitor_client/entities/
## 2. Start monitor core
Monitor core is distributed via dockerhub under the public repo [mbecker2020/monitor_core](https://hub.docker.com/r/mbecker2020/monitor_core).
Monitor core is distributed via Github Container Registry under the package [mbecker20/monitor_core](https://github.com/mbecker20/monitor/pkgs/container/monitor_core).
```sh
docker run -d --name monitor-core \
--network host \
-v $HOME/.monitor/core.config.toml:/config/config.toml \
-p 9000:9000 \
mbecker2020/monitor_core
ghcr.io/mbecker20/monitor_core
```
## First login
@@ -12,7 +12,7 @@ By default, Monitor will deploy the latest available version of the build, or yo
Also by default, Monitor will use the same docker account that is attached to the build in order to pull the image on the periphery server. If that account is not available on the server, you can specify another available account to use instead, this account just needs to have read access to the docker repository.
### Using a custom image
You can also manually specify an image name, like `mongo` or `mbecker2020/random_image:0.1.1`.
You can also manually specify an image name, like `mongo` or `ghcr.io/mbecker20/random_image:0.1.1`.
If the image repository is private, you can still select an available docker account to use to pull the image.
+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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

+235 -42
View File
@@ -184,15 +184,7 @@ export const DoubleInput = <
);
};
export const AccountSelector = ({
disabled,
id,
type,
account_type,
selected,
onSelect,
placeholder,
}: {
export const AccountSelectorConfig = (params: {
disabled: boolean;
id?: string;
type: "Server" | "None" | "Builder";
@@ -200,6 +192,28 @@ export const AccountSelector = ({
selected: string | undefined;
onSelect: (id: string) => void;
placeholder: string;
}) => {
return (
<ConfigItem label={`${params.account_type} Account`}>
<AccountSelector {...params} />
</ConfigItem>
);
};
export const AccountSelector = ({
disabled,
id,
type,
account_type,
selected,
onSelect,
}: {
disabled: boolean;
id?: string;
type: "Server" | "None" | "Builder";
account_type: keyof Types.GetBuilderAvailableAccountsResponse;
selected: string | undefined;
onSelect: (id: string) => void;
}) => {
const [request, params] =
type === "Server" || type === "None"
@@ -207,30 +221,28 @@ export const AccountSelector = ({
: ["GetBuilderAvailableAccounts", { builder: id }];
const accounts = useRead(request as any, params).data;
return (
<ConfigItem label={`${account_type} Account`}>
<Select
value={type === "Builder" ? selected || undefined : selected}
onValueChange={(value) => {
onSelect(value === "Empty" ? "" : value);
}}
<Select
value={selected}
onValueChange={(value) => {
onSelect(value === "Empty" ? "" : value);
}}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>{placeholder}</SelectItem>
{(accounts as any)?.[account_type]?.map((account: string) => (
<SelectItem key={account} value={account}>
{account}
</SelectItem>
))}
</SelectContent>
</Select>
</ConfigItem>
<SelectValue placeholder="Select Account" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>None</SelectItem>
{(accounts as any)?.[account_type]?.map((account: string) => (
<SelectItem key={account} value={account}>
{account}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
@@ -440,19 +452,73 @@ export const ImageRegistryConfig = ({
registry,
setRegistry,
disabled,
type,
resource_id,
registry_types,
}: {
registry: Types.ImageRegistry | undefined;
setRegistry: (registry: Types.ImageRegistry) => void;
disabled: boolean;
type: "Build" | "Deployment";
// For builds, its builder id. For servers, its server id.
resource_id?: string;
registry_types?: Types.ImageRegistry["type"][];
}) => {
const _registry = registry ?? default_registry_config("None");
const cloud_params =
_registry.type === "DockerHub" || _registry.type === "Ghcr"
? _registry.params
: undefined;
if (_registry.type === "None" || _registry.type === "Custom") {
return (
<ConfigItem label="Image Registry">
<RegistryTypeSelector
registry={_registry}
setRegistry={setRegistry}
disabled={disabled}
deployment={type === "Deployment"}
registry_types={registry_types}
/>
</ConfigItem>
);
}
return (
<ConfigItem label="Image Registry">
<RegistryTypeSelector
registry={_registry}
setRegistry={setRegistry}
disabled={disabled}
/>
<div className="flex items-center justify-stretch gap-4">
{type === "Build" && cloud_params?.account && (
<OrganizationSelector
value={cloud_params?.organization}
set={(organization) =>
setRegistry({
..._registry,
params: { ..._registry.params, organization },
})
}
disabled={disabled}
type={_registry.type === "DockerHub" ? "Docker" : "Github"}
/>
)}
<AccountSelector
id={resource_id}
type={type === "Build" ? "Builder" : "Server"}
account_type={_registry.type === "DockerHub" ? "docker" : "github"}
selected={cloud_params?.account}
onSelect={(account) =>
setRegistry({
..._registry,
params: { ..._registry.params, account },
})
}
disabled={disabled}
/>
<RegistryTypeSelector
registry={_registry}
setRegistry={setRegistry}
disabled={disabled}
deployment={type === "Deployment"}
registry_types={registry_types}
/>
</div>
</ConfigItem>
);
};
@@ -466,17 +532,23 @@ const REGISTRY_TYPES: Types.ImageRegistry["type"][] = [
const RegistryTypeSelector = ({
registry,
setRegistry,
registry_types = REGISTRY_TYPES,
disabled,
deployment,
}: {
registry: Types.ImageRegistry;
setRegistry: (registry: Types.ImageRegistry) => void;
registry_types?: Types.ImageRegistry["type"][];
disabled: boolean;
deployment?: boolean;
}) => {
return (
<Select
value={registry.type}
onValueChange={(type: Types.ImageRegistry["type"]) => {
setRegistry(default_registry_config(type));
value={to_readable_registry_type(registry.type, deployment)}
onValueChange={(type) => {
setRegistry(
default_registry_config(from_readable_registry_type(type, deployment))
);
}}
disabled={disabled}
>
@@ -487,9 +559,49 @@ const RegistryTypeSelector = ({
<SelectValue placeholder="Select Registry" />
</SelectTrigger>
<SelectContent align="end">
{REGISTRY_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{type}
{registry_types.map((type) => {
const t = to_readable_registry_type(type, deployment);
return (
<SelectItem key={type} value={t}>
{t}
</SelectItem>
);
})}
</SelectContent>
</Select>
);
};
const OrganizationSelector = ({
value,
set,
disabled,
type,
}: {
value?: string;
set: (org: string) => void;
disabled: boolean;
type: "Docker" | "Github";
}) => {
const organizations = useRead(`List${type}Organizations`, {}).data;
if (!organizations || organizations.length === 0) return null;
return (
<Select
value={value}
onValueChange={(v) => set(v === "Empty" ? "" : v)}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder="Select Organization" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>None</SelectItem>
{organizations?.map((org) => (
<SelectItem key={org} value={org}>
{org}
</SelectItem>
))}
</SelectContent>
@@ -497,6 +609,22 @@ const RegistryTypeSelector = ({
);
};
const to_readable_registry_type = (
type: Types.ImageRegistry["type"],
deployment?: boolean
) => {
if (deployment && type === "None") return "Same as build";
return type;
};
const from_readable_registry_type = (
readable: string,
deployment?: boolean
) => {
if (deployment && readable === "Same as build") return "None";
return readable as Types.ImageRegistry["type"];
};
const default_registry_config = (
type: Types.ImageRegistry["type"]
): Types.ImageRegistry => {
@@ -511,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>
);
};
+25 -7
View File
@@ -16,10 +16,12 @@ export const ExportButton = ({
targets,
user_groups,
tags,
include_variables,
}: {
targets?: Types.ResourceTarget[];
user_groups?: string[];
tags?: string[];
include_variables?: boolean;
}) => {
const [open, setOpen] = useState(false);
return (
@@ -30,12 +32,16 @@ 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>
{targets || user_groups ? (
<ExportTargetsLoader targets={targets} user_groups={user_groups} />
{targets || user_groups || include_variables ? (
<ExportTargetsLoader
targets={targets}
user_groups={user_groups}
include_variables={include_variables}
/>
) : (
<ExportAllLoader tags={tags} />
)}
@@ -45,21 +51,33 @@ export const ExportButton = ({
};
const ExportTargetsLoader = ({
user_groups,
targets,
user_groups,
include_variables,
}: {
user_groups?: string[];
targets?: Types.ResourceTarget[];
user_groups?: string[];
include_variables?: boolean;
}) => {
const { data, isPending } = useRead("ExportResourcesToToml", {
targets: targets ? targets : [],
user_groups: user_groups ? user_groups : [],
include_variables,
});
return <ExportPre loading={isPending} content={data?.toml} />;
};
const ExportAllLoader = ({ tags }: { tags?: string[] }) => {
const { data, isPending } = useRead("ExportAllResourcesToToml", { tags });
const ExportAllLoader = ({
tags,
include_variables,
}: {
tags?: string[];
include_variables?: boolean;
}) => {
const { data, isPending } = useRead("ExportAllResourcesToToml", {
tags,
include_variables,
});
return <ExportPre loading={isPending} content={data?.toml} />;
};
+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,
]
);
};
@@ -15,6 +15,7 @@ const ALERT_TYPES: Types.AlertData["type"][] = [
"ServerMem",
"ServerDisk",
"ContainerStateChange",
"ResourceSyncPendingUpdates",
"AwsBuilderTerminationFailed",
];
@@ -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 (
@@ -1,21 +1,21 @@
import { Config } from "@components/config";
import {
AccountSelector,
AccountSelectorConfig,
AddExtraArgMenu,
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,
@@ -115,7 +115,7 @@ export const BuildConfig = ({
github_account:
(update.builder_id ?? config.builder_id ? true : false) &&
((account, set) => (
<AccountSelector
<AccountSelectorConfig
id={update.builder_id ?? config.builder_id ?? undefined}
type="Builder"
account_type="github"
@@ -130,13 +130,19 @@ export const BuildConfig = ({
{
label: "Docker",
components: {
image_registry: (registry, set) => (
<ImageRegistryConfig
registry={registry}
setRegistry={(image_registry) => set({ image_registry })}
disabled={disabled}
/>
),
image_registry: (registry, set) => {
const builder_id = update.builder_id ?? config.builder_id;
if (!builder_id) return null;
return (
<ImageRegistryConfig
registry={registry}
setRegistry={(image_registry) => set({ image_registry })}
type="Build"
resource_id={builder_id}
disabled={disabled}
/>
);
},
build_path: true,
dockerfile_path: true,
// docker_account: (account, set) =>
@@ -203,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]"
>
@@ -219,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}
/>
),
},
},
@@ -251,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,
},
},
@@ -264,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">
@@ -312,7 +300,6 @@ const BuildArgs = ({
placeholder="VARIABLE=value"
value={args}
onChange={(e) => setArgs(e.target.value)}
onBlur={update}
disabled={disabled}
/>
</ConfigItem>
@@ -337,140 +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>
);
};
// const DockerOrganizations = ({
// value,
// set,
// disabled,
// }: {
// value?: string;
// set: (input: Partial<Types.BuildConfig>) => void;
// disabled: boolean;
// }) => {
// const docker_organizations = useRead("ListDockerOrganizations", {}).data;
// return (
// <ConfigItem label="Docker Organization">
// <Select
// value={value}
// onValueChange={(value) =>
// set({ docker_organization: value === "Empty" ? "" : value })
// }
// disabled={disabled}
// >
// <SelectTrigger
// className="w-full lg:w-[300px] max-w-[50%]"
// disabled={disabled}
// >
// <SelectValue placeholder="Select Organization" />
// </SelectTrigger>
// <SelectContent>
// <SelectItem value={"Empty"}>None</SelectItem>
// {docker_organizations?.map((org) => (
// <SelectItem key={org} value={org}>
// {org}
// </SelectItem>
// ))}
// </SelectContent>
// </Select>
// </ConfigItem>
// );
// };
@@ -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>
);
};

Some files were not shown because too many files have changed in this diff Show More