Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aa9513dd0 | ||
|
|
26b216b478 | ||
|
|
166299bb57 | ||
|
|
03c47eb3dc | ||
|
|
1fcb4ad085 | ||
|
|
f51af8fbe1 | ||
|
|
4a975e1b92 | ||
|
|
ba556e3284 | ||
|
|
299a326942 | ||
|
|
a5d4b9aefb | ||
|
|
40b820ae42 | ||
|
|
7028bf2996 | ||
|
|
75ebd0e6c0 | ||
|
|
426153df66 | ||
|
|
5bd423a6a6 | ||
|
|
c24131d383 | ||
|
|
9f54b6c26a | ||
|
|
ab8ae51ece | ||
|
|
ef2a83ff16 | ||
|
|
7872771aee | ||
|
|
b12cf858d8 | ||
|
|
38dba91c3a | ||
|
|
ea8136aa57 | ||
|
|
f956e12e28 | ||
|
|
207ea52b95 | ||
|
|
caf28d3a26 | ||
|
|
8fff45649d | ||
|
|
de5df70e11 | ||
|
|
3df010ac2a | ||
|
|
2d3beb708e | ||
|
|
1dc22d01c4 | ||
|
|
eb029d0408 | ||
|
|
f926932181 | ||
|
|
cc96d80c6a | ||
|
|
144b49495c | ||
|
|
de9354bdc7 | ||
|
|
38bfee84d7 | ||
|
|
ec33d9fb9e | ||
|
|
0a66937b1d | ||
|
|
43cc0c3bc1 | ||
|
|
c14b395c70 | ||
|
|
7b8529a7c6 | ||
|
|
547c089581 | ||
|
|
4fe5e461b3 | ||
|
|
edfb873f7c | ||
|
|
5ef5294c44 | ||
|
|
5d3c50e04f | ||
|
|
f10efbb5ba | ||
|
|
39ce98161b | ||
|
|
cff6e79eee | ||
|
|
dedf22ede8 | ||
|
|
6955b92a99 | ||
|
|
5c63eeab02 | ||
|
|
4c14a4ae20 | ||
|
|
29fd856a2d | ||
|
|
195bdbd94a | ||
|
|
298ccd945c | ||
|
|
436e4e79e9 | ||
|
|
8b8c89d976 | ||
|
|
25c8d25636 | ||
|
|
ea242de2e4 | ||
|
|
be03547407 | ||
|
|
9c0d28b311 | ||
|
|
f269deb99c | ||
|
|
3df8163131 | ||
|
|
33a16a9bd2 | ||
|
|
215e7d1bdc | ||
|
|
25e0905c0c | ||
|
|
1c07ccea85 | ||
|
|
405ec1b8cc | ||
|
|
4f212bd06f | ||
|
|
074f4ea2db | ||
|
|
c9abccaf02 | ||
|
|
6428fa6de2 | ||
|
|
883f54431d | ||
|
|
28dc030e2b | ||
|
|
145d933e63 | ||
|
|
9772ca1a1c | ||
|
|
4059b69201 | ||
|
|
8e175ea5a1 | ||
|
|
d931b8b4e7 | ||
|
|
0982800ad2 | ||
|
|
4382ad0b3b | ||
|
|
e7891f7870 | ||
|
|
6bada46841 | ||
|
|
eae6cbd228 | ||
|
|
a0ee6180b2 | ||
|
|
3ce3de8768 | ||
|
|
6c46993b61 | ||
|
|
fbd9d14aaa |
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,6 @@ use monitor_client::entities::{
|
||||
alert::Alert, server::stats::SeverityLevel,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use termination_signal::tokio::immediate_term_handle;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Env {
|
||||
@@ -57,13 +56,15 @@ async fn app() -> anyhow::Result<()> {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let term_signal = immediate_term_handle()?;
|
||||
let mut term_signal = tokio::signal::unix::signal(
|
||||
tokio::signal::unix::SignalKind::terminate(),
|
||||
)?;
|
||||
|
||||
let app = tokio::spawn(app());
|
||||
|
||||
tokio::select! {
|
||||
res = app => return res?,
|
||||
_ = term_signal => {},
|
||||
res = app => return res?,
|
||||
_ = term_signal.recv() => {},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -19,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")?
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:?}");
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod aws;
|
||||
|
||||
#[allow(unused)]
|
||||
pub mod hetzner;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{colored, muted, Color};
|
||||
use monitor_client::entities::{toml::ResourcesToml, update::Log};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use super::{colored, muted};
|
||||
|
||||
pub fn read_resources(
|
||||
path: &Path,
|
||||
) -> anyhow::Result<(ResourcesToml, Log)> {
|
||||
let mut res = ResourcesToml::default();
|
||||
let mut log = format!("reading resources from {path:?}");
|
||||
read_resources_recursive(path, &mut res, &mut log)?;
|
||||
let mut log =
|
||||
format!("{}: reading resources from {path:?}", muted("INFO"));
|
||||
read_resources_recursive(path, &mut res, &mut log).with_context(
|
||||
|| format!("failed to read resources from {path:?}"),
|
||||
)?;
|
||||
Ok((res, Log::simple("read remote resources", log)))
|
||||
}
|
||||
|
||||
@@ -30,20 +32,15 @@ fn read_resources_recursive(
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let more = match parse_toml_file::<ResourcesToml>(path) {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
warn!("failed to parse {:?}. skipping file | {e:#}", path);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let more = parse_toml_file::<ResourcesToml>(path)
|
||||
.context("failed to parse resource file")?;
|
||||
|
||||
log.push('\n');
|
||||
log.push_str(&format!(
|
||||
"{}: {} from {}",
|
||||
muted("INFO"),
|
||||
colored("adding resources", "green"),
|
||||
colored(&path.display().to_string(), "blue")
|
||||
colored("adding resources", Color::Green),
|
||||
colored(path.display(), Color::Blue)
|
||||
));
|
||||
|
||||
resources.servers.extend(more.servers);
|
||||
@@ -63,15 +60,9 @@ fn read_resources_recursive(
|
||||
.context("failed to read directory contents")?;
|
||||
for entry in directory.into_iter().flatten() {
|
||||
let path = entry.path();
|
||||
if let Err(e) = read_resources_recursive(&path, resources, log)
|
||||
{
|
||||
log.push('\n');
|
||||
log.push_str(&format!(
|
||||
"{}: failed to read additional resources from {} | {e:#}",
|
||||
colored("ERROR", "red"),
|
||||
colored(&path.display().to_string(), "blue")
|
||||
));
|
||||
}
|
||||
read_resources_recursive(&path, resources, log).with_context(
|
||||
|| format!("failed to read resources from {path:?}"),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -84,5 +75,8 @@ fn parse_toml_file<T: DeserializeOwned>(
|
||||
) -> anyhow::Result<T> {
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.context("failed to read file contents")?;
|
||||
toml::from_str(&contents).context("failed to parse toml contents")
|
||||
toml::from_str(&contents)
|
||||
// the error without this comes through with multiple lines (\n) and looks bad
|
||||
.map_err(|e| anyhow!("{e:#}"))
|
||||
.context("failed to parse toml contents")
|
||||
}
|
||||
|
||||
@@ -1,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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
));
|
||||
|
||||
@@ -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)
|
||||
));
|
||||
|
||||
@@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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,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(())
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
#[allow(unused)]
|
||||
pub mod v0;
|
||||
pub mod v1_6;
|
||||
|
||||
@@ -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,
|
||||
)]
|
||||
|
||||
@@ -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!");
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod v0;
|
||||
pub mod v1_6;
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -36,6 +36,8 @@ pub struct GetContainerLogSearch {
|
||||
pub terms: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub combinator: SearchCombinator,
|
||||
#[serde(default)]
|
||||
pub invert: bool,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
|
||||
const sidebars: SidebarsConfig = {
|
||||
docs: [
|
||||
"intro",
|
||||
"resources",
|
||||
"core-setup",
|
||||
{
|
||||
type: "category",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 869 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 142 KiB |
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||