Compare commits

..

32 Commits

Author SHA1 Message Date
mbecker20
8b8c89d976 1.7.3 procedure stage alias 2024-06-11 00:51:16 -07:00
mbecker20
25c8d25636 1.7.2 default resource config parsing 2024-06-11 00:44:41 -07:00
mbecker20
ea242de2e4 default the config if not exists 2024-06-11 00:34:11 -07:00
mbecker20
be03547407 reorder struct fields for improved toml 2024-06-11 00:04:20 -07:00
mbecker20
9c0d28b311 allow inline arrow up to max length 2024-06-10 23:53:23 -07:00
mbecker20
f269deb99c update toml_pretty 2024-06-10 23:30:17 -07:00
mbecker20
3df8163131 improve procedure toml 2024-06-10 23:14:04 -07:00
mbecker20
33a16a9bd2 need 2 \n 2024-06-10 22:36:17 -07:00
mbecker20
215e7d1bdc update toml_pretty 2024-06-10 22:11:40 -07:00
mbecker20
25e0905c0c fix deserializers 2024-06-10 21:31:17 -07:00
mbecker20
1c07ccea85 bump toml for multiline string 2024-06-10 19:26:01 -07:00
mbecker20
405ec1b8cc bump toml_pretty for fix 2024-06-10 18:58:33 -07:00
mbecker20
4f212bd06f update toml_pretty with skip empty strings 2024-06-10 18:43:53 -07:00
mbecker20
074f4ea2db fix toml 2024-06-10 18:07:05 -07:00
mbecker20
c9abccaf02 build use string serialized version 2024-06-10 17:59:03 -07:00
mbecker20
6428fa6de2 1.7.1 2024-06-10 17:37:22 -07:00
mbecker20
883f54431d custom to toml serializer for api 2024-06-10 17:34:56 -07:00
mbecker20
28dc030e2b custom Vec<EnvVar>, Vec<Conversion> deserializers to support config them as string 2024-06-10 14:39:51 -07:00
mbecker20
145d933e63 pt-2 2024-06-10 01:47:46 -07:00
mbecker20
9772ca1a1c add Resource Sync system user 2024-06-10 01:46:26 -07:00
mbecker20
4059b69201 core auto refreshes all syncs every 5 min 2024-06-09 23:49:02 -07:00
mbecker20
8e175ea5a1 add pending sync alert variant 2024-06-09 23:23:40 -07:00
mbecker20
d931b8b4e7 fix deployment when image_type None 2024-06-09 23:15:52 -07:00
mbecker20
0982800ad2 update client to 1.7.0 2024-06-09 22:47:49 -07:00
mbecker20
4382ad0b3b migrate 1.6 to 1.7 2024-06-09 22:46:21 -07:00
mbecker20
e7891f7870 update docs for ghcr 2024-06-09 21:56:01 -07:00
mbecker20
6bada46841 add export variables / user groups 2024-06-09 21:32:53 -07:00
mbecker20
eae6cbd228 label the image 2024-06-09 20:55:09 -07:00
mbecker20
a0ee6180b2 finish 1.7.0 2024-06-09 19:45:46 -07:00
mbecker20
3ce3de8768 configure registry 2024-06-09 19:34:49 -07:00
mbecker20
6c46993b61 New Monitor logo cr. George Weston 2024-06-09 18:38:58 -07:00
mbecker20
fbd9d14aaa change handler loggin 2024-06-09 15:11:18 -07:00
51 changed files with 1415 additions and 280 deletions

85
Cargo.lock generated
View File

@@ -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",

View File

@@ -3,7 +3,7 @@ resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
[workspace.package]
version = "1.7.0"
version = "1.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"

View File

@@ -19,7 +19,7 @@ pub type ToDelete = Vec<String>;
type UpdatesResult<T> = (ToCreate<T>, ToUpdate<T>, ToDelete);
pub struct ToUpdateItem<T> {
pub struct ToUpdateItem<T: Default> {
pub id: String,
pub resource: ResourceToml<T>,
pub update_description: bool,
@@ -37,6 +37,7 @@ pub trait ResourceSync: Sized {
type PartialConfig: std::fmt::Debug
+ Clone
+ Send
+ Default
+ From<Self::Config>
+ From<Self::ConfigDiff>
+ Serialize

View File

@@ -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

View File

@@ -14,9 +14,21 @@ RUN cd frontend && yarn link @monitor/client && yarn && yarn build
# Final Image
FROM debian:bookworm-slim
# Install Deps
RUN apt update && apt install -y git ca-certificates
# Copy
COPY ./config_example/core.config.example.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /
COPY --from=frontend-builder /builder/frontend/dist /frontend
# Hint at the port
EXPOSE 9000
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/monitor
LABEL org.opencontainers.image.description="A tool to build and deploy software across many servers"
LABEL org.opencontainers.image.licenses=GPL-3.0
CMD ["./core"]

View File

@@ -14,7 +14,11 @@ use monitor_client::{
alerter::Alerter,
build::Build,
builder::{Builder, BuilderConfig},
deployment::{Deployment, DeploymentImage},
deployment::{
conversions_to_string, term_signal_labels_to_string,
Deployment, DeploymentImage,
},
environment_vars_to_string,
permission::{PermissionLevel, UserTarget},
procedure::Procedure,
repo::Repo,
@@ -30,8 +34,10 @@ use monitor_client::{
},
};
use mungos::find::find_collect;
use ordered_hash_map::OrderedHashMap;
use partial_derive2::PartialDiff;
use resolver_api::Resolve;
use serde_json::Value;
use crate::{
helpers::query::get_user_user_group_ids,
@@ -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)
}

View File

@@ -56,7 +56,7 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.jwt_valid_for),
sync_directory: env
.monitor_sync_directory
.map(|dir|
.map(|dir|
dir.parse()
.context("failed to parse env MONITOR_SYNC_DIRECTORY as valid path").unwrap())
.unwrap_or(config.sync_directory),

View File

@@ -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>")
}

View File

@@ -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,

View File

@@ -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();

View File

@@ -78,7 +78,10 @@ pub trait MonitorResource {
+ From<Self::PartialConfig>
+ PartialDiff<Self::PartialConfig, Self::ConfigDiff>
+ 'static;
type PartialConfig: From<Self::Config> + Serialize + MaybeNone;
type PartialConfig: Default
+ From<Self::Config>
+ Serialize
+ MaybeNone;
type ConfigDiff: Into<Self::PartialConfig>
+ Serialize
+ Diff

View File

@@ -1,9 +1,33 @@
use mungos::{init::MongoBuilder, mongodb::Collection};
use serde::{Deserialize, Serialize};
pub mod build;
pub mod deployment;
pub mod resource;
pub struct DbClient {
pub deployments: Collection<deployment::Deployment>,
pub builds: Collection<build::Build>,
}
impl DbClient {
pub async fn new(
legacy_uri: &str,
legacy_db_name: &str,
) -> DbClient {
let client = MongoBuilder::default()
.uri(legacy_uri)
.build()
.await
.expect("failed to init legacy mongo client");
let db = client.database(legacy_db_name);
DbClient {
deployments: db.collection("Deployment"),
builds: db.collection("Build"),
}
}
}
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
)]

View File

@@ -11,8 +11,15 @@ use serde::Deserialize;
mod legacy;
mod migrate;
#[derive(Deserialize)]
enum AppMode {
V0,
V1_6,
}
#[derive(Deserialize)]
struct Env {
app_mode: AppMode,
legacy_uri: String,
legacy_db_name: String,
target_uri: String,
@@ -28,13 +35,26 @@ async fn main() -> anyhow::Result<()> {
let env: Env = envy::from_env()?;
let legacy_db =
legacy::v0::DbClient::new(&env.legacy_uri, &env.legacy_db_name)
match env.app_mode {
AppMode::V0 => {
let legacy_db = legacy::v0::DbClient::new(
&env.legacy_uri,
&env.legacy_db_name,
)
.await;
let target_db =
DbClient::new(&env.target_uri, &env.target_db_name).await?;
migrate::migrate_all(&legacy_db, &target_db).await?;
let target_db =
DbClient::new(&env.target_uri, &env.target_db_name).await?;
migrate::v0::migrate_all(&legacy_db, &target_db).await?
}
AppMode::V1_6 => {
let db = legacy::v1_6::DbClient::new(
&env.target_uri,
&env.target_db_name,
)
.await;
migrate::v1_6::migrate_all_in_place(&db).await?
}
}
info!("finished!");

View File

@@ -0,0 +1,2 @@
pub mod v0;
pub mod v1_6;

View File

@@ -0,0 +1,74 @@
use anyhow::Context;
use monitor_client::entities::{
build::Build, deployment::Deployment,
};
use mungos::{
find::find_collect,
mongodb::bson::{doc, to_document},
};
use crate::legacy::v1_6;
pub async fn migrate_all_in_place(
db: &v1_6::DbClient,
) -> anyhow::Result<()> {
migrate_deployments_in_place(db).await?;
migrate_builds_in_place(db).await?;
Ok(())
}
pub async fn migrate_deployments_in_place(
db: &v1_6::DbClient,
) -> anyhow::Result<()> {
let deployments = find_collect(&db.deployments, None, None)
.await
.context("failed to get deployments")?
.into_iter()
.map(Into::into)
.collect::<Vec<Deployment>>();
info!("migrating {} deployments...", deployments.len());
for deployment in deployments {
db.deployments
.update_one(
doc! { "name": &deployment.name },
doc! { "$set": to_document(&deployment)? },
None,
)
.await
.context("failed to insert deployments on target")?;
}
info!("deployments have been migrated\n");
Ok(())
}
pub async fn migrate_builds_in_place(
db: &v1_6::DbClient,
) -> anyhow::Result<()> {
let builds = find_collect(&db.builds, None, None)
.await
.context("failed to get builds")?
.into_iter()
.map(Into::into)
.collect::<Vec<Build>>();
info!("migrating {} builds...", builds.len());
for build in builds {
db.builds
.update_one(
doc! { "name": &build.name },
doc! { "$set": to_document(&build)? },
None,
)
.await
.context("failed to insert builds on target")?;
}
info!("builds have been migrated\n");
Ok(())
}

View File

@@ -40,10 +40,7 @@ pub async fn docker_login(
.await;
Ok(true)
}
ImageRegistry::Ghcr(CloudRegistryConfig {
account,
..
}) => {
ImageRegistry::Ghcr(CloudRegistryConfig { account, .. }) => {
if account.is_empty() {
return Err(anyhow!(
"Must configure account for GithubContainerRegistry"

View File

@@ -29,7 +29,6 @@ async fn task(
req_id: Uuid,
request: crate::api::PeripheryRequest,
) -> anyhow::Result<String> {
info!("request {req_id} | {request:?}");
let timer = Instant::now();
let res =
@@ -48,7 +47,7 @@ async fn task(
}
let elapsed = timer.elapsed();
info!("request {req_id} | resolve time: {elapsed:?}");
debug!("request {req_id} | resolve time: {elapsed:?}");
res
}

View File

@@ -17,29 +17,29 @@ docker = ["dep:bollard"]
[dependencies]
# mogh
mongo_indexed = { workspace = true, optional = true }
serror.workspace = true
resolver_api.workspace = true
derive_default_builder.workspace = true
derive_empty_traits.workspace = true
async_timing_util.workspace = true
partial_derive2.workspace = true
derive_variants.workspace = true
derive_default_builder.workspace = true
async_timing_util.workspace = true
resolver_api.workspace = true
serror.workspace = true
# external
bollard = { workspace = true, optional = true }
reqwest.workspace = true
anyhow.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
envy.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tokio-tungstenite.workspace = true
futures.workspace = true
typeshare.workspace = true
strum.workspace = true
derive_builder.workspace = true
serde_json.workspace = true
tokio-util.workspace = true
thiserror.workspace = true
typeshare.workspace = true
futures.workspace = true
reqwest.workspace = true
tracing.workspace = true
anyhow.workspace = true
serde.workspace = true
tokio.workspace = true
strum.workspace = true
envy.workspace = true
uuid.workspace = true
clap.workspace = true
bson.workspace = true

View File

@@ -42,6 +42,13 @@ pub struct AlerterConfig {
#[partial_default(default_enabled())]
pub enabled: bool,
/// Where to route the alert messages.
///
/// Default: Custom endpoint `http://localhost:7000`
#[serde(default)]
#[builder(default)]
pub endpoint: AlerterEndpoint,
/// Only send specific alert types.
/// If empty, will send all alert types.
#[serde(default)]
@@ -53,13 +60,6 @@ pub struct AlerterConfig {
#[serde(default)]
#[builder(default)]
pub resources: Vec<ResourceTarget>,
/// Where to route the alert messages.
///
/// Default: Custom endpoint `http://localhost:7000`
#[serde(default)]
#[builder(default)]
pub endpoint: AlerterEndpoint,
}
impl AlerterConfig {

View File

@@ -71,11 +71,6 @@ pub struct BuildConfig {
#[builder(default)]
pub builder_id: String,
/// Whether to skip secret interpolation in the build_args.
#[serde(default)]
#[builder(default)]
pub skip_secret_interp: bool,
/// The current version of the build.
#[serde(default)]
#[builder(default)]
@@ -108,6 +103,11 @@ pub struct BuildConfig {
#[builder(default)]
pub pre_build: SystemCommand,
/// Configuration for the registry to push the built image to.
#[serde(default)]
#[builder(default)]
pub image_registry: ImageRegistry,
/// The path of the docker build context relative to the root of the repo.
/// Default: "." (the root of the repo).
#[serde(default = "default_build_path")]
@@ -121,36 +121,50 @@ pub struct BuildConfig {
#[partial_default(default_dockerfile_path())]
pub dockerfile_path: String,
/// Docker build arguments
/// Whether to skip secret interpolation in the build_args.
#[serde(default)]
#[builder(default)]
pub build_args: Vec<EnvironmentVar>,
/// Docker labels
#[serde(default)]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
/// Any extra docker cli arguments to be included in the build command
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
pub skip_secret_interp: bool,
/// Whether to use buildx to build (eg `docker buildx build ...`)
#[serde(default)]
#[builder(default)]
pub use_buildx: bool,
/// Configuration for the registry to push the built image to.
#[serde(default)]
#[builder(default)]
pub image_registry: ImageRegistry,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
#[builder(default = "default_webhook_enabled()")]
#[partial_default(default_webhook_enabled())]
pub webhook_enabled: bool,
/// Any extra docker cli arguments to be included in the build command
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
/// Docker build arguments
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub build_args: Vec<EnvironmentVar>,
/// Docker labels
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
}
impl BuildConfig {

View File

@@ -70,6 +70,12 @@ pub enum PartialBuilderConfig {
Aws(_PartialAwsBuilderConfig),
}
impl Default for PartialBuilderConfig {
fn default() -> Self {
Self::Aws(Default::default())
}
}
impl MaybeNone for PartialBuilderConfig {
fn is_none(&self) -> bool {
match self {
@@ -290,9 +296,6 @@ pub struct AwsBuilderConfig {
pub ami_id: String,
/// The subnet id to create the instance in.
pub subnet_id: String,
/// The security group ids to attach to the instance.
/// This should include a security group to allow core inbound access to the periphery port.
pub security_group_ids: Vec<String>,
/// The key pair name to attach to the instance
pub key_pair_name: String,
/// Whether to assign the instance a public IP address.
@@ -301,6 +304,9 @@ pub struct AwsBuilderConfig {
/// Whether core should use the public IP address to communicate with periphery on the builder.
/// If false, core will communicate with the instance using the private IP.
pub use_public_ip: bool,
/// The security group ids to attach to the instance.
/// This should include a security group to allow core inbound access to the periphery port.
pub security_group_ids: Vec<String>,
/// Which github accounts (usernames) are available on the AMI
#[serde(default)]

View File

@@ -1,11 +1,15 @@
use std::collections::HashMap;
use anyhow::Context;
use bson::{doc, Document};
use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use derive_variants::EnumVariants;
use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer, Serialize,
};
use strum::{Display, EnumString};
use typeshare::typeshare;
@@ -42,7 +46,7 @@ pub type _PartialDeploymentConfig = PartialDeploymentConfig;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]
#[partial_derive(Serialize, Deserialize, Debug, Clone)]
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
#[partial(skip_serializing_none, from, diff)]
pub struct DeploymentConfig {
/// The id of server the deployment is deployed on.
@@ -51,12 +55,6 @@ pub struct DeploymentConfig {
#[builder(default)]
pub server_id: String,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
#[partial_default(default_send_alerts())]
pub send_alerts: bool,
/// The image which the deployment deploys.
/// Can either be a user inputted image, or a Monitor build.
#[serde(default)]
@@ -84,46 +82,11 @@ pub struct DeploymentConfig {
#[builder(default)]
pub redeploy_on_build: bool,
/// Labels attached to various termination signal options.
/// Used to specify different shutdown functionality depending on the termination signal.
#[serde(default = "default_term_signal_labels")]
#[builder(default = "default_term_signal_labels()")]
#[partial_default(default_term_signal_labels())]
pub term_signal_labels: Vec<TerminationSignalLabel>,
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
#[serde(default)]
#[builder(default)]
pub termination_signal: TerminationSignal,
/// The termination timeout.
#[serde(default = "default_termination_timeout")]
#[builder(default = "default_termination_timeout()")]
#[partial_default(default_termination_timeout())]
pub termination_timeout: i32,
/// The container port mapping.
/// Irrelevant if container network is `host`.
/// Maps ports on host to ports on container.
#[serde(default)]
#[builder(default)]
pub ports: Vec<Conversion>,
/// The container volume mapping.
/// Maps files / folders on host to files / folders in container.
#[serde(default)]
#[builder(default)]
pub volumes: Vec<Conversion>,
/// The environment variables passed to the container.
#[serde(default)]
#[builder(default)]
pub environment: Vec<EnvironmentVar>,
/// The docker labels given to the container.
#[serde(default)]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
#[partial_default(default_send_alerts())]
pub send_alerts: bool,
/// The network attached to the container.
/// Default is `host`.
@@ -145,11 +108,81 @@ pub struct DeploymentConfig {
#[builder(default)]
pub command: String,
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
#[serde(default)]
#[builder(default)]
pub termination_signal: TerminationSignal,
/// The termination timeout.
#[serde(default = "default_termination_timeout")]
#[builder(default = "default_termination_timeout()")]
#[partial_default(default_termination_timeout())]
pub termination_timeout: i32,
/// Extra args which are interpolated into the `docker run` command,
/// and affect the container configuration.
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
/// Labels attached to various termination signal options.
/// Used to specify different shutdown functionality depending on the termination signal.
#[serde(
default = "default_term_signal_labels",
deserialize_with = "term_labels_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "option_term_labels_deserializer"
))]
#[builder(default = "default_term_signal_labels()")]
#[partial_default(default_term_signal_labels())]
pub term_signal_labels: Vec<TerminationSignalLabel>,
/// The container port mapping.
/// Irrelevant if container network is `host`.
/// Maps ports on host to ports on container.
#[serde(default, deserialize_with = "conversions_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_conversions_deserializer"
))]
#[builder(default)]
pub ports: Vec<Conversion>,
/// The container volume mapping.
/// Maps files / folders on host to files / folders in container.
#[serde(default, deserialize_with = "conversions_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_conversions_deserializer"
))]
#[builder(default)]
pub volumes: Vec<Conversion>,
/// The environment variables passed to the container.
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub environment: Vec<EnvironmentVar>,
/// The docker labels given to the container.
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
))]
#[builder(default)]
pub labels: Vec<EnvironmentVar>,
}
impl DeploymentConfig {
@@ -244,7 +277,7 @@ impl Default for DeploymentImage {
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Conversion {
/// reference on the server.
@@ -253,6 +286,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 {

View File

@@ -6,8 +6,11 @@ use build::CloudRegistryConfig;
use clap::Parser;
use derive_empty_traits::EmptyTraits;
use serde::{
de::{value::MapAccessDeserializer, Visitor},
Deserialize, Serialize,
de::{
value::{MapAccessDeserializer, SeqAccessDeserializer},
Visitor,
},
Deserialize, Deserializer, Serialize,
};
use serror::Serror;
use strum::{AsRefStr, Display, EnumString};
@@ -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,

View File

@@ -56,7 +56,8 @@ pub type _PartialProcedureConfig = PartialProcedureConfig;
#[partial(skip_serializing_none, from, diff)]
pub struct ProcedureConfig {
/// The stages to be run by the procedure.
#[serde(default)]
#[serde(default, alias = "stage")]
#[partial_attr(serde(alias = "stage"))]
#[builder(default)]
pub stages: Vec<ProcedureStage>,

View File

@@ -12,7 +12,7 @@ use super::update::ResourceTargetVariant;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
pub struct Resource<Config, Info: Default = ()> {
pub struct Resource<Config: Default, Info: Default = ()> {
/// The Mongo ID of the resource.
/// This field is de/serialized from/to JSON as
/// `{ "_id": { "$oid": "..." }, ...(rest of serialized Resource<T>) }`
@@ -50,6 +50,8 @@ pub struct Resource<Config, Info: Default = ()> {
pub info: Info,
/// Resource-specific configuration.
#[serde(default)]
#[builder(default)]
pub config: Config,
}

View File

@@ -47,6 +47,11 @@ pub struct ServerConfig {
/// Example: http://localhost:8120
pub address: String,
/// An optional region label
#[serde(default)]
#[builder(default)]
pub region: String,
/// Whether a server is enabled.
/// If a server is disabled,
/// you won't be able to perform any actions on it or see deployment's status.
@@ -94,11 +99,6 @@ pub struct ServerConfig {
#[partial_default(default_send_alerts())]
pub send_disk_alerts: bool,
/// An optional region label
#[serde(default)]
#[builder(default)]
pub region: String,
/// The percentage threshhold which triggers WARNING state for CPU.
#[serde(default = "default_cpu_warning")]
#[builder(default = "default_cpu_warning()")]

View File

@@ -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 {

View File

@@ -48,13 +48,6 @@ pub struct ResourcesToml {
)]
pub procedures: Vec<ResourceToml<PartialProcedureConfig>>,
#[serde(
default,
rename = "builder",
skip_serializing_if = "Vec::is_empty"
)]
pub builders: Vec<ResourceToml<PartialBuilderConfig>>,
#[serde(
default,
rename = "alerter",
@@ -62,6 +55,13 @@ pub struct ResourcesToml {
)]
pub alerters: Vec<ResourceToml<PartialAlerterConfig>>,
#[serde(
default,
rename = "builder",
skip_serializing_if = "Vec::is_empty"
)]
pub builders: Vec<ResourceToml<PartialBuilderConfig>>,
#[serde(
default,
rename = "server_template",
@@ -93,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,
}

View File

@@ -18,7 +18,20 @@ If the build directory is the root of the repository, you pass the build path as
The dockerfile's path is given relative to the build directory. So if your build directory is `build/directory` and the dockerfile is in `build/directory/Dockerfile.example`, you give the dockerfile path simply as `Dockerfile.example`.
Just as with private repos, you will need to select a docker account to use with `docker push`.
### Image registry
Monitor supports pushing to DockerHub and Github Container Registry (ghcr.io).
Any of the Docker / Github accounts that are specified in config, between the core config and builder, will be available to use.
Additionally, allowed organizations for both DockerHub and Github can be specified on the core config and attached to builds.
Doing so will cause the images to be published under the organization's namespace rather than the account's.
When connecting a build to a deployments, the default behavior is for the deployment to inherit the registry configuration from the build.
In cases where that account isn't available to the deployment, another account can be chosen in the deployment config.
:::note
In order to publish to the Github Container Registry, your Github access token must be given the `write:packages` permission.
See the Github docs [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic).
:::
### Adding build args

View File

@@ -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

View File

@@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -184,15 +184,7 @@ export const DoubleInput = <
);
};
export const AccountSelector = ({
disabled,
id,
type,
account_type,
selected,
onSelect,
placeholder,
}: {
export const AccountSelectorConfig = (params: {
disabled: boolean;
id?: string;
type: "Server" | "None" | "Builder";
@@ -200,6 +192,28 @@ export const AccountSelector = ({
selected: string | undefined;
onSelect: (id: string) => void;
placeholder: string;
}) => {
return (
<ConfigItem label={`${params.account_type} Account`}>
<AccountSelector {...params} />
</ConfigItem>
);
};
export const AccountSelector = ({
disabled,
id,
type,
account_type,
selected,
onSelect,
}: {
disabled: boolean;
id?: string;
type: "Server" | "None" | "Builder";
account_type: keyof Types.GetBuilderAvailableAccountsResponse;
selected: string | undefined;
onSelect: (id: string) => void;
}) => {
const [request, params] =
type === "Server" || type === "None"
@@ -207,30 +221,28 @@ export const AccountSelector = ({
: ["GetBuilderAvailableAccounts", { builder: id }];
const accounts = useRead(request as any, params).data;
return (
<ConfigItem label={`${account_type} Account`}>
<Select
value={type === "Builder" ? selected || undefined : selected}
onValueChange={(value) => {
onSelect(value === "Empty" ? "" : value);
}}
<Select
value={selected}
onValueChange={(value) => {
onSelect(value === "Empty" ? "" : value);
}}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>{placeholder}</SelectItem>
{(accounts as any)?.[account_type]?.map((account: string) => (
<SelectItem key={account} value={account}>
{account}
</SelectItem>
))}
</SelectContent>
</Select>
</ConfigItem>
<SelectValue placeholder="Select Account" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>None</SelectItem>
{(accounts as any)?.[account_type]?.map((account: string) => (
<SelectItem key={account} value={account}>
{account}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
@@ -440,19 +452,73 @@ export const ImageRegistryConfig = ({
registry,
setRegistry,
disabled,
type,
resource_id,
registry_types,
}: {
registry: Types.ImageRegistry | undefined;
setRegistry: (registry: Types.ImageRegistry) => void;
disabled: boolean;
type: "Build" | "Deployment";
// For builds, its builder id. For servers, its server id.
resource_id?: string;
registry_types?: Types.ImageRegistry["type"][];
}) => {
const _registry = registry ?? default_registry_config("None");
const cloud_params =
_registry.type === "DockerHub" || _registry.type === "Ghcr"
? _registry.params
: undefined;
if (_registry.type === "None" || _registry.type === "Custom") {
return (
<ConfigItem label="Image Registry">
<RegistryTypeSelector
registry={_registry}
setRegistry={setRegistry}
disabled={disabled}
deployment={type === "Deployment"}
registry_types={registry_types}
/>
</ConfigItem>
);
}
return (
<ConfigItem label="Image Registry">
<RegistryTypeSelector
registry={_registry}
setRegistry={setRegistry}
disabled={disabled}
/>
<div className="flex items-center justify-stretch gap-4">
{type === "Build" && cloud_params?.account && (
<OrganizationSelector
value={cloud_params?.organization}
set={(organization) =>
setRegistry({
..._registry,
params: { ..._registry.params, organization },
})
}
disabled={disabled}
type={_registry.type === "DockerHub" ? "Docker" : "Github"}
/>
)}
<AccountSelector
id={resource_id}
type={type === "Build" ? "Builder" : "Server"}
account_type={_registry.type === "DockerHub" ? "docker" : "github"}
selected={cloud_params?.account}
onSelect={(account) =>
setRegistry({
..._registry,
params: { ..._registry.params, account },
})
}
disabled={disabled}
/>
<RegistryTypeSelector
registry={_registry}
setRegistry={setRegistry}
disabled={disabled}
deployment={type === "Deployment"}
registry_types={registry_types}
/>
</div>
</ConfigItem>
);
};
@@ -466,17 +532,23 @@ const REGISTRY_TYPES: Types.ImageRegistry["type"][] = [
const RegistryTypeSelector = ({
registry,
setRegistry,
registry_types = REGISTRY_TYPES,
disabled,
deployment,
}: {
registry: Types.ImageRegistry;
setRegistry: (registry: Types.ImageRegistry) => void;
registry_types?: Types.ImageRegistry["type"][];
disabled: boolean;
deployment?: boolean;
}) => {
return (
<Select
value={registry.type}
onValueChange={(type: Types.ImageRegistry["type"]) => {
setRegistry(default_registry_config(type));
value={to_readable_registry_type(registry.type, deployment)}
onValueChange={(type) => {
setRegistry(
default_registry_config(from_readable_registry_type(type, deployment))
);
}}
disabled={disabled}
>
@@ -487,9 +559,49 @@ const RegistryTypeSelector = ({
<SelectValue placeholder="Select Registry" />
</SelectTrigger>
<SelectContent align="end">
{REGISTRY_TYPES.map((type) => (
<SelectItem key={type} value={type}>
{type}
{registry_types.map((type) => {
const t = to_readable_registry_type(type, deployment);
return (
<SelectItem key={type} value={t}>
{t}
</SelectItem>
);
})}
</SelectContent>
</Select>
);
};
const OrganizationSelector = ({
value,
set,
disabled,
type,
}: {
value?: string;
set: (org: string) => void;
disabled: boolean;
type: "Docker" | "Github";
}) => {
const organizations = useRead(`List${type}Organizations`, {}).data;
if (!organizations || organizations.length === 0) return null;
return (
<Select
value={value}
onValueChange={(v) => set(v === "Empty" ? "" : v)}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder="Select Organization" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>None</SelectItem>
{organizations?.map((org) => (
<SelectItem key={org} value={org}>
{org}
</SelectItem>
))}
</SelectContent>
@@ -497,6 +609,22 @@ const RegistryTypeSelector = ({
);
};
const to_readable_registry_type = (
type: Types.ImageRegistry["type"],
deployment?: boolean
) => {
if (deployment && type === "None") return "Same as build";
return type;
};
const from_readable_registry_type = (
readable: string,
deployment?: boolean
) => {
if (deployment && readable === "Same as build") return "None";
return readable as Types.ImageRegistry["type"];
};
const default_registry_config = (
type: Types.ImageRegistry["type"]
): Types.ImageRegistry => {

View File

@@ -16,10 +16,12 @@ export const ExportButton = ({
targets,
user_groups,
tags,
include_variables,
}: {
targets?: Types.ResourceTarget[];
user_groups?: string[];
tags?: string[];
include_variables?: boolean;
}) => {
const [open, setOpen] = useState(false);
return (
@@ -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} />;
};

View File

@@ -15,6 +15,7 @@ const ALERT_TYPES: Types.AlertData["type"][] = [
"ServerMem",
"ServerDisk",
"ContainerStateChange",
"ResourceSyncPendingUpdates",
"AwsBuilderTerminationFailed",
];

View File

@@ -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>
// );
// };

View File

@@ -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}

View File

@@ -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"}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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"

View File

@@ -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