forked from github-starred/komodo
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130ca8e1f1 | ||
|
|
ced4c21688 | ||
|
|
6ec7078024 | ||
|
|
b28d8f2506 | ||
|
|
c88a9291a0 | ||
|
|
1e82d19306 | ||
|
|
dd87e50cb2 | ||
|
|
4c8f96a30f | ||
|
|
c4f45e05f1 | ||
|
|
6aa382c7c1 | ||
|
|
ccb9f059e6 | ||
|
|
1cdcea0771 | ||
|
|
88dda0de80 | ||
|
|
30ed99e2b0 | ||
|
|
e5953b7541 | ||
|
|
1f9d01c59f | ||
|
|
cc5210a3d8 | ||
|
|
26559e2d3b | ||
|
|
7eeddb300f | ||
|
|
1e01bae16b | ||
|
|
87c03924e5 | ||
|
|
f0998b1d43 | ||
|
|
1995a04244 | ||
|
|
420fe6bcd5 | ||
|
|
d4e26c0553 | ||
|
|
5f5e7cb45e | ||
|
|
8aa0304738 | ||
|
|
8ec98c33a4 | ||
|
|
2667182ca3 |
152
Cargo.lock
generated
152
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
40
docsite/docs/core-setup.md
Normal file
40
docsite/docs/core-setup.md
Normal 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
|
||||
@@ -19,6 +19,7 @@ const sidebars = {
|
||||
// But you can create a sidebar manually
|
||||
docs: [
|
||||
"intro",
|
||||
"core-setup",
|
||||
{
|
||||
type: "category",
|
||||
label: "connecting servers",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
131
frontend/src/components/home/BuildSummary.tsx
Normal file
131
frontend/src/components/home/BuildSummary.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%" }}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -180,6 +180,10 @@ svg {
|
||||
background-color: rgba(c.$blue, 0.8);
|
||||
}
|
||||
|
||||
.blue.opaque {
|
||||
background-color: c.$blue;
|
||||
}
|
||||
|
||||
.blue:hover {
|
||||
background-color: c.$lightblue;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
110
lib/monitor_client/README.md
Normal file
110
lib/monitor_client/README.md
Normal 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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
10
readme.md
10
readme.md
@@ -1,3 +1,13 @@
|
||||
# monitor 🦎
|
||||
|
||||
a tool to build and deploy software across many servers. [docs](https://mbecker20.github.io/monitor)
|
||||
|
||||
## screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
BIN
screenshots/desktop-deployment.png
Normal file
BIN
screenshots/desktop-deployment.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
BIN
screenshots/home-servers.png
Normal file
BIN
screenshots/home-servers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 546 KiB |
BIN
screenshots/mobile-deployment.png
Normal file
BIN
screenshots/mobile-deployment.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
BIN
screenshots/search-menu.png
Normal file
BIN
screenshots/search-menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 646 KiB |
@@ -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"
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user