mirror of
https://github.com/moghtech/komodo.git
synced 2026-11-06 13:42:48 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9418a6d963 | ||
|
|
57646b750f | ||
|
|
0d57f9411c | ||
|
|
7d396dd539 | ||
|
|
bfe762b71a | ||
|
|
16ede84bac | ||
|
|
4524db94db | ||
|
|
580dab4acd | ||
|
|
645382856a | ||
|
|
5c4e6a6dbb | ||
|
|
66810e1efb | ||
|
|
69a84882f0 | ||
|
|
41648436a5 | ||
|
|
083a88aa7b | ||
|
|
750f95c90d | ||
|
|
129f3ecd82 | ||
|
|
1b754f80ab | ||
|
|
968a882012 | ||
|
|
696ebdb26f | ||
|
|
8fee04607d | ||
|
|
6fe250244b | ||
|
|
b530af0eec | ||
|
|
21e9361079 | ||
|
|
524d2d956b | ||
|
|
aca9633941 | ||
|
|
32e1bd2dda | ||
|
|
cb363d1559 | ||
|
|
63eb74b9c8 | ||
|
|
bbcc27704f |
Generated
+80
-52
@@ -32,7 +32,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.5",
|
||||
@@ -175,9 +175,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
version = "1.5.0"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1234b742ac4a40a7d3459c6e3c99818271976a5a6ae3732cb415f4a9a94da7b6"
|
||||
checksum = "2368fb843e9eec932f7789d64d0e05850f4a79067188c657e572f1f5a7589df0"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -218,9 +218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.2.2"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75588e7ee5e8496eed939adac2035a6dbab9f7eb2acdd9ab2d31856dab6f3955"
|
||||
checksum = "9a4a5e448145999d7de17bf44a886900ecb834953408dae8aaf90465ce91c1dd"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
@@ -241,9 +241,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ec2"
|
||||
version = "1.46.0"
|
||||
version = "1.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8a9b096506d42000f97eb8bc8a6e3ec16ffc607b8b5caaa50814c8544a3bf97"
|
||||
checksum = "db7385f189e99cae1fbadbfa7e0ed58cd89f655e3b3884b8386fecd9d988b4d5"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -264,10 +264,32 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.28.0"
|
||||
name = "aws-sdk-ecr"
|
||||
version = "1.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee458e39982214c70432e87756227de474318283137c38d84be0aeebc77acced"
|
||||
checksum = "23c7734480f3e434de256544f534cda7ac08742808dd485f64ab86782cbb5c48"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8aee358b755b2738b3ffb8a5b54ee991b28c8a07483a0ff7d49a58305cc2609"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -287,9 +309,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.29.0"
|
||||
version = "1.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75562f0e82b87f41210c1de110f3fb169e189f962f4b2e94fd7f370d70e063c"
|
||||
checksum = "1d5ce026f0ae73e06b20be5932150dd0e9b063417fd7c3acf5ca97018b9cbd64"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -309,9 +331,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.28.0"
|
||||
version = "1.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a422d2f3080421ed23630ada0e474c76e4279c18b4a379bff2f1062e05cef466"
|
||||
checksum = "c820248cb02e4ea83630ad2e43d0721cdbccedba5ac902cd0b6fb84d7271f205"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -332,9 +354,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58b56f1cbe6fd4d0c2573df72868f20ab1c125ca9c9dbce17927a463433a2e57"
|
||||
checksum = "31eed8d45759b2c5fe7fd304dd70739060e9e0de509209036eabea14d0720cce"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-http",
|
||||
@@ -405,9 +427,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.5.4"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "607e8b53aeb2bc23fb332159d72a69650cd9643c161d76cd3b7f88ac00b5a1bb"
|
||||
checksum = "db83b08939838d18e33b5dbaf1a0f048f28c10bd28071ab7ce6f245451855414"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
@@ -419,6 +441,7 @@ dependencies = [
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.0.0",
|
||||
"httparse",
|
||||
"hyper 0.14.28",
|
||||
"hyper-rustls",
|
||||
"once_cell",
|
||||
@@ -431,9 +454,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.6.1"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b7d790d553d163c7d80a4e06e2906bf24b9172c9ebe045fc3a274e9358ab7bb"
|
||||
checksum = "1b570ea39eb95bd32543f6e4032bce172cb6209b9bc8c83c770d08169e875afc"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
@@ -448,9 +471,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.1.10"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b6764ba7e1c5ede1c9f9e4046645534f06c2581402461c559b481a420330a83"
|
||||
checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"bytes",
|
||||
@@ -483,15 +506,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "1.3.0"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fa328e19c849b20ef7ada4c9b581dd12351ff35ecc7642d06e69de4f98407c"
|
||||
checksum = "2009a9733865d0ebf428a314440bbe357cc10d0c16d86a8e15d32e9b47c1e80e"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"http 0.2.12",
|
||||
"rustc_version 0.4.0",
|
||||
"tracing",
|
||||
]
|
||||
@@ -780,9 +802,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bson"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
|
||||
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.13.1",
|
||||
@@ -866,9 +888,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -876,9 +898,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -888,9 +910,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.4"
|
||||
version = "4.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
|
||||
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -922,7 +944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"monitor_client",
|
||||
"run_command",
|
||||
@@ -1300,7 +1322,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
@@ -1426,7 +1451,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -1981,7 +2006,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -2050,7 +2075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "migrator"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2172,7 +2197,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_cli"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2192,7 +2217,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_client"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2215,7 +2240,7 @@ dependencies = [
|
||||
"strum 0.26.2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.23.0",
|
||||
"tokio-tungstenite 0.23.1",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"typeshare",
|
||||
@@ -2224,12 +2249,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_core"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
"aws-config",
|
||||
"aws-sdk-ec2",
|
||||
"aws-sdk-ecr",
|
||||
"axum 0.7.5",
|
||||
"axum-extra",
|
||||
"base64 0.22.1",
|
||||
@@ -2255,6 +2281,7 @@ dependencies = [
|
||||
"rand",
|
||||
"reqwest 0.12.4",
|
||||
"resolver_api",
|
||||
"run_command",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serror",
|
||||
@@ -2276,7 +2303,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_periphery"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2287,6 +2314,7 @@ dependencies = [
|
||||
"command",
|
||||
"dotenv",
|
||||
"envy",
|
||||
"formatting",
|
||||
"git",
|
||||
"logger",
|
||||
"merge_config_files",
|
||||
@@ -2611,7 +2639,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -3553,7 +3581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tests"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenv",
|
||||
@@ -3730,9 +3758,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.23.0"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
|
||||
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -3756,9 +3784,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.13"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
|
||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -3777,9 +3805,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.13"
|
||||
version = "0.22.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
@@ -4138,7 +4166,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"logger",
|
||||
|
||||
+9
-8
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -15,7 +15,7 @@ monitor_client = { path = "client/core/rs" }
|
||||
|
||||
[workspace.dependencies]
|
||||
# LOCAL
|
||||
monitor_client = "1.8.0"
|
||||
monitor_client = "1.9.0"
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
command = { path = "lib/command" }
|
||||
@@ -51,14 +51,14 @@ 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.23.0"
|
||||
tokio-tungstenite = "0.23.1"
|
||||
|
||||
# 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"
|
||||
toml = "0.8.13"
|
||||
toml = "0.8.14"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.86"
|
||||
@@ -73,7 +73,7 @@ opentelemetry = "0.23.0"
|
||||
tracing = "0.1.40"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
dotenv = "0.15.0"
|
||||
envy = "0.4.2"
|
||||
|
||||
@@ -93,11 +93,12 @@ bollard = "0.16.1"
|
||||
sysinfo = "0.30.12"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.5.0"
|
||||
aws-sdk-ec2 = "1.46.0"
|
||||
aws-config = "1.5.1"
|
||||
aws-sdk-ec2 = "1.51.1"
|
||||
aws-sdk-ecr = "1.33.0"
|
||||
|
||||
# MISC
|
||||
derive_builder = "0.20.0"
|
||||
typeshare = "1.0.3"
|
||||
colored = "2.1.0"
|
||||
bson = "2.10.0"
|
||||
bson = "2.11.0"
|
||||
|
||||
@@ -84,7 +84,7 @@ impl ResourceSync for Deployment {
|
||||
.get(build_id)
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_default(),
|
||||
version: version.clone(),
|
||||
version: *version,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ derive_variants.workspace = true
|
||||
mongo_indexed.workspace = true
|
||||
resolver_api.workspace = true
|
||||
toml_pretty.workspace = true
|
||||
run_command.workspace = true
|
||||
parse_csl.workspace = true
|
||||
mungos.workspace = true
|
||||
slack.workspace = true
|
||||
@@ -37,6 +38,7 @@ svi.workspace = true
|
||||
ordered_hash_map.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
aws-sdk-ecr.workspace = true
|
||||
aws-config.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum-extra.workspace = true
|
||||
|
||||
+6
-3
@@ -1,5 +1,5 @@
|
||||
# Build Core
|
||||
FROM rust:1.78.0-bookworm as core-builder
|
||||
FROM rust:1.79.0-bookworm as core-builder
|
||||
WORKDIR /builder
|
||||
COPY . .
|
||||
RUN cargo build -p monitor_core --release
|
||||
@@ -16,8 +16,11 @@ RUN cd frontend && yarn link @monitor/client && yarn && yarn build
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && apt install -y git ca-certificates
|
||||
|
||||
RUN apt update && apt install -y git curl unzip ca-certificates && \
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
|
||||
unzip awscliv2.zip && \
|
||||
./aws/install
|
||||
|
||||
# Copy
|
||||
COPY ./config_example/core.config.example.toml /config/config.toml
|
||||
COPY --from=core-builder /builder/target/release/core /
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::muted;
|
||||
use formatting::{format_serror, muted};
|
||||
use futures::future::join_all;
|
||||
use monitor_client::{
|
||||
api::execute::{
|
||||
CancelBuild, CancelBuildResponse, Deploy, RunBuild,
|
||||
},
|
||||
entities::{
|
||||
alert::{Alert, AlertData},
|
||||
all_logs_success,
|
||||
build::{Build, CloudRegistryConfig, ImageRegistry},
|
||||
builder::{AwsBuilderConfig, Builder, BuilderConfig},
|
||||
config::core::{AwsEcrConfig, AwsEcrConfigWithCredentials},
|
||||
deployment::DeploymentState,
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
server::Server,
|
||||
server::{stats::SeverityLevel, Server},
|
||||
server_template::aws::AwsServerTemplateConfig,
|
||||
to_monitor_name,
|
||||
update::{Log, Update},
|
||||
user::{auto_redeploy_user, User},
|
||||
},
|
||||
@@ -33,19 +36,22 @@ use periphery_client::{
|
||||
PeripheryClient,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
cloud::{
|
||||
aws::{
|
||||
launch_ec2_instance, terminate_ec2_instance_with_retry,
|
||||
Ec2Instance,
|
||||
ec2::{
|
||||
launch_ec2_instance, terminate_ec2_instance_with_retry,
|
||||
Ec2Instance,
|
||||
},
|
||||
ecr,
|
||||
},
|
||||
BuildCleanupData,
|
||||
},
|
||||
config::core_config,
|
||||
helpers::{
|
||||
alert::send_alerts,
|
||||
channel::build_cancel_channel,
|
||||
periphery_client,
|
||||
query::{get_deployment_state, get_global_variables},
|
||||
@@ -73,8 +79,8 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let registry_token =
|
||||
validate_account_extract_registry_token(&build)?;
|
||||
let (registry_token, aws_ecr) =
|
||||
validate_account_extract_registry_token_aws_ecr(&build).await?;
|
||||
|
||||
// get the action state for the build (or insert default).
|
||||
let action_state =
|
||||
@@ -86,7 +92,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
action_state.update(|state| state.building = true)?;
|
||||
|
||||
build.config.version.increment();
|
||||
update.version = build.config.version.clone();
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
@@ -137,9 +143,10 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
warn!("failed to get builder | {e:#}");
|
||||
update.logs.push(Log::error(
|
||||
"get builder",
|
||||
serialize_error_pretty(&e),
|
||||
format_serror(&e.context("failed to get builder").into()),
|
||||
));
|
||||
return handle_early_return(update).await;
|
||||
return handle_early_return(update, build.id, build.name)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,7 +172,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
info!("builder cleaned up");
|
||||
return handle_early_return(update).await
|
||||
return handle_early_return(update, build.id, build.name).await
|
||||
},
|
||||
};
|
||||
|
||||
@@ -176,8 +183,10 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed build at clone repo | {e:#}");
|
||||
update
|
||||
.push_error_log("clone repo", serialize_error_pretty(&e));
|
||||
update.push_error_log(
|
||||
"clone repo",
|
||||
format_serror(&e.context("failed to clone repo").into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +245,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
.request(api::build::Build {
|
||||
build: build.clone(),
|
||||
registry_token,
|
||||
aws_ecr,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
}) => res.context("failed at call to periphery to build"),
|
||||
_ = cancel.cancelled() => {
|
||||
@@ -243,7 +253,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
|
||||
cleanup_builder_instance(periphery, cleanup_data, &mut update)
|
||||
.await;
|
||||
return handle_early_return(update).await
|
||||
return handle_early_return(update, build.id, build.name).await
|
||||
},
|
||||
};
|
||||
|
||||
@@ -254,7 +264,10 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("error in build | {e:#}");
|
||||
update.push_error_log("build", serialize_error_pretty(&e))
|
||||
update.push_error_log(
|
||||
"build",
|
||||
format_serror(&e.context("failed to build").into()),
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -309,6 +322,27 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
handle_post_build_redeploy(&build.id).await;
|
||||
info!("post build redeploy handled");
|
||||
});
|
||||
} else {
|
||||
let target = update.target.clone();
|
||||
let version = update.version;
|
||||
let err = update.logs.iter().find(|l| !l.success).cloned();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: monitor_timestamp(),
|
||||
resolved_ts: Some(monitor_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::BuildFailed {
|
||||
id: build.id,
|
||||
name: build.name,
|
||||
err,
|
||||
version,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
|
||||
Ok(update)
|
||||
@@ -318,6 +352,8 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
#[instrument(skip(update))]
|
||||
async fn handle_early_return(
|
||||
mut update: Update,
|
||||
build_id: String,
|
||||
build_name: String,
|
||||
) -> anyhow::Result<Update> {
|
||||
update.finalize();
|
||||
// Need to manually update the update before cache refresh,
|
||||
@@ -335,6 +371,28 @@ async fn handle_early_return(
|
||||
refresh_build_state_cache().await;
|
||||
}
|
||||
update_update(update.clone()).await?;
|
||||
if !update.success {
|
||||
let target = update.target.clone();
|
||||
let version = update.version;
|
||||
let err = update.logs.iter().find(|l| !l.success).cloned();
|
||||
tokio::spawn(async move {
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
target,
|
||||
ts: monitor_timestamp(),
|
||||
resolved_ts: Some(monitor_timestamp()),
|
||||
resolved: true,
|
||||
level: SeverityLevel::Warning,
|
||||
data: AlertData::BuildFailed {
|
||||
id: build_id,
|
||||
name: build_name,
|
||||
version,
|
||||
err,
|
||||
},
|
||||
};
|
||||
send_alerts(&[alert]).await
|
||||
});
|
||||
}
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
@@ -637,20 +695,12 @@ async fn handle_post_build_redeploy(build_id: &str) {
|
||||
}
|
||||
});
|
||||
|
||||
let redeploy_results = join_all(futures).await;
|
||||
|
||||
let mut redeploys = Vec::<String>::new();
|
||||
let mut redeploy_failures = Vec::<String>::new();
|
||||
|
||||
for res in redeploy_results {
|
||||
if res.is_none() {
|
||||
for res in join_all(futures).await {
|
||||
let Some((id, res)) = res else {
|
||||
continue;
|
||||
}
|
||||
let (id, res) = res.unwrap();
|
||||
match res {
|
||||
Ok(_) => redeploys.push(id),
|
||||
Err(e) => redeploy_failures
|
||||
.push(format!("{id}: {}", serialize_error_pretty(&e))),
|
||||
};
|
||||
if let Err(e) = res {
|
||||
warn!("failed post build redeploy for deployment {id}: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,12 +738,13 @@ fn start_aws_builder_log(
|
||||
}
|
||||
|
||||
/// This will make sure that a build with non-none image registry has an account attached,
|
||||
/// and will check the core config for a token matching requirements (otherwise it is left to periphery)
|
||||
fn validate_account_extract_registry_token(
|
||||
/// and will check the core config for a token / aws ecr config matching requirements.
|
||||
/// Otherwise it is left to periphery.
|
||||
async fn validate_account_extract_registry_token_aws_ecr(
|
||||
build: &Build,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
) -> anyhow::Result<(Option<String>, Option<AwsEcrConfig>)> {
|
||||
match &build.config.image_registry {
|
||||
ImageRegistry::None(_) => Ok(None),
|
||||
ImageRegistry::None(_) => Ok((None, None)),
|
||||
ImageRegistry::DockerHub(CloudRegistryConfig {
|
||||
account, ..
|
||||
}) => {
|
||||
@@ -702,7 +753,7 @@ fn validate_account_extract_registry_token(
|
||||
"Must attach account to use DockerHub image registry"
|
||||
));
|
||||
}
|
||||
Ok(core_config().docker_accounts.get(account).cloned())
|
||||
Ok((core_config().docker_accounts.get(account).cloned(), None))
|
||||
}
|
||||
ImageRegistry::Ghcr(CloudRegistryConfig { account, .. }) => {
|
||||
if account.is_empty() {
|
||||
@@ -710,8 +761,40 @@ fn validate_account_extract_registry_token(
|
||||
"Must attach account to use GithubContainerRegistry"
|
||||
));
|
||||
}
|
||||
Ok(core_config().github_accounts.get(account).cloned())
|
||||
Ok((core_config().github_accounts.get(account).cloned(), None))
|
||||
}
|
||||
ImageRegistry::AwsEcr(label) => {
|
||||
let config = core_config().aws_ecr_registries.get(label);
|
||||
let token = match config {
|
||||
Some(AwsEcrConfigWithCredentials {
|
||||
region,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
..
|
||||
}) => {
|
||||
let token = ecr::get_ecr_token(
|
||||
region,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
)
|
||||
.await
|
||||
.context("failed to get aws ecr token")?;
|
||||
ecr::maybe_create_repo(
|
||||
&to_monitor_name(&build.name),
|
||||
region.to_string(),
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
)
|
||||
.await
|
||||
.context("failed to create aws ecr repo")?;
|
||||
Some(token)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok((token, config.map(AwsEcrConfig::from)))
|
||||
}
|
||||
ImageRegistry::Custom(_) => {
|
||||
Err(anyhow!("Custom image registry is not implemented"))
|
||||
}
|
||||
ImageRegistry::Custom(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::format_serror;
|
||||
use futures::future::join_all;
|
||||
use monitor_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
build::{Build, ImageRegistry},
|
||||
config::core::AwsEcrConfig,
|
||||
deployment::{Deployment, DeploymentImage},
|
||||
get_image_name,
|
||||
permission::PermissionLevel,
|
||||
@@ -18,9 +20,9 @@ use monitor_client::{
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
cloud::aws::ecr,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
periphery_client,
|
||||
@@ -83,7 +85,13 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
let version = match deployment.config.image {
|
||||
DeploymentImage::Build { build_id, version } => {
|
||||
let build = resource::get::<Build>(&build_id).await?;
|
||||
let image_name = get_image_name(&build);
|
||||
let image_name = get_image_name(&build, |label| {
|
||||
core_config()
|
||||
.aws_ecr_registries
|
||||
.get(label)
|
||||
.map(AwsEcrConfig::from)
|
||||
})
|
||||
.context("failed to create image name")?;
|
||||
let version = if version.is_none() {
|
||||
build.config.version
|
||||
} else {
|
||||
@@ -161,13 +169,40 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
update.version = version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let registry_token = match &deployment.config.image_registry {
|
||||
ImageRegistry::None(_) => None,
|
||||
ImageRegistry::DockerHub(params) => {
|
||||
core_config.docker_accounts.get(¶ms.account).cloned()
|
||||
}
|
||||
ImageRegistry::Ghcr(params) => {
|
||||
core_config.github_accounts.get(¶ms.account).cloned()
|
||||
let (registry_token, aws_ecr) = match &deployment
|
||||
.config
|
||||
.image_registry
|
||||
{
|
||||
ImageRegistry::None(_) => (None, None),
|
||||
ImageRegistry::DockerHub(params) => (
|
||||
core_config.docker_accounts.get(¶ms.account).cloned(),
|
||||
None,
|
||||
),
|
||||
ImageRegistry::Ghcr(params) => (
|
||||
core_config.github_accounts.get(¶ms.account).cloned(),
|
||||
None,
|
||||
),
|
||||
ImageRegistry::AwsEcr(label) => {
|
||||
let config = core_config
|
||||
.aws_ecr_registries
|
||||
.get(label)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"did not find config for aws ecr registry {label}"
|
||||
)
|
||||
})?;
|
||||
(
|
||||
Some(
|
||||
ecr::get_ecr_token(
|
||||
&config.region,
|
||||
&config.access_key_id,
|
||||
&config.secret_access_key,
|
||||
)
|
||||
.await
|
||||
.context("failed to create aws ecr login token")?,
|
||||
),
|
||||
Some(AwsEcrConfig::from(config)),
|
||||
)
|
||||
}
|
||||
ImageRegistry::Custom(_) => {
|
||||
return Err(anyhow!("Custom ImageRegistry not yet supported"))
|
||||
@@ -180,6 +215,7 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
stop_signal,
|
||||
stop_time,
|
||||
registry_token,
|
||||
aws_ecr,
|
||||
replacers: secret_replacers.into_iter().collect(),
|
||||
})
|
||||
.await
|
||||
@@ -188,7 +224,9 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"deploy container",
|
||||
serialize_error_pretty(&e),
|
||||
format_serror(
|
||||
&e.context("failed to deploy container").into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -248,9 +286,10 @@ impl Resolve<StartContainer, (User, Update)> for State {
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("start container", serialize_error_pretty(&e))
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"start container",
|
||||
format_serror(&e.context("failed to start container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
@@ -318,9 +357,10 @@ impl Resolve<StopContainer, (User, Update)> for State {
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("stop container", serialize_error_pretty(&e))
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
@@ -403,9 +443,11 @@ impl Resolve<StopAllContainers, (User, Update)> for State {
|
||||
if let Err(e) = res {
|
||||
update.push_error_log(
|
||||
"stop container failure",
|
||||
format!(
|
||||
"failed to stop container {name} ({id})\n\n{}",
|
||||
serialize_error_pretty(&e)
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"failed to stop container {name} ({id})"
|
||||
))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -474,9 +516,10 @@ impl Resolve<RemoveContainer, (User, Update)> for State {
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("stop container", serialize_error_pretty(&e))
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"stop container",
|
||||
format_serror(&e.context("failed to stop container").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.logs.push(log);
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use formatting::format_serror;
|
||||
use monitor_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
@@ -12,7 +13,7 @@ use monitor_client::{
|
||||
use mungos::by_id::find_one_by_id;
|
||||
use resolver_api::{derive::Resolver, Resolver};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::{serialize_error_pretty, Json};
|
||||
use serror::Json;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -92,7 +93,7 @@ async fn handler(
|
||||
let log = match handle.await {
|
||||
Ok(Err(e)) => {
|
||||
warn!("/execute request {req_id} task error: {e:#}",);
|
||||
Log::error("task error", serialize_error_pretty(&e))
|
||||
Log::error("task error", format_serror(&e.into()))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("/execute request {req_id} spawn error: {e:?}",);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use formatting::{bold, colored, muted, Color};
|
||||
use formatting::{bold, colored, format_serror, muted, Color};
|
||||
use monitor_client::{
|
||||
api::execute::RunProcedure,
|
||||
entities::{
|
||||
@@ -10,7 +10,6 @@ use monitor_client::{
|
||||
};
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
@@ -87,14 +86,8 @@ fn resolve_inner(
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(e) => update.push_error_log(
|
||||
"execution error",
|
||||
format!(
|
||||
"{}: {}",
|
||||
colored("ERROR", Color::Red),
|
||||
serialize_error_pretty(&e)
|
||||
),
|
||||
),
|
||||
Err(e) => update
|
||||
.push_error_log("execution error", format_serror(&e.into())),
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::anyhow;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
@@ -16,7 +17,6 @@ use mungos::{
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
@@ -71,7 +71,10 @@ impl Resolve<CloneRepo, (User, Update)> for State {
|
||||
{
|
||||
Ok(logs) => logs,
|
||||
Err(e) => {
|
||||
vec![Log::error("clone repo", serialize_error_pretty(&e))]
|
||||
vec![Log::error(
|
||||
"clone repo",
|
||||
format_serror(&e.context("failed to clone repo").into()),
|
||||
)]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,7 +132,10 @@ impl Resolve<PullRepo, (User, Update)> for State {
|
||||
{
|
||||
Ok(logs) => logs,
|
||||
Err(e) => {
|
||||
vec![Log::error("pull repo", serialize_error_pretty(&e))]
|
||||
vec![Log::error(
|
||||
"pull repo",
|
||||
format_serror(&e.context("failed to pull repo").into()),
|
||||
)]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::{
|
||||
api::execute::*,
|
||||
entities::{
|
||||
@@ -11,7 +12,6 @@ use monitor_client::{
|
||||
};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, update::update_update},
|
||||
@@ -54,9 +54,12 @@ impl Resolve<PruneContainers, (User, Update)> for State {
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("prune containers", serialize_error_pretty(&e))
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"prune containers",
|
||||
format_serror(
|
||||
&e.context("failed to prune containers").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
update.success = log.success;
|
||||
@@ -105,9 +108,10 @@ impl Resolve<PruneNetworks, (User, Update)> for State {
|
||||
server.name
|
||||
)) {
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
Log::error("prune networks", serialize_error_pretty(&e))
|
||||
}
|
||||
Err(e) => Log::error(
|
||||
"prune networks",
|
||||
format_serror(&e.context("failed to prune networks").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.success = log.success;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::format_serror;
|
||||
use monitor_client::{
|
||||
api::{execute::LaunchServer, write::CreateServer},
|
||||
entities::{
|
||||
@@ -11,10 +12,11 @@ use monitor_client::{
|
||||
};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
cloud::{aws::launch_ec2_instance, hetzner::launch_hetzner_server},
|
||||
cloud::{
|
||||
aws::ec2::launch_ec2_instance, hetzner::launch_hetzner_server,
|
||||
},
|
||||
helpers::update::update_update,
|
||||
resource,
|
||||
state::{db_client, State},
|
||||
@@ -130,10 +132,7 @@ impl Resolve<LaunchServer, (User, Update)> for State {
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"create server",
|
||||
format!(
|
||||
"failed to create server\n\n{}",
|
||||
serialize_error_pretty(&e)
|
||||
),
|
||||
format_serror(&e.context("failed to create server").into()),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::{colored, Color};
|
||||
use formatting::{colored, format_serror, Color};
|
||||
use mongo_indexed::doc;
|
||||
use monitor_client::{
|
||||
api::{execute::RunSync, write::RefreshResourceSyncPending},
|
||||
@@ -20,7 +20,6 @@ use monitor_client::{
|
||||
};
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -359,9 +358,9 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
warn!("failed to refresh sync {} after run | {e:#}", sync.name);
|
||||
update.push_error_log(
|
||||
"refresh sync",
|
||||
format!(
|
||||
"failed to refresh sync pending after run | {}",
|
||||
serialize_error_pretty(&e)
|
||||
format_serror(
|
||||
&e.context("failed to refresh sync pending after run")
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ mod variable;
|
||||
enum ReadRequest {
|
||||
GetVersion(GetVersion),
|
||||
GetCoreInfo(GetCoreInfo),
|
||||
GetAvailableAwsEcrLabels(GetAvailableAwsEcrLabels),
|
||||
|
||||
// ==== USER ====
|
||||
ListUsers(ListUsers),
|
||||
@@ -231,3 +232,13 @@ impl Resolve<GetCoreInfo, User> for State {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<GetAvailableAwsEcrLabels, User> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetAvailableAwsEcrLabels {}: GetAvailableAwsEcrLabels,
|
||||
_: User,
|
||||
) -> anyhow::Result<GetAvailableAwsEcrLabelsResponse> {
|
||||
Ok(core_config().aws_ecr_registries.keys().cloned().collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
@@ -13,7 +14,6 @@ use monitor_client::{
|
||||
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -112,8 +112,10 @@ impl Resolve<CreateNetwork, User> for State {
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update
|
||||
.push_error_log("create network", serialize_error_pretty(&e)),
|
||||
Err(e) => update.push_error_log(
|
||||
"create network",
|
||||
format_serror(&e.context("failed to create network").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
@@ -149,8 +151,10 @@ impl Resolve<DeleteNetwork, User> for State {
|
||||
.await
|
||||
{
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update
|
||||
.push_error_log("delete network", serialize_error_pretty(&e)),
|
||||
Err(e) => update.push_error_log(
|
||||
"delete network",
|
||||
format_serror(&e.context("failed to delete network").into()),
|
||||
),
|
||||
};
|
||||
|
||||
update.finalize();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::format_serror;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
@@ -27,10 +28,10 @@ use mungos::{
|
||||
mongodb::bson::{doc, to_document},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
alert::send_alerts,
|
||||
query::get_id_to_tags,
|
||||
sync::{
|
||||
deployment,
|
||||
@@ -234,7 +235,7 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
message: None,
|
||||
data: PendingSyncUpdatesData::Err(
|
||||
PendingSyncUpdatesDataErr {
|
||||
message: serialize_error_pretty(&e),
|
||||
message: format_serror(&e.into()),
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -294,6 +295,7 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
|
||||
.context("failed to open existing pending resource sync updates alert")
|
||||
.inspect_err(|e| warn!("{e:#}"))
|
||||
.ok();
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
// CLOSE ALERT
|
||||
(Some(existing), false) => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use aws_config::BehaviorVersion;
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
use aws_sdk_ec2::{
|
||||
config::Region,
|
||||
types::{
|
||||
BlockDeviceMapping, EbsBlockDevice,
|
||||
InstanceNetworkInterfaceSpecification, InstanceStateChange,
|
||||
@@ -0,0 +1,82 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
use aws_sdk_ecr::Client as EcrClient;
|
||||
use run_command::async_run_command;
|
||||
|
||||
#[tracing::instrument(skip(access_key_id, secret_access_key))]
|
||||
async fn make_ecr_client(
|
||||
region: String,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
) -> EcrClient {
|
||||
std::env::set_var("AWS_ACCESS_KEY_ID", access_key_id);
|
||||
std::env::set_var("AWS_SECRET_ACCESS_KEY", secret_access_key);
|
||||
let region = Region::new(region);
|
||||
let config = aws_config::defaults(BehaviorVersion::v2024_03_28())
|
||||
.region(region)
|
||||
.load()
|
||||
.await;
|
||||
EcrClient::new(&config)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(access_key_id, secret_access_key))]
|
||||
pub async fn maybe_create_repo(
|
||||
repo: &str,
|
||||
region: String,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let client =
|
||||
make_ecr_client(region, access_key_id, secret_access_key).await;
|
||||
|
||||
let existing = client
|
||||
.describe_repositories()
|
||||
.send()
|
||||
.await
|
||||
.context("failed to describe existing repositories")?
|
||||
.repositories
|
||||
.unwrap_or_default();
|
||||
|
||||
if existing.iter().any(|r| {
|
||||
if let Some(name) = r.repository_name() {
|
||||
name == repo
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
client
|
||||
.create_repository()
|
||||
.repository_name(repo)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to create repository")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a token docker login.
|
||||
///
|
||||
/// Requires the aws cli be installed on the host
|
||||
#[tracing::instrument(skip(access_key_id, secret_access_key))]
|
||||
pub async fn get_ecr_token(
|
||||
region: &str,
|
||||
access_key_id: &str,
|
||||
secret_access_key: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let log = async_run_command(&format!(
|
||||
"AWS_ACCESS_KEY_ID={access_key_id} AWS_SECRET_ACCESS_KEY={secret_access_key} aws ecr get-login-password --region {region}"
|
||||
))
|
||||
.await;
|
||||
|
||||
if log.success() {
|
||||
Ok(log.stdout)
|
||||
} else {
|
||||
Err(
|
||||
anyhow!("stdout: {} | stderr: {}", log.stdout, log.stderr)
|
||||
.context("failed to get aws ecr login token"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod ec2;
|
||||
pub mod ecr;
|
||||
@@ -157,6 +157,7 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
secrets: config.secrets,
|
||||
github_accounts: config.github_accounts,
|
||||
docker_accounts: config.docker_accounts,
|
||||
aws_ecr_registries: config.aws_ecr_registries,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+198
-43
@@ -6,11 +6,12 @@ use monitor_client::entities::{
|
||||
alerter::*,
|
||||
deployment::DeploymentState,
|
||||
server::stats::SeverityLevel,
|
||||
update::ResourceTargetVariant,
|
||||
};
|
||||
use mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use slack::types::Block;
|
||||
|
||||
use crate::state::db_client;
|
||||
use crate::{config::core_config, state::db_client};
|
||||
|
||||
#[instrument]
|
||||
pub async fn send_alerts(alerts: &[Alert]) {
|
||||
@@ -61,7 +62,12 @@ async fn send_alert(alerters: &[Alerter], alert: &Alert) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if resource target not configured on the alerter
|
||||
// Don't send if resource is in the blacklist
|
||||
if alerter.config.except_resources.contains(&alert.target) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Don't send if whitelist configured and target is not included
|
||||
if !alerter.config.resources.is_empty()
|
||||
&& !alerter.config.resources.contains(&alert.target)
|
||||
{
|
||||
@@ -126,7 +132,12 @@ async fn send_slack_alert(
|
||||
) -> anyhow::Result<()> {
|
||||
let level = fmt_level(alert.level);
|
||||
let (text, blocks): (_, Option<_>) = match &alert.data {
|
||||
AlertData::ServerUnreachable { name, region, .. } => {
|
||||
AlertData::ServerUnreachable {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
err,
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
@@ -143,10 +154,18 @@ async fn send_slack_alert(
|
||||
SeverityLevel::Critical => {
|
||||
let text =
|
||||
format!("{level} | *{name}*{region} is *unreachable* ❌");
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {e:#?}"))
|
||||
.unwrap_or_default();
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} is *unreachable* ❌"
|
||||
"*{name}*{region} is *unreachable* ❌{err}"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
@@ -155,70 +174,136 @@ async fn send_slack_alert(
|
||||
}
|
||||
}
|
||||
AlertData::ServerCpu {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
percentage,
|
||||
..
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈 🚨");
|
||||
let blocks = vec![
|
||||
Block::header(format!("{level} 🚨")),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} cpu usage at *{percentage:.1}%* 📈 🚨"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%*");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} cpu usage at *{percentage:.1}%*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => {
|
||||
let text = format!("{level} | *{name}*{region} cpu usage at *{percentage:.1}%* 📈");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} cpu usage at *{percentage:.1}%* 📈"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ServerMem {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
used_gb,
|
||||
total_gb,
|
||||
..
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
let text =
|
||||
format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾 🚨");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} memory usage at *{percentage:.1}%* 💾 🚨"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => {
|
||||
let text = format!("{level} | *{name}*{region} memory usage at *{percentage:.1}%* 💾");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} memory usage at *{percentage:.1}%* 💾"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ServerDisk {
|
||||
id,
|
||||
name,
|
||||
region,
|
||||
path,
|
||||
used_gb,
|
||||
total_gb,
|
||||
..
|
||||
} => {
|
||||
let region = fmt_region(region);
|
||||
let percentage = 100.0 * used_gb / total_gb;
|
||||
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿 🚨");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} disk usage at *{percentage:.1}%* 💿 🚨"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(ResourceTargetVariant::Server, id)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => {
|
||||
let text = format!("{level} | *{name}*{region} disk usage at *{percentage:.1}%* | mount point: *{path:?}* 💿");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} disk usage at *{percentage:.1}%* 💿"
|
||||
)),
|
||||
Block::section(format!(
|
||||
"mount point: {path:?} | using *{used_gb:.1} GiB* / *{total_gb:.1} GiB*"
|
||||
)),
|
||||
Block::section(resource_link(ResourceTargetVariant::Server, id)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
name,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
..
|
||||
} => {
|
||||
let to = fmt_docker_container_state(to);
|
||||
@@ -226,7 +311,11 @@ async fn send_slack_alert(
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: {server_name}\nprevious: {from}"
|
||||
"server: {server_name}\nprevious: {from}",
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Deployment,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
@@ -236,12 +325,12 @@ async fn send_slack_alert(
|
||||
message,
|
||||
} => {
|
||||
let text = format!(
|
||||
"{level} | Failed to terminated AWS builder instance"
|
||||
"{level} | Failed to terminated AWS builder instance "
|
||||
);
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"instance id: **{instance_id}**\n{message}"
|
||||
"instance id: *{instance_id}*\n{message}"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
@@ -252,8 +341,40 @@ async fn send_slack_alert(
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"sync id: **{id}**\nsync name: **{name}**"
|
||||
"sync id: *{id}*\nsync name: *{name}*",
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::ResourceSync,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::BuildFailed {
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
err,
|
||||
} => {
|
||||
let text = format!("{level} | Build {name} has failed");
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|log| {
|
||||
let stdout = (!log.stdout.is_empty())
|
||||
.then(|| format!("\nstdout: {}", log.stdout))
|
||||
.unwrap_or_default();
|
||||
let stderr = (!log.stderr.is_empty())
|
||||
.then(|| format!("\nstderr: {}", log.stderr))
|
||||
.unwrap_or_default();
|
||||
format!("\nfailed at stage: {}{stdout}{stderr}", log.stage)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"build id: *{id}*\nbuild name: *{name}*\nversion: v{version}{err}",
|
||||
)),
|
||||
Block::section(resource_link(ResourceTargetVariant::Build, id))
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
@@ -286,7 +407,41 @@ fn fmt_docker_container_state(state: &DeploymentState) -> String {
|
||||
fn fmt_level(level: SeverityLevel) -> &'static str {
|
||||
match level {
|
||||
SeverityLevel::Critical => "CRITICAL 🚨",
|
||||
SeverityLevel::Warning => "WARNING 🚨",
|
||||
SeverityLevel::Warning => "WARNING ‼️",
|
||||
SeverityLevel::Ok => "OK ✅",
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_link(
|
||||
resource_type: ResourceTargetVariant,
|
||||
id: &str,
|
||||
) -> String {
|
||||
let path = match resource_type {
|
||||
ResourceTargetVariant::System => unreachable!(),
|
||||
ResourceTargetVariant::Build => format!("/builds/{id}"),
|
||||
ResourceTargetVariant::Builder => {
|
||||
format!("/builders/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Deployment => {
|
||||
format!("/deployments/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Server => {
|
||||
format!("/servers/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Repo => format!("/repos/{id}"),
|
||||
ResourceTargetVariant::Alerter => {
|
||||
format!("/alerters/{id}")
|
||||
}
|
||||
ResourceTargetVariant::Procedure => {
|
||||
format!("/procedures/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ServerTemplate => {
|
||||
format!("/server-templates/{id}")
|
||||
}
|
||||
ResourceTargetVariant::ResourceSync => {
|
||||
format!("/resource-syncs/{id}")
|
||||
}
|
||||
};
|
||||
|
||||
format!("{}{path}", core_config().host)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use mongo_indexed::Document;
|
||||
use monitor_client::entities::{
|
||||
permission::{Permission, PermissionLevel, UserTarget},
|
||||
server::Server,
|
||||
update::ResourceTarget,
|
||||
user::User,
|
||||
};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use mungos::mongodb::bson::{doc, Bson};
|
||||
use periphery_client::PeripheryClient;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
@@ -117,3 +118,23 @@ pub async fn create_permission<T>(
|
||||
error!("failed to create permission for {target:?} | {e:#}");
|
||||
};
|
||||
}
|
||||
|
||||
/// Flattens a document only one level deep
|
||||
///
|
||||
/// eg `{ config: { label: "yes", thing: { field1: "ok", field2: "ok" } } }` ->
|
||||
/// `{ "config.label": "yes", "config.thing": { field1: "ok", field2: "ok" } }`
|
||||
pub fn flatten_document(doc: Document) -> Document {
|
||||
let mut target = Document::new();
|
||||
|
||||
for (outer_field, bson) in doc {
|
||||
if let Bson::Document(doc) = bson {
|
||||
for (inner_field, bson) in doc {
|
||||
target.insert(format!("{outer_field}.{inner_field}"), bson);
|
||||
}
|
||||
} else {
|
||||
target.insert(outer_field, bson);
|
||||
}
|
||||
}
|
||||
|
||||
target
|
||||
}
|
||||
|
||||
@@ -849,7 +849,7 @@ impl ResourceSync for Deployment {
|
||||
.get(build_id)
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_default(),
|
||||
version: version.clone(),
|
||||
version: *version,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ pub async fn alert_servers(
|
||||
|
||||
let mut alerts_to_open = Vec::<(Alert, SendAlerts)>::new();
|
||||
let mut alerts_to_update = Vec::<(Alert, SendAlerts)>::new();
|
||||
let mut alert_ids_to_close = Vec::<(String, SendAlerts)>::new();
|
||||
let mut alert_ids_to_close = Vec::<(Alert, SendAlerts)>::new();
|
||||
|
||||
for server_status in server_statuses {
|
||||
let Some(server) = servers.remove(&server_status.id) else {
|
||||
@@ -101,13 +101,10 @@ pub async fn alert_servers(
|
||||
}
|
||||
|
||||
// Close an open alert
|
||||
(
|
||||
ServerState::Ok | ServerState::Disabled,
|
||||
Some(health_alert),
|
||||
) => alert_ids_to_close.push((
|
||||
health_alert.id.clone(),
|
||||
server.info.send_unreachable_alerts,
|
||||
)),
|
||||
(ServerState::Ok | ServerState::Disabled, Some(alert)) => {
|
||||
alert_ids_to_close
|
||||
.push((alert.clone(), server.info.send_unreachable_alerts));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -165,8 +162,20 @@ pub async fn alert_servers(
|
||||
alerts_to_update.push((alert, server.info.send_cpu_alerts));
|
||||
}
|
||||
}
|
||||
(SeverityLevel::Ok, Some(alert)) => alert_ids_to_close
|
||||
.push((alert.id.clone(), server.info.send_cpu_alerts)),
|
||||
(SeverityLevel::Ok, Some(alert)) => {
|
||||
let mut alert = alert.clone();
|
||||
alert.data = AlertData::ServerCpu {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
percentage: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.cpu_perc as f64)
|
||||
.unwrap_or(0.0),
|
||||
};
|
||||
alert_ids_to_close.push((alert, server.info.send_cpu_alerts))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -229,8 +238,25 @@ pub async fn alert_servers(
|
||||
alerts_to_update.push((alert, server.info.send_mem_alerts));
|
||||
}
|
||||
}
|
||||
(SeverityLevel::Ok, Some(alert)) => alert_ids_to_close
|
||||
.push((alert.id.clone(), server.info.send_mem_alerts)),
|
||||
(SeverityLevel::Ok, Some(alert)) => {
|
||||
let mut alert = alert.clone();
|
||||
alert.data = AlertData::ServerMem {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
total_gb: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.mem_total_gb)
|
||||
.unwrap_or(0.0),
|
||||
used_gb: server_status
|
||||
.stats
|
||||
.as_ref()
|
||||
.map(|s| s.mem_used_gb)
|
||||
.unwrap_or(0.0),
|
||||
};
|
||||
alert_ids_to_close.push((alert, server.info.send_mem_alerts))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -291,8 +317,23 @@ pub async fn alert_servers(
|
||||
.push((alert, server.info.send_disk_alerts));
|
||||
}
|
||||
}
|
||||
(SeverityLevel::Ok, Some(alert)) => alert_ids_to_close
|
||||
.push((alert.id.clone(), server.info.send_disk_alerts)),
|
||||
(SeverityLevel::Ok, Some(alert)) => {
|
||||
let mut alert = alert.clone();
|
||||
let disk = server_status.stats.as_ref().and_then(|stats| {
|
||||
stats.disks.iter().find(|disk| disk.mount == *path)
|
||||
});
|
||||
alert.level = *health;
|
||||
alert.data = AlertData::ServerDisk {
|
||||
id: server_status.id.clone(),
|
||||
name: server.name.clone(),
|
||||
region: optional_string(&server.info.region),
|
||||
path: path.to_owned(),
|
||||
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
|
||||
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
|
||||
};
|
||||
alert_ids_to_close
|
||||
.push((alert, server.info.send_disk_alerts))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -402,22 +443,20 @@ async fn update_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
async fn resolve_alerts(alert_ids: &[(String, SendAlerts)]) {
|
||||
if alert_ids.is_empty() {
|
||||
async fn resolve_alerts(alerts: &[(Alert, SendAlerts)]) {
|
||||
if alerts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let send_alerts_map =
|
||||
alert_ids.iter().cloned().collect::<HashMap<_, _>>();
|
||||
|
||||
let close = || async {
|
||||
let alert_ids = alert_ids
|
||||
let close = || async move {
|
||||
let alert_ids = alerts
|
||||
.iter()
|
||||
.map(|(id, _)| {
|
||||
ObjectId::from_str(id)
|
||||
.map(|(alert, _)| {
|
||||
ObjectId::from_str(&alert.id)
|
||||
.context("failed to convert alert id to ObjectId")
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.alerts
|
||||
@@ -432,28 +471,23 @@ async fn resolve_alerts(alert_ids: &[(String, SendAlerts)]) {
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to resolve alerts on db")?;
|
||||
let mut closed = find_collect(
|
||||
&db_client().await.alerts,
|
||||
doc! { "_id": { "$in": &alert_ids } },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to get closed alerts from db")?;
|
||||
.context("failed to resolve alerts on db")
|
||||
.inspect_err(|e| warn!("{e:#}"))
|
||||
.ok();
|
||||
|
||||
for closed in &mut closed {
|
||||
closed.level = SeverityLevel::Ok;
|
||||
}
|
||||
let ts = monitor_timestamp();
|
||||
|
||||
let closed = closed
|
||||
.into_iter()
|
||||
.filter(|closed| {
|
||||
if let ResourceTarget::Server(id) = &closed.target {
|
||||
send_alerts_map.get(id).cloned().unwrap_or(true)
|
||||
} else {
|
||||
error!("got resource target other than server in resolve_server_alerts");
|
||||
true
|
||||
}
|
||||
let closed = alerts
|
||||
.iter()
|
||||
.filter(|(_, send)| *send)
|
||||
.map(|(alert, _)| {
|
||||
let mut alert = alert.clone();
|
||||
|
||||
alert.resolved = true;
|
||||
alert.resolved_ts = Some(ts);
|
||||
alert.level = SeverityLevel::Ok;
|
||||
|
||||
alert
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::entities::{
|
||||
build::Build,
|
||||
deployment::{
|
||||
@@ -16,7 +17,6 @@ use monitor_client::entities::{
|
||||
};
|
||||
use mungos::mongodb::Collection;
|
||||
use periphery_client::api::container::RemoveContainer;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -187,10 +187,12 @@ impl super::MonitorResource for Deployment {
|
||||
Err(e) => {
|
||||
update.push_error_log(
|
||||
"remove container",
|
||||
format!(
|
||||
"failed to retrieve server at {} from db.\n\nerror: {}",
|
||||
deployment.config.server_id,
|
||||
serialize_error_pretty(&e)
|
||||
format_serror(
|
||||
&e.context(format!(
|
||||
"failed to retrieve server at {} from db.",
|
||||
deployment.config.server_id
|
||||
))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
return Ok(());
|
||||
@@ -211,9 +213,8 @@ impl super::MonitorResource for Deployment {
|
||||
// Leaving it for completeness sake
|
||||
update.push_error_log(
|
||||
"remove container",
|
||||
format!(
|
||||
"failed to remove container on periphery.\n\nerror: {}",
|
||||
serialize_error_pretty(&e),
|
||||
format_serror(
|
||||
&e.context("failed to get periphery client").into(),
|
||||
),
|
||||
);
|
||||
return Ok(());
|
||||
@@ -230,9 +231,8 @@ impl super::MonitorResource for Deployment {
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"remove container",
|
||||
format!(
|
||||
"failed to remove container.\n\nerror: {}",
|
||||
serialize_error_pretty(&e)
|
||||
format_serror(
|
||||
&e.context("failed to remove container").into(),
|
||||
),
|
||||
),
|
||||
};
|
||||
@@ -270,7 +270,7 @@ async fn validate_config(
|
||||
.context("cannot create deployment with this build attached. user must have at least read permissions on the build to perform this action.")?;
|
||||
config.image = Some(DeploymentImage::Build {
|
||||
build_id: build.id,
|
||||
version: version.clone(),
|
||||
version: *version,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::format_serror;
|
||||
use futures::future::join_all;
|
||||
use monitor_client::{
|
||||
api::write::CreateTag,
|
||||
@@ -26,12 +27,11 @@ use mungos::{
|
||||
use partial_derive2::{Diff, FieldDiff, MaybeNone, PartialDiff};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
create_permission,
|
||||
create_permission, flatten_document,
|
||||
query::{
|
||||
get_resource_ids_for_non_admin, get_tag,
|
||||
get_user_permission_on_resource, id_or_name_filter,
|
||||
@@ -422,10 +422,12 @@ pub async fn update<T: MonitorResource>(
|
||||
let config_doc = T::update_document(resource, config)
|
||||
.context("failed to serialize config to bson document")?;
|
||||
|
||||
let update_doc = flatten_document(doc! { "config": config_doc });
|
||||
|
||||
update_one_by_id(
|
||||
T::coll().await,
|
||||
&id,
|
||||
mungos::update::Update::FlattenSet(doc! { "config": config_doc }),
|
||||
doc! { "$set": update_doc },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -580,7 +582,7 @@ pub async fn delete<T: MonitorResource>(
|
||||
);
|
||||
|
||||
if let Err(e) = T::post_delete(&resource, &mut update).await {
|
||||
update.push_error_log("post delete", serialize_error_pretty(&e));
|
||||
update.push_error_log("post delete", format_serror(&e.into()));
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::entities::{
|
||||
permission::PermissionLevel,
|
||||
repo::{
|
||||
@@ -18,7 +19,6 @@ use mungos::{
|
||||
mongodb::{bson::doc, options::FindOneOptions, Collection},
|
||||
};
|
||||
use periphery_client::api::git::DeleteRepo;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client,
|
||||
@@ -158,7 +158,7 @@ impl super::MonitorResource for Repo {
|
||||
Ok(log) => update.logs.push(log),
|
||||
Err(e) => update.push_error_log(
|
||||
"delete repo on periphery",
|
||||
serialize_error_pretty(&e),
|
||||
format_serror(&e.context("failed to delete repo").into()),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+14
-13
@@ -17,28 +17,29 @@ path = "src/main.rs"
|
||||
# local
|
||||
monitor_client = { workspace = true, features = ["docker"] }
|
||||
periphery_client.workspace = true
|
||||
formatting.workspace = true
|
||||
command.workspace = true
|
||||
logger.workspace = true
|
||||
git.workspace = true
|
||||
# mogh
|
||||
serror = { workspace = true, features = ["axum"] }
|
||||
async_timing_util.workspace = true
|
||||
merge_config_files.workspace = true
|
||||
parse_csl.workspace = true
|
||||
run_command.workspace = true
|
||||
svi.workspace = true
|
||||
async_timing_util.workspace = true
|
||||
resolver_api.workspace = true
|
||||
run_command.workspace = true
|
||||
parse_csl.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
tokio.workspace = true
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
dotenv.workspace = true
|
||||
envy.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
bollard.workspace = true
|
||||
clap.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
bollard.workspace = true
|
||||
sysinfo.workspace = true
|
||||
dotenv.workspace = true
|
||||
anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
axum.workspace = true
|
||||
envy.workspace = true
|
||||
clap.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -12,17 +12,27 @@ use crate::{
|
||||
};
|
||||
|
||||
impl Resolve<Build> for State {
|
||||
#[instrument(name = "Build", skip(self, replacers))]
|
||||
#[instrument(
|
||||
name = "Build",
|
||||
skip(self, registry_token, replacers, aws_ecr)
|
||||
)]
|
||||
async fn resolve(
|
||||
&self,
|
||||
Build {
|
||||
build,
|
||||
registry_token,
|
||||
replacers,
|
||||
aws_ecr,
|
||||
}: Build,
|
||||
_: (),
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
docker::build::build(&build, registry_token, replacers).await
|
||||
docker::build::build(
|
||||
&build,
|
||||
registry_token,
|
||||
replacers,
|
||||
aws_ecr.as_ref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ impl Resolve<Deploy> for State {
|
||||
stop_time,
|
||||
registry_token,
|
||||
replacers,
|
||||
aws_ecr,
|
||||
}: Deploy,
|
||||
_: (),
|
||||
) -> anyhow::Result<Log> {
|
||||
@@ -212,6 +213,7 @@ impl Resolve<Deploy> for State {
|
||||
.into(),
|
||||
registry_token,
|
||||
replacers,
|
||||
aws_ecr.as_ref(),
|
||||
)
|
||||
.await;
|
||||
Ok(res)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use anyhow::Context;
|
||||
use command::run_monitor_command;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::entities::{
|
||||
build::{Build, BuildConfig},
|
||||
config::core::AwsEcrConfig,
|
||||
get_image_name, optional_string, to_monitor_name,
|
||||
update::Log,
|
||||
EnvironmentVar, Version,
|
||||
};
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::config::periphery_config;
|
||||
|
||||
@@ -23,6 +24,7 @@ pub async fn build(
|
||||
build: &Build,
|
||||
registry_token: Option<String>,
|
||||
core_replacers: Vec<(String, String)>,
|
||||
aws_ecr: Option<&AwsEcrConfig>,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let Build {
|
||||
name,
|
||||
@@ -45,24 +47,28 @@ pub async fn build(
|
||||
let mut logs = Vec::new();
|
||||
|
||||
// Maybe docker login
|
||||
let should_push =
|
||||
match docker_login(image_registry, registry_token.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(should_push) => should_push,
|
||||
Err(e) => {
|
||||
logs.push(Log::error(
|
||||
"docker login",
|
||||
serialize_error_pretty(
|
||||
&e.context("failed to login to docker registry"),
|
||||
),
|
||||
));
|
||||
return Ok(logs);
|
||||
}
|
||||
};
|
||||
let should_push = match docker_login(
|
||||
image_registry,
|
||||
registry_token.as_deref(),
|
||||
aws_ecr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(should_push) => should_push,
|
||||
Err(e) => {
|
||||
logs.push(Log::error(
|
||||
"docker login",
|
||||
format_serror(
|
||||
&e.context("failed to login to docker registry").into(),
|
||||
),
|
||||
));
|
||||
return Ok(logs);
|
||||
}
|
||||
};
|
||||
|
||||
let name = to_monitor_name(name);
|
||||
|
||||
// Get paths
|
||||
let name = to_monitor_name(name);
|
||||
let build_dir =
|
||||
periphery_config().repo_dir.join(&name).join(build_path);
|
||||
let dockerfile_path = match optional_string(dockerfile_path) {
|
||||
@@ -71,7 +77,8 @@ pub async fn build(
|
||||
};
|
||||
|
||||
// Get command parts
|
||||
let image_name = get_image_name(build);
|
||||
let image_name = get_image_name(build, |_| aws_ecr.cloned())
|
||||
.context("failed to make image name")?;
|
||||
let build_args = parse_build_args(build_args);
|
||||
let labels = parse_labels(labels);
|
||||
let extra_args = parse_extra_args(extra_args);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use command::run_monitor_command;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::entities::{
|
||||
config::core::AwsEcrConfig,
|
||||
deployment::{
|
||||
Conversion, Deployment, DeploymentConfig, DeploymentImage,
|
||||
DockerContainerStats, RestartMode, TerminationSignal,
|
||||
@@ -10,7 +12,6 @@ use monitor_client::entities::{
|
||||
EnvironmentVar, SearchCombinator,
|
||||
};
|
||||
use run_command::async_run_command;
|
||||
use serror::serialize_error_pretty;
|
||||
|
||||
use crate::config::periphery_config;
|
||||
|
||||
@@ -185,17 +186,19 @@ pub async fn deploy(
|
||||
stop_time: Option<i32>,
|
||||
registry_token: Option<String>,
|
||||
core_replacers: Vec<(String, String)>,
|
||||
aws_ecr: Option<&AwsEcrConfig>,
|
||||
) -> Log {
|
||||
if let Err(e) = docker_login(
|
||||
&deployment.config.image_registry,
|
||||
registry_token.as_deref(),
|
||||
aws_ecr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Log::error(
|
||||
"docker login",
|
||||
serialize_error_pretty(
|
||||
&e.context("failed to login to docker registry"),
|
||||
format_serror(
|
||||
&e.context("failed to login to docker registry").into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use anyhow::{anyhow, Context};
|
||||
use command::run_monitor_command;
|
||||
use monitor_client::entities::{
|
||||
build::{CloudRegistryConfig, ImageRegistry},
|
||||
config::core::AwsEcrConfig,
|
||||
update::Log,
|
||||
EnvironmentVar,
|
||||
};
|
||||
@@ -19,6 +20,8 @@ pub async fn docker_login(
|
||||
registry: &ImageRegistry,
|
||||
// For local token override from core.
|
||||
registry_token: Option<&str>,
|
||||
// For local config override from core.
|
||||
aws_ecr: Option<&AwsEcrConfig>,
|
||||
) -> anyhow::Result<bool> {
|
||||
match registry {
|
||||
ImageRegistry::None(_) => Ok(false),
|
||||
@@ -34,11 +37,19 @@ pub async fn docker_login(
|
||||
Some(token) => token,
|
||||
None => get_docker_token(account)?,
|
||||
};
|
||||
async_run_command(&format!(
|
||||
let log = async_run_command(&format!(
|
||||
"docker login -u {account} -p {registry_token}",
|
||||
))
|
||||
.await;
|
||||
Ok(true)
|
||||
if log.success() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"dockerhub login error: stdout: {} | stderr: {}",
|
||||
log.stdout,
|
||||
log.stderr
|
||||
))
|
||||
}
|
||||
}
|
||||
ImageRegistry::Ghcr(CloudRegistryConfig { account, .. }) => {
|
||||
if account.is_empty() {
|
||||
@@ -50,11 +61,37 @@ pub async fn docker_login(
|
||||
Some(token) => token,
|
||||
None => get_github_token(account)?,
|
||||
};
|
||||
async_run_command(&format!(
|
||||
let log = async_run_command(&format!(
|
||||
"docker login ghcr.io -u {account} -p {registry_token}",
|
||||
))
|
||||
.await;
|
||||
Ok(true)
|
||||
if log.success() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"ghcr login error: stdout: {} | stderr: {}",
|
||||
log.stdout,
|
||||
log.stderr
|
||||
))
|
||||
}
|
||||
}
|
||||
ImageRegistry::AwsEcr(label) => {
|
||||
let AwsEcrConfig { region, account_id } = aws_ecr
|
||||
.with_context(|| {
|
||||
format!("Could not find aws ecr config for label {label}")
|
||||
})?;
|
||||
let registry_token = registry_token
|
||||
.context("aws ecr build missing registry token from core")?;
|
||||
let log = async_run_command(&format!("docker login {account_id}.dkr.ecr.{region}.amazonaws.com -u AWS -p {registry_token}")).await;
|
||||
if log.success() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"aws ecr login error: stdout: {} | stderr: {}",
|
||||
log.stdout,
|
||||
log.stderr
|
||||
))
|
||||
}
|
||||
}
|
||||
ImageRegistry::Custom(_) => todo!(),
|
||||
}
|
||||
|
||||
@@ -92,3 +92,18 @@ pub struct GetCoreInfoResponse {
|
||||
/// Whether UI write access should be disabled
|
||||
pub ui_write_disabled: bool,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Get the available aws ecr config labels from the core config.
|
||||
/// Response: [GetAvailableAwsEcrLabelsResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(MonitorReadRequest)]
|
||||
#[response(GetAvailableAwsEcrLabelsResponse)]
|
||||
pub struct GetAvailableAwsEcrLabels {}
|
||||
|
||||
#[typeshare]
|
||||
pub type GetAvailableAwsEcrLabelsResponse = Vec<String>;
|
||||
@@ -7,8 +7,11 @@ use typeshare::typeshare;
|
||||
use crate::entities::{MongoId, I64};
|
||||
|
||||
use super::{
|
||||
_Serror, deployment::DeploymentState, server::stats::SeverityLevel,
|
||||
update::ResourceTarget,
|
||||
_Serror,
|
||||
deployment::DeploymentState,
|
||||
server::stats::SeverityLevel,
|
||||
update::{Log, ResourceTarget},
|
||||
Version,
|
||||
};
|
||||
|
||||
/// Representation of an alert in the system.
|
||||
@@ -70,6 +73,9 @@ pub struct Alert {
|
||||
)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
pub enum AlertData {
|
||||
/// A null alert
|
||||
None {},
|
||||
|
||||
/// A server could not be reached.
|
||||
ServerUnreachable {
|
||||
/// The id of the server
|
||||
@@ -148,12 +154,25 @@ pub enum AlertData {
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// A resource sync has pending updates
|
||||
ResourceSyncPendingUpdates {
|
||||
/// The id of the resource sync
|
||||
id: String,
|
||||
/// The name of the resource sync
|
||||
name: String,
|
||||
},
|
||||
|
||||
None {},
|
||||
/// A build has failed
|
||||
BuildFailed {
|
||||
/// The id of the build
|
||||
id: String,
|
||||
/// The name of the build
|
||||
name: String,
|
||||
/// The version that failed to build
|
||||
version: Version,
|
||||
/// The reason build failed
|
||||
err: Option<Log>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for AlertData {
|
||||
|
||||
@@ -60,6 +60,11 @@ pub struct AlerterConfig {
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub resources: Vec<ResourceTarget>,
|
||||
|
||||
/// DON'T send alerts on these resources.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub except_resources: Vec<ResourceTarget>,
|
||||
}
|
||||
|
||||
impl AlerterConfig {
|
||||
@@ -76,9 +81,10 @@ impl Default for AlerterConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: default_enabled(),
|
||||
endpoint: Default::default(),
|
||||
alert_types: Default::default(),
|
||||
resources: Default::default(),
|
||||
endpoint: Default::default(),
|
||||
except_resources: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,10 @@ pub enum ImageRegistry {
|
||||
/// See [the Github docs](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#pushing-container-images)
|
||||
/// for information on creating an access token
|
||||
Ghcr(CloudRegistryConfig),
|
||||
/// Push the image to Aws Elastic Container Registry
|
||||
///
|
||||
/// The string held in 'params' should match a label of an `aws_ecr_registry` in the core config.
|
||||
AwsEcr(String),
|
||||
/// Todo. Will point to a custom "Registry" resource by id
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
@@ -253,6 +253,19 @@ fn default_config_path() -> String {
|
||||
/// [docker_accounts]
|
||||
/// # docker_username_1 = "docker_token_1"
|
||||
/// # docker_username_2 = "docker_token_2"
|
||||
///
|
||||
/// ## configure aws ecr registries
|
||||
/// # [aws_ecr_registry.label_1]
|
||||
/// # region = "us-east-1"
|
||||
/// # account_id = "123456677"
|
||||
/// # access_key_id = "your_aws_key_id_1"
|
||||
/// # secret_access_key = "your_aws_secret_key_1"
|
||||
///
|
||||
/// # [aws_ecr_registry.label_2]
|
||||
/// # region = "us-west-1"
|
||||
/// # account_id = "123456677"
|
||||
/// # access_key_id = "your_aws_key_id_2"
|
||||
/// # secret_access_key = "your_aws_secret_key_2"
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CoreConfig {
|
||||
@@ -381,6 +394,10 @@ pub struct CoreConfig {
|
||||
/// account configured.
|
||||
#[serde(default)]
|
||||
pub docker_accounts: HashMap<String, String>,
|
||||
|
||||
/// Configure aws ecr registries.
|
||||
#[serde(default, alias = "aws_ecr_registry")]
|
||||
pub aws_ecr_registries: HashMap<String, AwsEcrConfigWithCredentials>,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
@@ -518,3 +535,34 @@ pub struct AwsCredentials {
|
||||
pub struct HetznerCredentials {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Provide configuration for an Aws Ecr registry.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct AwsEcrConfigWithCredentials {
|
||||
/// The Aws region
|
||||
pub region: String,
|
||||
/// The Aws account id
|
||||
pub account_id: String,
|
||||
/// The Aws ACCESS_KEY_ID
|
||||
pub access_key_id: String,
|
||||
/// The Aws SECRET_ACCESS_KEY
|
||||
pub secret_access_key: String,
|
||||
}
|
||||
|
||||
/// Provide configuration for an Aws Ecr registry.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct AwsEcrConfig {
|
||||
/// The Aws region
|
||||
pub region: String,
|
||||
/// The Aws account id
|
||||
pub account_id: String,
|
||||
}
|
||||
|
||||
impl AwsEcrConfig {
|
||||
pub fn from(config: &AwsEcrConfigWithCredentials) -> AwsEcrConfig {
|
||||
AwsEcrConfig {
|
||||
region: config.region.to_string(),
|
||||
account_id: config.account_id.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use anyhow::Context;
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use build::CloudRegistryConfig;
|
||||
use clap::Parser;
|
||||
use config::core::AwsEcrConfig;
|
||||
use derive_empty_traits::EmptyTraits;
|
||||
use serde::{
|
||||
de::{
|
||||
@@ -112,9 +113,10 @@ pub fn get_image_name(
|
||||
config: build::BuildConfig { image_registry, .. },
|
||||
..
|
||||
}: &build::Build,
|
||||
) -> String {
|
||||
aws_ecr: impl FnOnce(&String) -> Option<AwsEcrConfig>,
|
||||
) -> anyhow::Result<String> {
|
||||
let name = to_monitor_name(name);
|
||||
match image_registry {
|
||||
let name = match image_registry {
|
||||
// TODO
|
||||
build::ImageRegistry::None(_) => name,
|
||||
build::ImageRegistry::DockerHub(CloudRegistryConfig {
|
||||
@@ -141,11 +143,20 @@ pub fn get_image_name(
|
||||
name
|
||||
}
|
||||
}
|
||||
build::ImageRegistry::AwsEcr(label) => {
|
||||
let AwsEcrConfig {
|
||||
region, account_id, ..
|
||||
} = aws_ecr(label).with_context(|| {
|
||||
format!("didn't find aws ecr config for registry {label}")
|
||||
})?;
|
||||
format!("{account_id}.dkr.ecr.{region}.amazonaws.com/{name}")
|
||||
}
|
||||
build::ImageRegistry::Custom(_) => {
|
||||
// TODO
|
||||
name
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub fn to_monitor_name(name: &str) -> String {
|
||||
@@ -204,7 +215,7 @@ impl SystemCommand {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Serialize, Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct Version {
|
||||
pub major: i32,
|
||||
pub minor: i32,
|
||||
|
||||
@@ -18,6 +18,7 @@ export type UserResponses = {
|
||||
export type ReadResponses = {
|
||||
GetVersion: Types.GetVersionResponse;
|
||||
GetCoreInfo: Types.GetCoreInfoResponse;
|
||||
GetAvailableAwsEcrLabels: Types.GetAvailableAwsEcrLabelsResponse;
|
||||
|
||||
// ==== USER ====
|
||||
ListUsers: Types.ListUsersResponse;
|
||||
|
||||
@@ -112,6 +112,9 @@ export type ResourceTarget =
|
||||
|
||||
/** The variants of data related to the alert. */
|
||||
export type AlertData =
|
||||
/** A null alert */
|
||||
| { type: "None", data: {
|
||||
}}
|
||||
/** A server could not be reached. */
|
||||
| { type: "ServerUnreachable", data: {
|
||||
/** The id of the server */
|
||||
@@ -184,11 +187,23 @@ export type AlertData =
|
||||
/** A reason for the failure */
|
||||
message: string;
|
||||
}}
|
||||
/** A resource sync has pending updates */
|
||||
| { type: "ResourceSyncPendingUpdates", data: {
|
||||
/** The id of the resource sync */
|
||||
id: string;
|
||||
/** The name of the resource sync */
|
||||
name: string;
|
||||
}}
|
||||
| { type: "None", data: {
|
||||
/** A build has failed */
|
||||
| { type: "BuildFailed", data: {
|
||||
/** The id of the build */
|
||||
id: string;
|
||||
/** The name of the build */
|
||||
name: string;
|
||||
/** The version that failed to build */
|
||||
version: Version;
|
||||
/** The reason build failed */
|
||||
err?: Log;
|
||||
}};
|
||||
|
||||
/** Representation of an alert in the system. */
|
||||
@@ -264,6 +279,8 @@ export interface AlerterConfig {
|
||||
* If empty, will send alerts for all resources.
|
||||
*/
|
||||
resources?: ResourceTarget[];
|
||||
/** DON'T send alerts on these resources. */
|
||||
except_resources?: ResourceTarget[];
|
||||
}
|
||||
|
||||
export type Alerter = Resource<AlerterConfig, undefined>;
|
||||
@@ -320,6 +337,12 @@ export type ImageRegistry =
|
||||
* for information on creating an access token
|
||||
*/
|
||||
| { type: "Ghcr", params: CloudRegistryConfig }
|
||||
/**
|
||||
* Push the image to Aws Elastic Container Registry
|
||||
*
|
||||
* The string held in 'params' should match a label of an `aws_ecr_registry` in the core config.
|
||||
*/
|
||||
| { type: "AwsEcr", params: string }
|
||||
/** Todo. Will point to a custom "Registry" resource by id */
|
||||
| { type: "Custom", params: string };
|
||||
|
||||
@@ -649,6 +672,8 @@ export type GetDeploymentActionStateResponse = DeploymentActionState;
|
||||
|
||||
export type ListCommonDeploymentExtraArgsResponse = string[];
|
||||
|
||||
export type GetAvailableAwsEcrLabelsResponse = string[];
|
||||
|
||||
export type UserTarget =
|
||||
/** User Id */
|
||||
| { type: "User", id: string }
|
||||
@@ -2280,6 +2305,13 @@ export interface GetCoreInfoResponse {
|
||||
ui_write_disabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available aws ecr config labels from the core config.
|
||||
* Response: [GetAvailableAwsEcrLabelsResponse].
|
||||
*/
|
||||
export interface GetAvailableAwsEcrLabels {
|
||||
}
|
||||
|
||||
/**
|
||||
* List permissions for the calling user.
|
||||
* Does not include any permissions on UserGroups they may be a part of.
|
||||
@@ -3896,6 +3928,7 @@ export type ExecuteRequest =
|
||||
export type ReadRequest =
|
||||
| { type: "GetVersion", params: GetVersion }
|
||||
| { type: "GetCoreInfo", params: GetCoreInfo }
|
||||
| { type: "GetAvailableAwsEcrLabels", params: GetAvailableAwsEcrLabels }
|
||||
| { type: "ListUsers", params: ListUsers }
|
||||
| { type: "GetUsername", params: GetUsername }
|
||||
| { type: "ListApiKeys", params: ListApiKeys }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use monitor_client::entities::{
|
||||
server::docker_image::ImageSummary, update::Log,
|
||||
config::core::AwsEcrConfig, server::docker_image::ImageSummary,
|
||||
update::Log,
|
||||
};
|
||||
use resolver_api::derive::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -10,7 +11,10 @@ pub struct Build {
|
||||
pub build: monitor_client::entities::build::Build,
|
||||
/// Override registry token with one sent from core.
|
||||
pub registry_token: Option<String>,
|
||||
/// Propogate AwsEcrConfig from core
|
||||
pub aws_ecr: Option<AwsEcrConfig>,
|
||||
/// Propogate any secret replacers from core interpolation.
|
||||
#[serde(default)]
|
||||
pub replacers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use monitor_client::entities::{
|
||||
config::core::AwsEcrConfig,
|
||||
deployment::{
|
||||
ContainerSummary, Deployment, DockerContainerStats,
|
||||
TerminationSignal,
|
||||
@@ -107,6 +108,9 @@ pub struct Deploy {
|
||||
pub stop_time: Option<i32>,
|
||||
/// Override registry token with one sent from core.
|
||||
pub registry_token: Option<String>,
|
||||
/// Propogate AwsEcrConfig from core
|
||||
pub aws_ecr: Option<AwsEcrConfig>,
|
||||
/// Propogate any secret replacers from core interpolation.
|
||||
#[serde(default)]
|
||||
pub replacers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ passkey = "a_random_passkey"
|
||||
## default: 0 (pruning disabled)
|
||||
# keep_stats_for_days = 14
|
||||
|
||||
## number of days to keep alerts around, or 0 to disable pruning.
|
||||
## alerts older than this number of days are deleted daily
|
||||
## default: 0 (pruning disabled)
|
||||
# keep_alerts_for_days = 14
|
||||
|
||||
## token that has to be given to github during repo webhook config as the secret
|
||||
## default: empty (none)
|
||||
# github_webhook_secret = "your_random_webhook_secret"
|
||||
@@ -119,3 +124,16 @@ mongo.address = "localhost:27017"
|
||||
# [docker_accounts]
|
||||
# docker_username_1 = "docker_token_1"
|
||||
# docker_username_2 = "docker_token_2"
|
||||
|
||||
## configure aws ecr registries
|
||||
# [aws_ecr_registry.label_1]
|
||||
# region = "us-east-1"
|
||||
# account_id = "1234455"
|
||||
# access_key_id = "your_aws_key_id_1"
|
||||
# secret_access_key = "your_aws_secret_key_1"
|
||||
|
||||
# [aws_ecr_registry.label_2]
|
||||
# region = "us-west-1"
|
||||
# account_id = "1234455"
|
||||
# access_key_id = "your_aws_key_id_2"
|
||||
# secret_access_key = "your_aws_secret_key_2"
|
||||
@@ -246,6 +246,42 @@ export const AccountSelector = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const AwsEcrLabelSelector = ({
|
||||
disabled,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
selected: string | undefined;
|
||||
onSelect: (id: string) => void;
|
||||
}) => {
|
||||
const labels = useRead("GetAvailableAwsEcrLabels", {}).data;
|
||||
return (
|
||||
<Select
|
||||
value={selected}
|
||||
onValueChange={(value) => {
|
||||
onSelect(value === "Empty" ? "" : value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full lg:w-[200px] max-w-[50%]"
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectValue placeholder="Select Ecr Config" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"Empty"}>None</SelectItem>
|
||||
{labels?.map((label: string) => (
|
||||
<SelectItem key={label} value={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputList = <T extends { [key: string]: unknown }>({
|
||||
field,
|
||||
values,
|
||||
@@ -482,6 +518,31 @@ export const ImageRegistryConfig = ({
|
||||
</ConfigItem>
|
||||
);
|
||||
}
|
||||
if (_registry.type === "AwsEcr") {
|
||||
return (
|
||||
<ConfigItem label="Image Registry">
|
||||
<div className="flex items-center justify-stretch gap-4">
|
||||
<AwsEcrLabelSelector
|
||||
selected={_registry.params}
|
||||
onSelect={(label) =>
|
||||
setRegistry({
|
||||
type: "AwsEcr",
|
||||
params: label,
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<RegistryTypeSelector
|
||||
registry={_registry}
|
||||
setRegistry={setRegistry}
|
||||
disabled={disabled}
|
||||
deployment={type === "Deployment"}
|
||||
registry_types={registry_types}
|
||||
/>
|
||||
</div>
|
||||
</ConfigItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ConfigItem label="Image Registry">
|
||||
<div className="flex items-center justify-stretch gap-4">
|
||||
@@ -527,6 +588,7 @@ const REGISTRY_TYPES: Types.ImageRegistry["type"][] = [
|
||||
"None",
|
||||
"DockerHub",
|
||||
"Ghcr",
|
||||
"AwsEcr"
|
||||
];
|
||||
|
||||
const RegistryTypeSelector = ({
|
||||
@@ -635,6 +697,8 @@ const default_registry_config = (
|
||||
return { type, params: { account: "", organization: "" } };
|
||||
case "Ghcr":
|
||||
return { type, params: { account: "", organization: "" } };
|
||||
case "AwsEcr":
|
||||
return { type, params: "" };
|
||||
case "Custom":
|
||||
return { type, params: "" };
|
||||
}
|
||||
@@ -703,4 +767,4 @@ export const SecretSelector = ({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -28,8 +28,8 @@ export const ExportButton = ({
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="flex gap-2 items-center">
|
||||
Toml
|
||||
<FileDown className="w-4 h-4" />
|
||||
Toml
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-[900px] max-w-[95vw]">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from "@ui/button";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Link, Outlet, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -18,23 +18,16 @@ import { Topbar } from "./topbar";
|
||||
import { usableResourcePath } from "@lib/utils";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { ResourceName } from "./resources/common";
|
||||
import { useShiftKeyListener } from "@lib/hooks";
|
||||
|
||||
export const Layout = () => {
|
||||
const nav = useNavigate();
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
// This will ignore Shift + S if it is sent from input / textarea
|
||||
const target = e.target as any;
|
||||
if (target.matches("input") || target.matches("textarea")) return;
|
||||
|
||||
if (e.shiftKey && e.key === "H") {
|
||||
e.preventDefault();
|
||||
nav("/");
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", keydown);
|
||||
return () => document.removeEventListener("keydown", keydown);
|
||||
});
|
||||
useShiftKeyListener("H", () => nav("/"));
|
||||
useShiftKeyListener("G", () => nav("/servers"));
|
||||
useShiftKeyListener("D", () => nav("/deployments"));
|
||||
useShiftKeyListener("B", () => nav("/builds"));
|
||||
useShiftKeyListener("R", () => nav("/repos"));
|
||||
useShiftKeyListener("P", () => nav("/procedures"));
|
||||
return (
|
||||
<>
|
||||
<Topbar />
|
||||
|
||||
@@ -16,6 +16,7 @@ const ALERT_TYPES: Types.AlertData["type"][] = [
|
||||
"ServerDisk",
|
||||
"ContainerStateChange",
|
||||
"ResourceSyncPendingUpdates",
|
||||
"BuildFailed",
|
||||
"AwsBuilderTerminationFailed",
|
||||
];
|
||||
|
||||
|
||||
@@ -53,6 +53,15 @@ export const AlerterConfig = ({ id }: { id: string }) => {
|
||||
resources={resources!}
|
||||
set={(resources) => set({ resources })}
|
||||
disabled={disabled}
|
||||
blacklist={false}
|
||||
/>
|
||||
),
|
||||
except_resources: (resources, set) => (
|
||||
<ResourcesConfig
|
||||
resources={resources!}
|
||||
set={(except_resources) => set({ except_resources })}
|
||||
disabled={disabled}
|
||||
blacklist={true}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -22,15 +22,18 @@ export const ResourcesConfig = ({
|
||||
resources,
|
||||
set,
|
||||
disabled,
|
||||
blacklist,
|
||||
}: {
|
||||
resources: Types.ResourceTarget[];
|
||||
set: (resources: Types.ResourceTarget[]) => void;
|
||||
disabled: boolean;
|
||||
blacklist: boolean;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const servers = useRead("ListServers", {}).data ?? [];
|
||||
const deployments = useRead("ListDeployments", {}).data ?? [];
|
||||
const builds = useRead("ListBuilds", {}).data ?? [];
|
||||
const all_resources = [
|
||||
...servers.map((server) => {
|
||||
return {
|
||||
@@ -54,6 +57,14 @@ export const ResourcesConfig = ({
|
||||
? true
|
||||
: false,
|
||||
})),
|
||||
...builds.map((build) => ({
|
||||
type: "Build",
|
||||
id: build.id,
|
||||
name: build.name.toLowerCase(),
|
||||
enabled: resources.find((r) => r.type === "Build" && r.id === build.id)
|
||||
? true
|
||||
: false,
|
||||
})),
|
||||
];
|
||||
const searchSplit = search.split(" ");
|
||||
const filtered_resources = searchSplit.length
|
||||
@@ -63,11 +74,12 @@ export const ResourcesConfig = ({
|
||||
})
|
||||
: all_resources;
|
||||
return (
|
||||
<ConfigItem label="Resources">
|
||||
<ConfigItem label={`Resource ${blacklist ? "Blacklist" : "Whitelist"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
{resources.length ? (
|
||||
<div className="text-muted-foreground">
|
||||
Alerts filtered by {resources.length} resources
|
||||
Alerts {blacklist ? "blacklisted" : "whitelisted"} by{" "}
|
||||
{resources.length} resources
|
||||
</div>
|
||||
) : undefined}
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -141,7 +153,10 @@ export const ResourcesConfig = ({
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Enabled" />
|
||||
<SortableHeader
|
||||
column={column}
|
||||
title={blacklist ? "Blacklist" : "Whitelist"}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
|
||||
@@ -23,11 +23,17 @@ export const AlerterTable = ({
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "info.alerter_type",
|
||||
accessorKey: "info.endpoint_type",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Type" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "info.enabled",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Enabled" />
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
cell: ({ row }) => <TableTags tag_ids={row.original.tags} />,
|
||||
|
||||
@@ -349,7 +349,7 @@ export const LabelsConfig = ({
|
||||
<div className="py-2 w-full flex justify-end">
|
||||
<DoubleInput
|
||||
disabled={disabled}
|
||||
inputClassName="w-[200px] max-w-full"
|
||||
inputClassName="w-[300px] 2xl:w-[400px] max-w-full"
|
||||
containerClassName="w-fit"
|
||||
values={labels}
|
||||
leftval="variable"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { HardDrive, Rocket, Server } from "lucide-react";
|
||||
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
|
||||
import { cn } from "@lib/utils";
|
||||
import { useServer } from "../server";
|
||||
import {
|
||||
@@ -26,6 +26,7 @@ import { RunBuild } from "../build/actions";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
|
||||
import { DeploymentConfig } from "./config";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
// const configOrLog = atomWithStorage("config-or-log-v1", "Config");
|
||||
|
||||
@@ -161,6 +162,17 @@ export const DeploymentComponents: RequiredResourceComponents = {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
Alerts: ({ id }) => {
|
||||
return (
|
||||
<Link
|
||||
to={`/deployments/${id}/alerts`}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Alerts
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Actions: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
XOctagon,
|
||||
AreaChart,
|
||||
Milestone,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { Section } from "@components/layouts";
|
||||
import { RenameServer } from "./actions";
|
||||
@@ -205,6 +206,14 @@ export const ServerComponents: RequiredResourceComponents = {
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
Alerts: ({ id }) => {
|
||||
return (
|
||||
<Link to={`/servers/${id}/alerts`} className="flex gap-2 items-center">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Alerts
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Actions: {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { tagsAtom, useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import {
|
||||
tagsAtom,
|
||||
useInvalidate,
|
||||
useRead,
|
||||
useShiftKeyListener,
|
||||
useWrite,
|
||||
} from "@lib/hooks";
|
||||
import { cn, filterBySplit } from "@lib/utils";
|
||||
import { Types } from "@monitor/client";
|
||||
import { Badge } from "@ui/badge";
|
||||
@@ -14,7 +20,7 @@ import {
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { useAtom } from "jotai";
|
||||
import { MinusCircle, PlusCircle, SearchX, Tag } from "lucide-react";
|
||||
import { MinusCircle, PlusCircle, SearchX, Tag, X } from "lucide-react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
type TargetExcludingSystem = Exclude<Types.ResourceTarget, { type: "System" }>;
|
||||
@@ -22,15 +28,28 @@ type TargetExcludingSystem = Exclude<Types.ResourceTarget, { type: "System" }>;
|
||||
export const TagsFilter = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const [tags, setTags] = useAtom(tagsAtom);
|
||||
const [tags, setTags] = useAtom<string[]>(tagsAtom);
|
||||
const all_tags = useRead("ListTags", {}).data;
|
||||
const filtered = filterBySplit(all_tags, search, (item) => item.name);
|
||||
useShiftKeyListener("T", () => setOpen(true));
|
||||
useShiftKeyListener("C", () => setTags([]));
|
||||
return (
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex gap-3 items-center">
|
||||
{tags.length > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="px-2 py-1.5 h-fit"
|
||||
onClick={() => setTags([])}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<TagsFilterTags
|
||||
tag_ids={tags}
|
||||
onBadgeClick={(tag_id) => setTags(tags.filter((id) => id !== tag_id))}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
@@ -39,11 +58,16 @@ export const TagsFilter = () => {
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="flex gap-3">
|
||||
Filter by Tag <Tag className="w-3 h-3" />
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<Tag className="w-3 h-3" />
|
||||
Tag Filter
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] max-h-[200px] p-0" sideOffset={12}>
|
||||
<PopoverContent
|
||||
className="w-[200px] max-h-[200px] p-0"
|
||||
sideOffset={12}
|
||||
align="end"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Search Tags"
|
||||
@@ -199,6 +223,8 @@ export const AddTags = ({ target }: { target: TargetExcludingSystem }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useShiftKeyListener("T", () => setOpen(true));
|
||||
|
||||
const all_tags = useRead("ListTags", {}).data ?? [];
|
||||
const all_tag_names = all_tags.map((tag) => tag.name);
|
||||
|
||||
@@ -247,8 +273,14 @@ export const AddTags = ({ target }: { target: TargetExcludingSystem }) => {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="secondary" className="px-2 py-0 h-auto">
|
||||
<Button variant="secondary" className="px-2 py-0.5 h-fit">
|
||||
<PlusCircle className="w-3" />
|
||||
{/* <Badge
|
||||
variant="outline"
|
||||
className="text-muted-foreground hidden md:inline-flex"
|
||||
>
|
||||
shift + t
|
||||
</Badge> */}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" sideOffset={12} align="end">
|
||||
@@ -289,10 +321,7 @@ export const AddTags = ({ target }: { target: TargetExcludingSystem }) => {
|
||||
</CommandItem>
|
||||
))}
|
||||
{search && !all_tag_names.includes(search) && (
|
||||
<CommandItem
|
||||
onSelect={create_tag}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<CommandItem onSelect={create_tag} className="cursor-pointer">
|
||||
<div className="w-full p-1 flex items-center justify-between">
|
||||
Create Tag
|
||||
<PlusCircle className="w-4" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRead, useResourceParamType } from "@lib/hooks";
|
||||
import { useRead, useResourceParamType, useShiftKeyListener } from "@lib/hooks";
|
||||
import { ResourceComponents } from "./resources";
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
FileQuestion,
|
||||
FolderTree,
|
||||
Home,
|
||||
Keyboard,
|
||||
SearchX,
|
||||
Settings,
|
||||
User,
|
||||
@@ -36,7 +37,7 @@ import { ThemeToggle } from "@ui/theme";
|
||||
import { UsableResource } from "@types";
|
||||
import { useAtom } from "jotai";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -48,23 +49,18 @@ import {
|
||||
import { ResourceLink } from "./resources/common";
|
||||
import { HomeView, homeViewAtom } from "@main";
|
||||
import { Types } from "@monitor/client";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/dialog";
|
||||
import { Badge } from "@ui/badge";
|
||||
|
||||
export const Topbar = () => {
|
||||
const [omniOpen, setOmniOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
// This will ignore Shift + S if it is sent from input / textarea
|
||||
const target = e.target as any;
|
||||
if (target.matches("input") || target.matches("textarea")) return;
|
||||
|
||||
if (e.shiftKey && e.key === "S") {
|
||||
e.preventDefault();
|
||||
setOmniOpen(true);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", keydown);
|
||||
return () => document.removeEventListener("keydown", keydown);
|
||||
});
|
||||
useShiftKeyListener("S", () => setOmniOpen(true));
|
||||
const version = useRead("GetVersion", {}).data?.version;
|
||||
return (
|
||||
<div className="sticky top-0 h-[70px] border-b z-50 w-full bg-card text-card-foreground shadow flex items-center">
|
||||
@@ -95,9 +91,6 @@ export const Topbar = () => {
|
||||
className="hidden lg:flex justify-self-center"
|
||||
/>
|
||||
<div className="flex md:gap-2 justify-self-end items-center">
|
||||
{/* <div className="hidden lg:block px-2 text-muted-foreground cursor-default">
|
||||
v{version}
|
||||
</div> */}
|
||||
<a
|
||||
href="https://docs.monitor.mogh.tech"
|
||||
target="_blank"
|
||||
@@ -109,6 +102,7 @@ export const Topbar = () => {
|
||||
</a>
|
||||
<OmniSearch setOpen={setOmniOpen} className="lg:hidden" />
|
||||
<WsStatusIndicator />
|
||||
<KeyboardShortcuts />
|
||||
<TopbarUpdates />
|
||||
<ThemeToggle />
|
||||
<Logout />
|
||||
@@ -302,7 +296,7 @@ const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hidden sm:flex justify-start items-center gap-2 w-48 px-3"
|
||||
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
|
||||
>
|
||||
<Components.Icon id={selected?.id} />
|
||||
{selected ? selected.name : `All ${type}s`}
|
||||
@@ -375,7 +369,7 @@ const UserGroupDropdown = ({ group_id }: { group_id: string | undefined }) => {
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hidden sm:flex justify-start items-center gap-2 w-48 px-3"
|
||||
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
{selected ? selected.name : "All User Groups"}
|
||||
@@ -455,7 +449,7 @@ const UsersDropdown = ({ user_id }: { user_id: string | undefined }) => {
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hidden sm:flex justify-start items-center gap-2 w-48 px-3"
|
||||
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
|
||||
>
|
||||
<UserAvatar avatar={avatar} />
|
||||
{selected ? selected.username : "All Users"}
|
||||
@@ -511,3 +505,64 @@ const UsersDropdown = ({ user_id }: { user_id: string | undefined }) => {
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const KeyboardShortcuts = () => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" className="items-center gap-2">
|
||||
<Keyboard className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Keyboard Shortcuts</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3 grid-cols-2 pt-8">
|
||||
<KeyboardShortcut label="Go Home" keys={["Shift", "H"]} />
|
||||
|
||||
<KeyboardShortcut label="Go to Servers" keys={["Shift", "G"]} />
|
||||
<KeyboardShortcut label="Go to Deployments" keys={["Shift", "D"]} />
|
||||
<KeyboardShortcut label="Go to Builds" keys={["Shift", "B"]} />
|
||||
<KeyboardShortcut label="Go to Repos" keys={["Shift", "R"]} />
|
||||
<KeyboardShortcut label="Go to Procedures" keys={["Shift", "P"]} />
|
||||
|
||||
<KeyboardShortcut label="Search" keys={["Shift", "S"]} />
|
||||
<KeyboardShortcut label="Add Filter Tag" keys={["Shift", "T"]} />
|
||||
<KeyboardShortcut
|
||||
label="Clear Filter Tags"
|
||||
keys={["Shift", "C"]}
|
||||
divider={false}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const KeyboardShortcut = ({
|
||||
label,
|
||||
keys,
|
||||
divider = true,
|
||||
}: {
|
||||
label: string;
|
||||
keys: string[];
|
||||
divider?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>{label}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{keys.map((key) => (
|
||||
<Badge variant="secondary" key={key}>
|
||||
{key}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{divider && (
|
||||
<div className="col-span-full bg-gray-600 h-[1px] opacity-40" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -321,3 +321,20 @@ export const useLocalStorage = <T>(
|
||||
};
|
||||
return [state, set];
|
||||
};
|
||||
|
||||
export const useShiftKeyListener = (listenKey: string, onPress: () => void) => {
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
// This will ignore Shift + listenKey if it is sent from input / textarea
|
||||
const target = e.target as any;
|
||||
if (target.matches("input") || target.matches("textarea")) return;
|
||||
|
||||
if (e.shiftKey && e.key === listenKey) {
|
||||
e.preventDefault();
|
||||
onPress();
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", keydown);
|
||||
return () => document.removeEventListener("keydown", keydown);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,14 +16,16 @@ import { AlertTriangle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
const ALERT_TYPES = {
|
||||
const ALERT_TYPES: { [key: string]: Types.AlertData["type"][] } = {
|
||||
Server: ["ServerUnreachable", "ServerCpu", "ServerMem", "ServerDisk"],
|
||||
Deployment: ["ContainerStateChange"],
|
||||
Build: ["BuildFailed"],
|
||||
};
|
||||
|
||||
const FALLBACK_ALERT_TYPES = [
|
||||
...ALERT_TYPES.Server,
|
||||
...ALERT_TYPES.Deployment,
|
||||
...ALERT_TYPES.Build,
|
||||
"AwsBuilderTerminationFailed",
|
||||
];
|
||||
|
||||
@@ -43,7 +45,7 @@ export const Alerts = () => {
|
||||
query: {
|
||||
"target.type": type,
|
||||
"target.id": id,
|
||||
variant: variant === "All" ? undefined : variant,
|
||||
"data.type": variant === "All" ? undefined : variant,
|
||||
resolved: onlyOpen ? false : undefined,
|
||||
},
|
||||
page,
|
||||
|
||||
@@ -57,7 +57,7 @@ export const Resource = () => {
|
||||
{infoEntries.map(([key, Info], i) => (
|
||||
<Fragment key={key}>
|
||||
{i !== 0 && "| "}
|
||||
<Info key={i} id={id} />
|
||||
<Info id={id} />
|
||||
</Fragment>
|
||||
))}
|
||||
{infoEntries.length ? "| " : ""}
|
||||
|
||||
@@ -8,3 +8,4 @@ repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serror.workspace = true
|
||||
@@ -1,3 +1,5 @@
|
||||
use serror::Serror;
|
||||
|
||||
pub fn muted(content: impl std::fmt::Display) -> String {
|
||||
format!("<span class=\"text-muted-foreground\">{content}</span>")
|
||||
}
|
||||
@@ -30,3 +32,18 @@ impl std::fmt::Display for Color {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_serror(Serror { error, trace }: &Serror) -> String {
|
||||
let trace = (!trace.is_empty())
|
||||
.then(|| {
|
||||
let mut out = format!("\n\n{}:", muted("TRACE"));
|
||||
|
||||
for (i, msg) in trace.iter().enumerate() {
|
||||
out.push_str(&format!("\n\t{}: {msg}", muted(i + 1)));
|
||||
}
|
||||
|
||||
out
|
||||
})
|
||||
.unwrap_or_default();
|
||||
format!("{}: {error}{trace}", colored("ERROR", Color::Red))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user