Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b8c89d976 | ||
|
|
25c8d25636 | ||
|
|
ea242de2e4 | ||
|
|
be03547407 | ||
|
|
9c0d28b311 | ||
|
|
f269deb99c | ||
|
|
3df8163131 | ||
|
|
33a16a9bd2 | ||
|
|
215e7d1bdc | ||
|
|
25e0905c0c | ||
|
|
1c07ccea85 | ||
|
|
405ec1b8cc | ||
|
|
4f212bd06f | ||
|
|
074f4ea2db | ||
|
|
c9abccaf02 | ||
|
|
6428fa6de2 | ||
|
|
883f54431d | ||
|
|
28dc030e2b | ||
|
|
145d933e63 | ||
|
|
9772ca1a1c | ||
|
|
4059b69201 | ||
|
|
8e175ea5a1 | ||
|
|
d931b8b4e7 | ||
|
|
0982800ad2 | ||
|
|
4382ad0b3b | ||
|
|
e7891f7870 | ||
|
|
6bada46841 | ||
|
|
eae6cbd228 | ||
|
|
a0ee6180b2 | ||
|
|
3ce3de8768 | ||
|
|
6c46993b61 | ||
|
|
fbd9d14aaa |
85
Cargo.lock
generated
@@ -32,7 +32,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.5",
|
||||
@@ -923,7 +923,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"monitor_client",
|
||||
"run_command",
|
||||
@@ -1423,7 +1423,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -1482,6 +1482,15 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@@ -1968,7 +1977,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -2037,7 +2046,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "migrator"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2160,7 +2169,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_cli"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2180,7 +2189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_client"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2203,7 +2212,7 @@ dependencies = [
|
||||
"strum 0.26.2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.22.0",
|
||||
"tokio-tungstenite 0.23.0",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"typeshare",
|
||||
@@ -2212,7 +2221,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_core"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2235,6 +2244,7 @@ dependencies = [
|
||||
"mongo_indexed",
|
||||
"monitor_client",
|
||||
"mungos",
|
||||
"ordered_hash_map",
|
||||
"parse_csl",
|
||||
"partial_derive2",
|
||||
"periphery_client",
|
||||
@@ -2251,6 +2261,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"toml_pretty",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
@@ -2261,7 +2272,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_periphery"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2509,6 +2520,16 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered_hash_map"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab0e5f22bf6dd04abd854a8874247813a8fa2c8c1260eba6fbb150270ce7c176"
|
||||
dependencies = [
|
||||
"hashbrown 0.13.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.1"
|
||||
@@ -2587,7 +2608,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -3564,7 +3585,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tests"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenv",
|
||||
@@ -3736,19 +3757,19 @@ dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
"tungstenite 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d46baf930138837d65e25e3b33be49c9228579a6135dbf756b5cb9e4283e7cef"
|
||||
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
"tungstenite 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3799,6 +3820,18 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_pretty"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "100b9fea6caaa91d9e7837591cbda2958b6ff16318b13c8ba92254fdda13db47"
|
||||
dependencies = [
|
||||
"ordered_hash_map",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
version = "0.11.0"
|
||||
@@ -4042,6 +4075,24 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 1.1.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.10.0"
|
||||
@@ -4119,7 +4170,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"logger",
|
||||
|
||||
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.7.0"
|
||||
version = "1.7.3"
|
||||
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.6.2"
|
||||
monitor_client = "1.7.3"
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
command = { path = "lib/command" }
|
||||
logger = { path = "lib/logger" }
|
||||
@@ -34,6 +34,7 @@ partial_derive2 = "0.4.3"
|
||||
derive_variants = "1.0.0"
|
||||
mongo_indexed = "0.3.0"
|
||||
resolver_api = "1.1.0"
|
||||
toml_pretty = "1.1.1"
|
||||
parse_csl = "0.1.0"
|
||||
mungos = "0.5.6"
|
||||
svi = "1.0.1"
|
||||
@@ -50,9 +51,10 @@ axum = { version = "0.7.5", features = ["ws", "json"] }
|
||||
axum-extra = { version = "0.9.3", features = ["typed-header"] }
|
||||
tower = { version = "0.4.13", features = ["timeout"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "cors"] }
|
||||
tokio-tungstenite = "0.22.0"
|
||||
tokio-tungstenite = "0.23.0"
|
||||
|
||||
# SER/DE
|
||||
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
|
||||
@@ -19,7 +19,7 @@ pub type ToDelete = Vec<String>;
|
||||
|
||||
type UpdatesResult<T> = (ToCreate<T>, ToUpdate<T>, ToDelete);
|
||||
|
||||
pub struct ToUpdateItem<T> {
|
||||
pub struct ToUpdateItem<T: Default> {
|
||||
pub id: String,
|
||||
pub resource: ResourceToml<T>,
|
||||
pub update_description: bool,
|
||||
@@ -37,6 +37,7 @@ pub trait ResourceSync: Sized {
|
||||
type PartialConfig: std::fmt::Debug
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Default
|
||||
+ From<Self::Config>
|
||||
+ From<Self::ConfigDiff>
|
||||
+ Serialize
|
||||
|
||||
@@ -28,11 +28,13 @@ partial_derive2.workspace = true
|
||||
derive_variants.workspace = true
|
||||
mongo_indexed.workspace = true
|
||||
resolver_api.workspace = true
|
||||
toml_pretty.workspace = true
|
||||
parse_csl.workspace = true
|
||||
mungos.workspace = true
|
||||
slack.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
ordered_hash_map.workspace = true
|
||||
urlencoding.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
aws-config.workspace = true
|
||||
|
||||
@@ -14,9 +14,21 @@ RUN cd frontend && yarn link @monitor/client && yarn && yarn build
|
||||
|
||||
# Final Image
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install Deps
|
||||
RUN apt update && apt install -y git ca-certificates
|
||||
|
||||
# Copy
|
||||
COPY ./config_example/core.config.example.toml /config/config.toml
|
||||
COPY --from=core-builder /builder/target/release/core /
|
||||
COPY --from=frontend-builder /builder/frontend/dist /frontend
|
||||
|
||||
# Hint at the port
|
||||
EXPOSE 9000
|
||||
|
||||
# Label for Ghcr
|
||||
LABEL org.opencontainers.image.source=https://github.com/mbecker20/monitor
|
||||
LABEL org.opencontainers.image.description="A tool to build and deploy software across many servers"
|
||||
LABEL org.opencontainers.image.licenses=GPL-3.0
|
||||
|
||||
CMD ["./core"]
|
||||
@@ -14,7 +14,11 @@ use monitor_client::{
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::{Builder, BuilderConfig},
|
||||
deployment::{Deployment, DeploymentImage},
|
||||
deployment::{
|
||||
conversions_to_string, term_signal_labels_to_string,
|
||||
Deployment, DeploymentImage,
|
||||
},
|
||||
environment_vars_to_string,
|
||||
permission::{PermissionLevel, UserTarget},
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
@@ -30,8 +34,10 @@ use monitor_client::{
|
||||
},
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use ordered_hash_map::OrderedHashMap;
|
||||
use partial_derive2::PartialDiff;
|
||||
use resolver_api::Resolve;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_user_user_group_ids,
|
||||
@@ -305,7 +311,7 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
.context("failed to get variables from db")?;
|
||||
}
|
||||
|
||||
let toml = toml::to_string(&res)
|
||||
let toml = serialize_resources_toml(&res)
|
||||
.context("failed to serialize resources to toml")?;
|
||||
|
||||
Ok(ExportResourcesToTomlResponse { toml })
|
||||
@@ -530,3 +536,224 @@ fn convert_resource<R: MonitorResource>(
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_resources_toml(
|
||||
resources: &ResourcesToml,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut res = String::new();
|
||||
|
||||
let options = toml_pretty::Options::default()
|
||||
.tab(" ")
|
||||
.skip_empty_string(true)
|
||||
.max_inline_array_length(30);
|
||||
|
||||
for server in &resources.servers {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[server]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&server, options)
|
||||
.context("failed to serialize servers to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for deployment in &resources.deployments {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[deployment]]\n");
|
||||
let mut parsed: OrderedHashMap<String, Value> =
|
||||
serde_json::from_str(&serde_json::to_string(&deployment)?)?;
|
||||
let config = parsed
|
||||
.get_mut("config")
|
||||
.context("deployment has no config?")?
|
||||
.as_object_mut()
|
||||
.context("config is not object?")?;
|
||||
if let Some(term_signal_labels) =
|
||||
&deployment.config.term_signal_labels
|
||||
{
|
||||
config.insert(
|
||||
"term_signal_labels".to_string(),
|
||||
Value::String(term_signal_labels_to_string(
|
||||
term_signal_labels,
|
||||
)),
|
||||
);
|
||||
}
|
||||
if let Some(ports) = &deployment.config.ports {
|
||||
config.insert(
|
||||
"ports".to_string(),
|
||||
Value::String(conversions_to_string(ports)),
|
||||
);
|
||||
}
|
||||
if let Some(volumes) = &deployment.config.volumes {
|
||||
config.insert(
|
||||
"volumes".to_string(),
|
||||
Value::String(conversions_to_string(volumes)),
|
||||
);
|
||||
}
|
||||
if let Some(environment) = &deployment.config.environment {
|
||||
config.insert(
|
||||
"environment".to_string(),
|
||||
Value::String(environment_vars_to_string(environment)),
|
||||
);
|
||||
}
|
||||
if let Some(labels) = &deployment.config.labels {
|
||||
config.insert(
|
||||
"labels".to_string(),
|
||||
Value::String(environment_vars_to_string(labels)),
|
||||
);
|
||||
}
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&parsed, options)
|
||||
.context("failed to serialize deployments to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for build in &resources.builds {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
let mut parsed: OrderedHashMap<String, Value> =
|
||||
serde_json::from_str(&serde_json::to_string(&build)?)?;
|
||||
let config = parsed
|
||||
.get_mut("config")
|
||||
.context("build has no config?")?
|
||||
.as_object_mut()
|
||||
.context("config is not object?")?;
|
||||
if let Some(version) = &build.config.version {
|
||||
config.insert(
|
||||
"version".to_string(),
|
||||
Value::String(version.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(build_args) = &build.config.build_args {
|
||||
config.insert(
|
||||
"build_args".to_string(),
|
||||
Value::String(environment_vars_to_string(build_args)),
|
||||
);
|
||||
}
|
||||
if let Some(labels) = &build.config.labels {
|
||||
config.insert(
|
||||
"labels".to_string(),
|
||||
Value::String(environment_vars_to_string(labels)),
|
||||
);
|
||||
}
|
||||
res.push_str("[[build]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&parsed, options)
|
||||
.context("failed to serialize builds to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for repo in &resources.repos {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[repo]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&repo, options)
|
||||
.context("failed to serialize repos to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for procedure in &resources.procedures {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
let mut parsed: OrderedHashMap<String, Value> =
|
||||
serde_json::from_str(&serde_json::to_string(&procedure)?)?;
|
||||
let config = parsed
|
||||
.get_mut("config")
|
||||
.context("procedure has no config?")?
|
||||
.as_object_mut()
|
||||
.context("config is not object?")?;
|
||||
|
||||
let stages = config
|
||||
.remove("stages")
|
||||
.context("procedure config has no stages")?;
|
||||
let stages = stages.as_array().context("stages is not array")?;
|
||||
|
||||
res.push_str("[[procedure]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&parsed, options)
|
||||
.context("failed to serialize procedures to toml")?,
|
||||
);
|
||||
|
||||
for stage in stages {
|
||||
res.push_str("\n\n[[procedure.config.stage]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(stage, options)
|
||||
.context("failed to serialize procedures to toml")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for alerter in &resources.alerters {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[alerter]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&alerter, options)
|
||||
.context("failed to serialize alerters to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for builder in &resources.builders {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[builder]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&builder, options)
|
||||
.context("failed to serialize builders to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for server_template in &resources.server_templates {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[server_template]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&server_template, options)
|
||||
.context("failed to serialize server_templates to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for resource_sync in &resources.resource_syncs {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[resource_sync]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&resource_sync, options)
|
||||
.context("failed to serialize resource_syncs to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for variable in &resources.variables {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[variable]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&variable, options)
|
||||
.context("failed to serialize variables to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
for user_group in &resources.user_groups {
|
||||
if !res.is_empty() {
|
||||
res.push_str("\n\n##\n\n");
|
||||
}
|
||||
res.push_str("[[user_group]]\n");
|
||||
res.push_str(
|
||||
&toml_pretty::to_string(&user_group, options)
|
||||
.context("failed to serialize user_groups to toml")?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub fn core_config() -> &'static CoreConfig {
|
||||
.unwrap_or(config.jwt_valid_for),
|
||||
sync_directory: env
|
||||
.monitor_sync_directory
|
||||
.map(|dir|
|
||||
.map(|dir|
|
||||
dir.parse()
|
||||
.context("failed to parse env MONITOR_SYNC_DIRECTORY as valid path").unwrap())
|
||||
.unwrap_or(config.sync_directory),
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
use async_timing_util::{wait_until_timelength, Timelength};
|
||||
use monitor_client::{
|
||||
api::write::RefreshResourceSyncPending, entities::user::sync_user,
|
||||
};
|
||||
use mungos::find::find_collect;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::state::{db_client, State};
|
||||
|
||||
pub mod remote;
|
||||
pub mod resource;
|
||||
pub mod user_groups;
|
||||
@@ -6,6 +15,33 @@ pub mod variables;
|
||||
mod file;
|
||||
mod resources;
|
||||
|
||||
pub fn spawn_sync_refresh_loop() {
|
||||
tokio::spawn(async move {
|
||||
let db = db_client().await;
|
||||
let user = sync_user();
|
||||
loop {
|
||||
wait_until_timelength(Timelength::FiveMinutes, 0).await;
|
||||
let Ok(syncs) = find_collect(&db.resource_syncs, None, None)
|
||||
.await
|
||||
.inspect_err(|e| warn!("failed to get resource syncs from db in refresh task | {e:#}")) else {
|
||||
continue;
|
||||
};
|
||||
for sync in syncs {
|
||||
State
|
||||
.resolve(
|
||||
RefreshResourceSyncPending { sync: sync.id },
|
||||
user.clone(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!("failed to refresh resource sync in refresh task | sync: {} | {e:#}", sync.name)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn muted(content: &str) -> String {
|
||||
format!("<span class=\"text-muted-foreground\">{content}</span>")
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ pub type ToDelete = Vec<String>;
|
||||
|
||||
type UpdatesResult<T> = (ToCreate<T>, ToUpdate<T>, ToDelete);
|
||||
|
||||
pub struct ToUpdateItem<T> {
|
||||
pub struct ToUpdateItem<T: Default> {
|
||||
pub id: String,
|
||||
pub resource: ResourceToml<T>,
|
||||
pub update_description: bool,
|
||||
|
||||
@@ -35,6 +35,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
// Spawn monitoring loops
|
||||
monitor::spawn_monitor_loop()?;
|
||||
helpers::prune::spawn_prune_loop();
|
||||
helpers::sync::spawn_sync_refresh_loop();
|
||||
resource::spawn_build_state_refresh_loop();
|
||||
resource::spawn_repo_state_refresh_loop();
|
||||
resource::spawn_procedure_state_refresh_loop();
|
||||
|
||||
@@ -78,7 +78,10 @@ pub trait MonitorResource {
|
||||
+ From<Self::PartialConfig>
|
||||
+ PartialDiff<Self::PartialConfig, Self::ConfigDiff>
|
||||
+ 'static;
|
||||
type PartialConfig: From<Self::Config> + Serialize + MaybeNone;
|
||||
type PartialConfig: Default
|
||||
+ From<Self::Config>
|
||||
+ Serialize
|
||||
+ MaybeNone;
|
||||
type ConfigDiff: Into<Self::PartialConfig>
|
||||
+ Serialize
|
||||
+ Diff
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
use mungos::{init::MongoBuilder, mongodb::Collection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod build;
|
||||
pub mod deployment;
|
||||
pub mod resource;
|
||||
|
||||
pub struct DbClient {
|
||||
pub deployments: Collection<deployment::Deployment>,
|
||||
pub builds: Collection<build::Build>,
|
||||
}
|
||||
|
||||
impl DbClient {
|
||||
pub async fn new(
|
||||
legacy_uri: &str,
|
||||
legacy_db_name: &str,
|
||||
) -> DbClient {
|
||||
let client = MongoBuilder::default()
|
||||
.uri(legacy_uri)
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to init legacy mongo client");
|
||||
let db = client.database(legacy_db_name);
|
||||
DbClient {
|
||||
deployments: db.collection("Deployment"),
|
||||
builds: db.collection("Build"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
)]
|
||||
|
||||
@@ -11,8 +11,15 @@ use serde::Deserialize;
|
||||
mod legacy;
|
||||
mod migrate;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
enum AppMode {
|
||||
V0,
|
||||
V1_6,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Env {
|
||||
app_mode: AppMode,
|
||||
legacy_uri: String,
|
||||
legacy_db_name: String,
|
||||
target_uri: String,
|
||||
@@ -28,13 +35,26 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let env: Env = envy::from_env()?;
|
||||
|
||||
let legacy_db =
|
||||
legacy::v0::DbClient::new(&env.legacy_uri, &env.legacy_db_name)
|
||||
match env.app_mode {
|
||||
AppMode::V0 => {
|
||||
let legacy_db = legacy::v0::DbClient::new(
|
||||
&env.legacy_uri,
|
||||
&env.legacy_db_name,
|
||||
)
|
||||
.await;
|
||||
let target_db =
|
||||
DbClient::new(&env.target_uri, &env.target_db_name).await?;
|
||||
|
||||
migrate::migrate_all(&legacy_db, &target_db).await?;
|
||||
let target_db =
|
||||
DbClient::new(&env.target_uri, &env.target_db_name).await?;
|
||||
migrate::v0::migrate_all(&legacy_db, &target_db).await?
|
||||
}
|
||||
AppMode::V1_6 => {
|
||||
let db = legacy::v1_6::DbClient::new(
|
||||
&env.target_uri,
|
||||
&env.target_db_name,
|
||||
)
|
||||
.await;
|
||||
migrate::v1_6::migrate_all_in_place(&db).await?
|
||||
}
|
||||
}
|
||||
|
||||
info!("finished!");
|
||||
|
||||
|
||||
2
bin/migrator/src/migrate/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod v0;
|
||||
pub mod v1_6;
|
||||
74
bin/migrator/src/migrate/v1_6.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use anyhow::Context;
|
||||
use monitor_client::entities::{
|
||||
build::Build, deployment::Deployment,
|
||||
};
|
||||
use mungos::{
|
||||
find::find_collect,
|
||||
mongodb::bson::{doc, to_document},
|
||||
};
|
||||
|
||||
use crate::legacy::v1_6;
|
||||
|
||||
pub async fn migrate_all_in_place(
|
||||
db: &v1_6::DbClient,
|
||||
) -> anyhow::Result<()> {
|
||||
migrate_deployments_in_place(db).await?;
|
||||
migrate_builds_in_place(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn migrate_deployments_in_place(
|
||||
db: &v1_6::DbClient,
|
||||
) -> anyhow::Result<()> {
|
||||
let deployments = find_collect(&db.deployments, None, None)
|
||||
.await
|
||||
.context("failed to get deployments")?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<Deployment>>();
|
||||
|
||||
info!("migrating {} deployments...", deployments.len());
|
||||
|
||||
for deployment in deployments {
|
||||
db.deployments
|
||||
.update_one(
|
||||
doc! { "name": &deployment.name },
|
||||
doc! { "$set": to_document(&deployment)? },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to insert deployments on target")?;
|
||||
}
|
||||
|
||||
info!("deployments have been migrated\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn migrate_builds_in_place(
|
||||
db: &v1_6::DbClient,
|
||||
) -> anyhow::Result<()> {
|
||||
let builds = find_collect(&db.builds, None, None)
|
||||
.await
|
||||
.context("failed to get builds")?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<Build>>();
|
||||
|
||||
info!("migrating {} builds...", builds.len());
|
||||
|
||||
for build in builds {
|
||||
db.builds
|
||||
.update_one(
|
||||
doc! { "name": &build.name },
|
||||
doc! { "$set": to_document(&build)? },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to insert builds on target")?;
|
||||
}
|
||||
|
||||
info!("builds have been migrated\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -40,10 +40,7 @@ pub async fn docker_login(
|
||||
.await;
|
||||
Ok(true)
|
||||
}
|
||||
ImageRegistry::Ghcr(CloudRegistryConfig {
|
||||
account,
|
||||
..
|
||||
}) => {
|
||||
ImageRegistry::Ghcr(CloudRegistryConfig { account, .. }) => {
|
||||
if account.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Must configure account for GithubContainerRegistry"
|
||||
|
||||
@@ -29,7 +29,6 @@ async fn task(
|
||||
req_id: Uuid,
|
||||
request: crate::api::PeripheryRequest,
|
||||
) -> anyhow::Result<String> {
|
||||
info!("request {req_id} | {request:?}");
|
||||
let timer = Instant::now();
|
||||
|
||||
let res =
|
||||
@@ -48,7 +47,7 @@ async fn task(
|
||||
}
|
||||
|
||||
let elapsed = timer.elapsed();
|
||||
info!("request {req_id} | resolve time: {elapsed:?}");
|
||||
debug!("request {req_id} | resolve time: {elapsed:?}");
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
@@ -17,29 +17,29 @@ docker = ["dep:bollard"]
|
||||
[dependencies]
|
||||
# mogh
|
||||
mongo_indexed = { workspace = true, optional = true }
|
||||
serror.workspace = true
|
||||
resolver_api.workspace = true
|
||||
derive_default_builder.workspace = true
|
||||
derive_empty_traits.workspace = true
|
||||
async_timing_util.workspace = true
|
||||
partial_derive2.workspace = true
|
||||
derive_variants.workspace = true
|
||||
derive_default_builder.workspace = true
|
||||
async_timing_util.workspace = true
|
||||
resolver_api.workspace = true
|
||||
serror.workspace = true
|
||||
# external
|
||||
bollard = { workspace = true, optional = true }
|
||||
reqwest.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
envy.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
futures.workspace = true
|
||||
typeshare.workspace = true
|
||||
strum.workspace = true
|
||||
derive_builder.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio-util.workspace = true
|
||||
thiserror.workspace = true
|
||||
typeshare.workspace = true
|
||||
futures.workspace = true
|
||||
reqwest.workspace = true
|
||||
tracing.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
tokio.workspace = true
|
||||
strum.workspace = true
|
||||
envy.workspace = true
|
||||
uuid.workspace = true
|
||||
clap.workspace = true
|
||||
bson.workspace = true
|
||||
@@ -42,6 +42,13 @@ pub struct AlerterConfig {
|
||||
#[partial_default(default_enabled())]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Where to route the alert messages.
|
||||
///
|
||||
/// Default: Custom endpoint `http://localhost:7000`
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub endpoint: AlerterEndpoint,
|
||||
|
||||
/// Only send specific alert types.
|
||||
/// If empty, will send all alert types.
|
||||
#[serde(default)]
|
||||
@@ -53,13 +60,6 @@ pub struct AlerterConfig {
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub resources: Vec<ResourceTarget>,
|
||||
|
||||
/// Where to route the alert messages.
|
||||
///
|
||||
/// Default: Custom endpoint `http://localhost:7000`
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub endpoint: AlerterEndpoint,
|
||||
}
|
||||
|
||||
impl AlerterConfig {
|
||||
|
||||
@@ -71,11 +71,6 @@ pub struct BuildConfig {
|
||||
#[builder(default)]
|
||||
pub builder_id: String,
|
||||
|
||||
/// Whether to skip secret interpolation in the build_args.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
/// The current version of the build.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
@@ -108,6 +103,11 @@ pub struct BuildConfig {
|
||||
#[builder(default)]
|
||||
pub pre_build: SystemCommand,
|
||||
|
||||
/// Configuration for the registry to push the built image to.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub image_registry: ImageRegistry,
|
||||
|
||||
/// The path of the docker build context relative to the root of the repo.
|
||||
/// Default: "." (the root of the repo).
|
||||
#[serde(default = "default_build_path")]
|
||||
@@ -121,36 +121,50 @@ pub struct BuildConfig {
|
||||
#[partial_default(default_dockerfile_path())]
|
||||
pub dockerfile_path: String,
|
||||
|
||||
/// Docker build arguments
|
||||
/// Whether to skip secret interpolation in the build_args.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub build_args: Vec<EnvironmentVar>,
|
||||
|
||||
/// Docker labels
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub labels: Vec<EnvironmentVar>,
|
||||
|
||||
/// Any extra docker cli arguments to be included in the build command
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub extra_args: Vec<String>,
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
/// Whether to use buildx to build (eg `docker buildx build ...`)
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub use_buildx: bool,
|
||||
|
||||
/// Configuration for the registry to push the built image to.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub image_registry: ImageRegistry,
|
||||
|
||||
/// Whether incoming webhooks actually trigger action.
|
||||
#[serde(default = "default_webhook_enabled")]
|
||||
#[builder(default = "default_webhook_enabled()")]
|
||||
#[partial_default(default_webhook_enabled())]
|
||||
pub webhook_enabled: bool,
|
||||
|
||||
/// Any extra docker cli arguments to be included in the build command
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub extra_args: Vec<String>,
|
||||
|
||||
/// Docker build arguments
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::env_vars_deserializer"
|
||||
)]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "super::option_env_vars_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub build_args: Vec<EnvironmentVar>,
|
||||
|
||||
/// Docker labels
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::env_vars_deserializer"
|
||||
)]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "super::option_env_vars_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub labels: Vec<EnvironmentVar>,
|
||||
}
|
||||
|
||||
impl BuildConfig {
|
||||
|
||||
@@ -70,6 +70,12 @@ pub enum PartialBuilderConfig {
|
||||
Aws(_PartialAwsBuilderConfig),
|
||||
}
|
||||
|
||||
impl Default for PartialBuilderConfig {
|
||||
fn default() -> Self {
|
||||
Self::Aws(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeNone for PartialBuilderConfig {
|
||||
fn is_none(&self) -> bool {
|
||||
match self {
|
||||
@@ -290,9 +296,6 @@ pub struct AwsBuilderConfig {
|
||||
pub ami_id: String,
|
||||
/// The subnet id to create the instance in.
|
||||
pub subnet_id: String,
|
||||
/// The security group ids to attach to the instance.
|
||||
/// This should include a security group to allow core inbound access to the periphery port.
|
||||
pub security_group_ids: Vec<String>,
|
||||
/// The key pair name to attach to the instance
|
||||
pub key_pair_name: String,
|
||||
/// Whether to assign the instance a public IP address.
|
||||
@@ -301,6 +304,9 @@ pub struct AwsBuilderConfig {
|
||||
/// Whether core should use the public IP address to communicate with periphery on the builder.
|
||||
/// If false, core will communicate with the instance using the private IP.
|
||||
pub use_public_ip: bool,
|
||||
/// The security group ids to attach to the instance.
|
||||
/// This should include a security group to allow core inbound access to the periphery port.
|
||||
pub security_group_ids: Vec<String>,
|
||||
|
||||
/// Which github accounts (usernames) are available on the AMI
|
||||
#[serde(default)]
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use bson::{doc, Document};
|
||||
use derive_builder::Builder;
|
||||
use derive_default_builder::DefaultBuilder;
|
||||
use derive_variants::EnumVariants;
|
||||
use partial_derive2::Partial;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{
|
||||
de::{value::SeqAccessDeserializer, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use strum::{Display, EnumString};
|
||||
use typeshare::typeshare;
|
||||
|
||||
@@ -42,7 +46,7 @@ pub type _PartialDeploymentConfig = PartialDeploymentConfig;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]
|
||||
#[partial_derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct DeploymentConfig {
|
||||
/// The id of server the deployment is deployed on.
|
||||
@@ -51,12 +55,6 @@ pub struct DeploymentConfig {
|
||||
#[builder(default)]
|
||||
pub server_id: String,
|
||||
|
||||
/// Whether to send ContainerStateChange alerts for this deployment.
|
||||
#[serde(default = "default_send_alerts")]
|
||||
#[builder(default = "default_send_alerts()")]
|
||||
#[partial_default(default_send_alerts())]
|
||||
pub send_alerts: bool,
|
||||
|
||||
/// The image which the deployment deploys.
|
||||
/// Can either be a user inputted image, or a Monitor build.
|
||||
#[serde(default)]
|
||||
@@ -84,46 +82,11 @@ pub struct DeploymentConfig {
|
||||
#[builder(default)]
|
||||
pub redeploy_on_build: bool,
|
||||
|
||||
/// Labels attached to various termination signal options.
|
||||
/// Used to specify different shutdown functionality depending on the termination signal.
|
||||
#[serde(default = "default_term_signal_labels")]
|
||||
#[builder(default = "default_term_signal_labels()")]
|
||||
#[partial_default(default_term_signal_labels())]
|
||||
pub term_signal_labels: Vec<TerminationSignalLabel>,
|
||||
|
||||
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub termination_signal: TerminationSignal,
|
||||
|
||||
/// The termination timeout.
|
||||
#[serde(default = "default_termination_timeout")]
|
||||
#[builder(default = "default_termination_timeout()")]
|
||||
#[partial_default(default_termination_timeout())]
|
||||
pub termination_timeout: i32,
|
||||
|
||||
/// The container port mapping.
|
||||
/// Irrelevant if container network is `host`.
|
||||
/// Maps ports on host to ports on container.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub ports: Vec<Conversion>,
|
||||
|
||||
/// The container volume mapping.
|
||||
/// Maps files / folders on host to files / folders in container.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub volumes: Vec<Conversion>,
|
||||
|
||||
/// The environment variables passed to the container.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub environment: Vec<EnvironmentVar>,
|
||||
|
||||
/// The docker labels given to the container.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub labels: Vec<EnvironmentVar>,
|
||||
/// Whether to send ContainerStateChange alerts for this deployment.
|
||||
#[serde(default = "default_send_alerts")]
|
||||
#[builder(default = "default_send_alerts()")]
|
||||
#[partial_default(default_send_alerts())]
|
||||
pub send_alerts: bool,
|
||||
|
||||
/// The network attached to the container.
|
||||
/// Default is `host`.
|
||||
@@ -145,11 +108,81 @@ pub struct DeploymentConfig {
|
||||
#[builder(default)]
|
||||
pub command: String,
|
||||
|
||||
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub termination_signal: TerminationSignal,
|
||||
|
||||
/// The termination timeout.
|
||||
#[serde(default = "default_termination_timeout")]
|
||||
#[builder(default = "default_termination_timeout()")]
|
||||
#[partial_default(default_termination_timeout())]
|
||||
pub termination_timeout: i32,
|
||||
|
||||
/// Extra args which are interpolated into the `docker run` command,
|
||||
/// and affect the container configuration.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub extra_args: Vec<String>,
|
||||
|
||||
/// Labels attached to various termination signal options.
|
||||
/// Used to specify different shutdown functionality depending on the termination signal.
|
||||
#[serde(
|
||||
default = "default_term_signal_labels",
|
||||
deserialize_with = "term_labels_deserializer"
|
||||
)]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_term_labels_deserializer"
|
||||
))]
|
||||
#[builder(default = "default_term_signal_labels()")]
|
||||
#[partial_default(default_term_signal_labels())]
|
||||
pub term_signal_labels: Vec<TerminationSignalLabel>,
|
||||
|
||||
/// The container port mapping.
|
||||
/// Irrelevant if container network is `host`.
|
||||
/// Maps ports on host to ports on container.
|
||||
#[serde(default, deserialize_with = "conversions_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_conversions_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub ports: Vec<Conversion>,
|
||||
|
||||
/// The container volume mapping.
|
||||
/// Maps files / folders on host to files / folders in container.
|
||||
#[serde(default, deserialize_with = "conversions_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "option_conversions_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub volumes: Vec<Conversion>,
|
||||
|
||||
/// The environment variables passed to the container.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::env_vars_deserializer"
|
||||
)]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "super::option_env_vars_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub environment: Vec<EnvironmentVar>,
|
||||
|
||||
/// The docker labels given to the container.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::env_vars_deserializer"
|
||||
)]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
deserialize_with = "super::option_env_vars_deserializer"
|
||||
))]
|
||||
#[builder(default)]
|
||||
pub labels: Vec<EnvironmentVar>,
|
||||
}
|
||||
|
||||
impl DeploymentConfig {
|
||||
@@ -244,7 +277,7 @@ impl Default for DeploymentImage {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct Conversion {
|
||||
/// reference on the server.
|
||||
@@ -253,6 +286,161 @@ pub struct Conversion {
|
||||
pub container: String,
|
||||
}
|
||||
|
||||
pub fn conversions_to_string(conversions: &[Conversion]) -> String {
|
||||
conversions
|
||||
.iter()
|
||||
.map(|Conversion { local, container }| {
|
||||
format!("{local}={container}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub fn conversions_from_str(
|
||||
value: &str,
|
||||
) -> anyhow::Result<Vec<Conversion>> {
|
||||
let res = value
|
||||
.split('\n')
|
||||
.map(|line| line.trim())
|
||||
.enumerate()
|
||||
.filter(|(_, line)| !line.starts_with('#'))
|
||||
.map(|(i, line)| {
|
||||
let mut split = line.split('=');
|
||||
let local = split
|
||||
.next()
|
||||
.with_context(|| {
|
||||
format!("line {i} does not have 'local' key")
|
||||
})?
|
||||
.trim()
|
||||
.to_string();
|
||||
// remove trailing comments
|
||||
let mut container_split = split
|
||||
.next()
|
||||
.with_context(|| {
|
||||
format!("line {i} does not have 'container' key")
|
||||
})?
|
||||
.split('#');
|
||||
let container = container_split
|
||||
.next()
|
||||
.with_context(|| {
|
||||
format!("line {i} does not have 'container' key")
|
||||
})?
|
||||
.trim()
|
||||
.to_string();
|
||||
anyhow::Ok(Conversion { local, container })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn conversions_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<Conversion>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(ConversionVisitor)
|
||||
}
|
||||
|
||||
pub fn option_conversions_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<Conversion>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(OptionConversionVisitor)
|
||||
}
|
||||
|
||||
struct ConversionVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ConversionVisitor {
|
||||
type Value = Vec<Conversion>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "string or Vec<Conversion>")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
conversions_from_str(v)
|
||||
.map_err(|e| serde::de::Error::custom(format!("{e:#}")))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct ConversionInner {
|
||||
local: String,
|
||||
container: String,
|
||||
}
|
||||
|
||||
impl From<ConversionInner> for Conversion {
|
||||
fn from(value: ConversionInner) -> Self {
|
||||
Self {
|
||||
local: value.local,
|
||||
container: value.container,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res = Vec::<ConversionInner>::deserialize(
|
||||
SeqAccessDeserializer::new(seq),
|
||||
)?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionConversionVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for OptionConversionVisitor {
|
||||
type Value = Option<Vec<Conversion>>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "null or string or Vec<Conversion>")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
ConversionVisitor.visit_str(v).map(Some)
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
ConversionVisitor.visit_seq(seq).map(Some)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// A summary of a docker container on a server.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -401,6 +589,160 @@ pub struct TerminationSignalLabel {
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
pub fn term_signal_labels_to_string(
|
||||
labels: &[TerminationSignalLabel],
|
||||
) -> String {
|
||||
labels
|
||||
.iter()
|
||||
.map(|TerminationSignalLabel { signal, label }| {
|
||||
format!("{signal}={label}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub fn term_signal_labels_from_str(
|
||||
value: &str,
|
||||
) -> anyhow::Result<Vec<TerminationSignalLabel>> {
|
||||
let res = value
|
||||
.split('\n')
|
||||
.map(|line| line.trim())
|
||||
.enumerate()
|
||||
.filter(|(_, line)| !line.starts_with('#'))
|
||||
.map(|(i, line)| {
|
||||
let mut split = line.split('=');
|
||||
let signal = split
|
||||
.next()
|
||||
.with_context(|| format!("line {i} does not have signal"))?
|
||||
.trim()
|
||||
.parse::<TerminationSignal>()
|
||||
.with_context(|| {
|
||||
format!("line {i} does not have valid signal")
|
||||
})?;
|
||||
// remove trailing comments
|
||||
let mut label_split = split
|
||||
.next()
|
||||
.with_context(|| format!("line {i} does not have label"))?
|
||||
.split('#');
|
||||
let label = label_split
|
||||
.next()
|
||||
.with_context(|| format!("line {i} does not have label"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
anyhow::Ok(TerminationSignalLabel { signal, label })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn term_labels_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<TerminationSignalLabel>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(TermSignalLabelVisitor)
|
||||
}
|
||||
|
||||
pub fn option_term_labels_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<TerminationSignalLabel>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(OptionTermSignalLabelVisitor)
|
||||
}
|
||||
|
||||
struct TermSignalLabelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for TermSignalLabelVisitor {
|
||||
type Value = Vec<TerminationSignalLabel>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "string or Vec<TerminationSignalLabel>")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
term_signal_labels_from_str(v)
|
||||
.map_err(|e| serde::de::Error::custom(format!("{e:#}")))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct TermSignalLabelInner {
|
||||
signal: TerminationSignal,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl From<TermSignalLabelInner> for TerminationSignalLabel {
|
||||
fn from(value: TermSignalLabelInner) -> Self {
|
||||
Self {
|
||||
signal: value.signal,
|
||||
label: value.label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res = Vec::<TermSignalLabelInner>::deserialize(
|
||||
SeqAccessDeserializer::new(seq),
|
||||
)?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionTermSignalLabelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for OptionTermSignalLabelVisitor {
|
||||
type Value = Option<Vec<TerminationSignalLabel>>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "null or string or Vec<TerminationSignalLabel>")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
TermSignalLabelVisitor.visit_str(v).map(Some)
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
TermSignalLabelVisitor.visit_seq(seq).map(Some)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct DeploymentActionState {
|
||||
|
||||
@@ -6,8 +6,11 @@ use build::CloudRegistryConfig;
|
||||
use clap::Parser;
|
||||
use derive_empty_traits::EmptyTraits;
|
||||
use serde::{
|
||||
de::{value::MapAccessDeserializer, Visitor},
|
||||
Deserialize, Serialize,
|
||||
de::{
|
||||
value::{MapAccessDeserializer, SeqAccessDeserializer},
|
||||
Visitor,
|
||||
},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use serror::Serror;
|
||||
use strum::{AsRefStr, Display, EnumString};
|
||||
@@ -324,13 +327,162 @@ impl Version {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct EnvironmentVar {
|
||||
pub variable: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub fn environment_vars_to_string(vars: &[EnvironmentVar]) -> String {
|
||||
vars
|
||||
.iter()
|
||||
.map(|EnvironmentVar { variable, value }| {
|
||||
format!("{variable}={value}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub fn environment_vars_from_str(
|
||||
value: &str,
|
||||
) -> anyhow::Result<Vec<EnvironmentVar>> {
|
||||
let res = value
|
||||
.split('\n')
|
||||
.map(|line| line.trim())
|
||||
.enumerate()
|
||||
.filter(|(_, line)| !line.starts_with('#'))
|
||||
.map(|(i, line)| {
|
||||
let mut split = line.split('=');
|
||||
let variable = split
|
||||
.next()
|
||||
.with_context(|| format!("line {i} does not have variable"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
// remove trailing comments
|
||||
let mut value_split = split
|
||||
.next()
|
||||
.with_context(|| format!("line {i} does not have value"))?
|
||||
.split('#');
|
||||
let value = value_split
|
||||
.next()
|
||||
.with_context(|| format!("line {i} does not have value"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
anyhow::Ok(EnvironmentVar { variable, value })
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn env_vars_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<EnvironmentVar>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(EnvironmentVarVisitor)
|
||||
}
|
||||
|
||||
pub fn option_env_vars_deserializer<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<Vec<EnvironmentVar>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(OptionEnvVarVisitor)
|
||||
}
|
||||
|
||||
struct EnvironmentVarVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for EnvironmentVarVisitor {
|
||||
type Value = Vec<EnvironmentVar>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "string or Vec<EnvironmentVar>")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
environment_vars_from_str(v)
|
||||
.map_err(|e| serde::de::Error::custom(format!("{e:#}")))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct EnvironmentVarInner {
|
||||
variable: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl From<EnvironmentVarInner> for EnvironmentVar {
|
||||
fn from(value: EnvironmentVarInner) -> Self {
|
||||
Self {
|
||||
variable: value.variable,
|
||||
value: value.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res = Vec::<EnvironmentVarInner>::deserialize(
|
||||
SeqAccessDeserializer::new(seq),
|
||||
)?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionEnvVarVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for OptionEnvVarVisitor {
|
||||
type Value = Option<Vec<EnvironmentVar>>;
|
||||
|
||||
fn expecting(
|
||||
&self,
|
||||
formatter: &mut std::fmt::Formatter,
|
||||
) -> std::fmt::Result {
|
||||
write!(formatter, "null or string or Vec<EnvironmentVar>")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
EnvironmentVarVisitor.visit_str(v).map(Some)
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
EnvironmentVarVisitor.visit_seq(seq).map(Some)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LatestCommit {
|
||||
pub hash: String,
|
||||
|
||||
@@ -56,7 +56,8 @@ pub type _PartialProcedureConfig = PartialProcedureConfig;
|
||||
#[partial(skip_serializing_none, from, diff)]
|
||||
pub struct ProcedureConfig {
|
||||
/// The stages to be run by the procedure.
|
||||
#[serde(default)]
|
||||
#[serde(default, alias = "stage")]
|
||||
#[partial_attr(serde(alias = "stage"))]
|
||||
#[builder(default)]
|
||||
pub stages: Vec<ProcedureStage>,
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use super::update::ResourceTargetVariant;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct Resource<Config, Info: Default = ()> {
|
||||
pub struct Resource<Config: Default, Info: Default = ()> {
|
||||
/// The Mongo ID of the resource.
|
||||
/// This field is de/serialized from/to JSON as
|
||||
/// `{ "_id": { "$oid": "..." }, ...(rest of serialized Resource<T>) }`
|
||||
@@ -50,6 +50,8 @@ pub struct Resource<Config, Info: Default = ()> {
|
||||
pub info: Info,
|
||||
|
||||
/// Resource-specific configuration.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@ pub struct ServerConfig {
|
||||
/// Example: http://localhost:8120
|
||||
pub address: String,
|
||||
|
||||
/// An optional region label
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub region: String,
|
||||
|
||||
/// Whether a server is enabled.
|
||||
/// If a server is disabled,
|
||||
/// you won't be able to perform any actions on it or see deployment's status.
|
||||
@@ -94,11 +99,6 @@ pub struct ServerConfig {
|
||||
#[partial_default(default_send_alerts())]
|
||||
pub send_disk_alerts: bool,
|
||||
|
||||
/// An optional region label
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
pub region: String,
|
||||
|
||||
/// The percentage threshhold which triggers WARNING state for CPU.
|
||||
#[serde(default = "default_cpu_warning")]
|
||||
#[builder(default = "default_cpu_warning()")]
|
||||
|
||||
@@ -78,6 +78,12 @@ pub enum PartialServerTemplateConfig {
|
||||
Hetzner(hetzner::_PartialHetznerServerTemplateConfig),
|
||||
}
|
||||
|
||||
impl Default for PartialServerTemplateConfig {
|
||||
fn default() -> Self {
|
||||
Self::Aws(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeNone for PartialServerTemplateConfig {
|
||||
fn is_none(&self) -> bool {
|
||||
match self {
|
||||
|
||||
@@ -48,13 +48,6 @@ pub struct ResourcesToml {
|
||||
)]
|
||||
pub procedures: Vec<ResourceToml<PartialProcedureConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "builder",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub builders: Vec<ResourceToml<PartialBuilderConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "alerter",
|
||||
@@ -62,6 +55,13 @@ pub struct ResourcesToml {
|
||||
)]
|
||||
pub alerters: Vec<ResourceToml<PartialAlerterConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "builder",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
pub builders: Vec<ResourceToml<PartialBuilderConfig>>,
|
||||
|
||||
#[serde(
|
||||
default,
|
||||
rename = "server_template",
|
||||
@@ -93,7 +93,7 @@ pub struct ResourcesToml {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourceToml<PartialConfig> {
|
||||
pub struct ResourceToml<PartialConfig: Default> {
|
||||
/// The resource name. Required
|
||||
pub name: String,
|
||||
|
||||
@@ -106,6 +106,7 @@ pub struct ResourceToml<PartialConfig> {
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Resource specific configuration
|
||||
#[serde(default)]
|
||||
pub config: PartialConfig,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,20 @@ If the build directory is the root of the repository, you pass the build path as
|
||||
|
||||
The dockerfile's path is given relative to the build directory. So if your build directory is `build/directory` and the dockerfile is in `build/directory/Dockerfile.example`, you give the dockerfile path simply as `Dockerfile.example`.
|
||||
|
||||
Just as with private repos, you will need to select a docker account to use with `docker push`.
|
||||
### Image registry
|
||||
|
||||
Monitor supports pushing to DockerHub and Github Container Registry (ghcr.io).
|
||||
Any of the Docker / Github accounts that are specified in config, between the core config and builder, will be available to use.
|
||||
Additionally, allowed organizations for both DockerHub and Github can be specified on the core config and attached to builds.
|
||||
Doing so will cause the images to be published under the organization's namespace rather than the account's.
|
||||
|
||||
When connecting a build to a deployments, the default behavior is for the deployment to inherit the registry configuration from the build.
|
||||
In cases where that account isn't available to the deployment, another account can be chosen in the deployment config.
|
||||
|
||||
:::note
|
||||
In order to publish to the Github Container Registry, your Github access token must be given the `write:packages` permission.
|
||||
See the Github docs [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic).
|
||||
:::
|
||||
|
||||
### Adding build args
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ See [config docs](https://docs.rs/monitor_client/latest/monitor_client/entities/
|
||||
|
||||
## 2. Start monitor core
|
||||
|
||||
Monitor core is distributed via dockerhub under the public repo [mbecker2020/monitor_core](https://hub.docker.com/r/mbecker2020/monitor_core).
|
||||
Monitor core is distributed via Github Container Registry under the package [mbecker20/monitor_core](https://github.com/mbecker20/monitor/pkgs/container/monitor_core).
|
||||
|
||||
```sh
|
||||
docker run -d --name monitor-core \
|
||||
-v $HOME/.monitor/core.config.toml:/config/config.toml \
|
||||
-p 9000:9000 \
|
||||
mbecker2020/monitor_core
|
||||
ghcr.io/mbecker20/monitor_core
|
||||
```
|
||||
|
||||
## First login
|
||||
|
||||
@@ -12,7 +12,7 @@ By default, Monitor will deploy the latest available version of the build, or yo
|
||||
Also by default, Monitor will use the same docker account that is attached to the build in order to pull the image on the periphery server. If that account is not available on the server, you can specify another available account to use instead, this account just needs to have read access to the docker repository.
|
||||
|
||||
### Using a custom image
|
||||
You can also manually specify an image name, like `mongo` or `mbecker2020/random_image:0.1.1`.
|
||||
You can also manually specify an image name, like `mongo` or `ghcr.io/mbecker20/random_image:0.1.1`.
|
||||
|
||||
If the image repository is private, you can still select an available docker account to use to pull the image.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 869 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 33 KiB |
BIN
frontend/public/monitor-circle.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -184,15 +184,7 @@ export const DoubleInput = <
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSelector = ({
|
||||
disabled,
|
||||
id,
|
||||
type,
|
||||
account_type,
|
||||
selected,
|
||||
onSelect,
|
||||
placeholder,
|
||||
}: {
|
||||
export const AccountSelectorConfig = (params: {
|
||||
disabled: boolean;
|
||||
id?: string;
|
||||
type: "Server" | "None" | "Builder";
|
||||
@@ -200,6 +192,28 @@ export const AccountSelector = ({
|
||||
selected: string | undefined;
|
||||
onSelect: (id: string) => void;
|
||||
placeholder: string;
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem label={`${params.account_type} Account`}>
|
||||
<AccountSelector {...params} />
|
||||
</ConfigItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSelector = ({
|
||||
disabled,
|
||||
id,
|
||||
type,
|
||||
account_type,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
id?: string;
|
||||
type: "Server" | "None" | "Builder";
|
||||
account_type: keyof Types.GetBuilderAvailableAccountsResponse;
|
||||
selected: string | undefined;
|
||||
onSelect: (id: string) => void;
|
||||
}) => {
|
||||
const [request, params] =
|
||||
type === "Server" || type === "None"
|
||||
@@ -207,30 +221,28 @@ export const AccountSelector = ({
|
||||
: ["GetBuilderAvailableAccounts", { builder: id }];
|
||||
const accounts = useRead(request as any, params).data;
|
||||
return (
|
||||
<ConfigItem label={`${account_type} Account`}>
|
||||
<Select
|
||||
value={type === "Builder" ? selected || undefined : selected}
|
||||
onValueChange={(value) => {
|
||||
onSelect(value === "Empty" ? "" : value);
|
||||
}}
|
||||
<Select
|
||||
value={selected}
|
||||
onValueChange={(value) => {
|
||||
onSelect(value === "Empty" ? "" : value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full lg:w-[200px] max-w-[50%]"
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full lg:w-[200px] max-w-[50%]"
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"Empty"}>{placeholder}</SelectItem>
|
||||
{(accounts as any)?.[account_type]?.map((account: string) => (
|
||||
<SelectItem key={account} value={account}>
|
||||
{account}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ConfigItem>
|
||||
<SelectValue placeholder="Select Account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"Empty"}>None</SelectItem>
|
||||
{(accounts as any)?.[account_type]?.map((account: string) => (
|
||||
<SelectItem key={account} value={account}>
|
||||
{account}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -440,19 +452,73 @@ export const ImageRegistryConfig = ({
|
||||
registry,
|
||||
setRegistry,
|
||||
disabled,
|
||||
type,
|
||||
resource_id,
|
||||
registry_types,
|
||||
}: {
|
||||
registry: Types.ImageRegistry | undefined;
|
||||
setRegistry: (registry: Types.ImageRegistry) => void;
|
||||
disabled: boolean;
|
||||
type: "Build" | "Deployment";
|
||||
// For builds, its builder id. For servers, its server id.
|
||||
resource_id?: string;
|
||||
registry_types?: Types.ImageRegistry["type"][];
|
||||
}) => {
|
||||
const _registry = registry ?? default_registry_config("None");
|
||||
const cloud_params =
|
||||
_registry.type === "DockerHub" || _registry.type === "Ghcr"
|
||||
? _registry.params
|
||||
: undefined;
|
||||
if (_registry.type === "None" || _registry.type === "Custom") {
|
||||
return (
|
||||
<ConfigItem label="Image Registry">
|
||||
<RegistryTypeSelector
|
||||
registry={_registry}
|
||||
setRegistry={setRegistry}
|
||||
disabled={disabled}
|
||||
deployment={type === "Deployment"}
|
||||
registry_types={registry_types}
|
||||
/>
|
||||
</ConfigItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ConfigItem label="Image Registry">
|
||||
<RegistryTypeSelector
|
||||
registry={_registry}
|
||||
setRegistry={setRegistry}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="flex items-center justify-stretch gap-4">
|
||||
{type === "Build" && cloud_params?.account && (
|
||||
<OrganizationSelector
|
||||
value={cloud_params?.organization}
|
||||
set={(organization) =>
|
||||
setRegistry({
|
||||
..._registry,
|
||||
params: { ..._registry.params, organization },
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
type={_registry.type === "DockerHub" ? "Docker" : "Github"}
|
||||
/>
|
||||
)}
|
||||
<AccountSelector
|
||||
id={resource_id}
|
||||
type={type === "Build" ? "Builder" : "Server"}
|
||||
account_type={_registry.type === "DockerHub" ? "docker" : "github"}
|
||||
selected={cloud_params?.account}
|
||||
onSelect={(account) =>
|
||||
setRegistry({
|
||||
..._registry,
|
||||
params: { ..._registry.params, account },
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<RegistryTypeSelector
|
||||
registry={_registry}
|
||||
setRegistry={setRegistry}
|
||||
disabled={disabled}
|
||||
deployment={type === "Deployment"}
|
||||
registry_types={registry_types}
|
||||
/>
|
||||
</div>
|
||||
</ConfigItem>
|
||||
);
|
||||
};
|
||||
@@ -466,17 +532,23 @@ const REGISTRY_TYPES: Types.ImageRegistry["type"][] = [
|
||||
const RegistryTypeSelector = ({
|
||||
registry,
|
||||
setRegistry,
|
||||
registry_types = REGISTRY_TYPES,
|
||||
disabled,
|
||||
deployment,
|
||||
}: {
|
||||
registry: Types.ImageRegistry;
|
||||
setRegistry: (registry: Types.ImageRegistry) => void;
|
||||
registry_types?: Types.ImageRegistry["type"][];
|
||||
disabled: boolean;
|
||||
deployment?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Select
|
||||
value={registry.type}
|
||||
onValueChange={(type: Types.ImageRegistry["type"]) => {
|
||||
setRegistry(default_registry_config(type));
|
||||
value={to_readable_registry_type(registry.type, deployment)}
|
||||
onValueChange={(type) => {
|
||||
setRegistry(
|
||||
default_registry_config(from_readable_registry_type(type, deployment))
|
||||
);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -487,9 +559,49 @@ const RegistryTypeSelector = ({
|
||||
<SelectValue placeholder="Select Registry" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{REGISTRY_TYPES.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
{registry_types.map((type) => {
|
||||
const t = to_readable_registry_type(type, deployment);
|
||||
return (
|
||||
<SelectItem key={type} value={t}>
|
||||
{t}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
const OrganizationSelector = ({
|
||||
value,
|
||||
set,
|
||||
disabled,
|
||||
type,
|
||||
}: {
|
||||
value?: string;
|
||||
set: (org: string) => void;
|
||||
disabled: boolean;
|
||||
type: "Docker" | "Github";
|
||||
}) => {
|
||||
const organizations = useRead(`List${type}Organizations`, {}).data;
|
||||
if (!organizations || organizations.length === 0) return null;
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(v) => set(v === "Empty" ? "" : v)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full lg:w-[200px] max-w-[50%]"
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectValue placeholder="Select Organization" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"Empty"}>None</SelectItem>
|
||||
{organizations?.map((org) => (
|
||||
<SelectItem key={org} value={org}>
|
||||
{org}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -497,6 +609,22 @@ const RegistryTypeSelector = ({
|
||||
);
|
||||
};
|
||||
|
||||
const to_readable_registry_type = (
|
||||
type: Types.ImageRegistry["type"],
|
||||
deployment?: boolean
|
||||
) => {
|
||||
if (deployment && type === "None") return "Same as build";
|
||||
return type;
|
||||
};
|
||||
|
||||
const from_readable_registry_type = (
|
||||
readable: string,
|
||||
deployment?: boolean
|
||||
) => {
|
||||
if (deployment && readable === "Same as build") return "None";
|
||||
return readable as Types.ImageRegistry["type"];
|
||||
};
|
||||
|
||||
const default_registry_config = (
|
||||
type: Types.ImageRegistry["type"]
|
||||
): Types.ImageRegistry => {
|
||||
|
||||
@@ -16,10 +16,12 @@ export const ExportButton = ({
|
||||
targets,
|
||||
user_groups,
|
||||
tags,
|
||||
include_variables,
|
||||
}: {
|
||||
targets?: Types.ResourceTarget[];
|
||||
user_groups?: string[];
|
||||
tags?: string[];
|
||||
include_variables?: boolean;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
@@ -34,8 +36,12 @@ export const ExportButton = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export to Toml</DialogTitle>
|
||||
</DialogHeader>
|
||||
{targets || user_groups ? (
|
||||
<ExportTargetsLoader targets={targets} user_groups={user_groups} />
|
||||
{targets || user_groups || include_variables ? (
|
||||
<ExportTargetsLoader
|
||||
targets={targets}
|
||||
user_groups={user_groups}
|
||||
include_variables={include_variables}
|
||||
/>
|
||||
) : (
|
||||
<ExportAllLoader tags={tags} />
|
||||
)}
|
||||
@@ -45,21 +51,33 @@ export const ExportButton = ({
|
||||
};
|
||||
|
||||
const ExportTargetsLoader = ({
|
||||
user_groups,
|
||||
targets,
|
||||
user_groups,
|
||||
include_variables,
|
||||
}: {
|
||||
user_groups?: string[];
|
||||
targets?: Types.ResourceTarget[];
|
||||
user_groups?: string[];
|
||||
include_variables?: boolean;
|
||||
}) => {
|
||||
const { data, isPending } = useRead("ExportResourcesToToml", {
|
||||
targets: targets ? targets : [],
|
||||
user_groups: user_groups ? user_groups : [],
|
||||
include_variables,
|
||||
});
|
||||
return <ExportPre loading={isPending} content={data?.toml} />;
|
||||
};
|
||||
|
||||
const ExportAllLoader = ({ tags }: { tags?: string[] }) => {
|
||||
const { data, isPending } = useRead("ExportAllResourcesToToml", { tags });
|
||||
const ExportAllLoader = ({
|
||||
tags,
|
||||
include_variables,
|
||||
}: {
|
||||
tags?: string[];
|
||||
include_variables?: boolean;
|
||||
}) => {
|
||||
const { data, isPending } = useRead("ExportAllResourcesToToml", {
|
||||
tags,
|
||||
include_variables,
|
||||
});
|
||||
return <ExportPre loading={isPending} content={data?.toml} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ const ALERT_TYPES: Types.AlertData["type"][] = [
|
||||
"ServerMem",
|
||||
"ServerDisk",
|
||||
"ContainerStateChange",
|
||||
"ResourceSyncPendingUpdates",
|
||||
"AwsBuilderTerminationFailed",
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from "@components/config";
|
||||
import {
|
||||
AccountSelector,
|
||||
AccountSelectorConfig,
|
||||
AddExtraArgMenu,
|
||||
ConfigItem,
|
||||
ImageRegistryConfig,
|
||||
@@ -115,7 +115,7 @@ export const BuildConfig = ({
|
||||
github_account:
|
||||
(update.builder_id ?? config.builder_id ? true : false) &&
|
||||
((account, set) => (
|
||||
<AccountSelector
|
||||
<AccountSelectorConfig
|
||||
id={update.builder_id ?? config.builder_id ?? undefined}
|
||||
type="Builder"
|
||||
account_type="github"
|
||||
@@ -130,13 +130,19 @@ export const BuildConfig = ({
|
||||
{
|
||||
label: "Docker",
|
||||
components: {
|
||||
image_registry: (registry, set) => (
|
||||
<ImageRegistryConfig
|
||||
registry={registry}
|
||||
setRegistry={(image_registry) => set({ image_registry })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
image_registry: (registry, set) => {
|
||||
const builder_id = update.builder_id ?? config.builder_id;
|
||||
if (!builder_id) return null;
|
||||
return (
|
||||
<ImageRegistryConfig
|
||||
registry={registry}
|
||||
setRegistry={(image_registry) => set({ image_registry })}
|
||||
type="Build"
|
||||
resource_id={builder_id}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
},
|
||||
build_path: true,
|
||||
dockerfile_path: true,
|
||||
// docker_account: (account, set) =>
|
||||
@@ -437,40 +443,3 @@ const Secrets = ({
|
||||
);
|
||||
};
|
||||
|
||||
// const DockerOrganizations = ({
|
||||
// value,
|
||||
// set,
|
||||
// disabled,
|
||||
// }: {
|
||||
// value?: string;
|
||||
// set: (input: Partial<Types.BuildConfig>) => void;
|
||||
// disabled: boolean;
|
||||
// }) => {
|
||||
// const docker_organizations = useRead("ListDockerOrganizations", {}).data;
|
||||
// return (
|
||||
// <ConfigItem label="Docker Organization">
|
||||
// <Select
|
||||
// value={value}
|
||||
// onValueChange={(value) =>
|
||||
// set({ docker_organization: value === "Empty" ? "" : value })
|
||||
// }
|
||||
// disabled={disabled}
|
||||
// >
|
||||
// <SelectTrigger
|
||||
// className="w-full lg:w-[300px] max-w-[50%]"
|
||||
// disabled={disabled}
|
||||
// >
|
||||
// <SelectValue placeholder="Select Organization" />
|
||||
// </SelectTrigger>
|
||||
// <SelectContent>
|
||||
// <SelectItem value={"Empty"}>None</SelectItem>
|
||||
// {docker_organizations?.map((org) => (
|
||||
// <SelectItem key={org} value={org}>
|
||||
// {org}
|
||||
// </SelectItem>
|
||||
// ))}
|
||||
// </SelectContent>
|
||||
// </Select>
|
||||
// </ConfigItem>
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ReactNode, useState } from "react";
|
||||
import {
|
||||
AddExtraArgMenu,
|
||||
ConfigItem,
|
||||
ImageRegistryConfig,
|
||||
InputList,
|
||||
} from "@components/config/util";
|
||||
import { ImageConfig } from "./components/image";
|
||||
@@ -81,21 +82,29 @@ export const DeploymentConfig = ({
|
||||
image: (value, set) => (
|
||||
<ImageConfig image={value} set={set} disabled={disabled} />
|
||||
),
|
||||
// docker_account: (value, set) => (
|
||||
// <AccountSelector
|
||||
// id={update.server_id ?? config.server_id}
|
||||
// account_type="docker"
|
||||
// type="Server"
|
||||
// selected={value}
|
||||
// onSelect={(docker_account) => set({ docker_account })}
|
||||
// disabled={disabled}
|
||||
// placeholder={
|
||||
// (update.image?.type || config.image?.type) === "Build"
|
||||
// ? "Same as build"
|
||||
// : "None"
|
||||
// }
|
||||
// />
|
||||
// ),
|
||||
image_registry: (registry, set) => {
|
||||
const image_type = update.image?.type ?? config.image?.type;
|
||||
const build_id: string | undefined =
|
||||
(image_type === "Build" &&
|
||||
(update.image?.params as any)?.build_id) ??
|
||||
(config.image?.params as any)?.build_id;
|
||||
const build_registry_type = useRead("GetBuild", {
|
||||
build: build_id!,
|
||||
}).data?.config.image_registry?.type;
|
||||
const server_id = update.server_id ?? config.server_id;
|
||||
return (
|
||||
<ImageRegistryConfig
|
||||
registry={registry}
|
||||
setRegistry={(image_registry) => set({ image_registry })}
|
||||
type="Deployment"
|
||||
resource_id={server_id}
|
||||
disabled={disabled}
|
||||
registry_types={
|
||||
build_registry_type && ["None", build_registry_type]
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
restart: (value, set) => (
|
||||
<RestartModeSelector
|
||||
selected={value}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from "@components/config";
|
||||
import {
|
||||
AccountSelector,
|
||||
AccountSelectorConfig,
|
||||
ConfigItem,
|
||||
SystemCommand,
|
||||
} from "@components/config/util";
|
||||
@@ -55,7 +55,7 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
github_account: (value, set) => {
|
||||
const server_id = update.server_id || config.server_id;
|
||||
return (
|
||||
<AccountSelector
|
||||
<AccountSelectorConfig
|
||||
id={server_id}
|
||||
account_type="github"
|
||||
type={server_id ? "Server" : "None"}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Config } from "@components/config";
|
||||
import { AccountSelector, ConfigItem } from "@components/config/util";
|
||||
import { AccountSelectorConfig, ConfigItem } from "@components/config/util";
|
||||
import { useRead, useWrite } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { ReactNode, useState } from "react";
|
||||
@@ -44,7 +44,7 @@ export const ResourceSyncConfig = ({
|
||||
commit: { placeholder: "Enter specific commit hash. Optional." },
|
||||
github_account: (value, set) => {
|
||||
return (
|
||||
<AccountSelector
|
||||
<AccountSelectorConfig
|
||||
account_type="github"
|
||||
type="None"
|
||||
selected={value}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const TagsFilter = () => {
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="flex justify-evenly items-center">
|
||||
<CommandEmpty className="flex justify-evenly items-center pt-2">
|
||||
No Tags Found
|
||||
<SearchX className="w-3 h-3" />
|
||||
</CommandEmpty>
|
||||
|
||||
@@ -72,11 +72,16 @@ export const Topbar = () => {
|
||||
<div className="flex items-center gap-4 justify-self-start w-fit">
|
||||
<Link
|
||||
to={"/"}
|
||||
className="flex gap-3 items-start text-2xl tracking-widest lg:mx-2"
|
||||
// className="flex gap-3 items-start text-2xl tracking-widest lg:mx-2"
|
||||
className="flex gap-3 items-center text-2xl tracking-widest lg:mx-2"
|
||||
>
|
||||
<img
|
||||
{/* <img
|
||||
src="/monitor-lizard.png"
|
||||
className="w-9 h-7 dark:invert hidden lg:block"
|
||||
/> */}
|
||||
<img
|
||||
src="/monitor-circle.png"
|
||||
className="w-[28px] dark:invert hidden lg:block"
|
||||
/>
|
||||
MONITOR
|
||||
</Link>
|
||||
|
||||
@@ -53,7 +53,8 @@ export const UpdateUser = ({
|
||||
if (
|
||||
user_id === "Procedure" ||
|
||||
user_id === "Github" ||
|
||||
user_id === "Auto Redeploy"
|
||||
user_id === "Auto Redeploy" ||
|
||||
user_id === "Resource Sync"
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExportButton } from "@components/export";
|
||||
import { Section } from "@components/layouts";
|
||||
import { NewServiceUser, NewUserGroup } from "@components/users/new";
|
||||
import { UserTable } from "@components/users/table";
|
||||
@@ -18,7 +19,18 @@ export const UsersPage = ({ search }: { search: string }) => {
|
||||
<Section
|
||||
title="User Groups"
|
||||
icon={<Users className="w-4 h-4" />}
|
||||
actions={<NewUserGroup />}
|
||||
actions={
|
||||
<div className="flex items-center gap-4">
|
||||
{groups && groups.length ? (
|
||||
<ExportButton
|
||||
user_groups={groups
|
||||
?.map((group) => group._id?.$oid!)
|
||||
.filter((id) => id)}
|
||||
/>
|
||||
) : undefined}
|
||||
<NewUserGroup />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
tableKey="user-groups"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExportButton } from "@components/export";
|
||||
import { ConfirmButton, TextUpdateMenu } from "@components/util";
|
||||
import {
|
||||
useInvalidate,
|
||||
@@ -61,12 +62,15 @@ export const Variables = () => {
|
||||
});
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
placeholder="search..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
placeholder="search..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
<ExportButton include_variables />
|
||||
</div>
|
||||
|
||||
{updateMenuData && (
|
||||
<TextUpdateMenu
|
||||
|
||||