Compare commits

..

29 Commits

Author SHA1 Message Date
mbecker20
130ca8e1f1 bump versions to 0.3.2 2023-05-01 01:53:58 +00:00
mbecker20
ced4c21688 update monitor client to 0.3.1 2023-05-01 01:41:23 +00:00
mbecker20
6ec7078024 custom termination signals 2023-04-30 06:52:27 +00:00
mbecker20
b28d8f2506 update frontend types 2023-04-30 03:26:01 +00:00
mbecker20
c88a9291a0 support auto redeploy and custom stop signals 2023-04-30 00:10:59 +00:00
mbecker20
1e82d19306 build summary defaults to time view 2023-04-21 16:34:03 +00:00
mbecker20
dd87e50cb2 build stats summary 2023-04-21 08:52:17 +00:00
mbecker20
4c8f96a30f build stats card 2023-04-21 08:08:15 +00:00
mbecker20
c4f45e05f1 finish build stats api 2023-04-21 08:08:01 +00:00
mbecker20
6aa382c7c1 finish build stats api 2023-04-20 16:34:14 +00:00
mbecker20
ccb9f059e6 get build stats api 2023-04-20 07:34:49 +00:00
mbecker20
1cdcea0771 start on route to get daily build stats (time, count) 2023-04-19 07:02:21 +00:00
mbecker20
88dda0de80 update rename deployment to check whether deployment has repo attached, and if so, reclone it to account for name change. 2023-04-19 06:44:58 +00:00
mbecker20
30ed99e2b0 publish monitor client 0.3.1 with Readme 2023-04-18 07:56:30 +00:00
mbecker20
e5953b7541 monitor client readme 2023-04-18 07:55:05 +00:00
mbecker20
1f9d01c59f new home servers png 2023-04-18 06:31:23 +00:00
mbecker20
cc5210a3d8 fix server children add new button 2023-04-18 06:17:42 +00:00
mbecker20
26559e2d3b delete builds screenshots 2023-04-18 03:20:20 +00:00
mbecker20
7eeddb300f add link to screenshots docsite 2023-04-18 02:59:37 +00:00
mbecker20
1e01bae16b add screenshots to monitor readme 2023-04-18 02:47:46 +00:00
mbecker20
87c03924e5 remove second universal search 2023-04-18 02:46:46 +00:00
mbecker20
f0998b1d43 add universal search screenshot 2023-04-18 02:42:21 +00:00
mbecker20
1995a04244 add screenshots 2023-04-18 02:38:07 +00:00
mbecker20
420fe6bcd5 add build time to version selector 2023-04-17 08:26:34 +00:00
mbecker20
d4e26c0553 fix docker repo reference 2023-04-16 19:33:28 +00:00
mbecker20
5f5e7cb45e add note about oauth 2023-04-16 07:44:34 +00:00
beckerinj
8aa0304738 core setup doc 2023-04-16 03:32:06 -04:00
beckerinj
8ec98c33a4 first user is auto enabled and made admin 2023-04-16 03:23:30 -04:00
beckerinj
2667182ca3 update core example config 2023-04-16 02:49:00 -04:00
73 changed files with 1582 additions and 364 deletions

152
Cargo.lock generated
View File

@@ -824,7 +824,7 @@ dependencies = [
"jwt",
"merge_config_files",
"monitor_helpers",
"monitor_types 0.3.0",
"monitor_types 0.3.2",
"mungos",
"periphery_client",
"serde",
@@ -1058,10 +1058,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "db_client"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"anyhow",
"monitor_types 0.3.0",
"monitor_types 0.3.2",
"mungos",
]
@@ -1934,12 +1934,12 @@ dependencies = [
[[package]]
name = "monitor_cli"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"async_timing_util",
"clap",
"colored",
"monitor_types 0.3.0",
"monitor_types 0.3.2",
"rand",
"run_command",
"serde",
@@ -1951,12 +1951,12 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"anyhow",
"envy",
"futures-util",
"monitor_types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"monitor_types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest",
"serde",
"serde_derive",
@@ -1968,11 +1968,11 @@ dependencies = [
[[package]]
name = "monitor_helpers"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"anyhow",
"axum",
"monitor_types 0.3.0",
"monitor_types 0.3.2",
"rand",
"serde",
"serde_json",
@@ -1981,7 +1981,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"anyhow",
"async_timing_util",
@@ -1994,7 +1994,8 @@ dependencies = [
"futures",
"merge_config_files",
"monitor_helpers",
"monitor_types 0.3.0",
"monitor_types 0.3.2",
"parse_csl",
"run_command",
"serde",
"serde_derive",
@@ -2008,7 +2009,7 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"anyhow",
"bollard",
@@ -2025,9 +2026,9 @@ dependencies = [
[[package]]
name = "monitor_types"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bba1f3fb118a665daf75e64267e24630825319903fc6967c3939eeb6a2f2baa"
checksum = "6b50f6e811dc59b9f19b13b58bee4636f6f1bee2815d9c05f523b530284db75d"
dependencies = [
"anyhow",
"bollard",
@@ -2251,6 +2252,12 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "parse_csl"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffa94c2e5674923c67d7f3dfce1279507b191e10eb064881b46ed3e1256e5ca6"
[[package]]
name = "pbkdf2"
version = "0.11.0"
@@ -2268,11 +2275,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "periphery_client"
version = "0.3.0"
version = "0.3.2"
dependencies = [
"anyhow",
"futures-util",
"monitor_types 0.3.0",
"monitor_types 0.3.2",
"reqwest",
"serde",
"serde_json",
@@ -2858,9 +2865,9 @@ checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831"
[[package]]
name = "socket2"
version = "0.4.7"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
dependencies = [
"libc",
"winapi",
@@ -3083,14 +3090,13 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.26.0"
version = "1.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"parking_lot",
@@ -3098,18 +3104,18 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "1.8.2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.12",
]
[[package]]
@@ -3685,13 +3691,13 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.1",
"windows_aarch64_msvc 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm 0.42.1",
"windows_x86_64_msvc 0.42.1",
]
[[package]]
@@ -3700,7 +3706,16 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
"windows-targets 0.42.1",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.0",
]
[[package]]
@@ -3709,13 +3724,28 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.1",
"windows_aarch64_msvc 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm 0.42.1",
"windows_x86_64_msvc 0.42.1",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc 0.48.0",
]
[[package]]
@@ -3724,42 +3754,84 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.3.3"

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_cli"
version = "0.3.0"
version = "0.3.2"
edition = "2021"
authors = ["MoghTech"]
description = "monitor cli | tools to setup monitor system"

View File

@@ -10,19 +10,16 @@ port = 9000
# daily utc offset in hours to send daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight.
daily_offset_hours = 13
# number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily
keep_stats_for_days = 120
# secret used to generate the jwt. should be some randomly generated hash.
jwt_secret = "your_jwt_secret"
# can be 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk, 30-day
jwt_valid_for = "1-wk"
# webhook url given by slack app
# webhook url given by slack app that monitor will send alerts and a daily update to
slack_url = "your_slack_app_webhook_url"
# token that has to be given to github during webhook config as the secret
# token that has to be given to github during repo webhook config as the secret
github_webhook_secret = "your_random_webhook_secret"
# optional. an alternate base url that is used to recieve github webhook requests. if not provided, will use 'host' address as base
@@ -31,30 +28,20 @@ github_webhook_base_url = "https://monitor-github-webhook.mogh.tech"
# token used to authenticate core requests to periphery
passkey = "your_random_passkey"
# can be 30-sec, 1-min, 2-min, 5-min
# controls the granularity of the system stats collection by monitor core
# can be 15-sec, 30-sec, 1-min, 2-min, 5-min
monitoring_interval = "1-min"
# number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily
keep_stats_for_days = 14
# these will be used by the GUI to attach to builds. New build docker orgs will default to first org (or none if empty).
# when attached to build, image will be pushed to repo under the specified organization
docker_organizations = ["your_docker_org1", "your_docker_org_2"]
# allow or deny user login with username / password
local_auth = true
# these will be given in the GUI to attach to builds. New build docker orgs will default to first org (or none if empty).
docker_organizations = ["your_docker_org1", "your_docker_org_2"]
[aws]
access_key_id = "your_aws_key_id"
secret_access_key = "your_aws_secret_key"
default_region = "us-east-1"
default_ami_id = "your_periphery_ami"
default_key_pair_name = "your_default_key_pair_name"
default_instance_type = "m5.2xlarge"
default_volume_gb = 8
default_subnet_id = "your_default_subnet_id"
default_security_group_ids = ["sg_id_1", "sg_id_2"]
default_assign_public_ip = false
[aws.available_ami_accounts]
your_periphery_ami = { name = "default ami", github = ["github_username"], docker = ["docker_username"] }
[github_oauth]
enabled = true
id = "your_github_client_id"
@@ -68,4 +55,19 @@ secret = "your_google_client_secret"
[mongo]
uri = "your_mongo_uri"
app_name = "monitor_core"
db_name = "monitor"
db_name = "monitor" # this is the name of the mongo database that monitor will create its collections in.
[aws]
access_key_id = "your_aws_key_id"
secret_access_key = "your_aws_secret_key"
default_region = "us-east-1"
default_ami_name = "your_ami_name" # must be defined below in [aws.available_ami_accounts]
default_instance_type = "m5.2xlarge"
default_volume_gb = 8
default_subnet_id = "your_default_subnet_id"
default_security_group_ids = ["sg_id_1", "sg_id_2"]
default_key_pair_name = "your_default_key_pair_name"
default_assign_public_ip = false
[aws.available_ami_accounts]
your_ami_name = { ami_id = "ami-1234567890", github = ["github_username"], docker = ["docker_username"] }

View File

@@ -3,13 +3,14 @@ use std::time::Duration;
use anyhow::{anyhow, Context};
use aws_sdk_ec2::Client;
use diff::Diff;
use futures_util::future::join_all;
use helpers::{all_logs_success, to_monitor_name};
use mungos::{doc, to_bson};
use types::{
monitor_timestamp,
traits::{Busy, Permissioned},
AwsBuilderBuildConfig, Build, Log, Operation, PermissionLevel, Update, UpdateStatus,
UpdateTarget, Version,
AwsBuilderBuildConfig, Build, DockerContainerState, Log, Operation, PermissionLevel, Update,
UpdateStatus, UpdateTarget, Version,
};
use crate::{
@@ -415,6 +416,8 @@ impl State {
.await;
}
self.handle_post_build_redeploy(build_id, &mut update).await;
update.success = all_logs_success(&update.logs);
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
@@ -424,6 +427,85 @@ impl State {
Ok(update)
}
async fn handle_post_build_redeploy(&self, build_id: &str, update: &mut Update) {
let redeploy_deployments = self
.db
.deployments
.get_some(
doc! { "build_id": build_id, "redeploy_on_build": true },
None,
)
.await;
if let Ok(deployments) = redeploy_deployments {
let futures = deployments.into_iter().map(|d| async move {
let request_user = RequestUser {
id: "auto redeploy".to_string(),
is_admin: true,
..Default::default()
};
let state = self
.get_deployment_with_container_state(&request_user, &d.id)
.await
.map(|r| r.state)
.unwrap_or_default();
if state == DockerContainerState::Running {
Some((
d.id.clone(),
self.deploy_container(
&d.id,
&RequestUser {
id: "auto redeploy".to_string(),
is_admin: true,
..Default::default()
},
None,
None,
)
.await,
))
} else {
None
}
});
let redeploy_results = join_all(futures).await;
let mut redeploys = Vec::<String>::new();
let mut redeploy_failures = Vec::<String>::new();
for res in redeploy_results {
if res.is_none() {
continue;
}
let (id, res) = res.unwrap();
match res {
Ok(_) => redeploys.push(id),
Err(e) => redeploy_failures.push(format!("{id}: {e:#?}")),
}
}
if redeploys.len() > 0 {
update.logs.push(Log::simple(
"redeploy",
format!("redeployed deployments: {}", redeploys.join(", ")),
))
}
if redeploy_failures.len() > 0 {
update.logs.push(Log::simple(
"redeploy failures",
redeploy_failures.join("\n"),
))
}
} else if let Err(e) = redeploy_deployments {
update.logs.push(Log::simple(
"redeploys failed",
format!("failed to get deployments to redeploy: {e:#?}"),
))
}
}
async fn create_ec2_instance_for_build(
&self,
build: &Build,

View File

@@ -6,7 +6,8 @@ use types::{
monitor_timestamp,
traits::{Busy, Permissioned},
Deployment, DeploymentWithContainerState, DockerContainerState, Log, Operation,
PermissionLevel, ServerStatus, ServerWithStatus, Update, UpdateStatus, UpdateTarget,
PermissionLevel, ServerStatus, ServerWithStatus, TerminationSignal, Update, UpdateStatus,
UpdateTarget,
};
use crate::{
@@ -112,6 +113,8 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Deployment> {
if self.deployment_busy(deployment_id).await {
return Err(anyhow!("deployment busy"));
@@ -123,7 +126,7 @@ impl State {
let server = self.db.get_server(&deployment.server_id).await?;
let log = match self
.periphery
.container_remove(&server, &deployment.name)
.container_remove(&server, &deployment.name, stop_signal, stop_time)
.await
{
Ok(log) => log,
@@ -367,7 +370,9 @@ impl State {
self.update_update(update).await?;
return Err(deployment_state.err().unwrap());
}
let DeploymentWithContainerState { state, .. } = deployment_state.unwrap();
let DeploymentWithContainerState {
deployment, state, ..
} = deployment_state.unwrap();
if state != DockerContainerState::NotDeployed {
let log = self
.periphery
@@ -406,7 +411,6 @@ impl State {
)
.await
.context("failed to update deployment name on mongo");
if let Err(e) = res {
update
.logs
@@ -418,6 +422,20 @@ impl State {
))
}
if deployment.repo.is_some() {
let res = self.reclone_deployment(deployment_id, user, false).await;
if let Err(e) = res {
update
.logs
.push(Log::error("reclone repo", format!("{e:?}")));
} else {
update.logs.push(Log::simple(
"reclone repo",
"deployment repo cloned with new name".to_string(),
));
}
}
update.end_ts = monitor_timestamp().into();
update.status = UpdateStatus::Complete;
update.success = all_logs_success(&update.logs);
@@ -431,8 +449,9 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
check_deployment_busy: bool,
) -> anyhow::Result<Update> {
if self.deployment_busy(deployment_id).await {
if check_deployment_busy && self.deployment_busy(deployment_id).await {
return Err(anyhow!("deployment busy"));
}
{
@@ -494,6 +513,8 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Update> {
if self.deployment_busy(deployment_id).await {
return Err(anyhow!("deployment busy"));
@@ -503,7 +524,9 @@ impl State {
let entry = lock.entry(deployment_id.to_string()).or_default();
entry.deploying = true;
}
let res = self.deploy_container_inner(deployment_id, user).await;
let res = self
.deploy_container_inner(deployment_id, user, stop_signal, stop_time)
.await;
{
let mut lock = self.deployment_action_states.lock().await;
let entry = lock.entry(deployment_id.to_string()).or_default();
@@ -516,6 +539,8 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Update> {
let start_ts = monitor_timestamp();
let mut deployment = self
@@ -553,7 +578,11 @@ impl State {
update.id = self.add_update(update.clone()).await?;
let deploy_log = match self.periphery.deploy(&server, &deployment).await {
let deploy_log = match self
.periphery
.deploy(&server, &deployment, stop_signal, stop_time)
.await
{
Ok(log) => log,
Err(e) => Log::error("deploy container", format!("{e:#?}")),
};
@@ -642,6 +671,8 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Update> {
if self.deployment_busy(deployment_id).await {
return Err(anyhow!("deployment busy"));
@@ -651,7 +682,9 @@ impl State {
let entry = lock.entry(deployment_id.to_string()).or_default();
entry.stopping = true;
}
let res = self.stop_container_inner(deployment_id, user).await;
let res = self
.stop_container_inner(deployment_id, user, stop_signal, stop_time)
.await;
{
let mut lock = self.deployment_action_states.lock().await;
let entry = lock.entry(deployment_id.to_string()).or_default();
@@ -664,6 +697,8 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Update> {
let start_ts = monitor_timestamp();
let deployment = self
@@ -683,7 +718,7 @@ impl State {
let log = self
.periphery
.container_stop(&server, &deployment.name)
.container_stop(&server, &deployment.name, stop_signal, stop_time)
.await;
update.success = match log {
@@ -712,6 +747,8 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Update> {
if self.deployment_busy(deployment_id).await {
return Err(anyhow!("deployment busy"));
@@ -721,7 +758,9 @@ impl State {
let entry = lock.entry(deployment_id.to_string()).or_default();
entry.removing = true;
}
let res = self.remove_container_inner(deployment_id, user).await;
let res = self
.remove_container_inner(deployment_id, user, stop_signal, stop_time)
.await;
{
let mut lock = self.deployment_action_states.lock().await;
let entry = lock.entry(deployment_id.to_string()).or_default();
@@ -734,6 +773,8 @@ impl State {
&self,
deployment_id: &str,
user: &RequestUser,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Update> {
let start_ts = monitor_timestamp();
let deployment = self
@@ -753,7 +794,7 @@ impl State {
let log = self
.periphery
.container_remove(&server, &deployment.name)
.container_remove(&server, &deployment.name, stop_signal, stop_time)
.await;
update.success = match log {

View File

@@ -209,7 +209,7 @@ impl State {
}
StopContainer => {
let update = self
.stop_container(&target_id, user)
.stop_container(&target_id, user, Option::None, Option::None)
.await
.context(format!(
"failed at stop container for deployment (id: {target_id})"
@@ -218,7 +218,7 @@ impl State {
}
RemoveContainer => {
let update = self
.remove_container(&target_id, user)
.remove_container(&target_id, user, Option::None, Option::None)
.await
.context(format!(
"failed at remove container for deployment (id: {target_id})"
@@ -227,7 +227,7 @@ impl State {
}
DeployContainer => {
let update = self
.deploy_container(&target_id, user)
.deploy_container(&target_id, user, Option::None, Option::None)
.await
.context(format!(
"failed at deploy container for deployment (id: {target_id})"
@@ -236,14 +236,18 @@ impl State {
}
RecloneDeployment => {
let update = self
.reclone_deployment(&target_id, user)
.reclone_deployment(&target_id, user, true)
.await
.context(format!("failed at reclone deployment (id: {target_id})"))?;
updates.push(update);
}
PullDeployment => {
// implement this one
// let update = self.pull
let update = self
.pull_deployment_repo(&target_id, user)
.await
.context(format!("failed at pull deployment (id: {target_id})"))?;
updates.push(update);
}
// build
BuildBuild => {

View File

@@ -121,14 +121,14 @@ impl State {
.get_some(doc! { "server_id": server_id }, None)
.await?
.into_iter()
.map(|d| async move { self.delete_deployment(&d.id, user).await });
.map(|d| async move { self.delete_deployment(&d.id, user, None, None).await });
let delete_builds = self
.db
.builds
.get_some(doc! { "server_id": server_id }, None)
.await?
.into_iter()
.map(|d| async move { self.delete_deployment(&d.id, user).await });
.map(|d| async move { self.delete_deployment(&d.id, user, None, None).await });
let update_groups = self
.db
.groups

View File

@@ -1,18 +1,23 @@
use std::{cmp::Ordering, collections::HashMap};
use anyhow::Context;
use async_timing_util::unix_timestamp_ms;
use axum::{
extract::{Path, Query},
routing::{delete, get, patch, post},
Extension, Json, Router,
};
use futures_util::TryStreamExt;
use helpers::handle_anyhow_error;
use mungos::{doc, Deserialize, Document, FindOptions, Serialize};
use types::{
traits::Permissioned, AwsBuilderConfig, Build, BuildActionState, BuildVersionsReponse,
Operation, PermissionLevel, UpdateStatus,
monitor_ts_from_unix, traits::Permissioned, unix_from_monitor_ts, AwsBuilderConfig, Build,
BuildActionState, BuildVersionsReponse, Operation, PermissionLevel, UpdateStatus,
};
use typeshare::typeshare;
const NUM_VERSIONS_PER_PAGE: u64 = 10;
const ONE_DAY_MS: i64 = 86400000;
use crate::{
auth::{RequestUser, RequestUserExtension},
@@ -49,12 +54,35 @@ pub struct BuildVersionsQuery {
patch: Option<i32>,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
pub struct BuildStatsQuery {
#[serde(default)]
page: u32,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
pub struct BuildStatsResponse {
pub total_time: f64, // in hours
pub total_count: f64, // number of builds
pub days: Vec<BuildStatsDay>,
}
#[typeshare]
#[derive(Serialize, Deserialize, Default)]
pub struct BuildStatsDay {
pub time: f64,
pub count: f64,
pub ts: f64,
}
pub fn router() -> Router {
Router::new()
.route(
"/:id",
get(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Path(build_id): Path<BuildId>| async move {
let build = state
@@ -68,7 +96,7 @@ pub fn router() -> Router {
.route(
"/list",
get(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Query(query): Query<Document>| async move {
let builds = state
@@ -82,7 +110,7 @@ pub fn router() -> Router {
.route(
"/create",
post(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Json(build): Json<CreateBuildBody>| async move {
let build = state
@@ -96,7 +124,7 @@ pub fn router() -> Router {
.route(
"/create_full",
post(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Json(build): Json<Build>| async move {
let build = spawn_request_action(async move {
@@ -113,7 +141,7 @@ pub fn router() -> Router {
.route(
"/:id/copy",
post(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Path(BuildId { id }): Path<BuildId>,
Json(build): Json<CopyBuildBody>| async move {
@@ -131,7 +159,7 @@ pub fn router() -> Router {
.route(
"/:id/delete",
delete(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Path(build_id): Path<BuildId>| async move {
let build = spawn_request_action(async move {
@@ -148,7 +176,7 @@ pub fn router() -> Router {
.route(
"/update",
patch(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Json(build): Json<Build>| async move {
let build = spawn_request_action(async move {
@@ -165,7 +193,7 @@ pub fn router() -> Router {
.route(
"/:id/build",
post(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Path(build_id): Path<BuildId>| async move {
let update = spawn_request_action(async move {
@@ -182,7 +210,7 @@ pub fn router() -> Router {
.route(
"/:id/action_state",
get(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Path(BuildId { id }): Path<BuildId>| async move {
let action_state = state
@@ -196,7 +224,7 @@ pub fn router() -> Router {
.route(
"/:id/versions",
get(
|Extension(state): StateExtension,
|state: StateExtension,
Extension(user): RequestUserExtension,
Path(BuildId { id }),
Query(query): Query<BuildVersionsQuery>| async move {
@@ -210,7 +238,7 @@ pub fn router() -> Router {
)
.route(
"/aws_builder_defaults",
get(|Extension(state): StateExtension| async move {
get(|state: StateExtension| async move {
Json(AwsBuilderConfig {
access_key_id: String::new(),
secret_access_key: String::new(),
@@ -220,10 +248,17 @@ pub fn router() -> Router {
)
.route(
"/docker_organizations",
get(|Extension(state): StateExtension| async move {
get(|state: StateExtension| async move {
Json(state.config.docker_organizations.clone())
}),
)
.route(
"/stats",
get(|state: StateExtension, query: Query<BuildStatsQuery>| async move {
let stats = state.get_build_stats(query.page).await.map_err(handle_anyhow_error)?;
response!(Json(stats))
}),
)
}
impl State {
@@ -268,7 +303,7 @@ impl State {
Ok(action_state)
}
pub async fn get_build_versions(
async fn get_build_versions(
&self,
id: &str,
user: &RequestUser,
@@ -317,4 +352,86 @@ impl State {
.collect();
Ok(versions)
}
async fn get_build_stats(&self, page: u32) -> anyhow::Result<BuildStatsResponse> {
let curr_ts = unix_timestamp_ms() as i64;
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
let close_ts = next_day - page as i64 * 30 * ONE_DAY_MS;
let open_ts = close_ts - 30 * ONE_DAY_MS;
let mut build_updates = self
.db
.updates
.collection
.find(
doc! {
"start_ts": {
"$gte": monitor_ts_from_unix(open_ts)
.context("open_ts out of bounds")?,
"$lt": monitor_ts_from_unix(close_ts)
.context("close_ts out of bounds")?
},
"operation": Operation::BuildBuild.to_string(),
},
None,
)
.await?;
let mut days = HashMap::<i64, BuildStatsDay>::with_capacity(32);
let mut curr = open_ts;
while curr < close_ts {
let stats = BuildStatsDay {
ts: curr as f64,
..Default::default()
};
days.insert(curr, stats);
curr += ONE_DAY_MS;
}
while let Some(update) = build_updates.try_next().await? {
if let Some(end_ts) = update.end_ts {
let start_ts = unix_from_monitor_ts(&update.start_ts)
.context("failed to parse update start_ts")?;
let end_ts =
unix_from_monitor_ts(&end_ts).context("failed to parse update end_ts")?;
let day = start_ts - start_ts % ONE_DAY_MS;
let mut entry = days.entry(day).or_default();
entry.count += 1.0;
entry.time += ms_to_hour(end_ts - start_ts);
}
}
Ok(BuildStatsResponse::new(days.into_values().collect()))
}
}
impl BuildStatsResponse {
fn new(mut days: Vec<BuildStatsDay>) -> BuildStatsResponse {
days.sort_by(|a, b| {
if a.ts < b.ts {
Ordering::Less
} else {
Ordering::Greater
}
});
let mut total_time = 0.0;
let mut total_count = 0.0;
for day in &days {
total_time += day.time;
total_count += day.count;
}
BuildStatsResponse {
total_time,
total_count,
days,
}
}
}
const MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0;
fn ms_to_hour(duration: i64) -> f64 {
duration as f64 / MS_TO_HOUR_DIVISOR
}

View File

@@ -4,7 +4,7 @@ use anyhow::Context;
use axum::{
extract::{Path, Query},
routing::{delete, get, patch, post},
Extension, Json, Router,
Json, Router,
};
use futures_util::future::join_all;
use helpers::handle_anyhow_error;
@@ -12,7 +12,7 @@ use mungos::{doc, options::FindOneOptions, Deserialize, Document, Serialize};
use types::{
traits::Permissioned, Deployment, DeploymentActionState, DeploymentWithContainerState,
DockerContainerState, DockerContainerStats, Log, Operation, PermissionLevel, Server,
UpdateStatus,
TerminationSignal, UpdateStatus,
};
use typeshare::typeshare;
@@ -55,16 +55,23 @@ pub struct GetContainerLogQuery {
tail: Option<u32>,
}
#[typeshare]
#[derive(Deserialize)]
pub struct StopContainerQuery {
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
}
pub fn router() -> Router {
Router::new()
.route(
"/:id",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id })| async move {
let res = state
.get_deployment_with_container_state(&user, &deployment_id.id)
.get_deployment_with_container_state(&user, &id)
.await
.map_err(handle_anyhow_error)?;
response!(Json(res))
@@ -74,8 +81,8 @@ pub fn router() -> Router {
.route(
"/list",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
|state: StateExtension,
user: RequestUserExtension,
Query(query): Query<Document>| async move {
let deployments = state
.list_deployments_with_container_state(&user, query)
@@ -88,8 +95,8 @@ pub fn router() -> Router {
.route(
"/create",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
|state: StateExtension,
user: RequestUserExtension,
Json(deployment): Json<CreateDeploymentBody>| async move {
let deployment = state
.create_deployment(&deployment.name, deployment.server_id, &user)
@@ -102,8 +109,8 @@ pub fn router() -> Router {
.route(
"/create_full",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
|state: StateExtension,
user: RequestUserExtension,
Json(full_deployment): Json<Deployment>| async move {
let deployment = spawn_request_action(async move {
state
@@ -119,9 +126,9 @@ pub fn router() -> Router {
.route(
"/:id/copy",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(DeploymentId { id }): Path<DeploymentId>,
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id }),
Json(deployment): Json<CopyDeploymentBody>| async move {
let deployment = spawn_request_action(async move {
state
@@ -137,12 +144,13 @@ pub fn router() -> Router {
.route(
"/:id/delete",
delete(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId{ id }),
Query(StopContainerQuery { stop_signal, stop_time })| async move {
let deployment = spawn_request_action(async move {
state
.delete_deployment(&deployment_id.id, &user)
.delete_deployment(&id, &user, stop_signal, stop_time)
.await
.map_err(handle_anyhow_error)
})
@@ -154,8 +162,8 @@ pub fn router() -> Router {
.route(
"/update",
patch(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
|state: StateExtension,
user: RequestUserExtension,
Json(deployment): Json<Deployment>| async move {
let deployment = spawn_request_action(async move {
state
@@ -189,12 +197,12 @@ pub fn router() -> Router {
.route(
"/:id/reclone",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id })| async move {
let update = spawn_request_action(async move {
state
.reclone_deployment(&deployment_id.id, &user)
.reclone_deployment(&id, &user, true)
.await
.map_err(handle_anyhow_error)
})
@@ -206,12 +214,13 @@ pub fn router() -> Router {
.route(
"/:id/deploy",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id }),
Query(StopContainerQuery { stop_signal, stop_time })| async move {
let update = spawn_request_action(async move {
state
.deploy_container(&deployment_id.id, &user)
.deploy_container(&id, &user, stop_signal, stop_time)
.await
.map_err(handle_anyhow_error)
})
@@ -223,12 +232,12 @@ pub fn router() -> Router {
.route(
"/:id/start_container",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id })| async move {
let update = spawn_request_action(async move {
state
.start_container(&deployment_id.id, &user)
.start_container(&id, &user)
.await
.map_err(handle_anyhow_error)
})
@@ -240,12 +249,13 @@ pub fn router() -> Router {
.route(
"/:id/stop_container",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id }),
Query(StopContainerQuery { stop_signal, stop_time })| async move {
let update = spawn_request_action(async move {
state
.stop_container(&deployment_id.id, &user)
.stop_container(&id, &user, stop_signal, stop_time)
.await
.map_err(handle_anyhow_error)
})
@@ -257,12 +267,13 @@ pub fn router() -> Router {
.route(
"/:id/remove_container",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id }),
Query(StopContainerQuery { stop_signal, stop_time })| async move {
let update = spawn_request_action(async move {
state
.remove_container(&deployment_id.id, &user)
.remove_container(&id, &user, stop_signal, stop_time)
.await
.map_err(handle_anyhow_error)
})
@@ -274,12 +285,12 @@ pub fn router() -> Router {
.route(
"/:id/pull",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id })| async move {
let update = spawn_request_action(async move {
state
.pull_deployment_repo(&deployment_id.id, &user)
.pull_deployment_repo(&id, &user)
.await
.map_err(handle_anyhow_error)
})
@@ -291,8 +302,8 @@ pub fn router() -> Router {
.route(
"/:id/action_state",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id }): Path<DeploymentId>| async move {
let action_state = state
.get_deployment_action_states(id, &user)
@@ -305,12 +316,12 @@ pub fn router() -> Router {
.route(
"/:id/log",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>,
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id }),
Query(query): Query<GetContainerLogQuery>| async move {
let log = state
.get_deployment_container_log(&deployment_id.id, &user, query.tail)
.get_deployment_container_log(&id, &user, query.tail)
.await
.map_err(handle_anyhow_error)?;
response!(Json(log))
@@ -320,8 +331,8 @@ pub fn router() -> Router {
.route(
"/:id/stats",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id })| async move {
let stats = state
.get_deployment_container_stats(&id, &user)
@@ -334,8 +345,8 @@ pub fn router() -> Router {
.route(
"/:id/deployed_version",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
|state: StateExtension,
user: RequestUserExtension,
Path(DeploymentId { id })| async move {
let version = state
.get_deployment_deployed_version(&id, &user)

View File

@@ -68,10 +68,15 @@ async fn callback(
.context("failed to generate jwt")?,
None => {
let ts = monitor_timestamp();
let no_users_exist = state.db.users.find_one(None, None).await?.is_none();
let user = User {
username: github_user.login,
avatar: github_user.avatar_url.into(),
github_id: github_id.into(),
enabled: no_users_exist,
admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
created_at: ts.clone(),
updated_at: ts,
..Default::default()

View File

@@ -85,6 +85,7 @@ async fn callback(
.context("failed to generate jwt")?,
None => {
let ts = monitor_timestamp();
let no_users_exist = state.db.users.find_one(None, None).await?.is_none();
let user = User {
username: google_user
.email
@@ -95,6 +96,10 @@ async fn callback(
.to_string(),
avatar: google_user.picture.into(),
google_id: google_id.into(),
enabled: no_users_exist,
admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
created_at: ts.clone(),
updated_at: ts,
..Default::default()

View File

@@ -20,6 +20,7 @@ pub type RequestUserExtension = Extension<Arc<RequestUser>>;
type ExchangeTokenMap = Mutex<HashMap<String, (String, u128)>>;
#[derive(Default)]
pub struct RequestUser {
pub id: String,
pub is_admin: bool,

View File

@@ -47,6 +47,7 @@ async fn create_user_handler(
enabled: no_users_exist,
admin: no_users_exist,
create_server_permissions: no_users_exist,
create_build_permissions: no_users_exist,
created_at: ts.clone(),
updated_at: ts,
..Default::default()

View File

@@ -0,0 +1,40 @@
# core setup
setting up monitor core is fairly simple. there are some requirements to run monitor core:
- a valid configuration file
- an instance of MongoDB to which monitor core can connect
- docker must be installed on the host
## 1. create the configuration file
create a configuration file on the system, for example at `~/.monitor/core.config.toml`, and copy the [example config](https://github.com/mbecker20/monitor/blob/main/config_example/core.config.example.toml). fill in all the necessary information before continuing.
:::note
to enable OAuth2 login, you must create a client on the respective OAuth provider,
for example [google](https://developers.google.com/identity/protocols/oauth2)
or [github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps).
monitor uses the `web application` login flow.
the redirect uri is `<base_url>/auth/google/callback` for google and `<base_url>/auth/github/callback` for github.
:::
## 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).
```sh
docker run -d --name monitor-core \
-v $HOME/.monitor/core.config.toml:/config/config.toml \
-p 9000:9000 \
mbecker2020/monitor_core
```
## first login
monitor core should now be accessible on the specified port, so navigating to `http://<address>:<port>` will display the login page.
the first user to log in will be auto enabled and made admin. any additional users to create accounts will be disabled by default.
## https
monitor core itself only supports http, so a reverse proxy like [caddy](https://caddyserver.com/) should be used for https

View File

@@ -19,6 +19,7 @@ const sidebars = {
// But you can create a sidebar manually
docs: [
"intro",
"core-setup",
{
type: "category",
label: "connecting servers",

View File

@@ -17,8 +17,8 @@
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
display: grid;
gap: 1rem;
grid-template-columns: 1fr 1fr;
width: fit-content;
}

View File

@@ -17,22 +17,44 @@ function HomepageHeader() {
<div style={{ display: "flex", gap: "1rem", justifyContent: "center" }}>
<div style={{ position: "relative" }}>
<MonitorLogo width="600px" />
<h1 className="hero__title" style={{ margin: 0, position: "absolute", top: "40%", left: "50%", transform: "translate(-50%, -50%)" }}>
<h1
className="hero__title"
style={{
margin: 0,
position: "absolute",
top: "40%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
monitor
</h1>
</div>
</div>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/intro">
docs
</Link>
<Link
className="button button--secondary button--lg"
to="https://github.com/mbecker20/monitor"
>
github
</Link>
<div style={{ display: "flex", justifyContent: "center" }}>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/intro">
docs
</Link>
<Link
className="button button--secondary button--lg"
to="https://github.com/mbecker20/monitor"
>
github
</Link>
<Link
className="button button--secondary button--lg"
to="https://github.com/mbecker20/monitor#readme"
style={{
width: "100%",
boxSizing: "border-box",
gridColumn: "span 2",
}}
>
screenshots
</Link>
</div>
</div>
</div>
</header>

View File

@@ -14,7 +14,11 @@ export const NewGroup: Component<{}> = (p) => {
<Show
when={showNew()}
fallback={
<button class="green" onClick={toggleShowNew} style={{ height: "100%" }}>
<button
class="green"
onClick={toggleShowNew}
style={{ height: "100%" }}
>
<Icon type="plus" />
</button>
}
@@ -33,8 +37,12 @@ export const NewDeployment: Component<{ serverID: string }> = (p) => {
<Show
when={showNew()}
fallback={
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
<Icon type="plus" />
<button
class="green"
onClick={toggleShowNew}
style={{ width: "100%", height: "fit-content" }}
>
<Icon type="plus" width="1.2rem" />
</button>
}
>
@@ -56,8 +64,12 @@ export const NewBuild: Component<{}> = (p) => {
<Show
when={showNew()}
fallback={
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
<Icon type="plus" />
<button
class="green"
onClick={toggleShowNew}
style={{ width: "100%", height: "fit-content" }}
>
<Icon type="plus" width="1.2rem" />
</button>
}
>
@@ -87,14 +99,14 @@ const New: Component<{
}
};
return (
<Flex justifyContent="space-between">
<Flex justifyContent="space-between" style={{ height: "fit-content", width: "100%" }}>
<Input
ref={inputRef}
placeholder={p.placeholder}
value={name()}
onEdit={setName}
onEnter={create}
style={{ width: "20rem" }}
style={{ width: "100%", "min-width": "20rem" }}
/>
<Flex gap="0.4rem">
<button class="green" onClick={create}>

View File

@@ -1,4 +1,4 @@
import { Component, Match, Show, Switch } from "solid-js";
import { Component, Match, Setter, Show, Switch, createSignal } from "solid-js";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
@@ -15,8 +15,11 @@ import {
DockerContainerState,
PermissionLevel,
ServerStatus,
TerminationSignal,
TerminationSignalLabel,
} from "../../types";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
import Selector from "../shared/menu/Selector";
const Actions: Component<{}> = (p) => {
const { deployments, builds, servers, getPermissionOnDeployment } =
@@ -166,7 +169,13 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
// const deployment = () => deployments.get(params.id)!;
const actions = useActionStates();
const { deployments } = useAppState();
const name = () => deployments.get(params.id)?.deployment.name;
const deployment = () => deployments.get(params.id);
const name = () => deployment()?.deployment.name;
const [termSignalLabel, setTermSignalLabel] =
createSignal<TerminationSignalLabel>({
signal: "default" as TerminationSignal,
label: "",
});
return (
<Show
when={!actions.deploying}
@@ -194,9 +203,19 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
<ConfirmMenuButton
class="green"
onConfirm={() => {
client.deploy_container(params.id);
client.deploy_container(params.id, {
stop_signal: ((termSignalLabel().signal as any) === "default"
? undefined
: termSignalLabel().signal) as TerminationSignal,
});
}}
title="redeploy container"
configs={
<TermSignalSelector
termSignalLabel={termSignalLabel()}
setTermSignalLabel={setTermSignalLabel}
/>
}
match={name()!}
>
<Icon type={"reset"} />
@@ -216,6 +235,11 @@ const RemoveContainer = () => {
const actions = useActionStates();
const { deployments } = useAppState();
const name = () => deployments.get(params.id)?.deployment.name;
const [termSignalLabel, setTermSignalLabel] =
createSignal<TerminationSignalLabel>({
signal: "default" as TerminationSignal,
label: "",
});
return (
<Show
when={!actions.removing}
@@ -230,9 +254,19 @@ const RemoveContainer = () => {
<ConfirmMenuButton
class="red"
onConfirm={() => {
client.remove_container(params.id);
client.remove_container(params.id, {
stop_signal: ((termSignalLabel().signal as any) === "default"
? undefined
: termSignalLabel().signal) as TerminationSignal,
});
}}
title="destroy container"
configs={
<TermSignalSelector
termSignalLabel={termSignalLabel()}
setTermSignalLabel={setTermSignalLabel}
/>
}
match={name()!}
>
<Icon type="trash" />
@@ -282,6 +316,11 @@ const Stop = () => {
const actions = useActionStates();
const { deployments } = useAppState();
const name = () => deployments.get(params.id)?.deployment.name;
const [termSignalLabel, setTermSignalLabel] =
createSignal<TerminationSignalLabel>({
signal: "default" as TerminationSignal,
label: "",
});
return (
<Show
when={!actions.stopping}
@@ -296,9 +335,19 @@ const Stop = () => {
<ConfirmMenuButton
class="orange"
onConfirm={() => {
client.stop_container(params.id);
client.stop_container(params.id, {
stop_signal: ((termSignalLabel().signal as any) === "default"
? undefined
: termSignalLabel().signal) as TerminationSignal,
});
}}
title="stop container"
configs={
<TermSignalSelector
termSignalLabel={termSignalLabel()}
setTermSignalLabel={setTermSignalLabel}
/>
}
match={name()!}
>
<Icon type="pause" />
@@ -374,4 +423,49 @@ const Reclone = () => {
);
};
const TermSignalSelector: Component<{
termSignalLabel: TerminationSignalLabel;
setTermSignalLabel: Setter<TerminationSignalLabel>;
}> = (p) => {
const params = useParams();
const { deployments } = useAppState();
const deployment = () => deployments.get(params.id);
return (
<Show
when={
deployment()?.state === DockerContainerState.Running &&
(deployment()?.deployment.term_signal_labels?.length || 0) > 0
}
>
<Flex
class="full-width wrap"
justifyContent="space-between"
alignItems="center"
>
<div class="dimmed">termination signal: </div>
<Selector
targetClass="blue"
selected={p.termSignalLabel}
items={[
{ signal: "default", label: "" },
...(deployment()?.deployment.term_signal_labels || []),
]}
itemMap={({ signal, label }) => (
<Flex gap="0.5rem" alignItems="center">
<div>{signal}</div>
<Show when={label.length > 0}>
<div class="dimmed">{label}</div>
</Show>
</Flex>
)}
onSelect={(signal) =>
p.setTermSignalLabel(signal as TerminationSignalLabel)
}
position="bottom right"
/>
</Flex>
</Show>
);
};
export default Actions;

View File

@@ -3,7 +3,7 @@ import { Component, Show } from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import { PermissionLevel } from "../../types";
import { PermissionLevel, TerminationSignal } from "../../types";
import Description from "../Description";
import NotFound from "../NotFound";
import Grid from "../shared/layout/Grid";
@@ -16,6 +16,13 @@ import Updates from "./Updates";
const POLLING_RATE = 10000;
// let interval = -1;
export const TERM_SIGNALS = [
TerminationSignal.SigTerm,
TerminationSignal.SigInt,
TerminationSignal.SigQuit,
TerminationSignal.SigHup,
];
const Deployment: Component<{}> = (p) => {
const { user, user_id } = useUser();
const { servers, deployments } = useAppState();

View File

@@ -21,6 +21,8 @@ import { useAppDimensions } from "../../../../state/DimensionProvider";
import SimpleTabs from "../../../shared/tabs/SimpleTabs";
import ExtraArgs from "./container/ExtraArgs";
import WebhookUrl from "./container/WebhookUrl";
import RedeployOnBuild from "./container/RedeployOnBuild";
import TerminationSignals from "./container/TerminationSignals";
const Config: Component<{}> = () => {
const { deployment, reset, save, userCanUpdate } = useConfig();
@@ -40,6 +42,7 @@ const Config: Component<{}> = () => {
<Grid class="config-items scroller" placeItems="start center">
<Image />
<DockerAccount />
<TerminationSignals />
<Network />
<Restart />
<Env />
@@ -47,6 +50,7 @@ const Config: Component<{}> = () => {
<Mounts />
<ExtraArgs />
<PostImage />
<RedeployOnBuild />
<Show when={isMobile()}>
<div style={{ height: "1rem" }} />
</Show>

View File

@@ -3,8 +3,8 @@ import { client } from "../../../../..";
import { useAppState } from "../../../../../state/StateProvider";
import {
combineClasses,
string_to_version,
version_to_string,
readableVersion,
readableMonitorTimestamp,
} from "../../../../../util/helpers";
import Input from "../../../../shared/Input";
import Flex from "../../../../shared/layout/Flex";
@@ -14,11 +14,6 @@ import { useConfig } from "../Provider";
const Image: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
const { builds } = useAppState();
const [versions] = createResource(() => {
if (deployment.build_id) {
return client.get_build_versions(deployment.build_id);
}
});
return (
<Flex
class={combineClasses("config-item shadow")}
@@ -41,7 +36,8 @@ const Image: Component<{}> = (p) => {
<Selector
targetClass="blue"
selected={
(deployment.build_id && (builds.get(deployment.build_id)?.name || "unknown")) ||
(deployment.build_id &&
(builds.get(deployment.build_id)?.name || "unknown")) ||
"custom image"
}
items={[
@@ -65,33 +61,7 @@ const Image: Component<{}> = (p) => {
useSearch
/>
<Show when={deployment.build_id}>
<Selector
targetClass="blue"
selected={
deployment.build_version
? `v${version_to_string(deployment.build_version)}`
: "latest"
}
items={[
"latest",
...(versions()?.map(
(v) => `v${version_to_string(v.version)}`
) || []),
]}
onSelect={(version) => {
if (version === "latest") {
setDeployment("build_version", undefined);
} else {
setDeployment(
"build_version",
string_to_version(version.replace("v", ""))
);
}
}}
position="bottom right"
disabled={!userCanUpdate()}
useSearch
/>
<VersionSelector />
</Show>
</Show>
</Flex>
@@ -100,3 +70,54 @@ const Image: Component<{}> = (p) => {
};
export default Image;
const VersionSelector: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
const [versions] = createResource(() => {
if (deployment.build_id) {
return client.get_build_versions(deployment.build_id);
}
});
const selected = () => ({
version: deployment.build_version || {
major: 0,
minor: 0,
patch: 0,
},
ts: "",
});
return (
<Selector
targetClass="blue"
selected={selected()}
items={[
{ version: { major: 0, minor: 0, patch: 0 }, ts: "" },
...(versions() || []),
]}
itemMap={({ version, ts }) => (
<>
<div>
{version.major === 0 && version.minor === 0 && version.patch === 0
? "latest"
: readableVersion(version)}
</div>
<Show when={ts.length > 0}>
<div class="dimmed">{readableMonitorTimestamp(ts)}</div>
</Show>
</>
)}
searchItemMap={({ version }) => readableVersion(version)}
onSelect={({ version, ts }) => {
if (ts.length === 0) {
setDeployment("build_version", undefined);
} else {
setDeployment("build_version", version);
}
}}
position="bottom right"
disabled={!userCanUpdate()}
useSearch
/>
);
}

View File

@@ -0,0 +1,31 @@
import { Component, Show } from "solid-js";
import { useConfig } from "../Provider";
import Flex from "../../../../shared/layout/Flex";
const RedeployOnBuild: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
return (
<Show when={deployment.build_id}>
<Flex
class="config-item shadow"
justifyContent="space-between"
alignItems="center"
>
<h1>redeploy on build</h1>
<Show
when={userCanUpdate()}
fallback={<h2>{deployment.redeploy_on_build ? "yes" : "no"}</h2>}
>
<button
class={deployment.redeploy_on_build ? "green" : "red"}
onClick={() => setDeployment("redeploy_on_build", (v) => !v)}
>
{deployment.redeploy_on_build ? "yes" : "no"}
</button>
</Show>
</Flex>
</Show>
);
};
export default RedeployOnBuild;

View File

@@ -0,0 +1,92 @@
import { Component, For, Show, createSignal } from "solid-js";
import { useConfig } from "../Provider";
import Grid from "../../../../shared/layout/Grid";
import Flex from "../../../../shared/layout/Flex";
import Icon from "../../../../shared/Icon";
import { TERM_SIGNALS } from "../../../Deployment";
import { TerminationSignal } from "../../../../../types";
import Input from "../../../../shared/Input";
import Menu from "../../../../shared/menu/Menu";
const TerminationSignals: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
const signals_to_add = () =>
TERM_SIGNALS.filter(
(sig) =>
!deployment.term_signal_labels
?.map(({ signal }) => signal)
.includes(sig)
);
const onAdd = (signal: TerminationSignal) => {
setDeployment("term_signal_labels", (term_signals: any) => [
...term_signals,
{ signal, label: "" },
]);
};
const onRemove = (index: number) => {
setDeployment("term_signal_labels", (term_signals) =>
term_signals?.filter((_, i) => i !== index)
);
};
const [menuOpen, setMenuOpen] = createSignal(false);
return (
<Grid class="config-item shadow">
<Flex alignItems="center" justifyContent="space-between">
<h1>termination signals</h1>
<Show when={userCanUpdate()}>
<Menu
show={menuOpen()}
close={() => setMenuOpen(false)}
target={
<button class="green" onClick={() => setMenuOpen(true)}>
<Icon type="plus" />
</button>
}
content={
<For each={signals_to_add()}>
{(signal) => (
<button
class="grey"
onClick={() => {
onAdd(signal);
setMenuOpen(false);
}}
>
<h2>{signal}</h2>
</button>
)}
</For>
}
/>
</Show>
</Flex>
<Show when={(deployment.term_signal_labels?.length || 0) > 0}>
<Grid gridTemplateColumns="auto 1fr auto" placeItems="center start">
<For each={deployment.term_signal_labels}>
{({ signal, label }, index) => (
<>
<h2>{signal}</h2>
<Input
class="full-width"
placeholder="label this termination signal"
value={label}
onConfirm={(value) =>
setDeployment("term_signal_labels", index(), "label", value)
}
disabled={!userCanUpdate()}
/>
<Show when={userCanUpdate()}>
<button class="red" onClick={() => onRemove(index())}>
<Icon type="minus" />
</button>
</Show>
</>
)}
</For>
</Grid>
</Show>
</Grid>
);
};
export default TerminationSignals;

View File

@@ -0,0 +1,131 @@
import { Component, Show, createMemo } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import LightweightChart from "../shared/LightweightChart";
import Loading from "../shared/loading/Loading";
import { COLORS } from "../../style/colors";
import { BuildStatsResponse } from "../../util/client_types";
import { useLocalStorage } from "../../util/hooks";
const BuildSummary: Component<{}> = (p) => {
const { build_stats } = useAppState();
return (
<Grid class="full-size card" gridTemplateRows="auto 1fr" style={{ "padding-bottom": "0.6rem" }}>
<Show
when={build_stats.get()}
fallback={
<Grid class="full-size" placeItems="center">
<Loading type="three-dot" />
</Grid>
}
>
<Flex
justifyContent="space-between"
alignItems="center"
style={{ height: "fit-content" }}
>
<h2>last 30 days</h2>
<Flex>
<Flex alignItems="center" gap="0.5rem">
<div class="dimmed">build time: </div>
<h2>{build_stats.get()?.total_time.toFixed(1)} hrs</h2>
</Flex>
<Flex alignItems="center" gap="0.5rem">
<div class="dimmed">build count: </div>
<h2>{build_stats.get()?.total_count.toFixed()}</h2>
</Flex>
</Flex>
</Flex>
<BuildStatsChart build_stats={build_stats.get()!} />
</Show>
</Grid>
);
};
export default BuildSummary;
const BuildStatsChart: Component<{ build_stats: BuildStatsResponse }> = (p) => {
const [mode, setMode] = useLocalStorage<"time" | "count">(
"time",
"build-stats-chart-mode-v2"
);
const max = createMemo(() => {
return p.build_stats.days.reduce((max, day) => {
if (mode() === "count") {
if (day.count > max) {
return day.count;
} else return max;
} else {
if (day.time > max) {
return day.time;
} else return max;
}
}, 0);
});
return (
<div class="full-size" style={{ position: "relative" }}>
<LightweightChart
class="full-size"
style={{ "min-height": "200px" }}
histograms={[
{
line: p.build_stats.days.map((day) => ({
value: mode() === "count" ? day.count : (day.time * 60),
time: day.ts / 1000,
color:
mode() === "count"
? day.count > max() * 0.7
? COLORS.red
: day.count > max() * 0.35
? COLORS.orange
: COLORS.green
: day.time > max() * 0.7
? COLORS.red
: day.time > max() * 0.35
? COLORS.orange
: COLORS.green,
})),
priceLineVisible: false,
// priceFormat:
// mode() === "count"
// ? {
// minMove: 1,
// }
// : undefined,
priceFormat: {
minMove: 1,
}
},
]}
timeVisible={false}
options={{
grid: {
horzLines: { visible: false },
vertLines: { visible: false },
},
}}
disableScroll
/>
<button
class="blue opaque"
style={{
position: "absolute",
top: 0,
right: 0,
"z-index": 20,
padding: "0.3rem",
}}
onClick={() => setMode(mode => {
if (mode === "count") {
return "time"
} else {
return "count"
}
})}
>
{mode()}{mode() === "time" ? " (min)" : ""}
</button>
</div>
);
};

View File

@@ -1,30 +1,44 @@
import {
Component,
Component, Match, Show, Switch,
} from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import Grid from "../shared/layout/Grid";
import SimpleTabs from "../shared/tabs/SimpleTabs";
import { ControlledSimpleTabs } from "../shared/tabs/SimpleTabs";
import Summary from "./Summary";
import Builds from "./Tree/Builds";
import Groups from "./Tree/Groups";
import { TreeProvider } from "./Tree/Provider";
import Updates from "./Updates/Updates";
import { useLocalStorage } from "../../util/hooks";
import BuildSummary from "./BuildSummary";
const Home: Component<{}> = (p) => {
const { isSemiMobile } = useAppDimensions();
const [selectedTab, setTab] = useLocalStorage<"servers" | "builds">(
"servers",
"home-groups-servers-tab-v2"
);
return (
<>
<Grid
style={{ width: "100%" }}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<Summary />
<Switch>
<Match when={selectedTab() === "servers"}>
<Summary />
</Match>
<Match when={selectedTab() === "builds"}>
<BuildSummary />
</Match>
</Switch>
<Updates />
</Grid>
<TreeProvider>
<SimpleTabs
<ControlledSimpleTabs
selected={selectedTab}
set={setTab as any}
containerStyle={{ width: "100%" }}
localStorageKey="home-groups-servers-tab-v1"
tabs={[
{
title: "servers",
@@ -32,8 +46,8 @@ const Home: Component<{}> = (p) => {
},
{
title: "builds",
element: () => <Builds />
}
element: () => <Builds />,
},
]}
/>
</TreeProvider>

View File

@@ -74,7 +74,11 @@ const Groups: Component<{}> = (p) => {
/>
<Flex alignItems="center" style={{ width: "fit-content" }}>
<Selector
label={<div class="dimmed">sort by:</div>}
label={
<div class="dimmed" style={{ "white-space": "nowrap" }}>
sort by:
</div>
}
selected={sort()}
items={TREE_SORTS as any as string[]}
onSelect={(mode) => setSort(mode as TreeSortType)}

View File

@@ -7,6 +7,7 @@ import { PermissionLevel } from "../../types";
import { NewDeployment } from "../New";
import Deployment from "./Deployment";
import { useAppState } from "../../state/StateProvider";
import Flex from "../shared/layout/Flex";
const ServerChildren: Component<{ id: string }> = (p) => {
const { user } = useUser();
@@ -35,7 +36,9 @@ const ServerChildren: Component<{ id: string }> = (p) => {
PermissionLevel.Update
}
>
<NewDeployment serverID={p.id} />
<Flex class="full-width" alignItems="center">
<NewDeployment serverID={p.id} />
</Flex>
</Show>
</Grid>
</div>

View File

@@ -15,6 +15,7 @@ const ConfirmMenuButton: Component<{
title: string;
match: string;
info?: JSX.Element;
configs?: JSX.Element;
children: JSX.Element;
}> = (p) => {
const [show, toggleShow] = useToggle();
@@ -38,6 +39,7 @@ const ConfirmMenuButton: Component<{
title={p.title}
match={p.match}
info={p.info}
configs={p.configs}
onConfirm={p.onConfirm}
/>
)}
@@ -52,6 +54,7 @@ const ConfirmMenuContent: Component<{
match: string;
onConfirm?: () => void;
info?: JSX.Element;
configs?: JSX.Element;
}> = (p) => {
const [input, setInput] = createSignal("");
return (
@@ -71,6 +74,7 @@ const ConfirmMenuContent: Component<{
value={input()}
autofocus
/>
{p.configs}
<ConfirmButton
class={p.class}
style={{ width: "100%" }}

View File

@@ -1,7 +1,11 @@
import {
AreaSeriesPartialOptions,
BarSeriesPartialOptions,
ChartOptions,
ColorType,
createChart,
DeepPartial,
HistogramSeriesPartialOptions,
IChartApi,
ISeriesApi,
LineSeriesPartialOptions,
@@ -18,6 +22,7 @@ import {
export type LightweightValue = {
time: number | string;
value: number;
color?: string;
};
export type LightweightLine = {
@@ -28,9 +33,14 @@ export type LightweightArea = {
line: LightweightValue[];
} & AreaSeriesPartialOptions;
export type LightweightHistogram = {
line: LightweightValue[];
} & HistogramSeriesPartialOptions;
const LightweightChart: Component<{
lines?: LightweightLine[];
areas?: LightweightArea[];
histograms?: LightweightHistogram[];
class?: string;
style?: JSX.CSSProperties;
width?: string;
@@ -38,11 +48,15 @@ const LightweightChart: Component<{
disableScroll?: boolean;
onCreateLineSeries?: (series: ISeriesApi<"Line">) => void;
onCreateAreaSeries?: (series: ISeriesApi<"Area">) => void;
onCreateHistogramSeries?: (series: ISeriesApi<"Histogram">) => void;
timeVisible?: boolean;
options?: DeepPartial<ChartOptions>;
}> = (p) => {
let el: HTMLDivElement;
const [chart, setChart] = createSignal<IChartApi>();
let lineSeries: ISeriesApi<"Line">[] = [];
let areaSeries: ISeriesApi<"Area">[] = [];
let histogramSeries: ISeriesApi<"Histogram">[] = [];
const [loaded, setLoaded] = createSignal(false);
onMount(() => {
if (loaded()) return;
@@ -58,9 +72,10 @@ const LightweightChart: Component<{
horzLines: { color: "#3f454d" },
vertLines: { color: "#3f454d" },
},
timeScale: { timeVisible: true },
timeScale: { timeVisible: p.timeVisible ?? true },
handleScroll: p.disableScroll ? false : true,
handleScale: p.disableScroll ? false : true,
...p.options
});
chart.timeScale().fitContent();
setChart(chart);
@@ -95,6 +110,20 @@ const LightweightChart: Component<{
});
areaSeries = series;
}
for (const series of histogramSeries) {
chart()!.removeSeries(series);
}
if (p.histograms) {
const series = p.histograms.map((line) => {
const series = chart()!.addHistogramSeries(line);
series.setData(line.line as any);
if (p.onCreateHistogramSeries) {
p.onCreateHistogramSeries(series);
}
return series;
});
histogramSeries = series;
}
chart()!.timeScale().fitContent();
}
});

View File

@@ -15,10 +15,10 @@ import { Position } from "./helpers";
import Menu from "./Menu";
import s from "./menu.module.scss";
const Selector: Component<{
selected: string;
items: string[];
onSelect?: (item: string, index: number) => void;
const Selector = <T,>(p: {
selected: T;
items: T[];
onSelect?: (item: T, index: number) => void;
position?: Position;
targetClass?: string;
targetStyle?: JSX.CSSProperties;
@@ -33,12 +33,13 @@ const Selector: Component<{
itemClass?: string;
itemStyle?: JSX.CSSProperties;
label?: JSXElement;
itemMap?: (item: string) => string;
}> = (p) => {
itemMap?: (item: T) => JSXElement;
searchItemMap?: (item: T) => string;
}) => {
const [show, toggle] = useToggle();
const [search, setSearch] = createSignal("");
let search_ref: HTMLInputElement | undefined;
const current = () => (p.itemMap ? p.itemMap(p.selected) : p.selected);
const current = () => (p.itemMap ? p.itemMap(p.selected) : p.selected as JSXElement);
createEffect(() => {
if (show()) setTimeout(() => search_ref?.focus(), 200);
});
@@ -87,9 +88,11 @@ const Selector: Component<{
each={
p.useSearch
? p.items.filter((item) =>
p.itemMap
? p.itemMap(item).includes(search())
: item.includes(search())
p.searchItemMap
? p.searchItemMap(item).includes(search())
: p.itemMap && typeof p.itemMap(item) === "string"
? (p.itemMap(item) as string).includes(search())
: (item as string).includes(search())
)
: p.items
}
@@ -107,7 +110,7 @@ const Selector: Component<{
}}
class={combineClasses(p.itemClass, s.SelectorItem)}
>
{p.itemMap ? p.itemMap(item) : item}
{p.itemMap ? p.itemMap(item) : item as string}
</button>
)}
</For>

View File

@@ -9,7 +9,7 @@ export type Position =
| "bottom right"
| "bottom center";
export function getPositionClass(position: Position = "bottom") {
export function getPositionClass(position: Position = "bottom right") {
switch (position) {
case "left":
return s.Left;

View File

@@ -28,19 +28,19 @@ const SimpleTabs: Component<{
containerClass?: string;
containerStyle?: JSX.CSSProperties;
}> = (p) => {
const def = p.defaultSelected ? p.defaultSelected : p.tabs[0].title;
const defaultSelected = p.defaultSelected ? p.defaultSelected : p.tabs[0].title;
const [selected, set] = p.localStorageKey
? useLocalStorage(def, p.localStorageKey)
: createSignal(def);
? useLocalStorage(defaultSelected, p.localStorageKey)
: createSignal(defaultSelected);
createEffect(() => {
if (p.tabs.filter((tab) => tab.title === selected())[0] === undefined) {
set(p.tabs[0].title);
}
});
return <ControlledTabs selected={selected} set={set} {...p} />;
return <ControlledSimpleTabs selected={selected} set={set} {...p} />;
};
export const ControlledTabs: Component<{
export const ControlledSimpleTabs: Component<{
tabs: Tab[];
selected: Accessor<string>;
set: LocalStorageSetter<string>;

View File

@@ -12,7 +12,7 @@ import {
convertTsMsToLocalUnixTsInSecs,
get_to_one_sec_divisor,
} from "../../util/helpers";
import { useLocalStorage, useLocalStorageToggle } from "../../util/hooks";
import { useLocalStorageToggle } from "../../util/hooks";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import LightweightChart, { LightweightValue } from "../shared/LightweightChart";

View File

@@ -3,6 +3,7 @@ import { createContext, createResource, ParentComponent, Resource, useContext }
import { useWindowKeyDown } from "../util/hooks";
import {
useBuilds,
useBuildStats,
useDeployments,
useGroups,
useProcedures,
@@ -19,6 +20,7 @@ import connectToWs from "./ws";
import { useUser } from "./UserProvider";
import { AwsBuilderConfig, PermissionLevel, UpdateTarget } from "../types";
import { client } from "..";
import { BuildStatsResponse } from "../util/client_types";
export type State = {
usernames: ReturnType<typeof useUsernames>;
@@ -43,6 +45,7 @@ export type State = {
docker_organizations: Resource<string[]>;
github_webhook_base_url: Resource<string>;
name_from_update_target: (target: UpdateTarget) => string;
build_stats: ReturnType<typeof useBuildStats>;
};
const context = createContext<
@@ -93,6 +96,7 @@ export const AppStateProvider: ParentComponent = (p) => {
});
},
builds,
build_stats: useBuildStats(),
getPermissionOnBuild: (id: string) => {
const build = builds.get(id)!;
const permissions = build.permissions![userId] as
@@ -161,7 +165,7 @@ export const AppStateProvider: ParentComponent = (p) => {
} else {
return "unknown"
}
}
},
};
// createEffect(() => {

View File

@@ -13,6 +13,7 @@ import {
intoCollection,
keepOnlyInObj,
} from "../util/helpers";
import { BuildStatsResponse } from "../util/client_types";
type Collection<T> = Record<string, T>;
@@ -245,6 +246,26 @@ export function useBuilds() {
);
}
let build_stats_loading = false;
export function useBuildStats() {
const [stats, set] = createSignal<BuildStatsResponse>();
const reload = () => {
client.get_build_stats().then(set);
};
const get = () => {
if (stats()) {
return stats();
} else if (!build_stats_loading) {
build_stats_loading = true;
reload()
}
}
return {
get,
reload,
};
}
const deploymentIdPath = ["deployment", "_id", "$oid"];
export function useDeployments() {

View File

@@ -63,7 +63,15 @@ function connectToWs(state: State) {
}
async function handleMessage(
{ deployments, builds, servers, groups, procedures, updates }: State,
{
deployments,
builds,
servers,
groups,
procedures,
updates,
build_stats,
}: State,
update: Update
) {
updates.addOrUpdate(update);
@@ -135,13 +143,16 @@ async function handleMessage(
if (update.status === UpdateStatus.Complete) {
builds.delete(update.target.id!);
}
} else if (
[Operation.UpdateBuild, Operation.BuildBuild].includes(update.operation)
) {
} else if (update.operation === Operation.UpdateBuild) {
if (update.status === UpdateStatus.Complete) {
const build = await client.get_build(update.target.id!);
builds.update(build);
}
} else if (update.operation === Operation.BuildBuild) {
if (update.status === UpdateStatus.Complete) {
build_stats.reload();
client.get_build(update.target.id!).then((build) => builds.update(build));
}
}
// server

View File

@@ -180,6 +180,10 @@ svg {
background-color: rgba(c.$blue, 0.8);
}
.blue.opaque {
background-color: c.$blue;
}
.blue:hover {
background-color: c.$lightblue;
}

View File

@@ -105,7 +105,9 @@ export interface Deployment {
permissions?: PermissionsMap;
skip_secret_interp?: boolean;
docker_run_args: DockerRunArgs;
term_signal_labels?: TerminationSignalLabel[];
build_id?: string;
redeploy_on_build?: boolean;
build_version?: Version;
repo?: string;
branch?: string;
@@ -134,6 +136,11 @@ export interface DeploymentActionState {
renaming: boolean;
}
export interface TerminationSignalLabel {
signal: TerminationSignal;
label: string;
}
export interface DockerRunArgs {
image: string;
ports?: Conversion[];
@@ -412,6 +419,13 @@ export enum RestartMode {
UnlessStopped = "unless-stopped",
}
export enum TerminationSignal {
SigHup = "SIGHUP",
SigInt = "SIGINT",
SigQuit = "SIGQUIT",
SigTerm = "SIGTERM",
}
export enum AccountType {
Github = "github",
Docker = "docker",

View File

@@ -29,6 +29,8 @@ import {
UserCredentials,
} from "../types";
import {
BuildStatsQuery,
BuildStatsResponse,
BuildVersionsQuery,
CopyBuildBody,
CopyDeploymentBody,
@@ -44,6 +46,7 @@ import {
ModifyUserEnabledBody,
PermissionsUpdateBody,
RenameDeploymentBody,
StopContainerQuery,
UpdateDescriptionBody,
} from "./client_types";
import { generateQuery, QueryObject } from "./helpers";
@@ -217,20 +220,20 @@ export class Client {
return this.post(`/api/deployment/${deployment_id}/pull`);
}
deploy_container(deployment_id: string): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/deploy`);
deploy_container(deployment_id: string, query?: StopContainerQuery): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/deploy${generateQuery(query as any)}`);
}
start_container(deployment_id: string): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/start_container`);
}
stop_container(deployment_id: string): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/stop_container`);
stop_container(deployment_id: string, query?: StopContainerQuery): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/stop_container${generateQuery(query as any)}`);
}
remove_container(deployment_id: string): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/remove_container`);
remove_container(deployment_id: string, query?: StopContainerQuery): Promise<Update> {
return this.post(`/api/deployment/${deployment_id}/remove_container${generateQuery(query as any)}`);
}
async download_container_log(
@@ -401,6 +404,10 @@ export class Client {
return this.get(`/api/build/${id}/versions${generateQuery(query as any)}`);
}
get_build_stats(query?: BuildStatsQuery): Promise<BuildStatsResponse> {
return this.get(`/api/build/stats${generateQuery(query as any)}`);
}
create_build(body: CreateBuildBody): Promise<Build> {
return this.post("/api/build/create", body);
}

View File

@@ -2,7 +2,7 @@
Generated by typeshare 1.0.0
*/
import { PermissionLevel, PermissionsTarget, UpdateTarget } from "../types";
import { PermissionLevel, PermissionsTarget, TerminationSignal, UpdateTarget } from "../types";
export interface CreateBuildBody {
name: string;
@@ -19,6 +19,22 @@ export interface BuildVersionsQuery {
patch?: number;
}
export interface BuildStatsQuery {
page?: number;
}
export interface BuildStatsResponse {
total_time: number;
total_count: number;
days: BuildStatsDay[];
}
export interface BuildStatsDay {
time: number;
count: number;
ts: number;
}
export interface CreateDeploymentBody {
name: string;
server_id: string;
@@ -37,6 +53,11 @@ export interface GetContainerLogQuery {
tail?: number;
}
export interface StopContainerQuery {
stop_signal?: TerminationSignal;
stop_time?: number;
}
export interface CreateGroupBody {
name: string;
}

View File

@@ -1,6 +1,6 @@
[package]
name = "db_client"
version = "0.3.0"
version = "0.3.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_helpers"
version = "0.3.0"
version = "0.3.2"
edition = "2021"
authors = ["MoghTech"]
description = "helpers used as dependency for mogh tech monitor"

View File

@@ -1,26 +1,9 @@
use std::{borrow::Borrow, net::SocketAddr, str::FromStr};
use std::{net::SocketAddr, str::FromStr};
use anyhow::anyhow;
use axum::http::StatusCode;
use rand::{distributions::Alphanumeric, Rng};
use types::Log;
pub fn parse_comma_seperated_list<T: FromStr>(
comma_sep_list: impl Borrow<str>,
) -> anyhow::Result<Vec<T>> {
comma_sep_list
.borrow()
.split(",")
.filter(|item| item.len() > 0)
.map(|item| {
let item = item
.parse()
.map_err(|_| anyhow!("error parsing string {item} into type T"))?;
Ok::<T, anyhow::Error>(item)
})
.collect()
}
pub fn get_socket_addr(port: u16) -> SocketAddr {
SocketAddr::from_str(&format!("0.0.0.0:{}", port)).expect("failed to parse socket addr")
}

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_client"
version = "0.3.0"
version = "0.3.2"
edition = "2021"
authors = ["MoghTech"]
description = "a client to interact with the monitor system"
@@ -9,7 +9,8 @@ license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
monitor_types = "0.3.0"
monitor_types = "0.3.2"
# monitor_types = { path = "../types" }
reqwest = { version = "0.11", features = ["json"] }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio = { version = "1.25", features = ["full"] }

View File

@@ -0,0 +1,110 @@
# monitor client
## *interact with the monitor system programatically*
with this crate you can leverage all the functionality of monitor through rust code. for example, you can...
- create and manage complex deployments
- execute builds on specific schedules
- monitor server stats programatically
- program response actions to monitor updates sent over websocket
## initialize the client
you can initialize the client by directly passing args to the various initializers:
```rust
use monitor_client::MonitorClient;
let MONITOR_URL: &str = "https://monitor.mogh.tech";
let monitor = MonitorClient::new_with_token(MONITOR_URL, jwt_token).await?; // pass a valid jwt
let monitor = MonitorClient::new_with_password(MONITOR_URL, username, password).await?; // pass local user credentials
let monitor = MonitorClient::new_with_secret(MONITOR_URL, username, secret).await?; // pass api secret
```
or from the application environment / dotenv:
```sh
MONITOR_URL=https://monitor.mogh.tech # required
MONITOR_TOKEN=<jwt> # optional. pass the jwt directly.
MONITOR_USERNAME=<username> # required for password / secret login
MONITOR_PASSWORD=<password> # the users password
MONITOR_SECRET=<secret> # the api secret
```
to log in, you must pass either
1. MONITOR_TOKEN
2. MONITOR_USERNAME and MONITOR_PASSWORD
3. MONITOR_USERNAME and MONITOR_SECRET
you can then initialize the client using this method:
```rust
let monitor = MonitorClient::new_from_env().await?;
```
## use the client
the following will select a server, build monitor core on it, and deploy it.
```rust
let server = monitor
.list_servers(None)
.await?
.pop()
.ok_or(anyhow!("no servers"))?;
let build = BuildBuilder::default()
.name("monitor_core".into())
.server_id(server.server.id.clone().into())
.repo("mbecker20/monitor".to_string().into())
.branch("main".to_string().into())
.docker_build_args(
DockerBuildArgs {
build_path: ".".into(),
dockerfile_path: "core/Dockerfile".to_string().into(),
..Default::default()
}
.into(),
)
.pre_build(
Command {
path: "frontend".into(),
command: "yarn && yarn build".into(),
}
.into(),
)
.build()?;
let build = monitor.create_full_build(&build).await?;
println!("{build:#?}");
let build_update = monitor.build(&build.id).await?;
println!("{build_update:#?}");
let deployment = DeploymentBuilder::default()
.name("monitor_core_1".into())
.server_id(server.server.id.clone())
.build_id(build.id.clone().into())
.docker_run_args(
DockerRunArgsBuilder::default()
.volumes(vec![Conversion {
local: "/home/max/.monitor/core.config.toml".into(),
container: "/config/config.toml".into(),
}])
.build()?,
)
.build()?;
let deployment = monitor.create_full_deployment(&deployment).await?;
println!("{deployment:#?}");
let deploy_update = monitor.deploy_container(&deployment.id).await?;
println!("{deploy_update:#?}");
```
note. this crate re-exports the [monitor types](https://crates.io/crates/monitor_types) crate under the module "types"

View File

@@ -1,12 +0,0 @@
# monitor client
## *interact with the monitor system programatically*
with this crate you can leverage all the functionality of monitor through rust code. for example, you can...
- create and manage complex deployment cycles
- execute builds on specific schedules
- monitor server stats programatically
- program response actions to monitor updates sent over websocket
this crate re-exports the [monitor types](https://crates.io/crates/monitor_types) crate under the module "types"

View File

@@ -1,6 +1,6 @@
[package]
name = "periphery_client"
version = "0.3.0"
version = "0.3.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -6,7 +6,7 @@ use crate::PeripheryClient;
impl PeripheryClient {
pub async fn build(&self, server: &Server, build: &Build) -> anyhow::Result<Option<Vec<Log>>> {
let res = self
.post_json::<_, Vec<Log>>(server, "/build", build)
.post_json(server, "/build", build, ())
.await
.context("failed to build image on periphery");
match res {

View File

@@ -5,7 +5,7 @@ use crate::PeripheryClient;
impl PeripheryClient {
pub async fn run_command(&self, server: &Server, command: &Command) -> anyhow::Result<Log> {
self.post_json(server, &format!("/command"), command)
self.post_json(server, &format!("/command"), command, ())
.await
.context("failed to run command on periphery")
}

View File

@@ -1,6 +1,6 @@
use anyhow::Context;
use serde_json::json;
use types::{BasicContainerInfo, Deployment, DockerContainerStats, Log, Server};
use types::{BasicContainerInfo, Deployment, DockerContainerStats, Log, Server, TerminationSignal};
use crate::PeripheryClient;
@@ -37,6 +37,7 @@ impl PeripheryClient {
server,
"/container/start",
&json!({ "name": container_name }),
(),
)
.await
.context("failed to start container on periphery")
@@ -46,11 +47,14 @@ impl PeripheryClient {
&self,
server: &Server,
container_name: &str,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Log> {
self.post_json(
server,
"/container/stop",
&json!({ "name": container_name }),
(("stop_signal", stop_signal), ("stop_time", stop_time)),
)
.await
.context("failed to stop container on periphery")
@@ -60,11 +64,14 @@ impl PeripheryClient {
&self,
server: &Server,
container_name: &str,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Log> {
self.post_json(
server,
"/container/remove",
&json!({ "name": container_name }),
(("stop_signal", stop_signal), ("stop_time", stop_time)),
)
.await
.context("failed to remove container on periphery")
@@ -80,19 +87,31 @@ impl PeripheryClient {
server,
"/container/rename",
&json!({ "curr_name": curr_name, "new_name": new_name }),
(),
)
.await
.context("failed to rename container on periphery")
}
pub async fn deploy(&self, server: &Server, deployment: &Deployment) -> anyhow::Result<Log> {
self.post_json(server, "/container/deploy", deployment)
.await
.context("failed to deploy container on periphery")
pub async fn deploy(
&self,
server: &Server,
deployment: &Deployment,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> anyhow::Result<Log> {
self.post_json(
server,
"/container/deploy",
deployment,
(("stop_signal", stop_signal), ("stop_time", stop_time)),
)
.await
.context("failed to deploy container on periphery")
}
pub async fn container_prune(&self, server: &Server) -> anyhow::Result<Log> {
self.post_json(server, "/container/prune", &json!({}))
self.post_json(server, "/container/prune", &json!({}), ())
.await
.context("failed to prune containers on periphery")
}

View File

@@ -11,7 +11,7 @@ impl PeripheryClient {
clone_args: impl Into<CloneArgs>,
) -> anyhow::Result<Vec<Log>> {
let clone_args: CloneArgs = clone_args.into();
self.post_json(server, "/git/clone", &clone_args)
self.post_json(server, "/git/clone", &clone_args, ())
.await
.context("failed to clone repo on periphery")
}
@@ -27,13 +27,14 @@ impl PeripheryClient {
server,
"/git/pull",
&json!({ "name": name, "branch": branch, "on_pull": on_pull }),
(),
)
.await
.context("failed to pull repo on periphery")
}
pub async fn delete_repo(&self, server: &Server, build_name: &str) -> anyhow::Result<Log> {
self.post_json(server, "/git/delete", &json!({ "name": build_name }))
self.post_json(server, "/git/delete", &json!({ "name": build_name }), ())
.await
.context("failed to delete repo on periphery")
}

View File

@@ -11,7 +11,7 @@ impl PeripheryClient {
}
pub async fn image_prune(&self, server: &Server) -> anyhow::Result<Log> {
self.post_json(server, &format!("/image/prune"), &())
self.post_json(server, &format!("/image/prune"), &(), ())
.await
.context("failed to prune images on periphery")
}

View File

@@ -170,17 +170,19 @@ impl PeripheryClient {
}
}
async fn post_json<B: Serialize, R: DeserializeOwned>(
async fn post_json<B: Serialize, R: DeserializeOwned, Q: Serialize>(
&self,
server: &Server,
endpoint: &str,
body: &B,
query: impl Into<Option<Q>>,
) -> anyhow::Result<R> {
self.health_check(server).await?;
let res = self
.http_client
.post(format!("{}{endpoint}", server.address))
.header("authorization", &self.passkey)
.query(&query.into())
.json(body)
.send()
.await

View File

@@ -24,19 +24,20 @@ impl PeripheryClient {
"name": name,
"driver": driver
}),
(),
)
.await
.context("failed to create network on periphery")
}
pub async fn network_delete(&self, server: &Server, name: &str) -> anyhow::Result<Log> {
self.post_json(server, "/network/delete", &json!({ "name": name }))
self.post_json(server, "/network/delete", &json!({ "name": name }), ())
.await
.context("failed to delete network on periphery")
}
pub async fn network_prune(&self, server: &Server) -> anyhow::Result<Log> {
self.post_json(server, "/network/prune", &json!({}))
self.post_json(server, "/network/prune", &json!({}), ())
.await
.context("failed to prune networks on periphery")
}

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_types"
version = "0.3.0"
version = "0.3.2"
edition = "2021"
authors = ["MoghTech"]
description = "types for the mogh tech monitor"

View File

@@ -46,10 +46,20 @@ pub struct Deployment {
#[diff(attr(#[serde(skip_serializing_if = "docker_run_args_diff_no_change")]))]
pub docker_run_args: DockerRunArgs,
#[serde(default)]
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "vec_diff_no_change")]))]
pub term_signal_labels: Vec<TerminationSignalLabel>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub build_id: Option<String>,
#[serde(default)]
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub redeploy_on_build: bool,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub build_version: Option<Version>,
@@ -110,6 +120,18 @@ pub struct DeploymentActionState {
pub renaming: bool,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, Diff, Builder)]
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
pub struct TerminationSignalLabel {
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "termination_signal_diff_no_change")]))]
pub signal: TerminationSignal,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub label: String,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Diff, Builder)]
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
@@ -243,10 +265,22 @@ impl Default for DockerContainerState {
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy, Diff,
Serialize,
Deserialize,
Debug,
Display,
EnumString,
PartialEq,
Hash,
Eq,
Clone,
Copy,
Diff,
Default,
)]
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
pub enum RestartMode {
#[default]
#[serde(rename = "no")]
#[strum(serialize = "no")]
NoRestart,
@@ -261,8 +295,32 @@ pub enum RestartMode {
UnlessStopped,
}
impl Default for RestartMode {
fn default() -> RestartMode {
RestartMode::NoRestart
}
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Display,
EnumString,
PartialEq,
Hash,
Eq,
Clone,
Copy,
Diff,
Default,
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
pub enum TerminationSignal {
#[serde(alias = "1")]
SigHup,
#[serde(alias = "2")]
SigInt,
#[serde(alias = "3")]
SigQuit,
#[default]
#[serde(alias = "15")]
SigTerm,
}

View File

@@ -1,7 +1,7 @@
use diff::{Diff, OptionDiff, VecDiff};
use crate::{
deployment::{DockerRunArgsDiff, RestartModeDiff},
deployment::{DockerRunArgsDiff, RestartModeDiff, TerminationSignalDiff},
TimelengthDiff,
};
@@ -24,7 +24,7 @@ pub fn vec_diff_no_change<T: Diff>(vec_diff: &VecDiff<T>) -> bool {
vec_diff.0.is_empty()
}
// pub fn hashmap_diff_no_change<T: Diff>(hashmap_diff: &HashMapDiff<String, T>) -> bool {
// pub fn hashmap_diff_no_change<K: Hash + Eq, T: Diff>(hashmap_diff: &HashMapDiff<K, T>) -> bool {
// hashmap_diff.altered.is_empty() && hashmap_diff.removed.is_empty()
// }
@@ -48,3 +48,7 @@ pub fn restart_mode_diff_no_change(restart_mode: &RestartModeDiff) -> bool {
pub fn timelength_diff_no_change(timelength: &TimelengthDiff) -> bool {
timelength == &TimelengthDiff::NoChange
}
pub fn termination_signal_diff_no_change(term_signal: &TerminationSignalDiff) -> bool {
term_signal == &TerminationSignalDiff::NoChange
}

View File

@@ -1,8 +1,8 @@
use std::collections::HashMap;
use ::diff::Diff;
use anyhow::Context;
use chrono::{DateTime, SecondsFormat, Utc};
use anyhow::{anyhow, Context};
use chrono::{DateTime, LocalResult, SecondsFormat, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString};
use typeshare::typeshare;
@@ -199,7 +199,18 @@ pub enum PermissionsTarget {
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy, Diff,
Serialize,
Deserialize,
Debug,
Display,
EnumString,
PartialEq,
Hash,
Eq,
Clone,
Copy,
Diff,
Default,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
@@ -220,6 +231,7 @@ pub enum Timelength {
#[serde(rename = "30-sec")]
#[strum(serialize = "30-sec")]
ThirtySeconds,
#[default]
#[serde(rename = "1-min")]
#[strum(serialize = "1-min")]
OneMinute,
@@ -270,12 +282,6 @@ pub enum Timelength {
ThirtyDays,
}
impl Default for Timelength {
fn default() -> Timelength {
Timelength::OneMinute
}
}
pub fn monitor_timestamp() -> String {
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, false)
}
@@ -285,3 +291,11 @@ pub fn unix_from_monitor_ts(ts: &str) -> anyhow::Result<i64> {
.context("failed to parse rfc3339 timestamp")?
.timestamp_millis())
}
pub fn monitor_ts_from_unix(ts: i64) -> anyhow::Result<String> {
match Utc.timestamp_millis_opt(ts) {
LocalResult::Single(dt) => Ok(dt.to_rfc3339_opts(SecondsFormat::Millis, false)),
LocalResult::None => Err(anyhow!("out of bounds timestamp passed")),
_ => unreachable!(),
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_periphery"
version = "0.3.0"
version = "0.3.2"
edition = "2021"
authors = ["MoghTech"]
description = "monitor periphery binary"
@@ -34,3 +34,4 @@ daemonize = "0.5.0"
clap = { version = "4.2", features = ["derive"] }
svi = "0.1.3"
merge_config_files = "0.1.3"
parse_csl = "0.1.0"

View File

@@ -6,7 +6,7 @@ use axum::{
};
use helpers::handle_anyhow_error;
use serde::Deserialize;
use types::{Deployment, Log};
use types::{Deployment, Log, TerminationSignal};
use crate::{
helpers::{
@@ -32,6 +32,12 @@ struct GetLogQuery {
tail: Option<u64>, // default is 1000 if not passed
}
#[derive(Deserialize)]
struct StopContainerQuery {
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
}
pub fn router() -> Router {
Router::new()
.route(
@@ -79,15 +85,29 @@ pub fn router() -> Router {
)
.route(
"/stop",
post(|container: Json<Container>| async move {
Json(docker::stop_container(&container.name).await)
}),
post(
|query: Query<StopContainerQuery>, container: Json<Container>| async move {
Json(
docker::stop_container(&container.name, query.stop_signal, query.stop_time)
.await,
)
},
),
)
.route(
"/remove",
post(|container: Json<Container>| async move {
Json(docker::stop_and_remove_container(&container.name).await)
}),
post(
|query: Query<StopContainerQuery>, container: Json<Container>| async move {
Json(
docker::stop_and_remove_container(
&container.name,
query.stop_signal,
query.stop_time,
)
.await,
)
},
),
)
.route(
"/rename",
@@ -97,8 +117,8 @@ pub fn router() -> Router {
)
.route(
"/deploy",
post(|config, deployment| async move {
deploy(config, deployment)
post(|config, query, deployment| async move {
deploy(config, deployment, query)
.await
.map_err(handle_anyhow_error)
}),
@@ -118,6 +138,7 @@ pub fn router() -> Router {
async fn deploy(
Extension(config): PeripheryConfigExtension,
Json(deployment): Json<Deployment>,
Query(query): Query<StopContainerQuery>,
) -> anyhow::Result<Json<Log>> {
let log = match get_docker_token(&deployment.docker_run_args.docker_account, &config) {
Ok(docker_token) => tokio::spawn(async move {
@@ -126,6 +147,8 @@ async fn deploy(
&docker_token,
config.repo_dir.clone(),
&config.secrets,
query.stop_signal,
query.stop_time,
)
.await
})

View File

@@ -3,8 +3,8 @@ use std::sync::Arc;
use axum::Extension;
use clap::Parser;
use dotenv::dotenv;
use helpers::parse_comma_seperated_list;
use merge_config_files::parse_config_paths;
use parse_csl::parse_comma_seperated;
use serde::Deserialize;
use types::PeripheryConfig;
@@ -65,13 +65,13 @@ pub fn load() -> (Args, u16, PeripheryConfigExtension, HomeDirExtension) {
.config_path
.as_ref()
.unwrap_or(
&parse_comma_seperated_list(env.config_paths)
&parse_comma_seperated(&env.config_paths)
.expect("failed to parse config paths on environment into comma seperated list"),
)
.into_iter()
.map(|p| p.replace("~", &home_dir))
.collect::<Vec<_>>();
let env_match_keywords = parse_comma_seperated_list(env.config_keywords)
let env_match_keywords = parse_comma_seperated(&env.config_keywords)
.expect("failed to parse environemt CONFIG_KEYWORDS into comma seperated list");
let match_keywords = args
.config_keyword

View File

@@ -5,6 +5,7 @@ use helpers::to_monitor_name;
use run_command::async_run_command;
use types::{
Conversion, Deployment, DockerContainerStats, DockerRunArgs, EnvironmentVar, Log, RestartMode,
TerminationSignal,
};
use crate::helpers::{docker::parse_extra_args, run_monitor_command};
@@ -57,18 +58,40 @@ pub async fn start_container(container_name: &str) -> Log {
run_monitor_command("docker start", command).await
}
pub async fn stop_container(container_name: &str) -> Log {
let container_name = to_monitor_name(container_name);
let command = format!("docker stop {container_name}");
pub async fn stop_container(
container_name: &str,
signal: Option<TerminationSignal>,
time: Option<i32>,
) -> Log {
let command = stop_container_command(container_name, signal, time);
run_monitor_command("docker stop", command).await
}
pub async fn stop_and_remove_container(container_name: &str) -> Log {
let container_name = to_monitor_name(container_name);
let command = format!("docker stop {container_name} && docker container rm {container_name}");
pub async fn stop_and_remove_container(
container_name: &str,
signal: Option<TerminationSignal>,
time: Option<i32>,
) -> Log {
let stop_command = stop_container_command(container_name, signal, time);
let command = format!("{stop_command} && docker container rm {container_name}");
run_monitor_command("docker stop and remove", command).await
}
fn stop_container_command(
container_name: &str,
signal: Option<TerminationSignal>,
time: Option<i32>,
) -> String {
let container_name = to_monitor_name(container_name);
let signal = signal
.map(|signal| format!(" --signal {signal}"))
.unwrap_or_default();
let time = time
.map(|time| format!(" --time {time}"))
.unwrap_or_default();
format!("docker stop{signal}{time} {container_name}")
}
pub async fn rename_container(curr_name: &str, new_name: &str) -> Log {
let curr = to_monitor_name(curr_name);
let new = to_monitor_name(new_name);
@@ -86,12 +109,14 @@ pub async fn deploy(
docker_token: &Option<String>,
repo_dir: PathBuf,
secrets: &HashMap<String, String>,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
) -> Log {
if let Err(e) = docker_login(&deployment.docker_run_args.docker_account, docker_token).await {
return Log::error("docker login", format!("{e:#?}"));
}
let _ = pull_image(&deployment.docker_run_args.image).await;
let _ = stop_and_remove_container(&to_monitor_name(&deployment.name)).await;
let _ = stop_and_remove_container(&deployment.name, stop_signal, stop_time).await;
let command = docker_run_command(deployment, repo_dir);
if deployment.skip_secret_interp {
run_monitor_command("docker run", command).await

View File

@@ -28,11 +28,8 @@ pub async fn pull(
if on_pull.path.len() > 0 && on_pull.command.len() > 0 {
path.push(&on_pull.path);
let path = path.display().to_string();
let on_pull_log = run_monitor_command(
"on pull",
format!("cd {path} && {}", on_pull.command),
)
.await;
let on_pull_log =
run_monitor_command("on pull", format!("cd {path} && {}", on_pull.command)).await;
logs.push(on_pull_log);
}
}

View File

@@ -1,3 +1,13 @@
# monitor 🦎
a tool to build and deploy software across many servers. [docs](https://mbecker20.github.io/monitor)
## screenshots
![desktop-deployment](https://raw.githubusercontent.com/mbecker20/monitor/main/screenshots/desktop-deployment.png)
![home-servers](https://raw.githubusercontent.com/mbecker20/monitor/main/screenshots/home-servers.png)
![search-menu](https://raw.githubusercontent.com/mbecker20/monitor/main/screenshots/search-menu.png)
![mobile-deployment](https://raw.githubusercontent.com/mbecker20/monitor/main/screenshots/mobile-deployment.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
screenshots/search-menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

View File

@@ -6,8 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.22", features = ["full"] }
monitor_client = { path = "../lib/monitor_client" }
tokio = { version = "1", features = ["full"] }
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"

View File

@@ -1,8 +1,14 @@
#![allow(unused)]
use anyhow::Context;
use anyhow::{anyhow, Context};
use async_timing_util::unix_timestamp_ms;
use monitor_client::{types::Conversion, MonitorClient};
use monitor_client::{
types::{
BuildBuilder, Command, Conversion, Deployment, DeploymentBuilder, DockerBuildArgs,
DockerBuildArgsBuilder, DockerRunArgsBuilder, EnvironmentVar, TerminationSignal,
},
MonitorClient,
};
mod config;
mod tests;
@@ -17,23 +23,63 @@ async fn main() -> anyhow::Result<()> {
let start_ts = unix_timestamp_ms();
// let (server, deployment, build) = create_test_setup(&monitor, "test").await?;
let server = monitor
.list_servers(None)
.await?
.pop()
.ok_or(anyhow!("no servers"))?;
// let server_stats = get_server_stats(&monitor).await?;
// println!("server stats:\n{server_stats:#?}\n");
let build = BuildBuilder::default()
.name("monitor_core".into())
.server_id(server.server.id.clone().into())
.repo("mbecker20/monitor".to_string().into())
.branch("main".to_string().into())
.docker_build_args(
DockerBuildArgs {
build_path: ".".into(),
dockerfile_path: "core/Dockerfile".to_string().into(),
..Default::default()
}
.into(),
)
.pre_build(
Command {
path: "frontend".into(),
command: "yarn && yarn build".into(),
}
.into(),
)
.build()?;
// subscribe_to_server_stats(&monitor).await?;
let build = monitor.create_full_build(&build).await?;
// let (update, container) = deploy_mongo(&monitor).await?;
// println!(
// "mongo deploy update:\n{update:#?}\n\ncontainer: {:#?}\n",
// container.container
// );
println!("{build:#?}");
// let update = test_build(&monitor).await?;
// println!("build update:\n{update:#?}");
let build_update = monitor.build(&build.id).await?;
// test_updates(&monitor).await.unwrap();
println!("{build_update:#?}");
let deployment = DeploymentBuilder::default()
.name("monitor_core_1".into())
.server_id(server.server.id.clone())
.build_id(build.id.clone().into())
.docker_run_args(
DockerRunArgsBuilder::default()
.volumes(vec![Conversion {
local: "/home/max/.monitor/core.config.toml".into(),
container: "/config/config.toml".into(),
}])
.build()?,
)
.build()?;
let deployment = monitor.create_full_deployment(&deployment).await?;
println!("{deployment:#?}");
let deploy_update = monitor.deploy_container(&deployment.id).await?;
println!("{deploy_update:#?}");
let update = test_aws_build(&monitor).await?;