forked from github-starred/komodo
Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
349fc297ce | ||
|
|
5ad87c03ed | ||
|
|
d16006f28f | ||
|
|
7f0452a5f5 | ||
|
|
c605b2f6fc | ||
|
|
6c2d8a8494 | ||
|
|
874691f729 | ||
|
|
cdf702e17d | ||
|
|
25fdb32627 | ||
|
|
e976ea0a3a | ||
|
|
34e6b4fc69 | ||
|
|
a2d77567b3 | ||
|
|
ecb460f9b5 | ||
|
|
63444b089c | ||
|
|
c787984b77 | ||
|
|
bf3d03e801 | ||
|
|
bc2e69b975 | ||
|
|
7b94fcf3da | ||
|
|
9cf03b8b88 | ||
|
|
a288edcf61 | ||
|
|
89cc18ad37 | ||
|
|
ffa3b671e1 | ||
|
|
f32eeb413b | ||
|
|
b5a5103cfc | ||
|
|
c5697e59f3 | ||
|
|
f030667ff4 | ||
|
|
e9fef5d97c | ||
|
|
f5818ac7ea | ||
|
|
c85ab4110d | ||
|
|
9690ea35b8 | ||
|
|
6300c8011b | ||
|
|
97f582b381 | ||
|
|
5135a9c228 | ||
|
|
b7d1212a82 | ||
|
|
7d9d0a9fc4 | ||
|
|
ed9aef4321 | ||
|
|
0aa638bdf4 | ||
|
|
0ec39d793d | ||
|
|
5579ba869c | ||
|
|
210940038c | ||
|
|
98a1a60362 | ||
|
|
86cf9116ba | ||
|
|
8b2defe0d9 | ||
|
|
50b14b3ce5 | ||
|
|
1bfb17cb5d | ||
|
|
b90acb66c7 | ||
|
|
7648b0dd10 | ||
|
|
2d69f1791a | ||
|
|
5ba887095a | ||
|
|
19b7405562 | ||
|
|
f5c5f734e1 | ||
|
|
8d1639bcaf | ||
|
|
e2446af00e | ||
|
|
1b39aaaa38 | ||
|
|
5a2a1a3d98 | ||
|
|
39eceb745b | ||
|
|
4c1ec5db33 | ||
|
|
8b68b9481e | ||
|
|
14843f83c6 | ||
|
|
e67d87e885 | ||
|
|
7d4d865d58 | ||
|
|
1e4aaff23c | ||
|
|
df3f4a5f4a | ||
|
|
1f8557300d | ||
|
|
bf17d705f0 | ||
|
|
0d24b792c6 | ||
|
|
fb61e36417 | ||
|
|
c39869d2f8 | ||
|
|
750e0274da | ||
|
|
a9d37ab667 | ||
|
|
eacb549d5e | ||
|
|
ce7cb8fe45 | ||
|
|
f9fe4e32b4 | ||
|
|
2c9fc2bad4 | ||
|
|
94949291c2 | ||
|
|
2944ba6ef9 | ||
|
|
997e68a31d | ||
|
|
bfb9d9e34d | ||
|
|
3b9219b586 | ||
|
|
7bf2a88ab1 | ||
|
|
d21ed093dc | ||
|
|
6e89671e91 | ||
|
|
ee1128a666 | ||
|
|
63b5deecd7 | ||
|
|
f4f97ce1a7 | ||
|
|
a666df099f | ||
|
|
21dd0ee072 | ||
|
|
bd2a1d4236 | ||
|
|
7acdbcfd8f | ||
|
|
58514c5c93 | ||
|
|
580e800923 | ||
|
|
29f6b19f33 | ||
|
|
e090247723 | ||
|
|
1374c26cd8 | ||
|
|
5467b40b2e | ||
|
|
165b9012da | ||
|
|
22630f665e | ||
|
|
3d867084ba | ||
|
|
171dd2d9e0 | ||
|
|
9709239f88 | ||
|
|
60d457b285 | ||
|
|
8b1d4793a7 | ||
|
|
f2166c8435 | ||
|
|
07d723a748 | ||
|
|
b36f485287 | ||
|
|
a121ae0828 | ||
|
|
e2b5a02008 | ||
|
|
575aa62625 | ||
|
|
ac88a2c4ed | ||
|
|
f1dcb71a8a | ||
|
|
30d04bc201 | ||
|
|
33a00bb1a2 | ||
|
|
ccca44ea89 | ||
|
|
ae5f36fe51 | ||
|
|
69ce1e4f36 | ||
|
|
6e444b9032 | ||
|
|
73eff72da4 | ||
|
|
698e3c214b | ||
|
|
9da77667dc | ||
|
|
c30793fb8f | ||
|
|
84fdaab24d | ||
|
|
cbd67bb609 | ||
|
|
00f58e9008 | ||
|
|
7738fab351 | ||
|
|
06e8f6589b | ||
|
|
57d9287724 | ||
|
|
2cc65595ee | ||
|
|
3dd2b97873 | ||
|
|
3c805ebbf7 | ||
|
|
a854160018 | ||
|
|
a99d9e5969 | ||
|
|
813b6c1182 | ||
|
|
2958f9589b | ||
|
|
69b4e26176 | ||
|
|
78b00f139d | ||
|
|
dc1e8de851 | ||
|
|
3187b335a3 | ||
|
|
54b5a2b420 | ||
|
|
14c6bd00a8 | ||
|
|
e9c3646450 | ||
|
|
4f20257479 | ||
|
|
65749991de | ||
|
|
237a1d802d | ||
|
|
e4336f19f3 | ||
|
|
c895e5e67f | ||
|
|
4e4e210736 | ||
|
|
09dfc8faa3 | ||
|
|
3c4f77cc78 | ||
|
|
c86880ccdb | ||
|
|
9db26a3037 | ||
|
|
711d27e15e | ||
|
|
e1d53598e6 | ||
|
|
f871bc3e03 | ||
|
|
69b359ce4a | ||
|
|
0dd914b6e4 | ||
|
|
f03d0b8930 | ||
|
|
a672c1cba3 | ||
|
|
233d9dab33 | ||
|
|
f5ac23834d | ||
|
|
03bc43b04e | ||
|
|
fdbd2a2181 | ||
|
|
ae9fe5c424 | ||
|
|
7de10b7277 | ||
|
|
8c1f4d7786 | ||
|
|
f3e84b52c6 | ||
|
|
d64c46f44d | ||
|
|
a6a58a25be | ||
|
|
37046ddbd8 | ||
|
|
6f697d292a | ||
|
|
4098c6b487 | ||
|
|
d2844b6558 |
17
.vscode/tasks.json
vendored
17
.vscode/tasks.json
vendored
@@ -92,15 +92,6 @@
|
||||
"cwd": "${workspaceFolder}/lib/types"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
"args": ["--allow-dirty"],
|
||||
"label": "publish monitor helpers",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/lib/helpers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
@@ -109,14 +100,6 @@
|
||||
"cwd": "${workspaceFolder}/lib/monitor_client"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
"label": "publish monitor periphery",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/periphery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "publish",
|
||||
|
||||
1089
Cargo.lock
generated
1089
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
FROM rust:latest as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY ./periphery ./periphery
|
||||
|
||||
COPY ./lib/types ./lib/types
|
||||
COPY ./lib/helpers ./lib/helpers
|
||||
|
||||
RUN cd periphery && cargo build --release
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
ARG DEPS_INSTALLER
|
||||
|
||||
COPY ./${DEPS_INSTALLER}.sh ./
|
||||
RUN sh ./${DEPS_INSTALLER}.sh
|
||||
|
||||
COPY --from=builder /builder/periphery/target/release/periphery /usr/local/bin/periphery
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD "periphery"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_cli"
|
||||
version = "0.1.17"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor cli | tools to setup monitor system"
|
||||
@@ -13,12 +13,13 @@ path = "src/main.rs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
monitor_types = { path = "../lib/types" }
|
||||
clap = "4.0"
|
||||
async_timing_util = "0.1.14"
|
||||
rand = "0.8"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
toml = "0.5"
|
||||
toml = "0.7"
|
||||
run_command = "0.0.5"
|
||||
colored = "2"
|
||||
strum = "0.24"
|
||||
|
||||
@@ -7,15 +7,13 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use async_timing_util::Timelength;
|
||||
use clap::ArgMatches;
|
||||
use colored::Colorize;
|
||||
use monitor_types::{CoreConfig, MongoConfig, PeripheryConfig, RestartMode, Timelength};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use run_command::run_command_pipe_to_terminal;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::types::{CoreConfig, MongoConfig, PeripheryConfig, RestartMode};
|
||||
|
||||
const CORE_IMAGE_NAME: &str = "mbecker2020/monitor_core";
|
||||
const PERIPHERY_IMAGE_NAME: &str = "mbecker2020/monitor_periphery";
|
||||
const PERIPHERY_CRATE: &str = "monitor_periphery";
|
||||
@@ -64,6 +62,7 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
|
||||
.map(|p| p.to_owned());
|
||||
|
||||
let config = CoreConfig {
|
||||
title: String::from("monitor"),
|
||||
host,
|
||||
port,
|
||||
jwt_valid_for,
|
||||
@@ -74,6 +73,8 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
|
||||
local_auth: true,
|
||||
github_oauth: Default::default(),
|
||||
google_oauth: Default::default(),
|
||||
aws: Default::default(),
|
||||
docker_organizations: Default::default(),
|
||||
mongo: MongoConfig {
|
||||
uri: mongo_uri,
|
||||
db_name: mongo_db_name,
|
||||
@@ -81,6 +82,8 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
|
||||
},
|
||||
jwt_secret: generate_secret(40),
|
||||
github_webhook_secret: generate_secret(30),
|
||||
github_webhook_base_url: None,
|
||||
passkey: generate_secret(30),
|
||||
};
|
||||
|
||||
write_to_toml(&path, &config);
|
||||
@@ -176,7 +179,10 @@ pub fn start_mongo(sub_matches: &ArgMatches) {
|
||||
}
|
||||
}
|
||||
|
||||
let command = format!("docker stop {name} && docker container rm {name} && docker run -d --name {name} -p {port}:27017 --network {network} -v {mount}:/data/db{env} --restart {restart} mongo --quiet");
|
||||
let stop =
|
||||
run_command_pipe_to_terminal(&format!("docker stop {name} && docker container rm {name}"));
|
||||
|
||||
let command = format!("docker run -d --name {name} -p {port}:27017 --network {network} -v {mount}:/data/db{env} --restart {restart} --log-opt max-size=15m --log-opt max-file=3 mongo --quiet");
|
||||
|
||||
let output = run_command_pipe_to_terminal(&command);
|
||||
|
||||
@@ -314,13 +320,16 @@ pub fn gen_periphery_config(sub_matches: &ArgMatches) {
|
||||
.map(|p| p.as_str())
|
||||
.unwrap_or("~/.monitor/repos")
|
||||
.to_string()
|
||||
.replace("~", env::var("HOME").unwrap().as_str());
|
||||
.replace("~", env::var("HOME").unwrap().as_str())
|
||||
.parse()
|
||||
.expect("failed to parse --repo_dir as path");
|
||||
|
||||
let config = PeripheryConfig {
|
||||
port,
|
||||
repo_dir,
|
||||
stats_polling_rate,
|
||||
allowed_ips,
|
||||
repo_dir,
|
||||
passkeys: vec![],
|
||||
secrets: Default::default(),
|
||||
github_accounts: Default::default(),
|
||||
docker_accounts: Default::default(),
|
||||
@@ -588,6 +597,7 @@ Description=agent to connect with monitor core
|
||||
|
||||
[Service]
|
||||
ExecStart={home}/.monitor/bin/periphery --config-path {config_path} --home-dir {home}
|
||||
Restart=on-failure
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use clap::{arg, Arg, Command};
|
||||
|
||||
mod helpers;
|
||||
mod types;
|
||||
|
||||
use helpers::*;
|
||||
|
||||
@@ -36,19 +35,19 @@ fn cli() -> Command {
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--mongo-uri <URI> "sets the mongo uri to use. default is 'mongodb://monitor-mongo'")
|
||||
arg!(--"mongo-uri" <URI> "sets the mongo uri to use. default is 'mongodb://monitor-mongo'")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--mongo-db-name <NAME> "sets the db name to use. default is 'monitor'")
|
||||
arg!(--"mongo-db-name" <NAME> "sets the db name to use. default is 'monitor'")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--jwt-valid-for <TIMELENGTH> "sets the length of time jwt stays valid for. default is 1-wk (one week)")
|
||||
arg!(--"jwt-valid-for" <TIMELENGTH> "sets the length of time jwt stays valid for. default is 1-wk (one week)")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--slack-url <URL> "sets the slack url to use for slack notifications")
|
||||
arg!(--"slack-url" <URL> "sets the slack url to use for slack notifications")
|
||||
.required(false)
|
||||
),
|
||||
)
|
||||
@@ -96,7 +95,7 @@ fn cli() -> Command {
|
||||
arg!(--name <NAME> "specify the name of the monitor core container. default is monitor-core")
|
||||
)
|
||||
.arg(
|
||||
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/core.config.toml")
|
||||
arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/core.config.toml")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
@@ -111,7 +110,7 @@ fn cli() -> Command {
|
||||
arg!(--restart <RESTART> "sets docker restart mode of monitor core container. default is unless-stopped")
|
||||
)
|
||||
.arg(
|
||||
arg!(--add-internal-host "adds the docker flag '--add-host=host.docker.internal:host-gateway'. default is true")
|
||||
arg!(--"add-internal-host" "adds the docker flag '--add-host=host.docker.internal:host-gateway'. default is true")
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -133,15 +132,15 @@ fn cli() -> Command {
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--stats-polling-rate <INTERVAL> "sets stats polling rate to control granularity of system stats returned. default is 5-sec. options: 1-sec, 5-sec, 10-sec, 30-sec, 1-min")
|
||||
arg!(--"stats-polling-rate" <INTERVAL> "sets stats polling rate to control granularity of system stats returned. default is 5-sec. options: 1-sec, 5-sec, 10-sec, 30-sec, 1-min")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--allowed-ips <IPS> "used to only accept requests from known ips. give ips as comma seperated list, like '--allowed_ips 127.0.0.1,10.20.30.43'. default is empty, which will not block any ip.")
|
||||
arg!(--"allowed-ips" <IPS> "used to only accept requests from known ips. give ips as comma seperated list, like '--allowed_ips 127.0.0.1,10.20.30.43'. default is empty, which will not block any ip.")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--repo-dir <PATH> "if running in container, this should be '/repos'. default is ~/.monitor/repos").required(false)
|
||||
arg!(--"repo-dir" <PATH> "if running in container, this should be '/repos'. default is ~/.monitor/repos").required(false)
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
@@ -157,7 +156,7 @@ fn cli() -> Command {
|
||||
arg!(--install "specify this to install periphery from crates.io")
|
||||
)
|
||||
.arg(
|
||||
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
|
||||
arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
|
||||
.required(false)
|
||||
)
|
||||
)
|
||||
@@ -171,7 +170,7 @@ fn cli() -> Command {
|
||||
arg!(--install "specify this to install periphery from crates.io")
|
||||
)
|
||||
.arg(
|
||||
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
|
||||
arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
@@ -183,32 +182,32 @@ fn cli() -> Command {
|
||||
.required(false)
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("container")
|
||||
.about("start up monitor periphery in docker container")
|
||||
.arg(
|
||||
arg!(--yes "used in scripts to skip 'enter to continue' step")
|
||||
)
|
||||
.arg(
|
||||
arg!(--name <NAME> "specify the name of the monitor periphery container. default is monitor-periphery")
|
||||
)
|
||||
.arg(
|
||||
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
|
||||
.required(false)
|
||||
)
|
||||
.arg(arg!(--repo-dir <PATH> "specify the folder on host to clone repos into. default is ~/.monitor/repos").required(false))
|
||||
.arg(
|
||||
arg!(--port <PORT> "sets port monitor periphery will run on. default is 8000")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--network <NETWORK> "sets docker network of monitor periphery container. default is bridge")
|
||||
.required(false)
|
||||
)
|
||||
.arg(
|
||||
arg!(--restart <RESTART> "sets docker restart mode of monitor periphery container. default is unless-stopped")
|
||||
)
|
||||
)
|
||||
// .subcommand(
|
||||
// Command::new("container")
|
||||
// .about("start up monitor periphery in docker container")
|
||||
// .arg(
|
||||
// arg!(--yes "used in scripts to skip 'enter to continue' step")
|
||||
// )
|
||||
// .arg(
|
||||
// arg!(--name <NAME> "specify the name of the monitor periphery container. default is monitor-periphery")
|
||||
// )
|
||||
// .arg(
|
||||
// arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
|
||||
// .required(false)
|
||||
// )
|
||||
// .arg(arg!(--"repo-dir" <PATH> "specify the folder on host to clone repos into. default is ~/.monitor/repos").required(false))
|
||||
// .arg(
|
||||
// arg!(--port <PORT> "sets port monitor periphery will run on. default is 8000")
|
||||
// .required(false)
|
||||
// )
|
||||
// .arg(
|
||||
// arg!(--network <NETWORK> "sets docker network of monitor periphery container. default is bridge")
|
||||
// .required(false)
|
||||
// )
|
||||
// .arg(
|
||||
// arg!(--restart <RESTART> "sets docker restart mode of monitor periphery container. default is unless-stopped")
|
||||
// )
|
||||
// )
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -239,7 +238,7 @@ fn main() {
|
||||
match periphery_start_command {
|
||||
("systemd", sub_matches) => start_periphery_systemd(sub_matches),
|
||||
("daemon", sub_matches) => start_periphery_daemon(sub_matches),
|
||||
("container", sub_matches) => start_periphery_container(sub_matches),
|
||||
// ("container", sub_matches) => start_periphery_container(sub_matches),
|
||||
_ => println!("\n❌ invalid call, should be 'monitor periphery start <daemon, container> <flags>' ❌\n")
|
||||
}
|
||||
}
|
||||
|
||||
140
cli/src/types.rs
140
cli/src/types.rs
@@ -1,140 +0,0 @@
|
||||
use std::{collections::HashMap, net::IpAddr};
|
||||
|
||||
use async_timing_util::Timelength;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use strum_macros::{Display, EnumString};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CoreConfig {
|
||||
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
|
||||
pub host: String,
|
||||
|
||||
// port the core web server runs on
|
||||
#[serde(default = "default_core_port")]
|
||||
pub port: u16,
|
||||
|
||||
// daily utc offset in hours to run daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight.
|
||||
#[serde(default)]
|
||||
pub daily_offset_hours: u8,
|
||||
|
||||
// number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily
|
||||
#[serde(default)]
|
||||
pub keep_stats_for_days: u64, // 0 means never prune
|
||||
|
||||
// jwt config
|
||||
pub jwt_secret: String,
|
||||
#[serde(default = "default_jwt_valid_for")]
|
||||
pub jwt_valid_for: Timelength,
|
||||
|
||||
// interval at which to collect server stats and alert for out of bounds
|
||||
pub monitoring_interval: Timelength,
|
||||
|
||||
// used to verify validity from github webhooks
|
||||
pub github_webhook_secret: String,
|
||||
|
||||
// integration with slack app
|
||||
pub slack_url: Option<String>,
|
||||
|
||||
// enable login with local auth
|
||||
pub local_auth: bool,
|
||||
|
||||
// github integration
|
||||
pub github_oauth: OauthCredentials,
|
||||
|
||||
// google integration
|
||||
pub google_oauth: OauthCredentials,
|
||||
|
||||
// mongo config
|
||||
pub mongo: MongoConfig,
|
||||
}
|
||||
|
||||
fn default_core_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
|
||||
fn default_jwt_valid_for() -> Timelength {
|
||||
Timelength::OneWeek
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct OauthCredentials {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MongoConfig {
|
||||
pub uri: String,
|
||||
#[serde(default = "default_core_mongo_app_name")]
|
||||
pub app_name: String,
|
||||
#[serde(default = "default_core_mongo_db_name")]
|
||||
pub db_name: String,
|
||||
}
|
||||
|
||||
fn default_core_mongo_app_name() -> String {
|
||||
"monitor_core".to_string()
|
||||
}
|
||||
|
||||
fn default_core_mongo_db_name() -> String {
|
||||
"monitor".to_string()
|
||||
}
|
||||
|
||||
pub type GithubUsername = String;
|
||||
pub type GithubToken = String;
|
||||
pub type GithubAccounts = HashMap<GithubUsername, GithubToken>;
|
||||
|
||||
pub type DockerUsername = String;
|
||||
pub type DockerToken = String;
|
||||
pub type DockerAccounts = HashMap<DockerUsername, DockerToken>;
|
||||
|
||||
pub type SecretsMap = HashMap<String, String>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PeripheryConfig {
|
||||
#[serde(default = "default_periphery_port")]
|
||||
pub port: u16,
|
||||
#[serde(default = "default_repo_dir")]
|
||||
pub repo_dir: String,
|
||||
#[serde(default = "default_stats_refresh_interval")]
|
||||
pub stats_polling_rate: Timelength,
|
||||
#[serde(default)]
|
||||
pub allowed_ips: Vec<IpAddr>,
|
||||
#[serde(default)]
|
||||
pub secrets: SecretsMap,
|
||||
#[serde(default)]
|
||||
pub github_accounts: GithubAccounts,
|
||||
#[serde(default)]
|
||||
pub docker_accounts: DockerAccounts,
|
||||
}
|
||||
|
||||
fn default_periphery_port() -> u16 {
|
||||
8000
|
||||
}
|
||||
|
||||
fn default_repo_dir() -> String {
|
||||
"/repos".to_string()
|
||||
}
|
||||
|
||||
fn default_stats_refresh_interval() -> Timelength {
|
||||
Timelength::FiveSeconds
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy)]
|
||||
pub enum RestartMode {
|
||||
#[serde(rename = "no")]
|
||||
#[strum(serialize = "no")]
|
||||
NoRestart,
|
||||
#[serde(rename = "on-failure")]
|
||||
#[strum(serialize = "on-failure")]
|
||||
OnFailure,
|
||||
#[serde(rename = "always")]
|
||||
#[strum(serialize = "always")]
|
||||
Always,
|
||||
#[serde(rename = "unless-stopped")]
|
||||
#[strum(serialize = "unless-stopped")]
|
||||
UnlessStopped,
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
# optional. this will be the document title on the web page (shows up as text in the browser tab). default is 'monitor'
|
||||
title = "monitor"
|
||||
|
||||
# this should be the url used to access monitor in browser, potentially behind DNS, eg https://monitor.mogh.tech or http://12.34.56.78:9000
|
||||
host = "http://localhost:9000"
|
||||
host = "https://monitor.mogh.tech"
|
||||
|
||||
# the port the core system will run on. if running core in docker container, leave as this port as 9000 and use port bind eg. -p 9001:9000
|
||||
port = 9000
|
||||
@@ -19,20 +22,46 @@ jwt_valid_for = "1-wk"
|
||||
# webhook url given by slack app
|
||||
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 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
|
||||
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
|
||||
monitoring_interval = "1-min"
|
||||
|
||||
# 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"
|
||||
secret = "your_github_client_secret"
|
||||
|
||||
[google_oauth]
|
||||
enabled = true
|
||||
id = "your_google_client_id"
|
||||
secret = "your_google_client_secret"
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
port = 9001 # optional. 9001 is default
|
||||
port = 8000 # optional. 8000 is default
|
||||
repo_dir = "/repos" # optional. /repos is default. no reason to change if running the docker container, just mount your desired repo dir to /repos in the container
|
||||
stats_polling_rate = "5-sec" # optional. 5-sec is default. can use 1-sec, 5-sec, 10-sec, 30-sec, 1-min. controls granularity of system stats recorded
|
||||
allowed_ips = ["127.0.0.1"] # optional. default is empty, which will not block any request by ip.
|
||||
passkeys = ["abcdefghijk"] # optional. default is empty, which will not require any passkey to be passed by core.
|
||||
|
||||
[secrets] # optional. can inject these values into your deployments configuration.
|
||||
secret_variable = "secret_value"
|
||||
@@ -12,4 +13,4 @@ github_username2 = "github_token2"
|
||||
|
||||
[docker_accounts] # optional
|
||||
docker_username1 = "docker_token1"
|
||||
docker_username2 = "docker_token2"
|
||||
docker_username2 = "docker_token2"
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
[package]
|
||||
name = "core"
|
||||
version = "0.1.8"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# helpers = { package = "monitor_helpers", path = "../lib/helpers" }
|
||||
# types = { package = "monitor_types", path = "../lib/types" }
|
||||
helpers = { package = "monitor_helpers", version = "0.1.9" }
|
||||
types = { package = "monitor_types", version = "0.1.9" }
|
||||
helpers = { package = "monitor_helpers", path = "../lib/helpers" }
|
||||
types = { package = "monitor_types", path = "../lib/types" }
|
||||
db = { package = "db_client", path = "../lib/db_client" }
|
||||
periphery = { package = "periphery_client", path = "../lib/periphery_client" }
|
||||
axum_oauth2 = { path = "../lib/axum_oauth2" }
|
||||
tokio = { version = "1.24", features = ["full"] }
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
tokio-util = "0.7"
|
||||
axum = { version = "0.6", features = ["ws", "json"] }
|
||||
axum-extra = { version = "0.4", features = ["spa"] }
|
||||
axum-extra = { version = "0.5.0", features = ["spa"] }
|
||||
tower = { version = "0.4", features = ["full"] }
|
||||
tower-http = { version = "0.3", features = ["cors"] }
|
||||
tower-http = { version = "0.4.0", features = ["cors"] }
|
||||
slack = { package = "slack_client_rs", version = "0.0.8" }
|
||||
mungos = "0.3.0"
|
||||
mungos = "0.3.14"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
dotenv = "0.15"
|
||||
envy = "0.4"
|
||||
anyhow = "1.0"
|
||||
bcrypt = "0.13"
|
||||
bcrypt = "0.14"
|
||||
jwt = "0.16"
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
@@ -36,4 +34,6 @@ async_timing_util = "0.1.14"
|
||||
futures-util = "0.3"
|
||||
diff-struct = "0.5"
|
||||
typeshare = "1.0.0"
|
||||
hex = "0.4"
|
||||
hex = "0.4"
|
||||
aws-config = "0.54"
|
||||
aws-sdk-ec2 = "0.24"
|
||||
@@ -3,8 +3,8 @@ WORKDIR /builder
|
||||
|
||||
COPY ./core ./core
|
||||
|
||||
# COPY ./lib/types ./lib/types
|
||||
# COPY ./lib/helpers ./lib/helpers
|
||||
COPY ./lib/types ./lib/types
|
||||
COPY ./lib/helpers ./lib/helpers
|
||||
|
||||
COPY ./lib/db_client ./lib/db_client
|
||||
COPY ./lib/periphery_client ./lib/periphery_client
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use diff::Diff;
|
||||
use helpers::{all_logs_success, to_monitor_name};
|
||||
@@ -5,15 +7,22 @@ use mungos::{doc, to_bson};
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
Build, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget,
|
||||
AwsBuilderBuildConfig, Build, Log, Operation, PermissionLevel, Update, UpdateStatus,
|
||||
UpdateTarget, Version,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::RequestUser,
|
||||
helpers::{any_option_diff_is_some, option_diff_is_some},
|
||||
cloud::aws::{
|
||||
self, create_ec2_client, create_instance_with_ami, terminate_ec2_instance, Ec2Instance,
|
||||
},
|
||||
helpers::empty_or_only_spaces,
|
||||
state::State,
|
||||
};
|
||||
|
||||
const BUILDER_POLL_RATE_SECS: u64 = 2;
|
||||
const BUILDER_POLL_MAX_TRIES: usize = 30;
|
||||
|
||||
impl State {
|
||||
pub async fn get_build_check_permissions(
|
||||
&self,
|
||||
@@ -39,18 +48,19 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_build(
|
||||
&self,
|
||||
name: &str,
|
||||
server_id: String,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Build> {
|
||||
self.get_server_check_permissions(&server_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
pub async fn create_build(&self, name: &str, user: &RequestUser) -> anyhow::Result<Build> {
|
||||
if !user.is_admin && !user.create_build_permissions {
|
||||
return Err(anyhow!("user does not have permission to create builds"));
|
||||
}
|
||||
let start_ts = monitor_timestamp();
|
||||
let build = Build {
|
||||
name: to_monitor_name(name),
|
||||
server_id,
|
||||
docker_organization: self
|
||||
.config
|
||||
.docker_organizations
|
||||
.get(0)
|
||||
.map(|d| d.to_string()),
|
||||
aws_config: Some(AwsBuilderBuildConfig::default()),
|
||||
permissions: [(user.id.clone(), PermissionLevel::Update)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
@@ -84,10 +94,7 @@ impl State {
|
||||
mut build: Build,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Build> {
|
||||
build.id = self
|
||||
.create_build(&build.name, build.server_id.clone(), user)
|
||||
.await?
|
||||
.id;
|
||||
build.id = self.create_build(&build.name, user).await?.id;
|
||||
let build = self.update_build(build, user).await?;
|
||||
Ok(build)
|
||||
}
|
||||
@@ -96,14 +103,13 @@ impl State {
|
||||
&self,
|
||||
target_id: &str,
|
||||
new_name: String,
|
||||
new_server_id: String,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Build> {
|
||||
let mut build = self
|
||||
.get_build_check_permissions(target_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
build.name = new_name;
|
||||
build.server_id = new_server_id;
|
||||
build.version = Version::default();
|
||||
let build = self.create_full_build(build, user).await?;
|
||||
Ok(build)
|
||||
}
|
||||
@@ -116,12 +122,6 @@ impl State {
|
||||
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let server = self.db.get_server(&build.server_id).await?;
|
||||
let delete_repo_log = self
|
||||
.periphery
|
||||
.delete_repo(&server, &build.name)
|
||||
.await
|
||||
.context("failed at deleting repo")?;
|
||||
self.db.builds.delete_one(build_id).await?;
|
||||
let update = Update {
|
||||
target: UpdateTarget::Build(build_id.to_string()),
|
||||
@@ -129,13 +129,10 @@ impl State {
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
logs: vec![
|
||||
delete_repo_log,
|
||||
Log::simple(
|
||||
"delete build",
|
||||
format!("deleted build {} on server {}", build.name, server.name),
|
||||
),
|
||||
],
|
||||
logs: vec![Log::simple(
|
||||
"delete build",
|
||||
format!("deleted build {}", build.name),
|
||||
)],
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -171,19 +168,39 @@ impl State {
|
||||
mut new_build: Build,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Build> {
|
||||
let start_ts = monitor_timestamp();
|
||||
let current_build = self
|
||||
.get_build_check_permissions(&new_build.id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
if let Some(new_server_id) = &new_build.server_id {
|
||||
if current_build.server_id.is_none()
|
||||
|| new_server_id != current_build.server_id.as_ref().unwrap()
|
||||
{
|
||||
self.get_server_check_permissions(new_server_id, user, PermissionLevel::Update)
|
||||
.await
|
||||
.context("user does not have permission to attach build to this server")?;
|
||||
}
|
||||
}
|
||||
|
||||
// none of these should be changed through this method
|
||||
new_build.name = current_build.name.clone();
|
||||
new_build.permissions = current_build.permissions.clone();
|
||||
new_build.server_id = current_build.server_id.clone();
|
||||
new_build.last_built_at = String::new();
|
||||
new_build.last_built_at = current_build.last_built_at.clone();
|
||||
new_build.created_at = current_build.created_at.clone();
|
||||
new_build.updated_at = start_ts.clone();
|
||||
|
||||
// filter out any build args that contain empty strings
|
||||
// these could only happen by accident
|
||||
new_build.docker_build_args = new_build.docker_build_args.map(|mut args| {
|
||||
args.build_args = args
|
||||
.build_args
|
||||
.into_iter()
|
||||
.filter(|a| !empty_or_only_spaces(&a.variable) && !empty_or_only_spaces(&a.value))
|
||||
.collect();
|
||||
args
|
||||
});
|
||||
|
||||
self.db
|
||||
.builds
|
||||
.update_one(&new_build.id, mungos::Update::Regular(new_build.clone()))
|
||||
@@ -192,41 +209,42 @@ impl State {
|
||||
|
||||
let diff = current_build.diff(&new_build);
|
||||
|
||||
let mut update = Update {
|
||||
let update = Update {
|
||||
operation: Operation::UpdateBuild,
|
||||
target: UpdateTarget::Build(new_build.id.clone()),
|
||||
start_ts,
|
||||
status: UpdateStatus::InProgress,
|
||||
status: UpdateStatus::Complete,
|
||||
logs: vec![Log::simple(
|
||||
"build update",
|
||||
serde_json::to_string_pretty(&diff).unwrap(),
|
||||
)],
|
||||
operator: user.id.clone(),
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
// update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
if any_option_diff_is_some(&[&diff.repo, &diff.branch, &diff.github_account])
|
||||
|| option_diff_is_some(&diff.on_clone)
|
||||
{
|
||||
let server = self.db.get_server(¤t_build.server_id).await?;
|
||||
match self.periphery.clone_repo(&server, &new_build).await {
|
||||
Ok(clone_logs) => {
|
||||
update.logs.extend(clone_logs);
|
||||
}
|
||||
Err(e) => update
|
||||
.logs
|
||||
.push(Log::error("cloning repo", format!("{e:#?}"))),
|
||||
}
|
||||
}
|
||||
// if any_option_diff_is_some(&[&diff.repo, &diff.branch, &diff.github_account])
|
||||
// || option_diff_is_some(&diff.on_clone)
|
||||
// {
|
||||
// let server = self.db.get_server(¤t_build.server_id).await?;
|
||||
// match self.periphery.clone_repo(&server, &new_build).await {
|
||||
// Ok(clone_logs) => {
|
||||
// update.logs.extend(clone_logs);
|
||||
// }
|
||||
// Err(e) => update
|
||||
// .logs
|
||||
// .push(Log::error("cloning repo", format!("{e:#?}"))),
|
||||
// }
|
||||
// }
|
||||
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.success = all_logs_success(&update.logs);
|
||||
update.status = UpdateStatus::Complete;
|
||||
// update.end_ts = Some(monitor_timestamp());
|
||||
// update.success = all_logs_success(&update.logs);
|
||||
// update.status = UpdateStatus::Complete;
|
||||
|
||||
self.update_update(update).await?;
|
||||
self.add_update(update).await?;
|
||||
|
||||
Ok(new_build)
|
||||
}
|
||||
@@ -251,12 +269,9 @@ impl State {
|
||||
|
||||
async fn build_inner(&self, build_id: &str, user: &RequestUser) -> anyhow::Result<Update> {
|
||||
let mut build = self
|
||||
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
|
||||
.get_build_check_permissions(build_id, user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
let server = self.db.get_server(&build.server_id).await?;
|
||||
|
||||
build.version.increment();
|
||||
|
||||
let mut update = Update {
|
||||
target: UpdateTarget::Build(build_id.to_string()),
|
||||
operation: Operation::BuildBuild,
|
||||
@@ -267,12 +282,95 @@ impl State {
|
||||
version: build.version.clone().into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
let (server, aws_client) = if let Some(server_id) = &build.server_id {
|
||||
let server = self.db.get_server(server_id).await;
|
||||
if let Err(e) = server {
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.success = false;
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("get build server", format!("{e:#?}")));
|
||||
self.update_update(update.clone()).await?;
|
||||
return Err(e);
|
||||
}
|
||||
let server = Ec2Instance {
|
||||
instance_id: String::new(),
|
||||
server: server.unwrap(),
|
||||
};
|
||||
(server, None)
|
||||
} else if build.aws_config.is_some() {
|
||||
let start_ts = monitor_timestamp();
|
||||
let res = self.create_ec2_instance_for_build(&build).await;
|
||||
if let Err(e) = res {
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.success = false;
|
||||
update.logs.push(Log {
|
||||
stage: "start build server".to_string(),
|
||||
stderr: format!("{e:#?}"),
|
||||
success: false,
|
||||
start_ts,
|
||||
end_ts: monitor_timestamp(),
|
||||
..Default::default()
|
||||
});
|
||||
self.update_update(update).await?;
|
||||
return Err(e);
|
||||
}
|
||||
let (server, aws_client, logs) = res.unwrap();
|
||||
update.logs.extend(logs);
|
||||
self.update_update(update.clone()).await?;
|
||||
(server, aws_client)
|
||||
} else {
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.success = false;
|
||||
update.logs.push(Log::error(
|
||||
"start build",
|
||||
"build has neither server_id nor aws_config attached".to_string(),
|
||||
));
|
||||
self.update_update(update).await?;
|
||||
return Err(anyhow!(
|
||||
"build has neither server_id or aws_config attached"
|
||||
));
|
||||
};
|
||||
|
||||
let clone_success = match self.periphery.clone_repo(&server.server, &build).await {
|
||||
Ok(clone_logs) => {
|
||||
update.logs.extend(clone_logs);
|
||||
all_logs_success(&update.logs)
|
||||
}
|
||||
Err(e) => {
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("clone repo", format!("{e:#?}")));
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !clone_success {
|
||||
let _ = self
|
||||
.periphery
|
||||
.delete_repo(&server.server, &build.name)
|
||||
.await;
|
||||
if let Some(aws_client) = aws_client {
|
||||
self.terminate_ec2_instance(aws_client, &server, &mut update)
|
||||
.await;
|
||||
}
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.success = false;
|
||||
self.update_update(update.clone()).await?;
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
self.update_update(update.clone()).await?;
|
||||
|
||||
let build_logs = match self
|
||||
.periphery
|
||||
.build(&server, &build)
|
||||
.build(&server.server, &build)
|
||||
.await
|
||||
.context("failed at call to periphery to build")
|
||||
{
|
||||
@@ -282,9 +380,9 @@ impl State {
|
||||
|
||||
match build_logs {
|
||||
Some(logs) => {
|
||||
let success = all_logs_success(&logs);
|
||||
update.logs.extend(logs);
|
||||
update.success = all_logs_success(&update.logs);
|
||||
if update.success {
|
||||
if success {
|
||||
let _ = self
|
||||
.db
|
||||
.builds
|
||||
@@ -305,73 +403,207 @@ impl State {
|
||||
.push(Log::error("build", "builder busy".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.periphery
|
||||
.delete_repo(&server.server, &build.name)
|
||||
.await;
|
||||
|
||||
if let Some(aws_client) = aws_client {
|
||||
self.terminate_ec2_instance(aws_client, &server, &mut update)
|
||||
.await;
|
||||
}
|
||||
|
||||
update.success = all_logs_success(&update.logs);
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
|
||||
self.update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub async fn reclone_build(
|
||||
async fn create_ec2_instance_for_build(
|
||||
&self,
|
||||
build_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.build_busy(build_id).await {
|
||||
return Err(anyhow!("build busy"));
|
||||
build: &Build,
|
||||
) -> anyhow::Result<(Ec2Instance, Option<aws::Client>, Vec<Log>)> {
|
||||
if build.aws_config.is_none() {
|
||||
return Err(anyhow!("build has no aws_config attached"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.build_action_states.lock().await;
|
||||
let entry = lock.entry(build_id.to_string()).or_default();
|
||||
entry.recloning = true;
|
||||
}
|
||||
let res = self.reclone_build_inner(build_id, user).await;
|
||||
{
|
||||
let mut lock = self.build_action_states.lock().await;
|
||||
let entry = lock.entry(build_id.to_string()).or_default();
|
||||
entry.recloning = false;
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
async fn reclone_build_inner(
|
||||
&self,
|
||||
build_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
let build = self
|
||||
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let server = self.db.get_server(&build.server_id).await?;
|
||||
let mut update = Update {
|
||||
target: UpdateTarget::Build(build_id.to_string()),
|
||||
operation: Operation::RecloneBuild,
|
||||
start_ts: monitor_timestamp(),
|
||||
status: UpdateStatus::InProgress,
|
||||
operator: user.id.clone(),
|
||||
let start_instance_ts = monitor_timestamp();
|
||||
let aws_config = build.aws_config.as_ref().unwrap();
|
||||
let region = aws_config
|
||||
.region
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_region)
|
||||
.to_string();
|
||||
let aws_client = create_ec2_client(
|
||||
region,
|
||||
&self.config.aws.access_key_id,
|
||||
self.config.aws.secret_access_key.clone(),
|
||||
)
|
||||
.await;
|
||||
let ami_name = aws_config
|
||||
.ami_name
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_ami_name);
|
||||
let ami_id = &self
|
||||
.config
|
||||
.aws
|
||||
.available_ami_accounts
|
||||
.get(ami_name)
|
||||
.ok_or(anyhow!("no ami id associated with ami name {ami_name}"))?
|
||||
.ami_id;
|
||||
let instance_type = aws_config
|
||||
.instance_type
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_instance_type);
|
||||
let subnet_id = aws_config
|
||||
.subnet_id
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_subnet_id);
|
||||
let security_group_ids = aws_config
|
||||
.security_group_ids
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_security_group_ids)
|
||||
.to_owned();
|
||||
let readable_sec_group_ids = security_group_ids.join(", ");
|
||||
let volume_size_gb = *aws_config
|
||||
.volume_gb
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_volume_gb);
|
||||
let key_pair_name = aws_config
|
||||
.key_pair_name
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_key_pair_name);
|
||||
let assign_public_ip = *aws_config
|
||||
.assign_public_ip
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_assign_public_ip);
|
||||
let instance = create_instance_with_ami(
|
||||
&aws_client,
|
||||
&format!("BUILDER-{}-v{}", build.name, build.version.to_string()),
|
||||
ami_id,
|
||||
instance_type,
|
||||
subnet_id,
|
||||
security_group_ids,
|
||||
volume_size_gb,
|
||||
key_pair_name,
|
||||
assign_public_ip,
|
||||
)
|
||||
.await?;
|
||||
let instance_id = &instance.instance_id;
|
||||
let start_log = Log {
|
||||
stage: "start build instance".to_string(),
|
||||
success: true,
|
||||
stdout: format!("instance id: {instance_id}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_size_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}"),
|
||||
start_ts: start_instance_ts,
|
||||
end_ts: monitor_timestamp(),
|
||||
..Default::default()
|
||||
};
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
update.success = match self.periphery.clone_repo(&server, &build).await {
|
||||
Ok(clone_logs) => {
|
||||
update.logs.extend(clone_logs);
|
||||
true
|
||||
let start_connect_ts = monitor_timestamp();
|
||||
let mut res = Ok(String::new());
|
||||
for _ in 0..BUILDER_POLL_MAX_TRIES {
|
||||
let version = self.periphery.get_version(&instance.server).await;
|
||||
if let Ok(version) = version {
|
||||
let connect_log = Log {
|
||||
stage: "build instance connected".to_string(),
|
||||
success: true,
|
||||
stdout: format!("established contact with periphery on builder\nperiphery version: v{version}"),
|
||||
start_ts: start_connect_ts,
|
||||
end_ts: monitor_timestamp(),
|
||||
..Default::default()
|
||||
};
|
||||
return Ok((instance, Some(aws_client), vec![start_log, connect_log]));
|
||||
}
|
||||
Err(e) => {
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("clone repo", format!("{e:#?}")));
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
|
||||
self.update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
res = version;
|
||||
tokio::time::sleep(Duration::from_secs(BUILDER_POLL_RATE_SECS)).await;
|
||||
}
|
||||
let _ = terminate_ec2_instance(&aws_client, &instance.instance_id).await;
|
||||
Err(anyhow!(
|
||||
"unable to reach periphery agent on build server\n{res:#?}"
|
||||
))
|
||||
}
|
||||
|
||||
async fn terminate_ec2_instance(
|
||||
&self,
|
||||
aws_client: aws::Client,
|
||||
server: &Ec2Instance,
|
||||
update: &mut Update,
|
||||
) {
|
||||
let res = terminate_ec2_instance(&aws_client, &server.instance_id).await;
|
||||
if let Err(e) = res {
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("terminate instance", format!("{e:#?}")))
|
||||
} else {
|
||||
update.logs.push(Log::simple(
|
||||
"terminate instance",
|
||||
format!("terminate instance id {}", server.instance_id),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// pub async fn reclone_build(
|
||||
// &self,
|
||||
// build_id: &str,
|
||||
// user: &RequestUser,
|
||||
// ) -> anyhow::Result<Update> {
|
||||
// if self.build_busy(build_id).await {
|
||||
// return Err(anyhow!("build busy"));
|
||||
// }
|
||||
// {
|
||||
// let mut lock = self.build_action_states.lock().await;
|
||||
// let entry = lock.entry(build_id.to_string()).or_default();
|
||||
// entry.recloning = true;
|
||||
// }
|
||||
// let res = self.reclone_build_inner(build_id, user).await;
|
||||
// {
|
||||
// let mut lock = self.build_action_states.lock().await;
|
||||
// let entry = lock.entry(build_id.to_string()).or_default();
|
||||
// entry.recloning = false;
|
||||
// }
|
||||
// res
|
||||
// }
|
||||
|
||||
// async fn reclone_build_inner(
|
||||
// &self,
|
||||
// build_id: &str,
|
||||
// user: &RequestUser,
|
||||
// ) -> anyhow::Result<Update> {
|
||||
// let build = self
|
||||
// .get_build_check_permissions(build_id, user, PermissionLevel::Update)
|
||||
// .await?;
|
||||
// let server = self.db.get_server(&build.server_id).await?;
|
||||
// let mut update = Update {
|
||||
// target: UpdateTarget::Build(build_id.to_string()),
|
||||
// operation: Operation::RecloneBuild,
|
||||
// start_ts: monitor_timestamp(),
|
||||
// status: UpdateStatus::InProgress,
|
||||
// operator: user.id.clone(),
|
||||
// success: true,
|
||||
// ..Default::default()
|
||||
// };
|
||||
// update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
// update.success = match self.periphery.clone_repo(&server, &build).await {
|
||||
// Ok(clone_logs) => {
|
||||
// update.logs.extend(clone_logs);
|
||||
// true
|
||||
// }
|
||||
// Err(e) => {
|
||||
// update
|
||||
// .logs
|
||||
// .push(Log::error("clone repo", format!("{e:#?}")));
|
||||
// false
|
||||
// }
|
||||
// };
|
||||
|
||||
// update.status = UpdateStatus::Complete;
|
||||
// update.end_ts = Some(monitor_timestamp());
|
||||
|
||||
// self.update_update(update.clone()).await?;
|
||||
|
||||
// Ok(update)
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use diff::Diff;
|
||||
use helpers::{all_logs_success, to_monitor_name};
|
||||
use mungos::doc;
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
Deployment, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget,
|
||||
Deployment, DeploymentWithContainerState, DockerContainerState, Log, Operation,
|
||||
PermissionLevel, ServerStatus, ServerWithStatus, Update, UpdateStatus, UpdateTarget,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::RequestUser,
|
||||
helpers::{any_option_diff_is_some, option_diff_is_some},
|
||||
helpers::{any_option_diff_is_some, empty_or_only_spaces, get_image_name, option_diff_is_some},
|
||||
state::State,
|
||||
};
|
||||
|
||||
@@ -119,10 +121,14 @@ impl State {
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let server = self.db.get_server(&deployment.server_id).await?;
|
||||
let log = self
|
||||
let log = match self
|
||||
.periphery
|
||||
.container_remove(&server, &deployment.name)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error("destroy container", format!("{e:#?}")),
|
||||
};
|
||||
self.db
|
||||
.deployments
|
||||
.delete_one(deployment_id)
|
||||
@@ -193,6 +199,33 @@ impl State {
|
||||
new_deployment.created_at = current_deployment.created_at.clone();
|
||||
new_deployment.updated_at = start_ts.clone();
|
||||
|
||||
// filter out any volumes, ports, env vars, extra args which are or contain empty strings
|
||||
// these could only happen by accident
|
||||
new_deployment.docker_run_args.volumes = new_deployment
|
||||
.docker_run_args
|
||||
.volumes
|
||||
.into_iter()
|
||||
.filter(|v| !empty_or_only_spaces(&v.local) && !empty_or_only_spaces(&v.container))
|
||||
.collect();
|
||||
new_deployment.docker_run_args.ports = new_deployment
|
||||
.docker_run_args
|
||||
.ports
|
||||
.into_iter()
|
||||
.filter(|p| !empty_or_only_spaces(&p.local) && !empty_or_only_spaces(&p.container))
|
||||
.collect();
|
||||
new_deployment.docker_run_args.environment = new_deployment
|
||||
.docker_run_args
|
||||
.environment
|
||||
.into_iter()
|
||||
.filter(|e| !empty_or_only_spaces(&e.variable) && !empty_or_only_spaces(&e.value))
|
||||
.collect();
|
||||
new_deployment.docker_run_args.extra_args = new_deployment
|
||||
.docker_run_args
|
||||
.extra_args
|
||||
.into_iter()
|
||||
.filter(|a| a.len() != 0)
|
||||
.collect();
|
||||
|
||||
self.db
|
||||
.deployments
|
||||
.update_one(
|
||||
@@ -243,6 +276,157 @@ impl State {
|
||||
Ok(new_deployment)
|
||||
}
|
||||
|
||||
pub async fn rename_deployment(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
new_name: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(&deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.renaming = true;
|
||||
}
|
||||
let res = self
|
||||
.rename_deployment_inner(deployment_id, new_name, user)
|
||||
.await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.renaming = false;
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
async fn rename_deployment_inner(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
new_name: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
let start_ts = monitor_timestamp();
|
||||
let deployment = self
|
||||
.get_deployment_check_permissions(deployment_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let mut update = Update {
|
||||
target: UpdateTarget::Deployment(deployment_id.to_string()),
|
||||
operation: Operation::RenameDeployment,
|
||||
start_ts,
|
||||
status: UpdateStatus::InProgress,
|
||||
operator: user.id.to_string(),
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
let server_with_status = self.get_server(&deployment.server_id, user).await;
|
||||
if server_with_status.is_err() {
|
||||
update.logs.push(Log::error(
|
||||
"get server",
|
||||
format!(
|
||||
"failed to get server info: {:?}",
|
||||
server_with_status.as_ref().err().unwrap()
|
||||
),
|
||||
));
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = monitor_timestamp().into();
|
||||
update.success = false;
|
||||
self.update_update(update).await?;
|
||||
return Err(server_with_status.err().unwrap());
|
||||
}
|
||||
let ServerWithStatus { server, status } = server_with_status.unwrap();
|
||||
if status != ServerStatus::Ok {
|
||||
update.logs.push(Log::error(
|
||||
"check server status",
|
||||
String::from("cannot rename deployment when periphery is disabled or unreachable"),
|
||||
));
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = monitor_timestamp().into();
|
||||
update.success = false;
|
||||
self.update_update(update).await?;
|
||||
return Err(anyhow!(
|
||||
"cannot rename deployment when periphery is disabled or unreachable"
|
||||
));
|
||||
}
|
||||
let deployment_state = self
|
||||
.get_deployment_with_container_state(user, deployment_id)
|
||||
.await;
|
||||
if deployment_state.is_err() {
|
||||
update.logs.push(Log::error(
|
||||
"check deployment status",
|
||||
format!(
|
||||
"could not get current state of deployment: {:?}",
|
||||
deployment_state.as_ref().err().unwrap()
|
||||
),
|
||||
));
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = monitor_timestamp().into();
|
||||
update.success = false;
|
||||
self.update_update(update).await?;
|
||||
return Err(deployment_state.err().unwrap());
|
||||
}
|
||||
let DeploymentWithContainerState { state, .. } = deployment_state.unwrap();
|
||||
if state != DockerContainerState::NotDeployed {
|
||||
let log = self
|
||||
.periphery
|
||||
.container_rename(&server, &deployment.name, new_name)
|
||||
.await;
|
||||
if log.is_err() {
|
||||
update.logs.push(Log::error(
|
||||
"rename container",
|
||||
format!("{:?}", log.as_ref().err().unwrap()),
|
||||
));
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = monitor_timestamp().into();
|
||||
update.success = false;
|
||||
self.update_update(update).await?;
|
||||
return Err(log.err().unwrap());
|
||||
}
|
||||
let log = log.unwrap();
|
||||
if !log.success {
|
||||
update.logs.push(log);
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = monitor_timestamp().into();
|
||||
update.success = false;
|
||||
self.update_update(update).await?;
|
||||
return Err(anyhow!("rename container on periphery not successful"));
|
||||
}
|
||||
update.logs.push(log);
|
||||
}
|
||||
let res = self
|
||||
.db
|
||||
.deployments
|
||||
.update_one(
|
||||
deployment_id,
|
||||
mungos::Update::<()>::Set(
|
||||
doc! { "name": to_monitor_name(new_name), "updated_at": monitor_timestamp() },
|
||||
),
|
||||
)
|
||||
.await
|
||||
.context("failed to update deployment name on mongo");
|
||||
|
||||
if let Err(e) = res {
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("mongo update", format!("{e:?}")));
|
||||
} else {
|
||||
update.logs.push(Log::simple(
|
||||
"mongo update",
|
||||
String::from("updated name on mongo"),
|
||||
))
|
||||
}
|
||||
|
||||
update.end_ts = monitor_timestamp().into();
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.success = all_logs_success(&update.logs);
|
||||
|
||||
self.update_update(update.clone()).await?;
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
pub async fn reclone_deployment(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
@@ -339,14 +523,12 @@ impl State {
|
||||
.await?;
|
||||
let version = if let Some(build_id) = &deployment.build_id {
|
||||
let build = self.db.get_build(build_id).await?;
|
||||
let image = if let Some(docker_account) = &build.docker_account {
|
||||
if deployment.docker_run_args.docker_account.is_none() {
|
||||
let image = get_image_name(&build);
|
||||
if deployment.docker_run_args.docker_account.is_none() {
|
||||
if let Some(docker_account) = &build.docker_account {
|
||||
deployment.docker_run_args.docker_account = Some(docker_account.to_string())
|
||||
}
|
||||
format!("{docker_account}/{}", to_monitor_name(&build.name))
|
||||
} else {
|
||||
to_monitor_name(&build.name)
|
||||
};
|
||||
};
|
||||
}
|
||||
let version = if let Some(version) = &deployment.build_version {
|
||||
version.clone()
|
||||
} else {
|
||||
|
||||
@@ -125,7 +125,7 @@ impl State {
|
||||
} in &new_procedure.stages
|
||||
{
|
||||
match operation {
|
||||
BuildBuild | RecloneBuild => {
|
||||
BuildBuild => {
|
||||
self.get_build_check_permissions(&target_id, user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
}
|
||||
@@ -253,13 +253,6 @@ impl State {
|
||||
.context(format!("failed at build (id: {target_id})"))?;
|
||||
updates.push(update);
|
||||
}
|
||||
RecloneBuild => {
|
||||
let update = self
|
||||
.reclone_build(&target_id, user)
|
||||
.await
|
||||
.context(format!("failed at reclone build (id: {target_id})"))?;
|
||||
updates.push(update);
|
||||
}
|
||||
// server
|
||||
PruneImagesServer => {
|
||||
let update = self.prune_images(&target_id, user).await.context(format!(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use diff::Diff;
|
||||
use helpers::to_monitor_name;
|
||||
use futures_util::future::join_all;
|
||||
use mungos::doc;
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
@@ -47,7 +48,7 @@ impl State {
|
||||
}
|
||||
let start_ts = monitor_timestamp();
|
||||
let server = Server {
|
||||
name: to_monitor_name(name),
|
||||
name: name.to_string(),
|
||||
address,
|
||||
permissions: [(user.id.clone(), PermissionLevel::Update)]
|
||||
.into_iter()
|
||||
@@ -102,21 +103,59 @@ impl State {
|
||||
.get_server_check_permissions(server_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
self.db.servers.delete_one(&server_id).await?;
|
||||
let update = Update {
|
||||
let mut update = Update {
|
||||
target: UpdateTarget::Server(server_id.to_string()),
|
||||
operation: Operation::DeleteServer,
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
logs: vec![Log::simple(
|
||||
"delete server",
|
||||
format!("deleted server {}", server.name),
|
||||
)],
|
||||
success: true,
|
||||
status: UpdateStatus::InProgress,
|
||||
..Default::default()
|
||||
};
|
||||
self.add_update(update).await?;
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
let res = {
|
||||
let delete_deployments = self
|
||||
.db
|
||||
.deployments
|
||||
.get_some(doc! { "server_id": server_id }, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|d| async move { self.delete_deployment(&d.id, user).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 });
|
||||
let update_groups = self
|
||||
.db
|
||||
.groups
|
||||
.update_many(doc! {}, doc! { "$pull": { "servers": server_id } });
|
||||
let (dep_res, build_res, group_res) = tokio::join!(
|
||||
join_all(delete_deployments),
|
||||
join_all(delete_builds),
|
||||
update_groups
|
||||
);
|
||||
dep_res.into_iter().collect::<anyhow::Result<Vec<_>>>()?;
|
||||
build_res.into_iter().collect::<anyhow::Result<Vec<_>>>()?;
|
||||
group_res?;
|
||||
self.db.servers.delete_one(&server_id).await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let log = match res {
|
||||
Ok(_) => Log::simple("delete server", format!("deleted server {}", server.name)),
|
||||
Err(e) => Log::error("delete server", format!("failed to delete server\n{e:#?}")),
|
||||
};
|
||||
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.success = log.success;
|
||||
update.logs.push(log);
|
||||
|
||||
self.update_update(update).await?;
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use axum::{
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize, Document, FindOptions, Serialize};
|
||||
use types::{
|
||||
traits::Permissioned, Build, BuildActionState, BuildVersionsReponse, Operation,
|
||||
PermissionLevel, UpdateStatus,
|
||||
traits::Permissioned, AwsBuilderConfig, Build, BuildActionState, BuildVersionsReponse,
|
||||
Operation, PermissionLevel, UpdateStatus,
|
||||
};
|
||||
use typeshare::typeshare;
|
||||
|
||||
@@ -31,14 +31,12 @@ struct BuildId {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CreateBuildBody {
|
||||
name: String,
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CopyBuildBody {
|
||||
name: String,
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -88,7 +86,7 @@ pub fn router() -> Router {
|
||||
Extension(user): RequestUserExtension,
|
||||
Json(build): Json<CreateBuildBody>| async move {
|
||||
let build = state
|
||||
.create_build(&build.name, build.server_id, &user)
|
||||
.create_build(&build.name, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(build))
|
||||
@@ -121,7 +119,7 @@ pub fn router() -> Router {
|
||||
Json(build): Json<CopyBuildBody>| async move {
|
||||
let build = spawn_request_action(async move {
|
||||
state
|
||||
.copy_build(&id, build.name, build.server_id, &user)
|
||||
.copy_build(&id, build.name, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -181,23 +179,6 @@ pub fn router() -> Router {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/reclone",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(build_id): Path<BuildId>| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.reclone_build(&build_id.id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
.await??;
|
||||
response!(Json(update))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/action_state",
|
||||
get(
|
||||
@@ -227,6 +208,22 @@ pub fn router() -> Router {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/aws_builder_defaults",
|
||||
get(|Extension(state): StateExtension| async move {
|
||||
Json(AwsBuilderConfig {
|
||||
access_key_id: String::new(),
|
||||
secret_access_key: String::new(),
|
||||
..state.config.aws.clone()
|
||||
})
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/docker_organizations",
|
||||
get(|Extension(state): StateExtension| async move {
|
||||
Json(state.config.docker_organizations.clone())
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
||||
@@ -8,10 +8,11 @@ use axum::{
|
||||
};
|
||||
use futures_util::future::join_all;
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{Deserialize, Document, Serialize};
|
||||
use mungos::{doc, options::FindOneOptions, Deserialize, Document, Serialize};
|
||||
use types::{
|
||||
traits::Permissioned, Deployment, DeploymentActionState, DeploymentWithContainerState,
|
||||
DockerContainerState, DockerContainerStats, Log, PermissionLevel, Server,
|
||||
DockerContainerState, DockerContainerStats, Log, Operation, PermissionLevel, Server,
|
||||
UpdateStatus,
|
||||
};
|
||||
use typeshare::typeshare;
|
||||
|
||||
@@ -42,6 +43,12 @@ pub struct CopyDeploymentBody {
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RenameDeploymentBody {
|
||||
new_name: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetContainerLogQuery {
|
||||
@@ -161,6 +168,24 @@ pub fn router() -> Router {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/rename",
|
||||
patch(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
deployment: Path<DeploymentId>,
|
||||
body: Json<RenameDeploymentBody>| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.rename_deployment(&deployment.id, &body.new_name, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
.await??;
|
||||
response!(Json(update))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/reclone",
|
||||
post(
|
||||
@@ -297,19 +322,33 @@ pub fn router() -> Router {
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
Path(DeploymentId { id })| async move {
|
||||
let stats = state
|
||||
.get_deployment_container_stats(&deployment_id.id, &user)
|
||||
.get_deployment_container_stats(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(stats))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/deployed_version",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(DeploymentId { id })| async move {
|
||||
let version = state
|
||||
.get_deployment_deployed_version(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(version)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
impl State {
|
||||
async fn get_deployment_with_container_state(
|
||||
pub async fn get_deployment_with_container_state(
|
||||
&self,
|
||||
user: &RequestUser,
|
||||
id: &str,
|
||||
@@ -443,4 +482,53 @@ impl State {
|
||||
.await?;
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
async fn get_deployment_deployed_version(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<String> {
|
||||
let deployment = self
|
||||
.get_deployment_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await?;
|
||||
if deployment.build_id.is_some() {
|
||||
let latest_deploy_update = self
|
||||
.db
|
||||
.updates
|
||||
.find_one(
|
||||
doc! {
|
||||
"target": {
|
||||
"type": "Deployment",
|
||||
"id": id
|
||||
},
|
||||
"operation": Operation::DeployContainer.to_string(),
|
||||
"status": UpdateStatus::Complete.to_string(),
|
||||
"success": true,
|
||||
},
|
||||
FindOneOptions::builder().sort(doc! { "_id": -1 }).build(),
|
||||
)
|
||||
.await
|
||||
.context("failed at query to get latest deploy update from mongo")?;
|
||||
if let Some(update) = latest_deploy_update {
|
||||
if let Some(version) = update.version {
|
||||
Ok(version.to_string())
|
||||
} else {
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
} else {
|
||||
let split = deployment
|
||||
.docker_run_args
|
||||
.image
|
||||
.split(':')
|
||||
.collect::<Vec<&str>>();
|
||||
if let Some(version) = split.get(1) {
|
||||
Ok(version.to_string())
|
||||
} else {
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ impl State {
|
||||
id: String::from(GITHUB_WEBHOOK_USER_ID),
|
||||
is_admin: true,
|
||||
create_server_permissions: false,
|
||||
create_build_permissions: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -103,6 +104,7 @@ impl State {
|
||||
id: String::from(GITHUB_WEBHOOK_USER_ID),
|
||||
is_admin: true,
|
||||
create_server_permissions: false,
|
||||
create_build_permissions: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -127,6 +129,7 @@ impl State {
|
||||
id: String::from(GITHUB_WEBHOOK_USER_ID),
|
||||
is_admin: true,
|
||||
create_server_permissions: false,
|
||||
create_build_permissions: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Path,
|
||||
http::{Request, StatusCode},
|
||||
middleware,
|
||||
routing::get,
|
||||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use futures_util::Future;
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::Deserialize;
|
||||
use types::User;
|
||||
use mungos::{doc, Deserialize};
|
||||
use types::{PermissionLevel, UpdateTarget, User};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_request, JwtExtension, RequestUserExtension},
|
||||
state::StateExtension,
|
||||
auth::{auth_request, JwtExtension, RequestUser, RequestUserExtension},
|
||||
response,
|
||||
state::{State, StateExtension},
|
||||
ResponseResult,
|
||||
};
|
||||
|
||||
pub mod build;
|
||||
@@ -27,24 +30,66 @@ pub mod secret;
|
||||
pub mod server;
|
||||
pub mod update;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateDescriptionBody {
|
||||
target: UpdateTarget,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/user",
|
||||
get(|jwt, req| async { get_user(jwt, req).await.map_err(handle_anyhow_error) }),
|
||||
"/title",
|
||||
get(|state: StateExtension| async move { state.config.title.clone() }),
|
||||
)
|
||||
.route("/user", get(get_request_user))
|
||||
.nest("/listener", github_listener::router())
|
||||
.nest(
|
||||
"/",
|
||||
Router::new()
|
||||
.route("/user/:id", get(get_user_at_id))
|
||||
.route(
|
||||
"/username/:id",
|
||||
get(|state, user_id| async {
|
||||
get_username(state, user_id)
|
||||
get(|state: StateExtension, Path(UserId { id })| async move {
|
||||
let user = state
|
||||
.db
|
||||
.get_user(&id)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
.context("failed to find user at id")
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(user.username))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/github_webhook_base_url",
|
||||
get(|state: StateExtension| async move {
|
||||
state
|
||||
.config
|
||||
.github_webhook_base_url
|
||||
.as_ref()
|
||||
.unwrap_or(&state.config.host)
|
||||
.to_string()
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/update_description",
|
||||
post(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
body: Json<UpdateDescriptionBody>| async move {
|
||||
state
|
||||
.update_description(&body.target, &body.description, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
},
|
||||
),
|
||||
)
|
||||
.route("/users", get(get_users))
|
||||
.nest("/build", build::router())
|
||||
.nest("/deployment", deployment::router())
|
||||
@@ -58,8 +103,11 @@ pub fn router() -> Router {
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::Result<Json<User>> {
|
||||
let mut user = jwt.authenticate(&req).await?;
|
||||
async fn get_request_user(
|
||||
Extension(jwt): JwtExtension,
|
||||
req: Request<Body>,
|
||||
) -> ResponseResult<Json<User>> {
|
||||
let mut user = jwt.authenticate(&req).await.map_err(handle_anyhow_error)?;
|
||||
user.password = None;
|
||||
for secret in &mut user.secrets {
|
||||
secret.hash = String::new();
|
||||
@@ -67,23 +115,10 @@ async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::R
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
async fn get_username(
|
||||
state: StateExtension,
|
||||
Path(UserId { id }): Path<UserId>,
|
||||
) -> anyhow::Result<String> {
|
||||
let user = state.db.get_user(&id).await?;
|
||||
Ok(user.username)
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
) -> Result<Json<Vec<User>>, (StatusCode, String)> {
|
||||
) -> ResponseResult<Json<Vec<User>>> {
|
||||
if user.is_admin {
|
||||
let users = state
|
||||
.db
|
||||
@@ -105,8 +140,33 @@ async fn get_users(
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_at_id(
|
||||
state: StateExtension,
|
||||
Path(UserId { id }): Path<UserId>,
|
||||
user: RequestUserExtension,
|
||||
) -> ResponseResult<Json<User>> {
|
||||
if user.is_admin {
|
||||
let mut user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&id)
|
||||
.await
|
||||
.context("failed at query to get user from mongo")
|
||||
.map_err(handle_anyhow_error)?
|
||||
.ok_or(anyhow!(""))
|
||||
.map_err(handle_anyhow_error)?;
|
||||
user.password = None;
|
||||
for secret in &mut user.secrets {
|
||||
secret.hash = String::new();
|
||||
}
|
||||
Ok(Json(user))
|
||||
} else {
|
||||
Err((StatusCode::UNAUTHORIZED, "user is not admin".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// need to run requested actions in here to prevent them being dropped mid action when user disconnects prematurely
|
||||
pub async fn spawn_request_action<A>(action: A) -> Result<A::Output, (StatusCode, String)>
|
||||
pub async fn spawn_request_action<A>(action: A) -> ResponseResult<A::Output>
|
||||
where
|
||||
A: Future + Send + 'static,
|
||||
A::Output: Send + 'static,
|
||||
@@ -117,3 +177,57 @@ where
|
||||
.map_err(handle_anyhow_error)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn update_description(
|
||||
&self,
|
||||
target: &UpdateTarget,
|
||||
description: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<()> {
|
||||
match target {
|
||||
UpdateTarget::Build(id) => {
|
||||
self.get_build_check_permissions(id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
self.db
|
||||
.builds
|
||||
.update_one::<()>(id, mungos::Update::Set(doc! { "description": description }))
|
||||
.await?;
|
||||
}
|
||||
UpdateTarget::Deployment(id) => {
|
||||
self.get_deployment_check_permissions(id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
self.db
|
||||
.deployments
|
||||
.update_one::<()>(id, mungos::Update::Set(doc! { "description": description }))
|
||||
.await?;
|
||||
}
|
||||
UpdateTarget::Server(id) => {
|
||||
self.get_server_check_permissions(id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
self.db
|
||||
.servers
|
||||
.update_one::<()>(id, mungos::Update::Set(doc! { "description": description }))
|
||||
.await?;
|
||||
}
|
||||
UpdateTarget::Group(id) => {
|
||||
self.get_group_check_permissions(id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
self.db
|
||||
.groups
|
||||
.update_one::<()>(id, mungos::Update::Set(doc! { "description": description }))
|
||||
.await?;
|
||||
}
|
||||
UpdateTarget::Procedure(id) => {
|
||||
self.get_procedure_check_permissions(id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
self.db
|
||||
.procedures
|
||||
.update_one::<()>(id, mungos::Update::Set(doc! { "description": description }))
|
||||
.await?;
|
||||
}
|
||||
_ => return Err(anyhow!("invalid target: {target:?}")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ struct ModifyUserCreateServerBody {
|
||||
create_server_permissions: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ModifyUserCreateBuildBody {
|
||||
user_id: String,
|
||||
create_build_permissions: bool,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
@@ -62,6 +69,15 @@ pub fn router() -> Router {
|
||||
response!(Json(update))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/modify_create_build",
|
||||
post(|state, user, body| async {
|
||||
let update = modify_user_create_build_permissions(state, user, body)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(update))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn update_permissions(
|
||||
@@ -268,7 +284,7 @@ async fn modify_user_create_server_permissions(
|
||||
"user does not have permissions for this action (not admin)"
|
||||
));
|
||||
}
|
||||
let user = state
|
||||
let target_user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&user_id)
|
||||
@@ -296,7 +312,62 @@ async fn modify_user_create_server_permissions(
|
||||
"modify user create server permissions",
|
||||
format!(
|
||||
"{update_type} create server permissions for {} (id: {})",
|
||||
user.username, user.id
|
||||
target_user.username, target_user.id
|
||||
),
|
||||
)],
|
||||
start_ts: ts.clone(),
|
||||
end_ts: Some(ts),
|
||||
status: UpdateStatus::Complete,
|
||||
success: true,
|
||||
operator: user.id.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
update.id = state.add_update(update.clone()).await?;
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
async fn modify_user_create_build_permissions(
|
||||
Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Json(ModifyUserCreateBuildBody {
|
||||
user_id,
|
||||
create_build_permissions,
|
||||
}): Json<ModifyUserCreateBuildBody>,
|
||||
) -> anyhow::Result<Update> {
|
||||
if !user.is_admin {
|
||||
return Err(anyhow!(
|
||||
"user does not have permissions for this action (not admin)"
|
||||
));
|
||||
}
|
||||
let target_user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&user_id)
|
||||
.await
|
||||
.context("failed at mongo query to find target user")?
|
||||
.ok_or(anyhow!("did not find any user with user_id {user_id}"))?;
|
||||
state
|
||||
.db
|
||||
.users
|
||||
.update_one::<Document>(
|
||||
&user_id,
|
||||
mungos::Update::Set(doc! { "create_build_permissions": create_build_permissions }),
|
||||
)
|
||||
.await?;
|
||||
let update_type = if create_build_permissions {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
};
|
||||
let ts = monitor_timestamp();
|
||||
let mut update = Update {
|
||||
target: UpdateTarget::System,
|
||||
operation: Operation::ModifyUserCreateBuildPermissions,
|
||||
logs: vec![Log::simple(
|
||||
"modify user create build permissions",
|
||||
format!(
|
||||
"{update_type} create build permissions for {} (id: {})",
|
||||
target_user.username, target_user.id
|
||||
),
|
||||
)],
|
||||
start_ts: ts.clone(),
|
||||
|
||||
@@ -339,6 +339,20 @@ pub fn router() -> Router {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/secrets",
|
||||
get(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(ServerId { id })| async move {
|
||||
let vars = state
|
||||
.get_available_secrets(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(vars))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/action_state",
|
||||
get(
|
||||
@@ -356,7 +370,11 @@ pub fn router() -> Router {
|
||||
}
|
||||
|
||||
impl State {
|
||||
async fn get_server(&self, id: &str, user: &RequestUser) -> anyhow::Result<ServerWithStatus> {
|
||||
pub async fn get_server(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<ServerWithStatus> {
|
||||
let server = self
|
||||
.get_server_check_permissions(id, user, PermissionLevel::Read)
|
||||
.await?;
|
||||
@@ -628,6 +646,18 @@ impl State {
|
||||
Ok(docker_accounts)
|
||||
}
|
||||
|
||||
async fn get_available_secrets(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let server = self
|
||||
.get_server_check_permissions(id, user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let vars = self.periphery.get_available_secrets(&server).await?;
|
||||
Ok(vars)
|
||||
}
|
||||
|
||||
async fn get_server_action_states(
|
||||
&self,
|
||||
id: String,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{extract::Query, routing::get, Extension, Json, Router};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, to_bson, ObjectId};
|
||||
use mungos::{doc, to_bson};
|
||||
use serde_json::Value;
|
||||
use types::{PermissionLevel, Update, UpdateTarget};
|
||||
|
||||
use crate::{
|
||||
auth::{RequestUser, RequestUserExtension},
|
||||
helpers::parse_comma_seperated_list,
|
||||
response,
|
||||
state::{State, StateExtension},
|
||||
};
|
||||
@@ -27,8 +26,27 @@ pub fn router() -> Router {
|
||||
.map(|v| v.as_str().unwrap_or("0").parse().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
let target = serde_json::from_str::<UpdateTarget>(&value.to_string()).ok();
|
||||
let show_builds = value
|
||||
.get("show_builds")
|
||||
.map(|b| {
|
||||
b.as_str()
|
||||
.unwrap_or("false")
|
||||
.parse::<bool>()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let operations = value
|
||||
.get("operations")
|
||||
.map(|o| {
|
||||
let o = o.as_str().unwrap_or_default();
|
||||
if o.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
parse_comma_seperated_list::<String>(o).ok()
|
||||
})
|
||||
.flatten();
|
||||
let updates = state
|
||||
.list_updates(target, offset, &user)
|
||||
.list_updates(target, offset, show_builds, operations, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(updates))
|
||||
@@ -82,18 +100,47 @@ impl State {
|
||||
&self,
|
||||
target: Option<UpdateTarget>,
|
||||
offset: u64,
|
||||
show_builds: bool,
|
||||
operations: Option<Vec<String>>,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Vec<Update>> {
|
||||
let filter = match target {
|
||||
let mut filter = match target {
|
||||
Some(target) => {
|
||||
self.permission_on_update_target(&target, user).await?;
|
||||
Some(doc! {
|
||||
"target": to_bson(&target).unwrap()
|
||||
})
|
||||
if let (UpdateTarget::Deployment(id), true) = (&target, show_builds) {
|
||||
let deployment = self
|
||||
.get_deployment_check_permissions(id, user, PermissionLevel::Read)
|
||||
.await?;
|
||||
if let Some(build_id) = &deployment.build_id {
|
||||
let build = self
|
||||
.get_build_check_permissions(build_id, user, PermissionLevel::Read)
|
||||
.await;
|
||||
if let Ok(_) = build {
|
||||
doc! {
|
||||
"$or": [
|
||||
{"target": to_bson(&target).unwrap()},
|
||||
{"target": { "type": "Build", "id": build_id }, "operation": "build_build"}
|
||||
],
|
||||
}
|
||||
} else {
|
||||
doc! {
|
||||
"target": to_bson(&target).unwrap()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doc! {
|
||||
"target": to_bson(&target).unwrap()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.permission_on_update_target(&target, user).await?;
|
||||
doc! {
|
||||
"target": to_bson(&target).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if user.is_admin {
|
||||
None
|
||||
doc! {}
|
||||
} else {
|
||||
let permissions_field = format!("permissions.{}", user.id);
|
||||
let target_filter = doc! {
|
||||
@@ -110,7 +157,7 @@ impl State {
|
||||
.await
|
||||
.context("failed at query to get users builds")?
|
||||
.into_iter()
|
||||
.map(|e| ObjectId::from_str(&e.id).unwrap())
|
||||
.map(|e| e.id)
|
||||
.collect::<Vec<_>>();
|
||||
let deployment_ids = self
|
||||
.db
|
||||
@@ -119,7 +166,7 @@ impl State {
|
||||
.await
|
||||
.context("failed at query to get users deployments")?
|
||||
.into_iter()
|
||||
.map(|e| ObjectId::from_str(&e.id).unwrap())
|
||||
.map(|e| e.id)
|
||||
.collect::<Vec<_>>();
|
||||
let server_ids = self
|
||||
.db
|
||||
@@ -128,7 +175,7 @@ impl State {
|
||||
.await
|
||||
.context("failed at query to get users servers")?
|
||||
.into_iter()
|
||||
.map(|e| ObjectId::from_str(&e.id).unwrap())
|
||||
.map(|e| e.id)
|
||||
.collect::<Vec<_>>();
|
||||
let procedure_ids = self
|
||||
.db
|
||||
@@ -137,7 +184,7 @@ impl State {
|
||||
.await
|
||||
.context("failed at query to get users procedures")?
|
||||
.into_iter()
|
||||
.map(|e| ObjectId::from_str(&e.id).unwrap())
|
||||
.map(|e| e.id)
|
||||
.collect::<Vec<_>>();
|
||||
let filter = doc! {
|
||||
"$or": [
|
||||
@@ -147,10 +194,13 @@ impl State {
|
||||
{ "target.type": "Procedure", "target.id": { "$in": &procedure_ids } }
|
||||
]
|
||||
};
|
||||
Some(filter)
|
||||
filter
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(operations) = operations {
|
||||
filter.insert("operation", doc! { "$in": operations });
|
||||
}
|
||||
let mut updates = self
|
||||
.db
|
||||
.updates
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct RequestUser {
|
||||
pub id: String,
|
||||
pub is_admin: bool,
|
||||
pub create_server_permissions: bool,
|
||||
pub create_build_permissions: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -109,6 +110,7 @@ impl JwtClient {
|
||||
id: claims.id,
|
||||
is_admin: user.admin,
|
||||
create_server_permissions: user.create_server_permissions,
|
||||
create_build_permissions: user.create_build_permissions,
|
||||
};
|
||||
Ok(user)
|
||||
} else {
|
||||
|
||||
199
core/src/cloud/aws.rs
Normal file
199
core/src/cloud/aws.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use aws_sdk_ec2::model::{
|
||||
BlockDeviceMapping, EbsBlockDevice, InstanceNetworkInterfaceSpecification, InstanceStateChange,
|
||||
InstanceStateName, InstanceStatus, ResourceType, Tag, TagSpecification,
|
||||
};
|
||||
pub use aws_sdk_ec2::{
|
||||
model::InstanceType,
|
||||
output::{DescribeInstanceStatusOutput, TerminateInstancesOutput},
|
||||
Client, Region,
|
||||
};
|
||||
use types::Server;
|
||||
|
||||
pub async fn create_ec2_client(
|
||||
region: String,
|
||||
access_key_id: &str,
|
||||
secret_access_key: String,
|
||||
) -> Client {
|
||||
// There may be a better way to pass these keys to client
|
||||
std::env::set_var("AWS_ACCESS_KEY_ID", access_key_id);
|
||||
std::env::set_var("AWS_SECRET_ACCESS_KEY", secret_access_key);
|
||||
let region = Region::new(region);
|
||||
let config = aws_config::from_env().region(region).load().await;
|
||||
let client = Client::new(&config);
|
||||
client
|
||||
}
|
||||
|
||||
pub struct Ec2Instance {
|
||||
pub instance_id: String,
|
||||
pub server: Server,
|
||||
}
|
||||
|
||||
const POLL_RATE_SECS: u64 = 2;
|
||||
const MAX_POLL_TRIES: usize = 30;
|
||||
|
||||
/// this will only resolve after the instance is running
|
||||
/// should still poll the periphery agent after creation
|
||||
pub async fn create_instance_with_ami(
|
||||
client: &Client,
|
||||
instance_name: &str,
|
||||
ami_id: &str,
|
||||
instance_type: &str,
|
||||
subnet_id: &str,
|
||||
security_group_ids: Vec<String>,
|
||||
volume_size_gb: i32,
|
||||
key_pair_name: &str,
|
||||
assign_public_ip: bool,
|
||||
) -> anyhow::Result<Ec2Instance> {
|
||||
let instance_type = InstanceType::from(instance_type);
|
||||
if let InstanceType::Unknown(t) = instance_type {
|
||||
return Err(anyhow!("unknown instance type {t:?}"));
|
||||
}
|
||||
let res = client
|
||||
.run_instances()
|
||||
.image_id(ami_id)
|
||||
.instance_type(instance_type)
|
||||
.block_device_mappings(
|
||||
BlockDeviceMapping::builder()
|
||||
.set_device_name(String::from("/dev/sda1").into())
|
||||
.set_ebs(
|
||||
EbsBlockDevice::builder()
|
||||
.volume_size(volume_size_gb)
|
||||
.build()
|
||||
.into(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.network_interfaces(
|
||||
InstanceNetworkInterfaceSpecification::builder()
|
||||
.subnet_id(subnet_id)
|
||||
.associate_public_ip_address(assign_public_ip)
|
||||
.set_groups(security_group_ids.into())
|
||||
.device_index(0)
|
||||
.build(),
|
||||
)
|
||||
.key_name(key_pair_name)
|
||||
.tag_specifications(
|
||||
TagSpecification::builder()
|
||||
.tags(Tag::builder().key("Name").value(instance_name).build())
|
||||
.resource_type(ResourceType::Instance)
|
||||
.build(),
|
||||
)
|
||||
.min_count(1)
|
||||
.max_count(1)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to start builder ec2 instance")?;
|
||||
let instance = res
|
||||
.instances()
|
||||
.ok_or(anyhow!("got None for created instances"))?
|
||||
.get(0)
|
||||
.ok_or(anyhow!("instances array is empty"))?;
|
||||
let instance_id = instance
|
||||
.instance_id()
|
||||
.ok_or(anyhow!("instance does not have instance_id"))?
|
||||
.to_string();
|
||||
for _ in 0..MAX_POLL_TRIES {
|
||||
let state_name = get_ec2_instance_state_name(&client, &instance_id).await?;
|
||||
if state_name == Some(InstanceStateName::Running) {
|
||||
let ip = if assign_public_ip {
|
||||
get_ec2_instance_public_ip(client, &instance_id).await?
|
||||
} else {
|
||||
instance
|
||||
.private_ip_address()
|
||||
.ok_or(anyhow!("instance does not have private ip"))?
|
||||
.to_string()
|
||||
};
|
||||
let server = Server {
|
||||
address: format!("http://{ip}:8000"),
|
||||
..Default::default()
|
||||
};
|
||||
return Ok(Ec2Instance {
|
||||
instance_id,
|
||||
server,
|
||||
});
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
|
||||
}
|
||||
Err(anyhow!("instance not running after polling"))
|
||||
}
|
||||
|
||||
pub async fn get_ec2_instance_status(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<Option<InstanceStatus>> {
|
||||
let status = client
|
||||
.describe_instance_status()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to get instance status from aws")?
|
||||
.instance_statuses()
|
||||
.ok_or(anyhow!("instance statuses is None"))?
|
||||
.get(0)
|
||||
.map(|s| s.to_owned());
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub async fn get_ec2_instance_state_name(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<Option<InstanceStateName>> {
|
||||
let status = get_ec2_instance_status(client, instance_id).await?;
|
||||
if status.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
let state = status
|
||||
.unwrap()
|
||||
.instance_state()
|
||||
.ok_or(anyhow!("instance state is None"))?
|
||||
.name()
|
||||
.ok_or(anyhow!("instance state name is None"))?
|
||||
.to_owned();
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
pub async fn get_ec2_instance_public_ip(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let ip = client
|
||||
.describe_instances()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to get instance status from aws")?
|
||||
.reservations()
|
||||
.ok_or(anyhow!("instance reservations is None"))?
|
||||
.get(0)
|
||||
.ok_or(anyhow!("instance reservations is empty"))?
|
||||
.instances()
|
||||
.ok_or(anyhow!("instances is None"))?
|
||||
.get(0)
|
||||
.ok_or(anyhow!("instances is empty"))?
|
||||
.public_ip_address()
|
||||
.ok_or(anyhow!("instance has no public ip"))?
|
||||
.to_string();
|
||||
|
||||
Ok(ip)
|
||||
}
|
||||
|
||||
pub async fn terminate_ec2_instance(
|
||||
client: &Client,
|
||||
instance_id: &str,
|
||||
) -> anyhow::Result<InstanceStateChange> {
|
||||
let res = client
|
||||
.terminate_instances()
|
||||
.instance_ids(instance_id)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to terminate instance from aws")?
|
||||
.terminating_instances()
|
||||
.ok_or(anyhow!("terminating instances is None"))?
|
||||
.get(0)
|
||||
.ok_or(anyhow!("terminating instances is empty"))?
|
||||
.to_owned();
|
||||
Ok(res)
|
||||
}
|
||||
1
core/src/cloud/mod.rs
Normal file
1
core/src/cloud/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod aws;
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use diff::{Diff, OptionDiff};
|
||||
use helpers::to_monitor_name;
|
||||
use types::Build;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! response {
|
||||
@@ -25,3 +30,39 @@ where
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn parse_comma_seperated_list<T: FromStr>(comma_sep_list: &str) -> anyhow::Result<Vec<T>> {
|
||||
comma_sep_list
|
||||
.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_image_name(build: &Build) -> String {
|
||||
let name = to_monitor_name(&build.name);
|
||||
match &build.docker_organization {
|
||||
Some(org) => format!("{org}/{name}"),
|
||||
None => match &build.docker_account {
|
||||
Some(acct) => format!("{acct}/{name}"),
|
||||
None => name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_or_only_spaces(word: &str) -> bool {
|
||||
if word.len() == 0 {
|
||||
return true;
|
||||
}
|
||||
for char in word.chars() {
|
||||
if char != ' ' {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
|
||||
use ::helpers::get_socket_addr;
|
||||
use auth::JwtClient;
|
||||
use axum::Router;
|
||||
use axum::{http::StatusCode, Router};
|
||||
use state::State;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
mod actions;
|
||||
mod api;
|
||||
mod auth;
|
||||
mod cloud;
|
||||
mod config;
|
||||
mod helpers;
|
||||
mod monitoring;
|
||||
mod state;
|
||||
mod ws;
|
||||
|
||||
type ResponseResult<T> = Result<T, (StatusCode, String)>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (config, spa_router) = config::load();
|
||||
|
||||
println!("starting monitor core on port {}...", config.port);
|
||||
@@ -39,6 +42,7 @@ async fn main() {
|
||||
|
||||
axum::Server::bind(&get_socket_addr(config.port))
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.expect("monitor core axum server crashed");
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::{cmp::Ordering, collections::HashMap, path::PathBuf};
|
||||
|
||||
use async_timing_util::{
|
||||
unix_timestamp_ms, wait_until_timelength, Timelength, ONE_DAY_MS, ONE_HOUR_MS,
|
||||
};
|
||||
@@ -12,7 +14,7 @@ use crate::state::State;
|
||||
pub struct AlertStatus {
|
||||
cpu_alert: bool,
|
||||
mem_alert: bool,
|
||||
disk_alert: bool,
|
||||
disk_alert: HashMap<PathBuf, bool>,
|
||||
component_alert: bool,
|
||||
}
|
||||
|
||||
@@ -98,17 +100,45 @@ impl State {
|
||||
}
|
||||
|
||||
async fn check_cpu(&self, server: &Server, stats: &SystemStats) {
|
||||
let lock = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none() || lock.get(&server.id).map(|s| s.cpu_alert).unwrap_or(false) {
|
||||
let server_alert_status = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none()
|
||||
|| server_alert_status
|
||||
.get(&server.id)
|
||||
.map(|s| s.cpu_alert)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
drop(lock);
|
||||
drop(server_alert_status);
|
||||
if stats.cpu_perc > server.cpu_alert {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" ({region})")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let mut top_procs = stats.processes.clone();
|
||||
top_procs.sort_by(|a, b| {
|
||||
if a.cpu_perc > b.cpu_perc {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}
|
||||
});
|
||||
let top_procs = top_procs
|
||||
.into_iter()
|
||||
.take(3)
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
format!(
|
||||
"\n{}. *{}* | *{:.1}%* CPU | *{:.1} GiB* MEM",
|
||||
i + 1,
|
||||
p.name,
|
||||
p.cpu_perc,
|
||||
p.mem_mb / 1024.0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
let mut blocks = vec![
|
||||
Block::header("WARNING 🚨"),
|
||||
Block::section(format!(
|
||||
@@ -116,6 +146,7 @@ impl State {
|
||||
server.name
|
||||
)),
|
||||
Block::section(format!("cpu: *{:.1}%*", stats.cpu_perc)),
|
||||
Block::section(format!("*top cpu processes*{top_procs}",)),
|
||||
];
|
||||
|
||||
if let Some(to_notify) = generate_to_notify(server) {
|
||||
@@ -148,17 +179,46 @@ impl State {
|
||||
}
|
||||
|
||||
async fn check_mem(&self, server: &Server, stats: &SystemStats) {
|
||||
let lock = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none() || lock.get(&server.id).map(|s| s.mem_alert).unwrap_or(false) {
|
||||
let server_alert_status = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none()
|
||||
|| server_alert_status
|
||||
.get(&server.id)
|
||||
.map(|s| s.mem_alert)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
drop(lock);
|
||||
if (stats.mem_used_gb / stats.mem_total_gb) * 100.0 > server.mem_alert {
|
||||
drop(server_alert_status);
|
||||
let usage_perc = (stats.mem_used_gb / stats.mem_total_gb) * 100.0;
|
||||
if usage_perc > server.mem_alert {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" ({region})")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let mut top_procs = stats.processes.clone();
|
||||
top_procs.sort_by(|a, b| {
|
||||
if a.mem_mb > b.mem_mb {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}
|
||||
});
|
||||
let top_procs = top_procs
|
||||
.into_iter()
|
||||
.take(3)
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
format!(
|
||||
"\n{}. *{}* | *{:.1}%* CPU | *{:.1} GiB* MEM",
|
||||
i + 1,
|
||||
p.name,
|
||||
p.cpu_perc,
|
||||
p.mem_mb / 1024.0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
let mut blocks = vec![
|
||||
Block::header("WARNING 🚨"),
|
||||
Block::section(format!(
|
||||
@@ -167,10 +227,9 @@ impl State {
|
||||
)),
|
||||
Block::section(format!(
|
||||
"memory: used *{:.2} GB* of *{:.2} GB* (*{:.1}%*)",
|
||||
stats.mem_used_gb,
|
||||
stats.mem_total_gb,
|
||||
(stats.mem_used_gb / stats.mem_total_gb) * 100.0
|
||||
stats.mem_used_gb, stats.mem_total_gb, usage_perc
|
||||
)),
|
||||
Block::section(format!("*top mem processes*{top_procs}",)),
|
||||
];
|
||||
|
||||
if let Some(to_notify) = generate_to_notify(server) {
|
||||
@@ -203,56 +262,63 @@ impl State {
|
||||
}
|
||||
|
||||
async fn check_disk(&self, server: &Server, stats: &SystemStats) {
|
||||
let lock = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none() || lock.get(&server.id).map(|s| s.disk_alert).unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
drop(lock);
|
||||
if (stats.disk.used_gb / stats.disk.total_gb) * 100.0 > server.disk_alert {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" ({region})")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let mut blocks = vec![
|
||||
Block::header("WARNING 🚨"),
|
||||
Block::section(format!(
|
||||
"*{}*{region} has high *disk usage* 💿 🚨",
|
||||
server.name
|
||||
)),
|
||||
Block::section(format!(
|
||||
"disk: used *{:.2} GB* of *{:.2} GB* (*{:.1}%*)",
|
||||
stats.disk.used_gb,
|
||||
stats.disk.total_gb,
|
||||
(stats.disk.used_gb / stats.disk.total_gb) * 100.0
|
||||
)),
|
||||
];
|
||||
|
||||
if let Some(to_notify) = generate_to_notify(server) {
|
||||
blocks.push(Block::section(to_notify))
|
||||
for disk in &stats.disk.disks {
|
||||
let server_alert_status = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none()
|
||||
|| server_alert_status
|
||||
.get(&server.id)
|
||||
.map(|s| *s.disk_alert.get(&disk.mount).unwrap_or(&false))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
drop(server_alert_status);
|
||||
let usage_perc = (disk.used_gb / disk.total_gb) * 100.0;
|
||||
if usage_perc > server.disk_alert {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" ({region})")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let mut blocks = vec![
|
||||
Block::header("WARNING 🚨"),
|
||||
Block::section(format!(
|
||||
"*{}*{region} has high *disk usage* (mount point *{}*) 💿 🚨",
|
||||
server.name,
|
||||
disk.mount.display()
|
||||
)),
|
||||
Block::section(format!(
|
||||
"disk: used *{:.2} GB* of *{:.2} GB* (*{:.1}%*)",
|
||||
disk.used_gb, disk.total_gb, usage_perc
|
||||
)),
|
||||
];
|
||||
|
||||
let res = self
|
||||
.slack
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send_message(
|
||||
format!(
|
||||
"WARNING 🚨 | *{}*{region} has high *disk usage* 💿 🚨",
|
||||
server.name
|
||||
),
|
||||
blocks,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
eprintln!(
|
||||
if let Some(to_notify) = generate_to_notify(server) {
|
||||
blocks.push(Block::section(to_notify))
|
||||
}
|
||||
|
||||
let res = self
|
||||
.slack
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send_message(
|
||||
format!(
|
||||
"WARNING 🚨 | *{}*{region} has high *disk usage* 💿 🚨",
|
||||
server.name
|
||||
),
|
||||
blocks,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
eprintln!(
|
||||
"failed to send message to slack | high disk usage on {} | usage: {:.2}GB of {:.2}GB | {e:?}",
|
||||
server.name, stats.disk.used_gb, stats.disk.total_gb,
|
||||
)
|
||||
} else {
|
||||
let mut lock = self.server_alert_status.lock().await;
|
||||
let entry = lock.entry(server.id.clone()).or_default();
|
||||
entry.disk_alert = true;
|
||||
} else {
|
||||
let mut lock = self.server_alert_status.lock().await;
|
||||
let entry = lock.entry(server.id.clone()).or_default();
|
||||
entry.disk_alert.insert(disk.mount.clone(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,7 +404,6 @@ impl State {
|
||||
let offset = self.config.daily_offset_hours as u128 * ONE_HOUR_MS;
|
||||
loop {
|
||||
wait_until_timelength(Timelength::OneDay, offset).await;
|
||||
println!("running daily update");
|
||||
let servers = self.get_enabled_servers_with_stats().await;
|
||||
if let Err(e) = &servers {
|
||||
eprintln!(
|
||||
@@ -347,8 +412,12 @@ impl State {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let servers = servers.unwrap();
|
||||
if servers.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut blocks = vec![Block::header("INFO | daily update"), Block::divider()];
|
||||
for (server, stats) in servers.unwrap() {
|
||||
for (server, stats) in servers {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" | {region}")
|
||||
} else {
|
||||
|
||||
@@ -32,9 +32,9 @@ impl State {
|
||||
let state = State {
|
||||
db: DbClient::new(config.mongo.clone()).await,
|
||||
slack: config.slack_url.clone().map(|url| slack::Client::new(&url)),
|
||||
periphery: PeripheryClient::new(config.passkey.clone()),
|
||||
config,
|
||||
update: UpdateWsChannel::new(),
|
||||
periphery: PeripheryClient::default(),
|
||||
build_action_states: Default::default(),
|
||||
deployment_action_states: Default::default(),
|
||||
server_action_states: Default::default(),
|
||||
@@ -78,7 +78,6 @@ impl State {
|
||||
}
|
||||
let futures = servers.unwrap().into_iter().map(|server| async move {
|
||||
let _ = self.periphery.image_prune(&server).await;
|
||||
let _ = self.periphery.container_prune(&server).await;
|
||||
});
|
||||
join_all(futures).await;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{ws::Message as AxumMessage, Path, Query, WebSocketUpgrade},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
@@ -13,7 +12,7 @@ use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use types::{traits::Permissioned, PermissionLevel, SystemStatsQuery};
|
||||
|
||||
use crate::{auth::JwtExtension, state::StateExtension};
|
||||
use crate::{auth::JwtExtension, state::StateExtension, ResponseResult};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerId {
|
||||
@@ -26,7 +25,7 @@ pub async fn ws_handler(
|
||||
path: Path<ServerId>,
|
||||
query: Query<SystemStatsQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
) -> ResponseResult<impl IntoResponse> {
|
||||
let server = state
|
||||
.db
|
||||
.get_server(&path.id)
|
||||
|
||||
56
docs/builds.md
Normal file
56
docs/builds.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# building images
|
||||
|
||||
Monitor builds docker images by cloning the source repository from Github, running ```docker build```, and pushing the resulting image to docker hub. Any repo containing a 'Dockerfile' is buildable using this method.
|
||||
|
||||
Build configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](https://github.com/mbecker20/monitor/blob/main/docs/paths.md).
|
||||
|
||||
## repo configuration
|
||||
To specify the github repo to build, just give it the name of the repo and the branch under *repo config*. The name is given like ```mbecker20/monitor```, it includes the username / organization that owns the repo.
|
||||
|
||||
Many repos are private, in this case a Github access token is required in the periphery.config.toml of the building server. these are specified in the config like ```username = "access_token"```. An account which has access to the repo and is available on the periphery server can be selected to use via the *github account* dropdown menu.
|
||||
|
||||
## docker build configuration
|
||||
|
||||
In order to docker build, monitor just needs to know the build directory and the path of the Dockerfile, you can configure these in the *build config* section.
|
||||
|
||||
If the build directory is the root of the repository, you pass the build path as ```.```. If the build directory is some folder of the repo, just pass the name of the the folder. Do not pass the preceding "/". for example ```build/directory```
|
||||
|
||||
The dockerfile's path is given relative to the build directory. So if your build directory is ```build/directory``` and the dockerfile is in ```build/directory/Dockerfile.example```, you give the dockerfile path simply as ```Dockerfile.example```.
|
||||
|
||||
Just as with private repos, you will need to select a docker account to use with ```docker push```.
|
||||
|
||||
## running a pre build command
|
||||
|
||||
Sometimes a command needs to be run before running ```docker build```, you can configure this in the *pre build* section.
|
||||
|
||||
There are two fields to pass for *pre build*. the first is *path*, which changes the working directory. To run the command in the root of the repo, just pass ```.```. The second field is *command*, this is the shell command to be executed after the repo is cloned.
|
||||
|
||||
For example, say your repo had a folder in it called ```scripts``` with a shell script ```on-clone.sh```. You would give *path* as ```scripts``` and command as ```sh on-clone.sh```. Or you could make *path* just ```.``` and then the command would be ```sh scripts/on-clone.sh```. Either way works fine.
|
||||
|
||||
## adding build args
|
||||
|
||||
The Dockerfile may make use of [build args](https://docs.docker.com/engine/reference/builder/#arg). Build args can be passed using the gui by pressing the ```edit``` button. They are passed in the menu just like in the would in a .env file:
|
||||
|
||||
```
|
||||
BUILD_ARG1=some_value
|
||||
BUILD_ARG2=some_other_value
|
||||
```
|
||||
|
||||
## builder configuration
|
||||
|
||||
A builder is a machine running monitor periphery and docker. Any server connected to monitor can be chosen as the builder for a build.
|
||||
|
||||
Building on a machine running production software is usually not a great idea, as this process can use a lot of the system resources. It is better to start up a temporary cloud machine dedicated for the build, then shut it down when the build is finished. Right now monitor supports AWS ec2 for this task.
|
||||
|
||||
### AWS builder
|
||||
|
||||
You can choose to build on AWS on the "builder" tab on the build's page. From here you can configure the AMI to use as a base to build the image. These must be configured in the monitor core configuration along with other information like defaults to use, AWS credentials, etc. This is explained on the [core setup page](https://github.com/mbecker20/monitor/blob/main/docs/setup.md).
|
||||
|
||||
## versioning
|
||||
|
||||
Monitor uses a major.minor.patch versioning scheme. Every build will auto increment the patch number, and push the image to docker hub with the version tag as well as the "latest" tag.
|
||||
|
||||
|
||||
[next: deploying](https://github.com/mbecker20/monitor/blob/main/docs/deployments.md)
|
||||
|
||||
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)
|
||||
103
docs/deployments.md
Normal file
103
docs/deployments.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# deploying applications
|
||||
|
||||
Monitor can deploy any docker images that it can access with the configured docker accounts. It works by parsing the deployment configuration into a ```docker run``` command. The configuration is stored on MongoDB, and records of all actions (update config, deploy, stop, etc.) are stored as well.
|
||||
|
||||
Deployment configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](https://github.com/mbecker20/monitor/blob/main/docs/paths.md).
|
||||
|
||||
## configuring the image
|
||||
|
||||
There are two options to configure the deployed image.
|
||||
|
||||
### attaching a monitor build
|
||||
If the software you want to deploy is built by monitor, you can attach the build directly to the deployment.
|
||||
|
||||
By default, monitor will deploy the latest available version of the build, or you can specify a specific version using the version dropdown.
|
||||
|
||||
Also by default, monitor will use the same docker account that is attached to the build in order to pull the image on the periphery server. If that account is not available on the server, you can specify another available account to use instead, this account just needs to have read access to the docker repository.
|
||||
|
||||
### using a custom image
|
||||
You can also manually specify an image name, like ```mongo``` or ```mbecker2020/random_image:0.1.1```.
|
||||
|
||||
If the image repository is private, you can select an available docker account to use to pull the image.
|
||||
|
||||
## configuring the network
|
||||
|
||||
One feature of docker is that it allows for the creation of [virtual networks between containers](https://docs.docker.com/network/). Monitor allows you to specify a docker virtual network to connect the container to, or to use the host system networking to bypass the docker virtual network.
|
||||
|
||||
The default selection is ```host```, which bypasses the docker virtual network layer.
|
||||
|
||||
If you do select select a network other than host, you can specify port bindings with the GUI. For example, if you are running mongo (which defaults to port 27017), you could use the mapping:
|
||||
|
||||
```
|
||||
27018 : 27017
|
||||
```
|
||||
|
||||
In this case, you would access mongo from outside of the container on port ```27018```.
|
||||
|
||||
Note that this is not the only affect of using a network other than ```host```. For example, containers running on different networks can not communicate, and ones on the same network can not reach other containers on ```localhost``` even when they are running on the same system. This behavior can be a bit confusing if you are not familiar with it, and it can be bypassed entirely by just using ```host``` network.
|
||||
|
||||
## configuring restart behavior
|
||||
|
||||
Docker, like systemd, has a couple options for handling when a container exits. See [docker restart policies](https://docs.docker.com/config/containers/start-containers-automatically/). Monitor allows you to select the appropriate restart behavior from these options.
|
||||
|
||||
## configuring environment variables
|
||||
|
||||
Monitor enables you to easily manage environment variables passed to the container. In the GUI, click the 'edit' button on the 'environment' card, this will bring up the environment menu.
|
||||
|
||||
You pass environment variables just as you would with a ```.env``` file:
|
||||
|
||||
```
|
||||
ENV_VAR_1=some_value
|
||||
ENV_VAR_2=some_other_value
|
||||
```
|
||||
|
||||
## configuring volumes
|
||||
|
||||
A docker container's filesystem is segregated from that of the host. However, it is still possible for a container to access system files and directories, this is accomplished by using [bind mounts](https://docs.docker.com/storage/bind-mounts/).
|
||||
|
||||
Say your container needs to read a config file located on the system at ```/home/ubuntu/config.toml```. You can specify the bind mount to be:
|
||||
|
||||
```
|
||||
/home/ubuntu/config.toml : /config/config.toml
|
||||
```
|
||||
|
||||
The first path is the one on the system, the second is the path in the container. Your application would then read the file at ```/config/config.toml``` in order to load its contents.
|
||||
|
||||
These can be configured easily with the GUI in the 'volumes' card. You can configure as many bind mounts as you need.
|
||||
|
||||
## extra args
|
||||
|
||||
Not all features of docker are mapped directly by monitor, only the most common. You can still specify any custom flags for monitor to include in the ```docker run``` command by utilizing 'extra args'. For example, you can enable log rotation using these two extra args:
|
||||
|
||||
```
|
||||
--log-opt max-size=10M
|
||||
```
|
||||
```
|
||||
--log-opt max-file=3
|
||||
```
|
||||
|
||||
## post image
|
||||
|
||||
Sometimes you need to specify some flags to be passed directly to the application. What is put here is inserted into the docker run command after the image. For example, to pass the ```--quiet``` flag to MongoDB, the docker run command would be:
|
||||
|
||||
```
|
||||
docker run -d --name mongo-db mongo:6.0.3 --quiet
|
||||
```
|
||||
|
||||
In order to achieve this with monitor, just pass ```--quiet``` to 'post image'.
|
||||
|
||||
## container lifetime management
|
||||
|
||||
The lifetime of a docker container is more like a virtual machine. They can be created, started, stopped, and destroyed. The lifetime management actions monitor presents to the user is relative to the containers state. For example, when the container is ```running```, you can either stop it, destroy it, or redeploy it.
|
||||
|
||||
### stopping a container
|
||||
|
||||
Sometimes you want to stop a running application but preserve its logs and configuration, either to be restarted later or to view the logs at a later time. It is more like *pausing* the application with its current config, as no configuration (like environment variable, volume mounts, etc.) will be changed when the container is started again. In order to restart an application with updated configuration, it must be *redeployed*.
|
||||
|
||||
### container redeploy
|
||||
|
||||
redeploying is the action of destroying a container and recreating it. If you update deployment config, these changes will not take effect until the container is redeployed. Just note this will destroy the previous containers logs along with the container itself.
|
||||
|
||||
[next: permissions](https://github.com/mbecker20/monitor/blob/main/docs/permissions.md)
|
||||
|
||||
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)
|
||||
46
docs/introduction.md
Normal file
46
docs/introduction.md
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
|
||||
# introduction
|
||||
|
||||
If you have many servers running many applications, it can be a challenge to keep things organized and easily accessible. Without structure, things can become messy quickly, which means operational issues are more likely to arise and they can take longer to resolve. Ultimately these issues hinder productivity and waste valuable time. Monitor is a web app to provide this structure for how applications are built, deployed, and managed across many servers.
|
||||
|
||||
## docker
|
||||
|
||||
Monitor is opinionated by design, and [docker](https://docs.docker.com/) is the tool of choice. Docker provides the ability to package applications and their runtime dependencies into a standalone bundle, called an *image*. This makes them easy to "ship" to any server and run without the hassle of setting up the runtime environment. Docker uses the image as a sort of template to create *containers*. Containers are kind of like virtual machines but with different performance characteristics, namely that processes contained still run natively on the system kernel. The file system is seperate though, and like virtual machines, they can be created, started, stopped, and destroyed.
|
||||
|
||||
## monitor
|
||||
|
||||
Monitor is a solution for handling for the following:
|
||||
|
||||
1. Build application source into auto-versioned images.
|
||||
2. Create, start, stop, and restart Docker containers, and view their status and logs.
|
||||
3. Keep a record of all the actions that are performed and by whom.
|
||||
4. View realtime and historical system resource usage.
|
||||
5. Alerting for server health, like high cpu, memory, disk, etc.
|
||||
|
||||
## architecture and components
|
||||
|
||||
Monitor is composed of a single core and any amount of connected servers running the periphery application.
|
||||
|
||||
### monitor core
|
||||
The core is a web server that hosts the core API and serves the frontend to be accessed in a web browser. All user interaction with the connected servers flow through the core. It is the stateful part of the system, with the application state stored on an instance of MongoDB.
|
||||
|
||||
### monitor periphery
|
||||
The periphery is a stateless web server that exposes API called by the core. The core calls this API to get system usage and container status / logs, clone git repos, and perform docker actions. It is only intended to be reached from the core, and has an address whitelist to limit the IPs allowed to call this API.
|
||||
|
||||
### monitor cli
|
||||
This is a simple standalone cli that helps perform some actions required to setup monitor core and periphery, like generating config files.
|
||||
|
||||
## core API
|
||||
|
||||
Monitor exposes powerful functionality over the core's REST API, enabling infrastructure engineers to manage deployments programmatically in addition to with the GUI. There is a [rust crate](https://crates.io/crates/monitor_client) to simplify programmatic interaction with the API, but in general this can be accomplished using any programming language that can make REST requests.
|
||||
|
||||
## permissioning
|
||||
|
||||
Monitor is a system designed to be used by many users, whether they are developers, operations personnel, or administrators. The ability to affect an applications state is very powerful, so monitor has a granular permissioning system to only provide this functionality to the intended users. The permissioning system is explained in detail in the [permissioning](https://github.com/mbecker20/monitor/blob/main/docs/permissions.md) section.
|
||||
|
||||
User sign-on is possible using username / password, or with Oauth (Github and Google). Allowed login methods can be configured from the [core config](https://github.com/mbecker20/monitor/blob/main/config_example/core.config.example.toml).
|
||||
|
||||
[next: connecting servers](https://github.com/mbecker20/monitor/blob/main/docs/servers.md)
|
||||
|
||||
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)
|
||||
38
docs/paths.md
Normal file
38
docs/paths.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# File Paths
|
||||
|
||||
When working with monitor, you might have to configure file or directory paths.
|
||||
|
||||
## Relative Paths
|
||||
|
||||
Where possible, it is better to use relative file paths. Using relative file paths removes the connection between the process being run and the particular server it runs one, making it easier to move things between servers.
|
||||
|
||||
Where you see relative paths:
|
||||
|
||||
- setting the build directory and path of the Dockerfile
|
||||
- setting a pre build command path
|
||||
- configuring a frontend mount (used for web apps)
|
||||
|
||||
For all of the above, the path can be given relative to the root of the configured repo
|
||||
|
||||
The one exception is the Dockerfile path, which is given relative to the build directory (This is done by Docker itself, and this pattern matches usage of the Docker CLI).
|
||||
|
||||
There are 3 kinds of paths to pass:
|
||||
|
||||
1. to specify the root of the repo, use ```.``` as the path
|
||||
2. to specify a folder in the repo, pass it with **no** preceding ```/```. For example, ```example_folder``` or ```folder1/folder2```
|
||||
3. to specify an absolute path on the servers filesystem, use a preceding slash, eg. ```/home/ubuntu/example```. This way should only be used if absolutely necessary.
|
||||
|
||||
### Implementation
|
||||
|
||||
relative file paths are joined with the path of the repo on the system using a Rust [PathBuf](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push).
|
||||
|
||||
## Docker Volume Paths
|
||||
|
||||
These are passed directly to the Docker CLI using ```--volume /path/on/system:/path/in/container```. So for these, the same rules apply as when using Docker on the command line. Paths here should be given as absolute, don't use ```~``` or even ```$HOME```.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
32
docs/permissions.md
Normal file
32
docs/permissions.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# permissioning resources
|
||||
|
||||
All monitor resources (servers, builds, deployment) have independant permission tables to allow for users to have granular access to these resources. By default, users do not see any resources until they are given at least read permissions.
|
||||
|
||||
## permission levels
|
||||
|
||||
There are 4 levels of permissions a user can have on a resource:
|
||||
|
||||
1. **None**. This is the lowest permission level, and means the user will not have any access to this resource. They will not see it in the GUI, and it will not show up if the user queries the core API directly. All attempts to view or update the resource will be blocked.
|
||||
|
||||
2. **Read**. This is the first permission level that grants any access. It will enable the user to see the resource in the GUI, read the configuration, and see any logs. Any attempts to update configuration or trigger any action will be blocked.
|
||||
|
||||
3. **Execute**. This level will allow the user to execute actions on the resource, like send a build command or trigger a redeploy. The user will still be blocked from updating configuration on the resource.
|
||||
|
||||
4. **Update**. The user has full access to the resource, they can execute any actions, update the configuration, and delete the resource.
|
||||
|
||||
## Administration
|
||||
|
||||
Users can be given admin priviledges by accessing the monitor MongoDB and setting ```admin: true``` on the intended user document. These users have unrestricted access to all monitor resources, like servers, builds, and deployments. Additionally, only these users can update other (non-admin) user's permissions on resources, an action not available to regular users even with **Update** level permissions.
|
||||
|
||||
Monitor admins are responsible for managing user accounts as well. When a user logs into monitor for the first time, they will not immediately be granted access. An admin must first **enable** the user, which can be done from the 'manage users' page (found in the user dropdown menu in the topbar). Users can also be **disabled** by an admin at any time, which blocks all their access to the GUI and API.
|
||||
|
||||
Users also have some configurable global permissions, these are:
|
||||
|
||||
- create server permission
|
||||
- create build permission
|
||||
|
||||
Only users with these permissions (as well as admins) can add additional servers to monitor, and can create additional builds, respectively.
|
||||
|
||||
[next: core setup](https://github.com/mbecker20/monitor/blob/main/docs/setup.md)
|
||||
|
||||
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)
|
||||
56
docs/servers.md
Normal file
56
docs/servers.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# connecting servers
|
||||
|
||||
Integrating a device into the monitor system has 2 steps:
|
||||
|
||||
1. Setup and start the periphery agent on the server
|
||||
2. Adding the server to monitor via the core API
|
||||
|
||||
## setup monitor periphery
|
||||
|
||||
The easiest way to do this is to follow the [monitor guide](https://github.com/mbecker20/monitor-guide). This is a repo containing directions and scripts enabling command line installation via ssh or remotely.
|
||||
|
||||
### manual install steps
|
||||
|
||||
1. Download the periphery binary from the latest [release](https://github.com/mbecker20/monitor/releases).
|
||||
|
||||
2. Create and edit your config files, following the [config example](https://github.com/mbecker20/monitor/blob/main/config_example/periphery.config.example.toml). The monitor cli can be used to add the boilerplate: ```monitor periphery gen-config --path /path/to/config.toml```. The files can be anywhere, and can be passed to periphery via the ```--config-path``` flag.
|
||||
|
||||
3. Ensure that inbound connectivity is allowed on the port specified in periphery.config.toml (default 8000).
|
||||
|
||||
4. Install docker. Make sure whatever user periphery is run as has access to the docker group without sudo.
|
||||
|
||||
5. Start the periphery binary with your preferred process manager, like systemd. The config read from the file is printed on startup, ensure that it is as expected.
|
||||
|
||||
## example periphery start command
|
||||
|
||||
```
|
||||
periphery \
|
||||
--config-path /path/to/periphery.config.base.toml \
|
||||
--config-path /other_path/to/periphery.config.overide.toml \
|
||||
--merge-nested-config \
|
||||
--home_dir /home/username
|
||||
```
|
||||
|
||||
## passing config files
|
||||
|
||||
when you pass multiple config files, later --config-path given in the command will always overide previous ones.
|
||||
|
||||
there are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the overide config. So if you pass ```allowed_ips = []``` in your overide config, the final allowed_ips will be an empty list as well.
|
||||
|
||||
```--merge-nested-config``` will merge config fields recursively and extend config array fields.
|
||||
|
||||
For example, with ```--merge-nested-config``` you can specify an allowed ip in the base config, and another in the overide config, they will both be present in the final config.
|
||||
|
||||
Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the overide config.
|
||||
|
||||
## adding the server to monitor
|
||||
|
||||
The easiest way to add the server is with the GUI. On the home page, click the + button to the right of the server search bar, configure the name and address of the server. The address is the full http/s url to the periphery server, eg http://12.34.56.78:8000.
|
||||
|
||||
Once it is added, you can use access the GUI to modify some config, like the alerting thresholds for cpu, memory and disk usage. A server can also be temporarily disabled, this will prevent alerting if it goes offline.
|
||||
|
||||
Since no state is stored on the periphery servers, you can easily redirect all deployments to be hosted on a different server. Just update the address to point to the new server.
|
||||
|
||||
[next: building](https://github.com/mbecker20/monitor/blob/main/docs/builds.md)
|
||||
|
||||
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)
|
||||
5
docs/setup.md
Normal file
5
docs/setup.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# setting up monitor core
|
||||
|
||||
|
||||
|
||||
[back to table of contents](https://github.com/mbecker20/monitor/blob/main/readme.md)
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="app"></div>
|
||||
<div id="root" class="app-bounder"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"check": "tsc",
|
||||
"build": "vite build && node post-build.mjs",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
@@ -17,7 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.6.0",
|
||||
"apexcharts": "^3.36.3",
|
||||
"@tanstack/solid-query": "^4.26.0",
|
||||
"axios": "^1.2.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"lightweight-charts": "^3.8.0",
|
||||
|
||||
12
frontend/public/icons/duplicate.svg
Normal file
12
frontend/public/icons/duplicate.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Rounded_Rectangle_2_1_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve">
|
||||
<g id="Rounded_Rectangle_2">
|
||||
<g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" fill="#fceade" d="M15,4H1C0.45,4,0,4.45,0,5v14c0,0.55,0.45,1,1,1h14c0.55,0,1-0.45,1-1V5
|
||||
C16,4.45,15.55,4,15,4z M14,18H2V6h12V18z M19,0H5C4.45,0,4,0.45,4,1v2h2V2h12v12h-1v2h2c0.55,0,1-0.45,1-1V1
|
||||
C20,0.45,19.55,0,19,0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 679 B |
@@ -8,12 +8,14 @@ const Deployment = lazy(() => import("./components/deployment/Deployment"));
|
||||
const Server = lazy(() => import("./components/server/Server"));
|
||||
const Build = lazy(() => import("./components/build/Build"));
|
||||
const Users = lazy(() => import("./components/users/Users"));
|
||||
const Stats = lazy(() => import("./components/stats/Stats"))
|
||||
const User = lazy(() => import("./components/users/User"));
|
||||
const Stats = lazy(() => import("./components/stats/Stats"));
|
||||
const Account = lazy(() => import("./components/account/Account"));
|
||||
|
||||
const App: Component = () => {
|
||||
const { user } = useUser();
|
||||
return (
|
||||
<>
|
||||
<div class="app">
|
||||
<Topbar />
|
||||
<Routes>
|
||||
<Route path="/" component={Home} />
|
||||
@@ -21,11 +23,13 @@ const App: Component = () => {
|
||||
<Route path="/deployment/:id" component={Deployment} />
|
||||
<Route path="/server/:id" component={Server} />
|
||||
<Route path="/server/:id/stats" component={Stats} />
|
||||
<Route path="/account" component={Account} />
|
||||
<Show when={user().admin}>
|
||||
<Route path="/users" component={Users} />
|
||||
<Route path="/user/:id" component={User} />
|
||||
</Show>
|
||||
</Routes>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
113
frontend/src/components/CopyMenu.tsx
Normal file
113
frontend/src/components/CopyMenu.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Component, createSignal, Show } from "solid-js";
|
||||
import { client, pushNotification } from "..";
|
||||
import { useAppState } from "../state/StateProvider";
|
||||
import { Build, Deployment } from "../types";
|
||||
import { getId } from "../util/helpers";
|
||||
import { useToggle } from "../util/hooks";
|
||||
import ConfirmButton from "./shared/ConfirmButton";
|
||||
import Icon from "./shared/Icon";
|
||||
import Input from "./shared/Input";
|
||||
import Flex from "./shared/layout/Flex";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import CenterMenu from "./shared/menu/CenterMenu";
|
||||
import HoverMenu from "./shared/menu/HoverMenu";
|
||||
import Selector from "./shared/menu/Selector";
|
||||
|
||||
const CopyMenu: Component<{
|
||||
type: "deployment" | "build";
|
||||
id: string;
|
||||
}> = (p) => {
|
||||
const navigate = useNavigate();
|
||||
const [show, toggleShow] = useToggle();
|
||||
const [newName, setNewName] = createSignal("");
|
||||
const { builds, deployments, servers } = useAppState();
|
||||
const curr_server = () => {
|
||||
if (p.type === "build") {
|
||||
return builds.get(p.id)!.server_id;
|
||||
} else {
|
||||
return deployments.get(p.id)!.deployment.server_id;
|
||||
}
|
||||
}
|
||||
const [selectedId, setSelected] = createSignal(curr_server());
|
||||
const name = () => {
|
||||
if (p.type === "build") {
|
||||
return builds.get(p.id)?.name;
|
||||
} else if (p.type === "deployment") {
|
||||
return deployments.get(p.id)?.deployment.name;
|
||||
}
|
||||
};
|
||||
const copy = () => {
|
||||
if (newName().length !== 0) {
|
||||
let promise: Promise<Build | Deployment>;
|
||||
if (p.type === "build") {
|
||||
promise = client.copy_build(p.id, {
|
||||
name: newName(),
|
||||
});
|
||||
} else {
|
||||
promise = client.copy_deployment(p.id, {
|
||||
name: newName(),
|
||||
server_id: selectedId()!,
|
||||
});
|
||||
}
|
||||
toggleShow();
|
||||
promise.then((val) => {
|
||||
navigate(`/${p.type}/${getId(val)}`);
|
||||
});
|
||||
} else {
|
||||
pushNotification("bad", "copy name cannot be empty");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<HoverMenu
|
||||
target={
|
||||
<CenterMenu
|
||||
show={show}
|
||||
toggleShow={toggleShow}
|
||||
title={`copy ${p.type} | ${name()}`}
|
||||
target={<Icon type="duplicate" />}
|
||||
targetClass="blue"
|
||||
content={() => (
|
||||
<Grid placeItems="center">
|
||||
<Flex alignItems="center">
|
||||
<Input
|
||||
placeholder="copy name"
|
||||
class="card dark"
|
||||
style={{ padding: "0.5rem" }}
|
||||
value={newName()}
|
||||
onEdit={setNewName}
|
||||
/>
|
||||
<Show when={p.type === "deployment"}>
|
||||
<Selector
|
||||
label="target: "
|
||||
selected={selectedId()!}
|
||||
items={servers.ids()!}
|
||||
onSelect={setSelected}
|
||||
itemMap={(id) => servers.get(id)!.server.name}
|
||||
targetClass="blue"
|
||||
targetStyle={{ display: "flex", gap: "0.5rem" }}
|
||||
searchStyle={{ width: "100%" }}
|
||||
position="bottom right"
|
||||
useSearch
|
||||
/>
|
||||
</Show>
|
||||
</Flex>
|
||||
<ConfirmButton
|
||||
class="green"
|
||||
style={{ width: "100%" }}
|
||||
onConfirm={copy}
|
||||
>
|
||||
copy {p.type}
|
||||
</ConfirmButton>
|
||||
</Grid>
|
||||
)}
|
||||
position="center"
|
||||
/>
|
||||
}
|
||||
content={`copy ${p.type}`}
|
||||
position="bottom center"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyMenu;
|
||||
126
frontend/src/components/Description.tsx
Normal file
126
frontend/src/components/Description.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Component, createSignal, onMount, Show } from "solid-js";
|
||||
import { client, pushNotification } from "..";
|
||||
import { useAppState } from "../state/StateProvider";
|
||||
import { UpdateTarget } from "../types";
|
||||
import { useToggle } from "../util/hooks";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import Loading from "./shared/loading/Loading";
|
||||
import CenterMenu from "./shared/menu/CenterMenu";
|
||||
import TextArea from "./shared/TextArea";
|
||||
|
||||
const Description: Component<{
|
||||
name: string;
|
||||
target: UpdateTarget;
|
||||
description?: string;
|
||||
userCanUpdate: boolean;
|
||||
}> = (p) => {
|
||||
const [show, toggleShow] = useToggle();
|
||||
const description = () => {
|
||||
if (p.description) {
|
||||
let [description] = p.description.split("\n");
|
||||
return description;
|
||||
} else {
|
||||
return "add a description";
|
||||
}
|
||||
};
|
||||
const [width, setWidth] = createSignal<number>();
|
||||
onMount(() => {
|
||||
setWidth(ref!?.clientWidth);
|
||||
});
|
||||
let ref: HTMLDivElement;
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
toggleShow={toggleShow}
|
||||
title={`description | ${p.name}`}
|
||||
targetClass="card grey"
|
||||
targetStyle={{ width: "100%", "justify-content": "flex-start" }}
|
||||
target={
|
||||
<div
|
||||
ref={ref! as any}
|
||||
class="ellipsis"
|
||||
style={{
|
||||
opacity: 0.7,
|
||||
width: width() ? `${width()}px` : "100%",
|
||||
"box-sizing": "border-box",
|
||||
"text-align": "left"
|
||||
}}
|
||||
>
|
||||
{width() ? description() : ""}
|
||||
</div>
|
||||
}
|
||||
content={() => (
|
||||
<DescriptionMenu
|
||||
target={p.target}
|
||||
description={p.description}
|
||||
userCanUpdate={p.userCanUpdate}
|
||||
toggleShow={toggleShow}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const DescriptionMenu: Component<{
|
||||
target: UpdateTarget;
|
||||
description?: string;
|
||||
userCanUpdate: boolean;
|
||||
toggleShow: () => void;
|
||||
}> = (p) => {
|
||||
const { builds, servers, deployments } = useAppState();
|
||||
let ref: HTMLTextAreaElement;
|
||||
onMount(() => {
|
||||
ref?.focus();
|
||||
});
|
||||
const [desc, setDesc] = createSignal(p.description);
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const update_description = () => {
|
||||
if (!p.userCanUpdate) return;
|
||||
setLoading(true);
|
||||
client
|
||||
.update_description({ target: p.target, description: desc() || "" })
|
||||
.then(() => {
|
||||
if (p.target.type === "Build") {
|
||||
builds.update({ ...builds.get(p.target.id)!, description: desc() });
|
||||
} else if (p.target.type === "Deployment") {
|
||||
const deployment = deployments.get(p.target.id)!;
|
||||
deployments.update({
|
||||
...deployment,
|
||||
deployment: { ...deployment.deployment, description: desc() },
|
||||
});
|
||||
} else if (p.target.type === "Server") {
|
||||
const server = servers.get(p.target.id)!;
|
||||
servers.update({
|
||||
...server,
|
||||
server: { ...server.server, description: desc() },
|
||||
});
|
||||
}
|
||||
p.toggleShow();
|
||||
})
|
||||
.catch(() => {
|
||||
pushNotification("bad", "failed to update description");
|
||||
p.toggleShow();
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Grid placeItems="center">
|
||||
<TextArea
|
||||
ref={ref! as any}
|
||||
placeholder="add a description"
|
||||
value={desc()}
|
||||
onEdit={setDesc}
|
||||
style={{ width: "900px", "max-width": "90vw", height: "70vh", padding: "1rem" }}
|
||||
disabled={!p.userCanUpdate}
|
||||
/>
|
||||
<Show when={p.userCanUpdate}>
|
||||
<Show when={!loading()} fallback={<Loading />}>
|
||||
<button class="green" onClick={update_description}>
|
||||
update
|
||||
</button>
|
||||
</Show>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Description;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, createSignal, onMount, Show } from "solid-js";
|
||||
import { client, pushNotification } from "../../..";
|
||||
import { useKeyDown, useToggle } from "../../../util/hooks";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Input from "../../shared/Input";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import { client, pushNotification } from "..";
|
||||
import { useKeyDown, useToggle } from "../util/hooks";
|
||||
import Icon from "./shared/Icon";
|
||||
import Input from "./shared/Input";
|
||||
import Flex from "./shared/layout/Flex";
|
||||
|
||||
export const NewGroup: Component<{}> = (p) => {
|
||||
const [showNew, toggleShowNew] = useToggle();
|
||||
@@ -47,10 +47,10 @@ export const NewDeployment: Component<{ serverID: string }> = (p) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const NewBuild: Component<{ serverID: string }> = (p) => {
|
||||
export const NewBuild: Component<{}> = (p) => {
|
||||
const [showNew, toggleShowNew] = useToggle();
|
||||
const create = (name: string) => {
|
||||
client.create_build({ name, server_id: p.serverID });
|
||||
client.create_build({ name });
|
||||
};
|
||||
return (
|
||||
<Show
|
||||
@@ -1,19 +1,24 @@
|
||||
import { Component } from "solid-js";
|
||||
import { Component, Show } from "solid-js";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import Loading from "./shared/loading/Loading";
|
||||
|
||||
const NotFound: Component<{ type: "deployment" | "server" | "build" }> = (p) => {
|
||||
return (
|
||||
<Grid
|
||||
placeItems="center"
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
>
|
||||
<Grid placeItems="center" style={{ width: "fit-content", height: "fit-content" }}>
|
||||
<h2>{p.type} at id not found</h2>
|
||||
const NotFound: Component<{
|
||||
type: "deployment" | "server" | "build";
|
||||
loaded: boolean;
|
||||
}> = (p) => {
|
||||
return (
|
||||
<Grid placeItems="center" style={{ height: "100%", width: "100%" }}>
|
||||
<Grid
|
||||
placeItems="center"
|
||||
style={{ width: "fit-content", height: "fit-content" }}
|
||||
>
|
||||
<Show when={p.loaded} fallback={<h2>loading {p.type}...</h2>}>
|
||||
<h2>{p.type} at id not found</h2>
|
||||
</Show>
|
||||
<Loading type="sonar" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
export default NotFound;
|
||||
|
||||
41
frontend/src/components/account/Account.tsx
Normal file
41
frontend/src/components/account/Account.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { readableMonitorTimestamp, readableUserType } from "../../util/helpers";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Resources from "./Resources";
|
||||
import Secrets from "./Secrets";
|
||||
|
||||
const Account: Component<{}> = (p) => {
|
||||
const { user, username } = useUser();
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>{username()}</h1>
|
||||
<Flex>
|
||||
<Show when={user().admin}>
|
||||
<div class="dimmed">admin</div>
|
||||
</Show>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">type:</div>
|
||||
<div>{user() ? readableUserType(user()!) : "unknown"}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">created:</div>
|
||||
<div>{readableMonitorTimestamp(user().created_at!)}</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Secrets />
|
||||
<Show when={!user().admin}>
|
||||
<Resources />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Account;
|
||||
124
frontend/src/components/account/Resources.tsx
Normal file
124
frontend/src/components/account/Resources.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, createMemo, createSignal, For } from "solid-js";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../types";
|
||||
import { getId } from "../../util/helpers";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
|
||||
const Resources: Component<{}> = (p) => {
|
||||
const { user, user_id } = useUser();
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { builds, deployments, servers } = useAppState();
|
||||
const [search, setSearch] = createSignal("");
|
||||
const _servers = createMemo(() => {
|
||||
return servers.filterArray((s) => {
|
||||
if (!s.server.name.includes(search())) return false;
|
||||
const p = s.server.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
const _deployments = createMemo(() => {
|
||||
return deployments.filterArray((d) => {
|
||||
if (!d.deployment.name.includes(search())) return false;
|
||||
const p = d.deployment.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
const _builds = createMemo(() => {
|
||||
return builds.filterArray((b) => {
|
||||
if (!b.name.includes(search())) return false;
|
||||
const p = b.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>servers</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_servers()}>
|
||||
{(item) => (
|
||||
<A
|
||||
class="card light shadow"
|
||||
href={`/server/${getId(item.server)}`}
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.server.name}</h2>
|
||||
<div class="dimmed">{item.server.region || "unknown region"}</div>
|
||||
</Grid>
|
||||
<div>{item.server.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>deployments</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_deployments()}>
|
||||
{(item) => (
|
||||
<A
|
||||
href={`/deployment/${getId(item.deployment)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.deployment.name}</h2>
|
||||
<div class="dimmed">
|
||||
{servers.get(item.deployment.server_id)?.server.name ||
|
||||
"unknown"}
|
||||
</div>
|
||||
</Grid>
|
||||
<div>{item.deployment.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>builds</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_builds()}>
|
||||
{(item) => (
|
||||
<A
|
||||
href={`/build/${getId(item)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<h2>{item.name}</h2>
|
||||
<div>{item.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Resources;
|
||||
172
frontend/src/components/account/Secrets.tsx
Normal file
172
frontend/src/components/account/Secrets.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { client, pushNotification } from "../..";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { copyToClipboard, readableMonitorTimestamp } from "../../util/helpers";
|
||||
import { useToggle } from "../../util/hooks";
|
||||
import ConfirmButton from "../shared/ConfirmButton";
|
||||
import Icon from "../shared/Icon";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import CenterMenu from "../shared/menu/CenterMenu";
|
||||
import Selector from "../shared/menu/Selector";
|
||||
|
||||
const Secrets: Component<{}> = (p) => {
|
||||
const { user, reloadUser } = useUser();
|
||||
const [showCreate, toggleShowCreate] = useToggle();
|
||||
return (
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Flex justifyContent="space-between">
|
||||
<h1>api secrets</h1>
|
||||
<CenterMenu
|
||||
show={showCreate}
|
||||
toggleShow={toggleShowCreate}
|
||||
targetClass="green"
|
||||
title="create secret"
|
||||
target={<Icon type="plus" />}
|
||||
content={() => <CreateNewSecretMenu />}
|
||||
position="center"
|
||||
/>
|
||||
</Flex>
|
||||
<For each={user().secrets}>
|
||||
{(secret) => (
|
||||
<Flex
|
||||
class="card light shadow wrap"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{secret.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>created:</div>
|
||||
<div>{readableMonitorTimestamp(secret.created_at)}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>expires:</div>
|
||||
<div>
|
||||
{secret.expires
|
||||
? readableMonitorTimestamp(secret.expires)
|
||||
: "never"}
|
||||
</div>
|
||||
</Flex>
|
||||
<ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() =>
|
||||
client.delete_api_secret(secret.name).then(reloadUser)
|
||||
}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Secrets;
|
||||
|
||||
const EXPIRE_LENGTHS = ["30 days", "90 days", "1 year", "never"] as const;
|
||||
type ExpireLength = typeof EXPIRE_LENGTHS[number];
|
||||
|
||||
const CreateNewSecretMenu = () => {
|
||||
const { reloadUser } = useUser();
|
||||
const [info, setInfo] = createStore<{
|
||||
name: string;
|
||||
expires: ExpireLength;
|
||||
loading: boolean;
|
||||
secret: string | undefined;
|
||||
}>({
|
||||
name: "",
|
||||
expires: "90 days",
|
||||
loading: false,
|
||||
secret: undefined,
|
||||
});
|
||||
const createSecret = async () => {
|
||||
if (info.name.length === 0) {
|
||||
pushNotification("bad", "secret name cannot be empty");
|
||||
}
|
||||
setInfo("loading", true);
|
||||
try {
|
||||
const secret = await client.create_api_secret({
|
||||
name: info.name,
|
||||
expires: createExpires(info.expires),
|
||||
});
|
||||
reloadUser();
|
||||
setInfo("loading", false);
|
||||
setInfo("secret", secret);
|
||||
} catch (error) {
|
||||
pushNotification("bad", "failed to create api secret");
|
||||
console.log(error);
|
||||
setInfo("loading", false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Show when={info.secret === undefined}>
|
||||
<Flex class="wrap" alignItems="center">
|
||||
<Input
|
||||
class="darkgrey"
|
||||
placeholder="name this secret"
|
||||
style={{ "font-size": "1.5rem" }}
|
||||
onEdit={(name) => setInfo("name", name)}
|
||||
/>
|
||||
<Selector
|
||||
selected={info.expires}
|
||||
items={EXPIRE_LENGTHS.map((e) => e as string)}
|
||||
onSelect={(selected) =>
|
||||
setInfo("expires", selected as ExpireLength)
|
||||
}
|
||||
targetClass="blue"
|
||||
/>
|
||||
<Show
|
||||
when={!info.loading}
|
||||
fallback={
|
||||
<button class="green">
|
||||
<Loading type="spinner" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<ConfirmButton class="green" onConfirm={createSecret}>
|
||||
create
|
||||
</ConfirmButton>
|
||||
</Show>
|
||||
</Flex>
|
||||
</Show>
|
||||
|
||||
<Show when={info.secret}>
|
||||
<div style={{ "place-self": "center" }}>
|
||||
note. you cannot see this again once this menu closes
|
||||
</div>
|
||||
<Flex class="wrap" alignItems="center">
|
||||
<pre class="card dark">{info.secret}</pre>
|
||||
<ConfirmButton
|
||||
class="blue"
|
||||
onFirstClick={() => {
|
||||
copyToClipboard(info.secret!);
|
||||
pushNotification("good", "copied secret to clipboard");
|
||||
}}
|
||||
confirm={<Icon type="check" />}
|
||||
>
|
||||
<Icon type="clipboard" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function createExpires(length: ExpireLength) {
|
||||
if (length === "never") {
|
||||
return undefined;
|
||||
}
|
||||
const add_days = length === "30 days" ? 30 : length === "90 days" ? 90 : 365;
|
||||
const add_ms = add_days * 24 * 60 * 60 * 1000;
|
||||
return new Date(Date.now() + add_ms).toISOString();
|
||||
}
|
||||
@@ -11,31 +11,29 @@ type State = {
|
||||
|
||||
const context = createContext<State>();
|
||||
|
||||
export const ActionStateProvider: ParentComponent<{}> = (p) => {
|
||||
export const ActionStateProvider: ParentComponent<{ build_id: string }> = (p) => {
|
||||
const { ws } = useAppState();
|
||||
const params = useParams();
|
||||
const [actions, setActions] = createStore<BuildActionState>({
|
||||
building: false,
|
||||
recloning: false,
|
||||
updating: false,
|
||||
});
|
||||
createEffect(() => {
|
||||
client.get_build_action_state(params.id).then(setActions);
|
||||
client.get_build_action_state(p.build_id).then(setActions);
|
||||
});
|
||||
onCleanup(
|
||||
ws.subscribe([Operation.BuildBuild], (update) => {
|
||||
if (update.target.id === params.id) {
|
||||
if (update.target.id === p.build_id) {
|
||||
setActions("building", update.status !== UpdateStatus.Complete);
|
||||
}
|
||||
})
|
||||
);
|
||||
onCleanup(
|
||||
ws.subscribe([Operation.RecloneBuild], (update) => {
|
||||
if (update.target.id === params.id) {
|
||||
setActions("recloning", update.status !== UpdateStatus.Complete);
|
||||
}
|
||||
})
|
||||
);
|
||||
// onCleanup(
|
||||
// ws.subscribe([Operation.RecloneBuild], (update) => {
|
||||
// if (update.target.id === params.id) {
|
||||
// setActions("recloning", update.status !== UpdateStatus.Complete);
|
||||
// }
|
||||
// })
|
||||
// );
|
||||
// onCleanup(
|
||||
// ws.subscribe([DELETE_BUILD], ({ complete, buildID }) => {
|
||||
// if (buildID === selected.id()) {
|
||||
|
||||
@@ -10,20 +10,21 @@ import { useActionStates } from "./ActionStateProvider";
|
||||
import { client } from "../..";
|
||||
import { combineClasses, getId } from "../../util/helpers";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { PermissionLevel } from "../../types";
|
||||
import { PermissionLevel, ServerStatus, ServerWithStatus } from "../../types";
|
||||
|
||||
const Actions: Component<{}> = (p) => {
|
||||
const { user } = useUser();
|
||||
const params = useParams() as { id: string };
|
||||
const { builds } = useAppState();
|
||||
const { builds, servers } = useAppState();
|
||||
const build = () => builds.get(params.id)!;
|
||||
const server = () => (build() && build().server_id) ? servers.get(build()!.server_id!) : undefined;
|
||||
const actions = useActionStates();
|
||||
const userCanExecute = () =>
|
||||
user().admin ||
|
||||
build().permissions![getId(user())] === PermissionLevel.Execute ||
|
||||
build().permissions![getId(user())] === PermissionLevel.Update;
|
||||
return (
|
||||
<Show when={userCanExecute()}>
|
||||
<Show when={userCanExecute() && (server() ? server()?.status === ServerStatus.Ok : true)}>
|
||||
<Grid class={combineClasses("card shadow")} gridTemplateRows="auto 1fr">
|
||||
<h1>actions</h1>
|
||||
<Grid style={{ height: "fit-content" }}>
|
||||
@@ -47,7 +48,7 @@ const Actions: Component<{}> = (p) => {
|
||||
</ConfirmButton>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
{/* <Flex class={combineClasses("action shadow")}>
|
||||
reclone{" "}
|
||||
<Show
|
||||
when={!actions.recloning}
|
||||
@@ -66,7 +67,7 @@ const Actions: Component<{}> = (p) => {
|
||||
<Icon type="reset" />
|
||||
</ConfirmButton>
|
||||
</Show>
|
||||
</Flex>
|
||||
</Flex> */}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Show>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { Operation, PermissionLevel } from "../../types";
|
||||
import { combineClasses, getId } from "../../util/helpers";
|
||||
import Description from "../Description";
|
||||
import NotFound from "../NotFound";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Actions from "./Actions";
|
||||
@@ -14,6 +14,7 @@ import BuildTabs from "./tabs/Tabs";
|
||||
import Updates from "./Updates";
|
||||
|
||||
const Build: Component<{}> = (p) => {
|
||||
const { user, user_id } = useUser();
|
||||
const { builds, ws } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
@@ -33,9 +34,12 @@ const Build: Component<{}> = (p) => {
|
||||
});
|
||||
});
|
||||
onCleanup(() => unsub);
|
||||
const userCanUpdate = () =>
|
||||
user().admin ||
|
||||
build()?.permissions![user_id()] === PermissionLevel.Update;
|
||||
return (
|
||||
<Show when={build()} fallback={<NotFound type="build" />}>
|
||||
<ActionStateProvider>
|
||||
<Show when={build()} fallback={<NotFound type="build" loaded={builds.loaded()} />}>
|
||||
<ActionStateProvider build_id={params.id}>
|
||||
<Grid
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -46,8 +50,14 @@ const Build: Component<{}> = (p) => {
|
||||
style={{ width: "100%" }}
|
||||
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
|
||||
>
|
||||
<Grid style={{ "flex-grow": 1, "grid-auto-rows": "auto 1fr" }}>
|
||||
<Grid style={{ "flex-grow": 1, "grid-auto-rows": "auto auto 1fr" }}>
|
||||
<Header />
|
||||
<Description
|
||||
target={{ type: "Build", id: params.id }}
|
||||
name={build()?.name!}
|
||||
description={build()?.description}
|
||||
userCanUpdate={userCanUpdate()}
|
||||
/>
|
||||
<Actions />
|
||||
</Grid>
|
||||
<Show when={!isSemiMobile()}>
|
||||
|
||||
@@ -9,12 +9,15 @@ import { combineClasses, getId, version_to_string } from "../../util/helpers";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import Updates from "./Updates";
|
||||
import { useLocalStorageToggle } from "../../util/hooks";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import { PermissionLevel } from "../../types";
|
||||
import { client } from "../..";
|
||||
import HoverMenu from "../shared/menu/HoverMenu";
|
||||
import CopyMenu from "../CopyMenu";
|
||||
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
|
||||
|
||||
const Header: Component<{}> = (p) => {
|
||||
const { builds } = useAppState();
|
||||
const { builds, servers } = useAppState();
|
||||
const params = useParams();
|
||||
const build = () => builds.get(params.id)!;
|
||||
const { user } = useUser();
|
||||
@@ -24,36 +27,64 @@ const Header: Component<{}> = (p) => {
|
||||
const userCanUpdate = () =>
|
||||
user().admin ||
|
||||
build().permissions![getId(user())] === PermissionLevel.Update;
|
||||
const server = () =>
|
||||
build().server_id ? servers.get(build().server_id!) : undefined;
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
<Grid
|
||||
gap="0.5rem"
|
||||
class={combineClasses("card shadow")}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
style={{
|
||||
position: "relative",
|
||||
cursor: isSemiMobile() ? "pointer" : undefined,
|
||||
height: "fit-content",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isSemiMobile()) toggleShowUpdates();
|
||||
}}
|
||||
>
|
||||
<Grid gap="0.1rem">
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>{build().name}</h1>
|
||||
<div style={{ opacity: 0.8 }}>
|
||||
build - v{version_to_string(build().version)}
|
||||
<Show when={userCanUpdate()}>
|
||||
<Flex alignItems="center">
|
||||
<CopyMenu type="build" id={params.id} />
|
||||
<HoverMenu
|
||||
target={
|
||||
<ConfirmMenuButton
|
||||
onConfirm={() => {
|
||||
client.delete_build(params.id);
|
||||
}}
|
||||
class="red"
|
||||
title="delete build"
|
||||
match={build().name}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmMenuButton>
|
||||
}
|
||||
content="delete build"
|
||||
position="bottom center"
|
||||
padding="0.5rem"
|
||||
/>
|
||||
</Flex>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Flex alignItems="center">
|
||||
<Show when={server()} fallback={<div style={{ opacity: 0.7 }}>{build().aws_config ? "aws build" : ""}</div>}>
|
||||
<A
|
||||
href={`/server/${build().server_id}`}
|
||||
class="text-hover"
|
||||
style={{ opacity: 0.7, padding: 0 }}
|
||||
>
|
||||
{server()?.server.name}
|
||||
</A>
|
||||
</Show>
|
||||
<div style={{ opacity: 0.7 }}>build</div>
|
||||
</Flex>
|
||||
<div style={{ opacity: 0.7 }}>
|
||||
v{version_to_string(build().version)}
|
||||
</div>
|
||||
</Grid>
|
||||
<Show when={userCanUpdate()}>
|
||||
<ConfirmButton
|
||||
onConfirm={() => {
|
||||
client.delete_build(params.id);
|
||||
}}
|
||||
class="red"
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Show when={isSemiMobile()}>
|
||||
<Flex gap="0.5rem" alignItems="center" class="show-updates-indicator">
|
||||
updates{" "}
|
||||
@@ -63,7 +94,7 @@ const Header: Component<{}> = (p) => {
|
||||
/>
|
||||
</Flex>
|
||||
</Show>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<Show when={isSemiMobile() && showUpdates()}>
|
||||
<Updates />
|
||||
</Show>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createStore, SetStoreFunction } from "solid-js/store";
|
||||
import { client } from "../../..";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { Build, Operation, PermissionLevel } from "../../../types";
|
||||
import { Build, Operation, PermissionLevel, ServerWithStatus } from "../../../types";
|
||||
import { getId } from "../../../util/helpers";
|
||||
|
||||
type ConfigBuild = Build & {
|
||||
@@ -22,6 +22,7 @@ type ConfigBuild = Build & {
|
||||
type State = {
|
||||
build: ConfigBuild;
|
||||
setBuild: SetStoreFunction<ConfigBuild>;
|
||||
server: () => ServerWithStatus | undefined
|
||||
reset: () => void;
|
||||
save: () => void;
|
||||
userCanUpdate: () => boolean;
|
||||
@@ -30,7 +31,7 @@ type State = {
|
||||
const context = createContext<State>();
|
||||
|
||||
export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
const { ws, builds } = useAppState();
|
||||
const { ws, builds, servers } = useAppState();
|
||||
const params = useParams();
|
||||
const { user } = useUser();
|
||||
const [build, set] = createStore({
|
||||
@@ -44,6 +45,10 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
set(...args);
|
||||
set("updated", true);
|
||||
};
|
||||
const server = () =>
|
||||
builds.get(params.id)?.server_id
|
||||
? servers.get(builds.get(params.id)!.server_id!)
|
||||
: undefined;
|
||||
|
||||
const load = () => {
|
||||
// console.log("load build");
|
||||
@@ -52,11 +57,11 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
...build,
|
||||
repo: build.repo,
|
||||
branch: build.branch,
|
||||
on_clone: build.on_clone,
|
||||
pre_build: build.pre_build,
|
||||
docker_build_args: build.docker_build_args,
|
||||
docker_account: build.docker_account,
|
||||
github_account: build.github_account,
|
||||
aws_config: build.aws_config,
|
||||
loaded: true,
|
||||
updated: false,
|
||||
saving: false,
|
||||
@@ -100,11 +105,12 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
|
||||
onCleanup(() => modify_unsub());
|
||||
|
||||
const userCanUpdate = () => user().admin || build.permissions[getId(user())] === PermissionLevel.Update;
|
||||
const userCanUpdate = () => user().admin || build.permissions![getId(user())] === PermissionLevel.Update;
|
||||
|
||||
const state = {
|
||||
build,
|
||||
setBuild,
|
||||
server,
|
||||
reset: load,
|
||||
save,
|
||||
userCanUpdate,
|
||||
|
||||
@@ -2,13 +2,11 @@ import { useParams } from "@solidjs/router";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../../types";
|
||||
import { getId } from "../../../util/helpers";
|
||||
import SimpleTabs from "../../shared/tabs/SimpleTabs";
|
||||
import { Tab } from "../../shared/tabs/Tabs";
|
||||
import BuildConfig from "./build-config/BuildConfig";
|
||||
import GitConfig from "./git-config/GitConfig";
|
||||
import Owners from "./Permissions";
|
||||
import BuilderConfig from "./builder/BuilderConfig";
|
||||
import BuildConfig from "./config/BuildConfig";
|
||||
import Permissions from "./Permissions";
|
||||
import { ConfigProvider } from "./Provider";
|
||||
|
||||
const BuildTabs: Component<{}> = (p) => {
|
||||
@@ -24,16 +22,16 @@ const BuildTabs: Component<{}> = (p) => {
|
||||
tabs={
|
||||
[
|
||||
{
|
||||
title: "repo",
|
||||
element: () => <GitConfig />,
|
||||
},
|
||||
{
|
||||
title: "build",
|
||||
title: "config",
|
||||
element: () => <BuildConfig />,
|
||||
},
|
||||
{
|
||||
title: "builder",
|
||||
element: () => <BuilderConfig />
|
||||
},
|
||||
user().admin && {
|
||||
title: "collaborators",
|
||||
element: () => <Owners />,
|
||||
title: "permissions",
|
||||
element: () => <Permissions />,
|
||||
},
|
||||
].filter((e) => e) as Tab[]
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import {
|
||||
combineClasses,
|
||||
parseDotEnvToEnvVars,
|
||||
parseEnvVarseToDotEnv,
|
||||
} from "../../../../util/helpers";
|
||||
import { useToggle } from "../../../../util/hooks";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import CenterMenu from "../../../shared/menu/CenterMenu";
|
||||
import TextArea from "../../../shared/TextArea";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const BuildArgs: Component<{}> = (p) => {
|
||||
const { build, userCanUpdate } = useConfig();
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>build args</h1>
|
||||
<Flex alignItems="center" gap="0.2rem">
|
||||
<Show
|
||||
when={
|
||||
!build.docker_build_args?.build_args ||
|
||||
build.docker_build_args.build_args.length === 0
|
||||
}
|
||||
>
|
||||
<div>none</div>
|
||||
</Show>
|
||||
<Show when={userCanUpdate()}>
|
||||
<EditBuildArgs />
|
||||
</Show>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const EditBuildArgs: Component<{}> = (p) => {
|
||||
const [show, toggle] = useToggle();
|
||||
const [buildArgs, setBuildArgs] = createSignal("");
|
||||
const { build, setBuild } = useConfig();
|
||||
createEffect(() => {
|
||||
setBuildArgs(
|
||||
parseEnvVarseToDotEnv(
|
||||
build.docker_build_args?.build_args
|
||||
? build.docker_build_args.build_args
|
||||
: []
|
||||
)
|
||||
);
|
||||
});
|
||||
const toggleShow = () => {
|
||||
if (show()) {
|
||||
setBuild("docker_build_args", {
|
||||
build_args: parseDotEnvToEnvVars(buildArgs()),
|
||||
});
|
||||
}
|
||||
toggle();
|
||||
};
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
toggleShow={toggleShow}
|
||||
title={`${build.name} build args`}
|
||||
target="edit"
|
||||
targetClass="blue"
|
||||
leftOfX={() => (
|
||||
<button class="green" onClick={toggleShow}>
|
||||
confirm
|
||||
</button>
|
||||
)}
|
||||
content={() => (
|
||||
<TextArea
|
||||
class="scroller"
|
||||
value={buildArgs()}
|
||||
onEdit={setBuildArgs}
|
||||
style={{
|
||||
width: "700px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildArgs;
|
||||
110
frontend/src/components/build/tabs/builder/AwsBuilderConfig.tsx
Normal file
110
frontend/src/components/build/tabs/builder/AwsBuilderConfig.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import Input from "../../../shared/Input";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Selector from "../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const AwsBuilderConfig: Component<{}> = (p) => {
|
||||
const { build } = useConfig();
|
||||
return (
|
||||
<>
|
||||
<Ami />
|
||||
<InstanceType />
|
||||
<VolumeSize />
|
||||
<Show when={!build.updated}>
|
||||
<div style={{ height: "4rem" }} />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Ami: Component = () => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
const default_ami_name = () => aws_builder_config()?.default_ami_name;
|
||||
const get_ami_name = () => build.aws_config?.ami_name || aws_builder_config()?.default_ami_name || "unknown";
|
||||
const ami_names = () => {
|
||||
if (aws_builder_config() === undefined) return [];
|
||||
return Object.keys(aws_builder_config()!.available_ami_accounts!);
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>ami</h1>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={get_ami_name()}
|
||||
items={ami_names()}
|
||||
onSelect={(ami_name) => {
|
||||
if (ami_name === default_ami_name()) {
|
||||
setBuild("aws_config", "ami_name", undefined);
|
||||
} else {
|
||||
setBuild("aws_config", "ami_name", ami_name);
|
||||
}
|
||||
}}
|
||||
itemMap={(i) => i.replaceAll("_", " ")}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
useSearch
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const InstanceType: Component = () => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
return (
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>instance type</h1>
|
||||
<Input
|
||||
placeholder={aws_builder_config()?.default_instance_type}
|
||||
value={build.aws_config?.instance_type}
|
||||
onEdit={(instance_type) =>
|
||||
setBuild("aws_config", "instance_type", instance_type)
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const VolumeSize: Component = () => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
return (
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>volume size</h1>
|
||||
<Flex gap="0.25rem" alignItems="center">
|
||||
<Input
|
||||
style={{ width: "4rem" }}
|
||||
placeholder={aws_builder_config()?.default_volume_gb?.toString()}
|
||||
value={
|
||||
build.aws_config?.volume_gb
|
||||
? build.aws_config.volume_gb.toString()
|
||||
: ""
|
||||
}
|
||||
onEdit={(volume_size) =>
|
||||
setBuild("aws_config", "volume_gb", Number(volume_size))
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
GB
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AwsBuilderConfig;
|
||||
@@ -1,43 +1,30 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { pushNotification, URL } from "../../../..";
|
||||
import { combineClasses, copyToClipboard, getId } from "../../../../util/helpers";
|
||||
import ConfirmButton from "../../../shared/ConfirmButton";
|
||||
import Icon from "../../../shared/Icon";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import Loading from "../../../shared/loading/Loading";
|
||||
import { useConfig } from "../Provider";
|
||||
import Git from "./Git";
|
||||
import OnClone from "./OnClone";
|
||||
import Loading from "../../../shared/loading/Loading";
|
||||
import BuilderType from "./BuilderType";
|
||||
import BuilderServer from "./BuilderServer";
|
||||
import AwsBuilderConfig from "./AwsBuilderConfig";
|
||||
|
||||
const GitConfig: Component<{}> = (p) => {
|
||||
const BuilderConfig: Component<{}> = (p) => {
|
||||
const { build, reset, save, userCanUpdate } = useConfig();
|
||||
const listenerUrl = () => `${URL}/api/listener/build/${getId(build)}`;
|
||||
return (
|
||||
<Show when={build.loaded}>
|
||||
<Grid class="config">
|
||||
<Grid class="config-items scroller">
|
||||
<Git />
|
||||
<OnClone />
|
||||
<Show when={userCanUpdate()}>
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>webhook url</h1>
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<div class="ellipsis" style={{ width: "250px" }}>
|
||||
{listenerUrl()}
|
||||
</div>
|
||||
<ConfirmButton
|
||||
class="blue"
|
||||
onFirstClick={() => {
|
||||
copyToClipboard(listenerUrl());
|
||||
pushNotification("good", "copied url to clipboard");
|
||||
}}
|
||||
confirm={<Icon type="check" />}
|
||||
>
|
||||
<Icon type="clipboard" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<BuilderType />
|
||||
<Show when={build.server_id}>
|
||||
<BuilderServer />
|
||||
<div style={{ height: "12rem" }} />
|
||||
</Show>
|
||||
<Show when={build.aws_config}>
|
||||
<AwsBuilderConfig />
|
||||
</Show>
|
||||
<Show when={!build.server_id && !build.aws_config}>
|
||||
<div style={{ height: "12rem" }} />
|
||||
</Show>
|
||||
</Grid>
|
||||
<Show when={userCanUpdate() && build.updated}>
|
||||
@@ -66,4 +53,4 @@ const GitConfig: Component<{}> = (p) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GitConfig;
|
||||
export default BuilderConfig;
|
||||
46
frontend/src/components/build/tabs/builder/BuilderServer.tsx
Normal file
46
frontend/src/components/build/tabs/builder/BuilderServer.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Component } from "solid-js";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { PermissionLevel } from "../../../../types";
|
||||
import { getId } from "../../../../util/helpers";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import Selector from "../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const BuilderServer: Component<{}> = (p) => {
|
||||
const { servers, getPermissionOnServer } = useAppState();
|
||||
const { setBuild, server, userCanUpdate } = useConfig();
|
||||
const availableServers = () => {
|
||||
if (!servers.loaded()) return [];
|
||||
return servers
|
||||
.ids()!
|
||||
.filter((id) => {
|
||||
return getPermissionOnServer(id) === PermissionLevel.Update;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>builder server</h1>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={server()?.server ? getId(server()!.server) : "select server"}
|
||||
items={availableServers()}
|
||||
onSelect={(server_id) => setBuild("server_id", server_id)}
|
||||
itemMap={(server_id) =>
|
||||
server_id === "select server"
|
||||
? "select server"
|
||||
: servers.get(server_id)!.server.name
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
position="bottom right"
|
||||
useSearch
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuilderServer;
|
||||
52
frontend/src/components/build/tabs/builder/BuilderType.tsx
Normal file
52
frontend/src/components/build/tabs/builder/BuilderType.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import Selector from "../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const BuilderType: Component<{}> = (p) => {
|
||||
const { servers } = useAppState();
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
const builderType = () => {
|
||||
if (build.server_id) {
|
||||
return "server";
|
||||
} else if (build.aws_config) {
|
||||
return "aws";
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>builder type</h1>
|
||||
<Show when={userCanUpdate()} fallback={<h2>{builderType()}</h2>}>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={builderType() || "select type"}
|
||||
items={["aws", "server"]}
|
||||
position="bottom right"
|
||||
onSelect={(type) => {
|
||||
if (type !== builderType()) {
|
||||
if (type === "server") {
|
||||
const server_id =
|
||||
servers.ids()?.length || 0 > 0
|
||||
? servers.ids()![0]
|
||||
: undefined;
|
||||
setBuild({ server_id, aws_config: undefined });
|
||||
} else if (type === "aws") {
|
||||
setBuild({ server_id: undefined, aws_config: {} });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuilderType;
|
||||
146
frontend/src/components/build/tabs/config/BuildArgs.tsx
Normal file
146
frontend/src/components/build/tabs/config/BuildArgs.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useParams } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../types";
|
||||
import {
|
||||
parseDotEnvToEnvVars,
|
||||
parseEnvVarseToDotEnv,
|
||||
} from "../../../../util/helpers";
|
||||
import { useToggle } from "../../../../util/hooks";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import CenterMenu from "../../../shared/menu/CenterMenu";
|
||||
import TextArea from "../../../shared/TextArea";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const BuildArgs: Component<{}> = (p) => {
|
||||
const { build, userCanUpdate } = useConfig();
|
||||
return (
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>build args</h1>
|
||||
<Flex alignItems="center">
|
||||
<Show
|
||||
when={
|
||||
!build.docker_build_args?.build_args ||
|
||||
build.docker_build_args.build_args.length === 0
|
||||
}
|
||||
>
|
||||
<div>none</div>
|
||||
</Show>
|
||||
<Show when={userCanUpdate()}>
|
||||
<EditBuildArgs />
|
||||
</Show>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const EditBuildArgs: Component<{}> = (p) => {
|
||||
const { aws_builder_config, builds, serverSecrets } = useAppState();
|
||||
const [show, toggle] = useToggle();
|
||||
const [buildArgs, setBuildArgs] = createSignal("");
|
||||
const params = useParams();
|
||||
const { build, setBuild, server } = useConfig();
|
||||
createEffect(() => {
|
||||
setBuildArgs(
|
||||
parseEnvVarseToDotEnv(
|
||||
build.docker_build_args?.build_args
|
||||
? build.docker_build_args.build_args
|
||||
: []
|
||||
)
|
||||
);
|
||||
});
|
||||
const toggleShow = () => {
|
||||
if (show()) {
|
||||
setBuild("docker_build_args", {
|
||||
build_args: parseDotEnvToEnvVars(buildArgs()),
|
||||
});
|
||||
}
|
||||
toggle();
|
||||
};
|
||||
const secrets = () => {
|
||||
if (builds.get(params.id)?.server_id) {
|
||||
return (
|
||||
serverSecrets.get(
|
||||
builds.get(params.id)!.server_id!,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || []
|
||||
);
|
||||
} else if (build.aws_config) {
|
||||
const ami_name =
|
||||
build.aws_config?.ami_name || aws_builder_config()?.default_ami_name;
|
||||
return ami_name
|
||||
? aws_builder_config()?.available_ami_accounts![ami_name].secrets || []
|
||||
: [];
|
||||
} else return [];
|
||||
};
|
||||
let ref: HTMLTextAreaElement;
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
toggleShow={toggleShow}
|
||||
title={`${build.name} build args`}
|
||||
target="edit"
|
||||
targetClass="blue"
|
||||
leftOfX={() => (
|
||||
<button class="green" onClick={toggleShow}>
|
||||
confirm
|
||||
</button>
|
||||
)}
|
||||
content={() => (
|
||||
<Grid>
|
||||
<Show when={secrets()?.length || 0 > 0}>
|
||||
<Flex class="wrap" justifyContent="flex-end" alignItems="center">
|
||||
<h2 class="dimmed">secrets:</h2>
|
||||
<For each={secrets()}>
|
||||
{(secret) => (
|
||||
<button
|
||||
class="blue"
|
||||
onClick={() =>
|
||||
setBuildArgs(
|
||||
(args) =>
|
||||
args.slice(0, ref.selectionStart) +
|
||||
`[[${secret}]]` +
|
||||
args.slice(ref.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{secret}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Flex>
|
||||
</Show>
|
||||
<TextArea
|
||||
ref={ref! as any}
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={buildArgs()}
|
||||
onEdit={setBuildArgs}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildArgs;
|
||||
@@ -9,6 +9,8 @@ import { useConfig } from "../Provider";
|
||||
import Loading from "../../../shared/loading/Loading";
|
||||
import BuildArgs from "./BuildArgs";
|
||||
import Version from "./Version";
|
||||
import Repo from "./Repo";
|
||||
import WebhookUrl from "./WebhookUrl";
|
||||
|
||||
const BuildConfig: Component<{}> = (p) => {
|
||||
const { build, reset, save, userCanUpdate } = useConfig();
|
||||
@@ -17,9 +19,13 @@ const BuildConfig: Component<{}> = (p) => {
|
||||
<Grid class="config">
|
||||
<Grid class="config-items scroller">
|
||||
<Version />
|
||||
<Repo />
|
||||
<Docker />
|
||||
<BuildArgs />
|
||||
<CliBuild />
|
||||
<BuildArgs />
|
||||
<Show when={userCanUpdate()}>
|
||||
<WebhookUrl />
|
||||
</Show>
|
||||
</Grid>
|
||||
<Show when={userCanUpdate() && build.updated}>
|
||||
<Show
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import {
|
||||
Component,
|
||||
createResource,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../types";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import Input from "../../../shared/Input";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
@@ -9,11 +14,19 @@ import Selector from "../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const Docker: Component<{}> = (p) => {
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
client.get_server_docker_accounts(build.server_id).then(setDockerAccounts);
|
||||
});
|
||||
const { aws_builder_config, serverDockerAccounts, docker_organizations } = useAppState();
|
||||
const { build, setBuild, server, userCanUpdate } = useConfig();
|
||||
const dockerAccounts = () => {
|
||||
if (build.server_id) {
|
||||
return serverDockerAccounts.get(build.server_id, server()?.status || ServerStatus.NotOk) || [];
|
||||
} else if (build.aws_config) {
|
||||
const ami_name =
|
||||
build.aws_config?.ami_name || aws_builder_config()?.default_ami_name;
|
||||
return ami_name
|
||||
? aws_builder_config()?.available_ami_accounts![ami_name].docker || []
|
||||
: [];
|
||||
} else return [];
|
||||
};
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>docker build</h1> {/* checkbox here? */}
|
||||
@@ -57,7 +70,7 @@ const Docker: Component<{}> = (p) => {
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={build.docker_account || "none"}
|
||||
items={["none", ...dockerAccounts()!]}
|
||||
items={["none", ...dockerAccounts()]}
|
||||
onSelect={(account) => {
|
||||
setBuild(
|
||||
"docker_account",
|
||||
@@ -68,6 +81,28 @@ const Docker: Component<{}> = (p) => {
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
<Show when={build.docker_organization || (docker_organizations() || []).length > 0}>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>dockerhub organization: </h2>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={build.docker_organization || "none"}
|
||||
items={["none", ...(docker_organizations() || [])]}
|
||||
onSelect={(account) => {
|
||||
setBuild(
|
||||
"docker_organization",
|
||||
account === "none" ? undefined : account
|
||||
);
|
||||
}}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
85
frontend/src/components/build/tabs/config/Repo.tsx
Normal file
85
frontend/src/components/build/tabs/config/Repo.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import { useConfig } from "../Provider";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Input from "../../../shared/Input";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../types";
|
||||
import Selector from "../../../shared/menu/Selector";
|
||||
|
||||
const Repo: Component<{}> = (p) => {
|
||||
const { aws_builder_config, serverGithubAccounts } = useAppState();
|
||||
const { build, setBuild, server, userCanUpdate } = useConfig();
|
||||
const githubAccounts = () => {
|
||||
if (build.server_id) {
|
||||
return (
|
||||
serverGithubAccounts.get(
|
||||
build.server_id,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || []
|
||||
);
|
||||
} else if (build.aws_config) {
|
||||
const ami_name =
|
||||
build.aws_config?.ami_name || aws_builder_config()?.default_ami_name;
|
||||
return ami_name
|
||||
? aws_builder_config()?.available_ami_accounts![ami_name].github || []
|
||||
: [];
|
||||
} else return [];
|
||||
};
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>repo config</h1>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>repo: </h2>
|
||||
<Input
|
||||
placeholder="ie. solidjs/solid"
|
||||
value={build.repo || ""}
|
||||
onEdit={(value) => setBuild("repo", value)}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>branch: </h2>
|
||||
<Input
|
||||
placeholder="defaults to main"
|
||||
value={build.branch || (userCanUpdate() ? "" : "main")}
|
||||
onEdit={(value) => setBuild("branch", value)}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
<Show when={githubAccounts()}>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>github account: </h2>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={build.github_account || "none"}
|
||||
items={["none", ...githubAccounts()]}
|
||||
onSelect={(account) => {
|
||||
setBuild(
|
||||
"github_account",
|
||||
account === "none" ? undefined : account
|
||||
);
|
||||
}}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Repo;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, createSignal, Show } from "solid-js";
|
||||
import { version_to_string } from "../../../../util/helpers";
|
||||
import Input from "../../../shared/Input";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
38
frontend/src/components/build/tabs/config/WebhookUrl.tsx
Normal file
38
frontend/src/components/build/tabs/config/WebhookUrl.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Component, createResource, Show } from "solid-js";
|
||||
import { client } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { getId } from "../../../../util/helpers";
|
||||
import CopyClipboard from "../../../shared/CopyClipboard";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import Loading from "../../../shared/loading/Loading";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const ListenerUrl: Component<{}> = (p) => {
|
||||
const { github_webhook_base_url } = useAppState();
|
||||
const { build } = useConfig();
|
||||
const listenerUrl = () => {
|
||||
if (github_webhook_base_url()) {
|
||||
return `${github_webhook_base_url()}/api/listener/build/${getId(build)}`;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Grid class="config-item shadow">
|
||||
<h1>webhook url</h1>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<Show when={listenerUrl()} fallback={<Loading type="three-dot" />}>
|
||||
<div class="ellipsis" style={{ width: "250px" }}>
|
||||
{listenerUrl()}
|
||||
</div>
|
||||
</Show>
|
||||
<CopyClipboard copyText={listenerUrl() || ""} copying="url" />
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListenerUrl;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import { useConfig } from "../Provider";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Input from "../../../shared/Input";
|
||||
import Selector from "../../../shared/menu/Selector";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import { client } from "../../../..";
|
||||
|
||||
const Git: Component<{}> = (p) => {
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
client.get_server_github_accounts(build.server_id).then(setGithubAccounts)
|
||||
});
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>github config</h1>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>repo: </h2>
|
||||
<Input
|
||||
placeholder="ie. solidjs/solid"
|
||||
value={build.repo || ""}
|
||||
onEdit={(value) => setBuild("repo", value)}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>branch: </h2>
|
||||
<Input
|
||||
placeholder="defaults to main"
|
||||
value={build.branch || (userCanUpdate() ? "" : "main")}
|
||||
onEdit={(value) => setBuild("branch", value)}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>github account: </h2>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={build.github_account || "none"}
|
||||
items={["none", ...githubAccounts()!]}
|
||||
onSelect={(account) => {
|
||||
setBuild(
|
||||
"github_account",
|
||||
account === "none" ? undefined : account
|
||||
);
|
||||
}}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Git;
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Component } from "solid-js";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import Input from "../../../shared/Input";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import { useConfig } from "../Provider";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
|
||||
const OnClone: Component = () => {
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>on clone</h1>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>path:</h2>
|
||||
<Input
|
||||
placeholder="relative to repo"
|
||||
value={build.on_clone?.path || ""}
|
||||
onEdit={(path) => {
|
||||
if (
|
||||
path.length === 0 &&
|
||||
(!build.on_clone ||
|
||||
!build.on_clone.command ||
|
||||
build.on_clone.command.length === 0)
|
||||
) {
|
||||
setBuild("on_clone", undefined);
|
||||
}
|
||||
setBuild("on_clone", { path });
|
||||
}}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<h2>command:</h2>
|
||||
<Input
|
||||
placeholder="command"
|
||||
value={build.on_clone?.command || ""}
|
||||
onEdit={(command) => {
|
||||
if (
|
||||
command.length === 0 &&
|
||||
(!build.on_clone ||
|
||||
!build.on_clone.path ||
|
||||
build.on_clone.path.length === 0)
|
||||
) {
|
||||
setBuild("on_clone", undefined);
|
||||
}
|
||||
setBuild("on_clone", { command });
|
||||
}}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnClone;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Match, Show, Switch } from "solid-js";
|
||||
import { client, pushNotification } from "../..";
|
||||
import { client } from "../..";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import ConfirmButton from "../shared/ConfirmButton";
|
||||
@@ -9,24 +9,33 @@ import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import HoverMenu from "../shared/menu/HoverMenu";
|
||||
import { useActionStates } from "./ActionStateProvider";
|
||||
import { combineClasses, getId } from "../../util/helpers";
|
||||
import { combineClasses } from "../../util/helpers";
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import { DockerContainerState, PermissionLevel } from "../../types";
|
||||
import {
|
||||
DockerContainerState,
|
||||
PermissionLevel,
|
||||
ServerStatus,
|
||||
} from "../../types";
|
||||
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
|
||||
|
||||
const Actions: Component<{}> = (p) => {
|
||||
const { deployments, builds, getPermissionOnDeployment } = useAppState();
|
||||
const { deployments, builds, servers, getPermissionOnDeployment } =
|
||||
useAppState();
|
||||
const params = useParams();
|
||||
const { user, user_id } = useUser();
|
||||
const deployment = () => deployments.get(params.id)!;
|
||||
const server = () =>
|
||||
deployment() && servers.get(deployment()!.deployment.server_id);
|
||||
const show = () => {
|
||||
const permissions = getPermissionOnDeployment(params.id);
|
||||
return (
|
||||
server()?.status === ServerStatus.Ok &&
|
||||
deployment() &&
|
||||
(user().admin ||
|
||||
permissions === PermissionLevel.Execute ||
|
||||
permissions === PermissionLevel.Update)
|
||||
);
|
||||
};
|
||||
const deployment = () => deployments.get(params.id)!;
|
||||
const showBuild = () => {
|
||||
const build = deployment().deployment.build_id
|
||||
? builds.get(deployment().deployment.build_id!)
|
||||
@@ -117,13 +126,17 @@ const Actions: Component<{}> = (p) => {
|
||||
};
|
||||
|
||||
const Build: Component = () => {
|
||||
const { ws, deployments } = useAppState();
|
||||
const { deployments } = useAppState();
|
||||
const params = useParams();
|
||||
const actions = useActionStates();
|
||||
const buildID = () => deployments.get(params.id)!.deployment.build_id!;
|
||||
return (
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
<A href={`/build/${buildID()}`} class="pointer">
|
||||
<A
|
||||
href={`/build/${buildID()}`}
|
||||
class="pointer"
|
||||
style={{ padding: 0, "font-size": "16px" }}
|
||||
>
|
||||
build
|
||||
</A>
|
||||
<Show
|
||||
@@ -152,6 +165,8 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
|
||||
const params = useParams();
|
||||
// const deployment = () => deployments.get(params.id)!;
|
||||
const actions = useActionStates();
|
||||
const { deployments } = useAppState();
|
||||
const name = () => deployments.get(params.id)?.deployment.name;
|
||||
return (
|
||||
<Show
|
||||
when={!actions.deploying}
|
||||
@@ -163,14 +178,30 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
|
||||
>
|
||||
<HoverMenu
|
||||
target={
|
||||
<ConfirmButton
|
||||
class="green"
|
||||
onConfirm={() => {
|
||||
client.deploy_container(params.id);
|
||||
}}
|
||||
<Show
|
||||
when={p.redeploy}
|
||||
fallback={
|
||||
<ConfirmButton
|
||||
class="green"
|
||||
onConfirm={() => {
|
||||
client.deploy_container(params.id);
|
||||
}}
|
||||
>
|
||||
<Icon type={"play"} />
|
||||
</ConfirmButton>
|
||||
}
|
||||
>
|
||||
<Icon type={p.redeploy ? "reset" : "play"} />
|
||||
</ConfirmButton>
|
||||
<ConfirmMenuButton
|
||||
class="green"
|
||||
onConfirm={() => {
|
||||
client.deploy_container(params.id);
|
||||
}}
|
||||
title="redeploy container"
|
||||
match={name()!}
|
||||
>
|
||||
<Icon type={"reset"} />
|
||||
</ConfirmMenuButton>
|
||||
</Show>
|
||||
}
|
||||
content={p.redeploy ? "redeploy container" : "deploy container"}
|
||||
position="bottom center"
|
||||
@@ -183,6 +214,8 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
|
||||
const RemoveContainer = () => {
|
||||
const params = useParams();
|
||||
const actions = useActionStates();
|
||||
const { deployments } = useAppState();
|
||||
const name = () => deployments.get(params.id)?.deployment.name;
|
||||
return (
|
||||
<Show
|
||||
when={!actions.removing}
|
||||
@@ -194,14 +227,16 @@ const RemoveContainer = () => {
|
||||
>
|
||||
<HoverMenu
|
||||
target={
|
||||
<ConfirmButton
|
||||
<ConfirmMenuButton
|
||||
class="red"
|
||||
onConfirm={() => {
|
||||
client.remove_container(params.id);
|
||||
}}
|
||||
title="destroy container"
|
||||
match={name()!}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
</ConfirmMenuButton>
|
||||
}
|
||||
content="delete container"
|
||||
position="bottom center"
|
||||
@@ -245,6 +280,8 @@ const Start = () => {
|
||||
const Stop = () => {
|
||||
const params = useParams();
|
||||
const actions = useActionStates();
|
||||
const { deployments } = useAppState();
|
||||
const name = () => deployments.get(params.id)?.deployment.name;
|
||||
return (
|
||||
<Show
|
||||
when={!actions.stopping}
|
||||
@@ -256,14 +293,16 @@ const Stop = () => {
|
||||
>
|
||||
<HoverMenu
|
||||
target={
|
||||
<ConfirmButton
|
||||
<ConfirmMenuButton
|
||||
class="orange"
|
||||
onConfirm={() => {
|
||||
client.stop_container(params.id);
|
||||
}}
|
||||
title="stop container"
|
||||
match={name()!}
|
||||
>
|
||||
<Icon type="pause" />
|
||||
</ConfirmButton>
|
||||
</ConfirmMenuButton>
|
||||
}
|
||||
content="stop container"
|
||||
position="bottom center"
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { MAX_PAGE_WIDTH } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../types";
|
||||
import { combineClasses, getId } from "../../util/helpers";
|
||||
import Description from "../Description";
|
||||
import NotFound from "../NotFound";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Actions from "./Actions";
|
||||
import { ActionStateProvider } from "./ActionStateProvider";
|
||||
import Header from "./Header";
|
||||
import { ConfigProvider } from "./tabs/config/Provider";
|
||||
import DeploymentTabs from "./tabs/Tabs";
|
||||
import Updates from "./Updates";
|
||||
|
||||
const Deployment2: Component<{}> = (p) => {
|
||||
const POLLING_RATE = 10000;
|
||||
// let interval = -1;
|
||||
|
||||
const Deployment: Component<{}> = (p) => {
|
||||
const { user, user_id } = useUser();
|
||||
const { servers, deployments } = useAppState();
|
||||
const { isSemiMobile } = useAppDimensions();
|
||||
const params = useParams();
|
||||
const deployment = () => deployments.get(params.id);
|
||||
const server = () =>
|
||||
deployment() && servers.get(deployment()!.deployment.server_id);
|
||||
const userCanUpdate = () =>
|
||||
user().admin ||
|
||||
deployment()?.deployment.permissions![user_id()] === PermissionLevel.Update;
|
||||
// clearInterval(interval);
|
||||
// interval = setInterval(async () => {
|
||||
// if (server()?.status === ServerStatus.Ok) {
|
||||
// const deployment = await client.get_deployment(params.id);
|
||||
// deployments.update(deployment);
|
||||
// }
|
||||
// }, POLLING_RATE);
|
||||
// onCleanup(() => clearInterval(interval));
|
||||
return (
|
||||
<Show
|
||||
when={deployment() && server()}
|
||||
fallback={<NotFound type="deployment" />}
|
||||
fallback={<NotFound type="deployment" loaded={deployments.loaded()} />}
|
||||
>
|
||||
<ActionStateProvider>
|
||||
<Grid
|
||||
@@ -38,8 +51,14 @@ const Deployment2: Component<{}> = (p) => {
|
||||
style={{ width: "100%" }}
|
||||
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
|
||||
>
|
||||
<Grid style={{ "flex-grow": 1, "grid-auto-rows": "auto 1fr" }}>
|
||||
<Grid style={{ "flex-grow": 1, "grid-auto-rows": "auto auto 1fr" }}>
|
||||
<Header />
|
||||
<Description
|
||||
target={{ type: "Deployment", id: params.id }}
|
||||
name={deployment()?.deployment.name!}
|
||||
description={deployment()?.deployment.description}
|
||||
userCanUpdate={userCanUpdate()}
|
||||
/>
|
||||
<Actions />
|
||||
</Grid>
|
||||
<Show when={!isSemiMobile()}>
|
||||
@@ -53,46 +72,4 @@ const Deployment2: Component<{}> = (p) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Deployment: Component<{}> = (p) => {
|
||||
const { servers, deployments } = useAppState();
|
||||
const params = useParams();
|
||||
const deployment = () => deployments.get(params.id);
|
||||
const server = () => deployment() && servers.get(deployment()!.deployment.server_id);
|
||||
const { isSemiMobile } = useAppDimensions();
|
||||
const { user } = useUser();
|
||||
const userCanUpdate = () => user().admin || deployment()?.deployment.permissions![getId(user())] === PermissionLevel.Update;
|
||||
return (
|
||||
<Show
|
||||
when={deployment() && server()}
|
||||
fallback={<NotFound type="deployment" />}
|
||||
>
|
||||
<ActionStateProvider>
|
||||
<Grid class={combineClasses("content")}>
|
||||
{/* left / actions */}
|
||||
<Grid class="left-content">
|
||||
<Header />
|
||||
<Actions />
|
||||
<Show when={!isSemiMobile() && userCanUpdate()}>
|
||||
<Updates />
|
||||
</Show>
|
||||
</Grid>
|
||||
{/* right / tabs */}
|
||||
<Show
|
||||
when={userCanUpdate()}
|
||||
fallback={
|
||||
<h2 class={combineClasses("card tabs shadow")}>
|
||||
you do not have permission to view this deployment
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<ConfigProvider>
|
||||
<DeploymentTabs />
|
||||
</ConfigProvider>
|
||||
</Show>
|
||||
</Grid>
|
||||
</ActionStateProvider>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default Deployment2;
|
||||
export default Deployment;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { Component, createResource, createSignal, Show } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { combineClasses, deploymentHeaderStateClass, getId } from "../../util/helpers";
|
||||
import ConfirmButton from "../shared/ConfirmButton";
|
||||
import {
|
||||
combineClasses,
|
||||
deploymentHeaderStateClass,
|
||||
getId,
|
||||
readableVersion,
|
||||
} from "../../util/helpers";
|
||||
import Icon from "../shared/Icon";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
@@ -11,11 +15,15 @@ import { useLocalStorageToggle } from "../../util/hooks";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import Updates from "./Updates";
|
||||
import { DockerContainerState, PermissionLevel } from "../../types";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import { client } from "../..";
|
||||
import CopyMenu from "../CopyMenu";
|
||||
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import { AutofocusInput } from "../shared/Input";
|
||||
|
||||
const Header: Component<{}> = (p) => {
|
||||
const { deployments } = useAppState();
|
||||
const { deployments, servers, builds } = useAppState();
|
||||
const params = useParams();
|
||||
const deployment = () => deployments.get(params.id)!;
|
||||
const { user } = useUser();
|
||||
@@ -31,6 +39,42 @@ const Header: Component<{}> = (p) => {
|
||||
user().admin ||
|
||||
deployment().deployment.permissions![getId(user())] ===
|
||||
PermissionLevel.Update;
|
||||
const server = () => servers.get(deployment().deployment.server_id);
|
||||
const [deployed_version] = createResource(() =>
|
||||
client.get_deployment_deployed_version(params.id)
|
||||
);
|
||||
const derived_image = () => {
|
||||
if (deployment().deployment.build_id) {
|
||||
const build = builds.get(deployment().deployment.build_id!);
|
||||
if (build === undefined) return "unknown";
|
||||
const version =
|
||||
deployment().state === DockerContainerState.NotDeployed
|
||||
? deployment().deployment.build_version
|
||||
? readableVersion(
|
||||
deployment().deployment.build_version!
|
||||
).replaceAll("v", "")
|
||||
: "latest"
|
||||
: deployed_version() || "unknown";
|
||||
return `${build.name}:${version}`;
|
||||
} else {
|
||||
return deployment().deployment.docker_run_args.image || "unknown";
|
||||
}
|
||||
};
|
||||
const image = () => {
|
||||
if (deployment().state === DockerContainerState.NotDeployed) {
|
||||
return derived_image();
|
||||
} else if (deployment().container?.image) {
|
||||
if (deployment().container!.image.includes("sha256:")) {
|
||||
return derived_image();
|
||||
}
|
||||
let [account, image] = deployment().container!.image.split("/");
|
||||
return image ? image : account;
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
const [editingName, setEditingName] = createSignal(false);
|
||||
const [updatingName, setUpdatingName] = createSignal(false);
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
@@ -46,29 +90,80 @@ const Header: Component<{}> = (p) => {
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>{deployment()!.deployment.name}</h1>
|
||||
<Show when={userCanUpdate()}>
|
||||
<HoverMenu
|
||||
target={
|
||||
<ConfirmButton
|
||||
onConfirm={() => {
|
||||
client.delete_deployment(params.id);
|
||||
}}
|
||||
class="red"
|
||||
<Flex alignItems="center">
|
||||
<Show
|
||||
when={editingName()}
|
||||
fallback={
|
||||
<button
|
||||
onClick={() => setEditingName(true)}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
<h1>{deployment()!.deployment.name}</h1>
|
||||
</button>
|
||||
}
|
||||
content="delete deployment"
|
||||
position="bottom center"
|
||||
padding="0.5rem"
|
||||
/>
|
||||
>
|
||||
<Show
|
||||
when={!updatingName()}
|
||||
fallback={<Loading type="three-dot" />}
|
||||
>
|
||||
<AutofocusInput
|
||||
value={deployment().deployment.name}
|
||||
placeholder={deployment().deployment.name}
|
||||
onEnter={async (new_name) => {
|
||||
setUpdatingName(true);
|
||||
await client.rename_deployment(params.id, new_name);
|
||||
setEditingName(false);
|
||||
setUpdatingName(false);
|
||||
}}
|
||||
onBlur={() => setEditingName(false)}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
<div style={{ opacity: 0.7 }}>{image()}</div>
|
||||
</Flex>
|
||||
<Show when={userCanUpdate()}>
|
||||
<Flex alignItems="center">
|
||||
<CopyMenu type="deployment" id={params.id} />
|
||||
<HoverMenu
|
||||
target={
|
||||
<ConfirmMenuButton
|
||||
onConfirm={() => {
|
||||
client.delete_deployment(params.id);
|
||||
}}
|
||||
class="red"
|
||||
title="delete deployment"
|
||||
match={deployment().deployment.name}
|
||||
info={
|
||||
<Show when={deployment().container}>
|
||||
<div style={{ opacity: 0.7 }}>
|
||||
warning! this will destroy this deployments container
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmMenuButton>
|
||||
}
|
||||
content="delete deployment"
|
||||
position="bottom center"
|
||||
padding="0.5rem"
|
||||
/>
|
||||
</Flex>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<div class={deploymentHeaderStateClass(deployment().state)}>
|
||||
{deployment().state}
|
||||
</div>
|
||||
<Flex alignItems="center">
|
||||
<A
|
||||
href={`/server/${deployment().deployment.server_id}`}
|
||||
class="text-hover"
|
||||
style={{ opacity: 0.7, padding: 0 }}
|
||||
>
|
||||
{server()?.server.name}
|
||||
</A>
|
||||
<div class={deploymentHeaderStateClass(deployment().state)}>
|
||||
{deployment().state}
|
||||
</div>
|
||||
</Flex>
|
||||
<Show when={status()}>
|
||||
<div style={{ opacity: 0.7 }}>{status()}</div>
|
||||
</Show>
|
||||
|
||||
@@ -4,16 +4,17 @@ import Grid from "../shared/layout/Grid";
|
||||
import Update from "../update/Update";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { combineClasses } from "../../util/helpers";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { Operation } from "../../types";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import { useParams } from "@solidjs/router";
|
||||
|
||||
const Updates: Component<{}> = (p) => {
|
||||
const { ws, deployments } = useAppState();
|
||||
const params = useParams();
|
||||
const updates = useUpdates({ type: "Deployment", id: params.id });
|
||||
const buildID = () => deployments.get(params.id)?.deployment.build_id;
|
||||
const deployment = () => deployments.get(params.id)!
|
||||
const updates = useUpdates({ type: "Deployment", id: params.id }, true);
|
||||
const buildID = () => deployment()?.deployment.build_id;
|
||||
let unsub = () => {};
|
||||
createEffect(() => {
|
||||
unsub();
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DockerContainerState,
|
||||
Log as LogType,
|
||||
Operation,
|
||||
ServerStatus,
|
||||
} from "../../../types";
|
||||
import { client } from "../../..";
|
||||
import SimpleTabs from "../../shared/tabs/SimpleTabs";
|
||||
@@ -26,18 +27,23 @@ import { useUser } from "../../../state/UserProvider";
|
||||
|
||||
const DeploymentTabs: Component<{}> = () => {
|
||||
const { user } = useUser();
|
||||
const { deployments, ws } = useAppState();
|
||||
const { deployments, ws, servers } = useAppState();
|
||||
const params = useParams();
|
||||
const deployment = () => deployments.get(params.id);
|
||||
const server = () =>
|
||||
deployment() && servers.get(deployment()!.deployment.server_id);
|
||||
const [logTail, setLogTail] = createSignal(50);
|
||||
const [log, setLog] = createSignal<LogType>();
|
||||
const status = () =>
|
||||
deployment()!.state === DockerContainerState.NotDeployed
|
||||
? "not deployed"
|
||||
: deployment()!.container?.state;
|
||||
// const status = () =>
|
||||
// deployment()!.state === DockerContainerState.NotDeployed
|
||||
// ? "not deployed"
|
||||
// : deployment()!.container?.state;
|
||||
const log_available = () =>
|
||||
server()?.status === ServerStatus.Ok &&
|
||||
deployment()?.state !== DockerContainerState.NotDeployed;
|
||||
const loadLog = async () => {
|
||||
console.log("load log");
|
||||
if (deployment()?.state !== DockerContainerState.NotDeployed) {
|
||||
if (log_available()) {
|
||||
console.log("load log");
|
||||
const log = await client.get_deployment_container_log(
|
||||
params.id,
|
||||
logTail()
|
||||
@@ -78,7 +84,7 @@ const DeploymentTabs: Component<{}> = () => {
|
||||
title: "config",
|
||||
element: () => <Config />,
|
||||
},
|
||||
status() !== "not deployed" && [
|
||||
log_available() && [
|
||||
{
|
||||
title: "log",
|
||||
element: () => (
|
||||
@@ -90,7 +96,7 @@ const DeploymentTabs: Component<{}> = () => {
|
||||
/>
|
||||
),
|
||||
},
|
||||
status() !== "not deployed" && {
|
||||
{
|
||||
title: "error log",
|
||||
titleElement: () => (
|
||||
<Flex gap="0.5rem" alignItems="center">
|
||||
@@ -117,7 +123,7 @@ const DeploymentTabs: Component<{}> = () => {
|
||||
},
|
||||
],
|
||||
user().admin && {
|
||||
title: "collaborators",
|
||||
title: "permissions",
|
||||
element: () => <Permissions />,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -17,19 +17,14 @@ import { Tab } from "../../../shared/tabs/Tabs";
|
||||
import RepoMount from "./mount-repo/RepoMount";
|
||||
import { OnClone, OnPull } from "./mount-repo/OnGit";
|
||||
import Loading from "../../../shared/loading/Loading";
|
||||
import Permissions from "../Permissions";
|
||||
import { pushNotification, URL } from "../../../..";
|
||||
import { combineClasses, copyToClipboard, getId } from "../../../../util/helpers";
|
||||
import { useAppDimensions } from "../../../../state/DimensionProvider";
|
||||
import { useUser } from "../../../../state/UserProvider";
|
||||
import SimpleTabs from "../../../shared/tabs/SimpleTabs";
|
||||
import ExtraArgs from "./container/ExtraArgs";
|
||||
import WebhookUrl from "./container/WebhookUrl";
|
||||
|
||||
const Config: Component<{}> = () => {
|
||||
const { deployment, reset, save, userCanUpdate } = useConfig();
|
||||
const { user } = useUser();
|
||||
const { isMobile } = useAppDimensions();
|
||||
const listenerUrl = () => `${URL}/api/listener/deployment/${getId(deployment)}`;
|
||||
return (
|
||||
<Show when={deployment.loaded}>
|
||||
<Grid class="config">
|
||||
@@ -44,14 +39,12 @@ const Config: Component<{}> = () => {
|
||||
element: () => (
|
||||
<Grid class="config-items scroller" placeItems="start center">
|
||||
<Image />
|
||||
<Show when={deployment.docker_run_args.image}>
|
||||
<DockerAccount />
|
||||
</Show>
|
||||
<DockerAccount />
|
||||
<Network />
|
||||
<Restart />
|
||||
<Env />
|
||||
<Ports />
|
||||
<Mounts />
|
||||
<Env />
|
||||
<ExtraArgs />
|
||||
<PostImage />
|
||||
<Show when={isMobile()}>
|
||||
@@ -66,31 +59,7 @@ const Config: Component<{}> = () => {
|
||||
<Grid class="config-items scroller" placeItems="start center">
|
||||
<Git />
|
||||
<Show when={userCanUpdate()}>
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>webhook url</h1>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<div class="ellipsis" style={{ width: "250px" }}>
|
||||
{listenerUrl()}
|
||||
</div>
|
||||
<ConfirmButton
|
||||
class="blue"
|
||||
onFirstClick={() => {
|
||||
copyToClipboard(listenerUrl());
|
||||
pushNotification(
|
||||
"good",
|
||||
"copied url to clipboard"
|
||||
);
|
||||
}}
|
||||
confirm={<Icon type="check" />}
|
||||
>
|
||||
<Icon type="clipboard" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<WebhookUrl />
|
||||
</Show>
|
||||
<RepoMount />
|
||||
<OnClone />
|
||||
|
||||
@@ -3,16 +3,24 @@ import {
|
||||
Accessor,
|
||||
createContext,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
ParentComponent,
|
||||
Resource,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { createStore, SetStoreFunction } from "solid-js/store";
|
||||
import { client, pushNotification } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { useUser } from "../../../../state/UserProvider";
|
||||
import { Deployment, Operation, PermissionLevel } from "../../../../types";
|
||||
import {
|
||||
Deployment,
|
||||
Operation,
|
||||
PermissionLevel,
|
||||
ServerStatus,
|
||||
ServerWithStatus,
|
||||
} from "../../../../types";
|
||||
import { getId } from "../../../../util/helpers";
|
||||
|
||||
type ConfigDeployment = Deployment & {
|
||||
@@ -25,16 +33,17 @@ type State = {
|
||||
editing: Accessor<boolean>;
|
||||
deployment: ConfigDeployment;
|
||||
setDeployment: SetStoreFunction<ConfigDeployment>;
|
||||
server: () => ServerWithStatus | undefined;
|
||||
reset: () => void;
|
||||
save: () => void;
|
||||
networks: Accessor<any[]>;
|
||||
networks: Resource<any[]>;
|
||||
userCanUpdate: () => boolean;
|
||||
};
|
||||
|
||||
const context = createContext<State>();
|
||||
|
||||
export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
const { ws, deployments } = useAppState();
|
||||
const { ws, deployments, servers } = useAppState();
|
||||
const params = useParams();
|
||||
const { user } = useUser();
|
||||
const [editing] = createSignal(false);
|
||||
@@ -86,17 +95,20 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
};
|
||||
createEffect(load);
|
||||
|
||||
const [networks, setNetworks] = createSignal<any[]>([]);
|
||||
createEffect(() => {
|
||||
console.log("load networks");
|
||||
client
|
||||
.get_docker_networks(deployments.get(params.id)!.deployment.server_id)
|
||||
.then(setNetworks);
|
||||
const server = () =>
|
||||
servers.get(deployments.get(params.id)!.deployment.server_id);
|
||||
|
||||
const [networks] = createResource(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
return client.get_docker_networks(
|
||||
deployments.get(params.id)!.deployment.server_id
|
||||
);
|
||||
} else return [];
|
||||
});
|
||||
|
||||
const save = () => {
|
||||
setDeployment("updating", true);
|
||||
client.update_deployment(deployment).catch(e => {
|
||||
client.update_deployment(deployment).catch((e) => {
|
||||
console.error(e);
|
||||
pushNotification("bad", "update deployment failed");
|
||||
setDeployment("updating", false);
|
||||
@@ -141,6 +153,7 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
editing,
|
||||
deployment,
|
||||
setDeployment,
|
||||
server,
|
||||
reset: load,
|
||||
save,
|
||||
networks,
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import { client } from "../../../../..";
|
||||
import { Component } from "solid-js";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../../types";
|
||||
import { combineClasses } from "../../../../../util/helpers";
|
||||
import Flex from "../../../../shared/layout/Flex";
|
||||
import Selector from "../../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const DockerAccount: Component<{}> = (p) => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
client
|
||||
.get_server_docker_accounts(deployment.server_id)
|
||||
.then(setDockerAccounts);
|
||||
});
|
||||
const { serverDockerAccounts } = useAppState();
|
||||
const { deployment, setDeployment, server, userCanUpdate } = useConfig();
|
||||
const dockerAccounts = () =>
|
||||
serverDockerAccounts.get(
|
||||
deployment.server_id,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || [];
|
||||
const when_none_selected = () => {
|
||||
if (deployment.build_id) {
|
||||
return "same as build";
|
||||
} else {
|
||||
return "none";
|
||||
}
|
||||
};
|
||||
const accounts = () => [when_none_selected(), ...dockerAccounts()];
|
||||
return (
|
||||
<Flex
|
||||
class={combineClasses("config-item shadow")}
|
||||
@@ -24,11 +32,14 @@ const DockerAccount: Component<{}> = (p) => {
|
||||
<h1>docker account</h1>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
items={["none", ...dockerAccounts()!]}
|
||||
selected={deployment.docker_run_args.docker_account || "none"}
|
||||
items={accounts()}
|
||||
selected={
|
||||
deployment.docker_run_args.docker_account || when_none_selected()
|
||||
}
|
||||
onSelect={(account) =>
|
||||
setDeployment("docker_run_args", {
|
||||
docker_account: account === "none" ? undefined : account,
|
||||
docker_account:
|
||||
account === when_none_selected() ? undefined : account,
|
||||
})
|
||||
}
|
||||
position="bottom right"
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../../../../..";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
parseDotEnvToEnvVars,
|
||||
@@ -17,7 +27,7 @@ const Env: Component<{}> = (p) => {
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>environment</h1>
|
||||
<Flex alignItems="center" gap="0.2rem">
|
||||
<Flex alignItems="center">
|
||||
<Show
|
||||
when={
|
||||
!deployment.docker_run_args.environment ||
|
||||
@@ -36,9 +46,10 @@ const Env: Component<{}> = (p) => {
|
||||
};
|
||||
|
||||
const EditDotEnv: Component<{}> = (p) => {
|
||||
const { serverSecrets } = useAppState();
|
||||
const [show, toggle] = useToggle();
|
||||
const [dotenv, setDotEnv] = createSignal("");
|
||||
const { deployment, setDeployment } = useConfig();
|
||||
const { deployment, setDeployment, server } = useConfig();
|
||||
createEffect(() => {
|
||||
setDotEnv(
|
||||
parseEnvVarseToDotEnv(
|
||||
@@ -56,6 +67,12 @@ const EditDotEnv: Component<{}> = (p) => {
|
||||
}
|
||||
toggle();
|
||||
};
|
||||
const secrets = () =>
|
||||
serverSecrets.get(
|
||||
deployment.server_id,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || [];
|
||||
let ref: HTMLTextAreaElement;
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
@@ -69,18 +86,44 @@ const EditDotEnv: Component<{}> = (p) => {
|
||||
</button>
|
||||
)}
|
||||
content={() => (
|
||||
<TextArea
|
||||
class="scroller"
|
||||
value={dotenv()}
|
||||
onEdit={setDotEnv}
|
||||
style={{
|
||||
width: "700px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
<Grid>
|
||||
<Show when={secrets()?.length || 0 > 0}>
|
||||
<Flex class="wrap" justifyContent="flex-end" alignItems="center">
|
||||
<h2 class="dimmed">secrets:</h2>
|
||||
<For each={secrets()}>
|
||||
{(secret) => (
|
||||
<button
|
||||
class="blue"
|
||||
onClick={() =>
|
||||
setDotEnv(
|
||||
(env) =>
|
||||
env.slice(0, ref.selectionStart) +
|
||||
`[[${secret}]]` +
|
||||
env.slice(ref.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{secret}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Flex>
|
||||
</Show>
|
||||
<TextArea
|
||||
ref={ref! as any}
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={dotenv()}
|
||||
onEdit={setDotEnv}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -22,21 +22,11 @@ const ExtraArgs: Component<{}> = (p) => {
|
||||
<Grid class="config-item shadow">
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<h1>extra args</h1>
|
||||
<Flex alignItems="center">
|
||||
<Show
|
||||
when={
|
||||
!deployment.docker_run_args.extra_args ||
|
||||
deployment.docker_run_args.extra_args.length === 0
|
||||
}
|
||||
>
|
||||
<div>none</div>
|
||||
</Show>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="green" onClick={onAdd}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="green" onClick={onAdd}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
<For each={[...deployment.docker_run_args.extra_args!.keys()]}>
|
||||
{(_, index) => (
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import { Component, createResource, Show } from "solid-js";
|
||||
import { client } from "../../../../..";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import { BuildVersionsReponse } from "../../../../../types";
|
||||
import { combineClasses, string_to_version, version_to_string } from "../../../../../util/helpers";
|
||||
import {
|
||||
combineClasses,
|
||||
string_to_version,
|
||||
version_to_string,
|
||||
} from "../../../../../util/helpers";
|
||||
import Input from "../../../../shared/Input";
|
||||
import Flex from "../../../../shared/layout/Flex";
|
||||
import Selector from "../../../../shared/menu/Selector";
|
||||
@@ -11,10 +14,9 @@ import { useConfig } from "../Provider";
|
||||
const Image: Component<{}> = (p) => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
const { builds } = useAppState();
|
||||
const [versions, setVersions] = createSignal<BuildVersionsReponse[]>([]);
|
||||
createEffect(() => {
|
||||
const [versions] = createResource(() => {
|
||||
if (deployment.build_id) {
|
||||
client.get_build_versions(deployment.build_id).then(setVersions);
|
||||
return client.get_build_versions(deployment.build_id);
|
||||
}
|
||||
});
|
||||
return (
|
||||
@@ -72,7 +74,9 @@ const Image: Component<{}> = (p) => {
|
||||
}
|
||||
items={[
|
||||
"latest",
|
||||
...versions().map((v) => `v${version_to_string(v.version)}`),
|
||||
...(versions()?.map(
|
||||
(v) => `v${version_to_string(v.version)}`
|
||||
) || []),
|
||||
]}
|
||||
onSelect={(version) => {
|
||||
if (version === "latest") {
|
||||
|
||||
@@ -20,6 +20,7 @@ const Network: Component<{}> = (p) => {
|
||||
onSelect={(network) => setDeployment("docker_run_args", { network })}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
searchStyle={{ width: "100%", "min-width": "12rem" }}
|
||||
useSearch
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -18,55 +18,74 @@ const Ports: Component<{}> = (p) => {
|
||||
setDeployment("docker_run_args", "ports", (ports) => ports!.filter((_, i) => i !== index));
|
||||
};
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>ports</h1>
|
||||
<Flex alignItems="center">
|
||||
<Show when={!deployment.docker_run_args.ports || deployment.docker_run_args.ports.length === 0}>
|
||||
<div>none</div>
|
||||
</Show>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="green" onClick={onAdd}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<For each={deployment.docker_run_args.ports}>
|
||||
{({ local, container }, index) => (
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<Input
|
||||
placeholder="system"
|
||||
value={local}
|
||||
style={{ width: "40%" }}
|
||||
onEdit={(value) =>
|
||||
setDeployment("docker_run_args", "ports", index(), "local", value)
|
||||
<Show when={deployment.docker_run_args.network !== "host"}>
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>ports</h1>
|
||||
<Flex alignItems="center">
|
||||
<Show
|
||||
when={
|
||||
!deployment.docker_run_args.ports ||
|
||||
deployment.docker_run_args.ports.length === 0
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
{" : "}
|
||||
<Input
|
||||
placeholder="container"
|
||||
value={container}
|
||||
style={{ width: "40%" }}
|
||||
onEdit={(value) =>
|
||||
setDeployment("docker_run_args", "ports", index(), "container", value)
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
>
|
||||
<div>none</div>
|
||||
</Show>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="red" onClick={() => onRemove(index())}>
|
||||
<Icon type="minus" />
|
||||
<button class="green" onClick={onAdd}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Flex>
|
||||
<For each={deployment.docker_run_args.ports}>
|
||||
{({ local, container }, index) => (
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<Input
|
||||
placeholder="system"
|
||||
value={local}
|
||||
style={{ width: "40%" }}
|
||||
onEdit={(value) =>
|
||||
setDeployment(
|
||||
"docker_run_args",
|
||||
"ports",
|
||||
index(),
|
||||
"local",
|
||||
value
|
||||
)
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
{" : "}
|
||||
<Input
|
||||
placeholder="container"
|
||||
value={container}
|
||||
style={{ width: "40%" }}
|
||||
onEdit={(value) =>
|
||||
setDeployment(
|
||||
"docker_run_args",
|
||||
"ports",
|
||||
index(),
|
||||
"container",
|
||||
value
|
||||
)
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="red" onClick={() => onRemove(index())}>
|
||||
<Icon type="minus" />
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -23,21 +23,11 @@ const Volumes: Component<{}> = (p) => {
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<h1>volumes</h1>
|
||||
<Flex alignItems="center">
|
||||
<Show
|
||||
when={
|
||||
!deployment.docker_run_args.volumes ||
|
||||
deployment.docker_run_args.volumes.length === 0
|
||||
}
|
||||
>
|
||||
<div>none</div>
|
||||
</Show>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="green" onClick={onAdd}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="green" onClick={onAdd}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
<For each={deployment.docker_run_args.volumes}>
|
||||
{({ local, container }, index) => (
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Component, createResource, Show } from "solid-js";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import { getId } from "../../../../../util/helpers";
|
||||
import CopyClipboard from "../../../../shared/CopyClipboard";
|
||||
import Flex from "../../../../shared/layout/Flex";
|
||||
import Grid from "../../../../shared/layout/Grid";
|
||||
import Loading from "../../../../shared/loading/Loading";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const WebhookUrl: Component<{}> = (p) => {
|
||||
const { github_webhook_base_url } = useAppState();
|
||||
const { deployment } = useConfig();
|
||||
const listenerUrl = () => {
|
||||
if (github_webhook_base_url()) {
|
||||
return `${github_webhook_base_url()}/api/listener/deployment/${getId(
|
||||
deployment
|
||||
)}`;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Grid class="config-item shadow">
|
||||
<h1>webhook url</h1>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
style={{ "flex-wrap": "wrap" }}
|
||||
>
|
||||
<Show when={listenerUrl()} fallback={<Loading type="three-dot" />}>
|
||||
<div class="ellipsis" style={{ "max-width": "250px" }}>
|
||||
{listenerUrl()}
|
||||
</div>
|
||||
</Show>
|
||||
<CopyClipboard copyText={listenerUrl() || ""} copying="url" />
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebhookUrl;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, createEffect, createSignal } from "solid-js";
|
||||
import { client } from "../../../../..";
|
||||
import { Component, createResource } from "solid-js";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../../types";
|
||||
import { combineClasses } from "../../../../../util/helpers";
|
||||
import Input from "../../../../shared/Input";
|
||||
import Flex from "../../../../shared/layout/Flex";
|
||||
@@ -8,11 +9,13 @@ import Selector from "../../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const Git: Component<{}> = (p) => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
client.get_server_github_accounts(deployment.server_id).then(setGithubAccounts);
|
||||
});
|
||||
const { serverGithubAccounts } = useAppState();
|
||||
const { deployment, server, setDeployment, userCanUpdate } = useConfig();
|
||||
const githubAccounts = () =>
|
||||
serverGithubAccounts.get(
|
||||
deployment.server_id,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || [];
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>github config</h1>
|
||||
@@ -51,7 +54,7 @@ const Git: Component<{}> = (p) => {
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={deployment.github_account || "none"}
|
||||
items={["none", ...githubAccounts()!]}
|
||||
items={["none", ...githubAccounts()]}
|
||||
onSelect={(account) => {
|
||||
setDeployment(
|
||||
"github_account",
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import { client, pushNotification } from "../../../..";
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { pushNotification } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { DockerContainerState, Log as LogType } from "../../../../types";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import { useBuffer } from "../../../../util/hooks";
|
||||
import { useBuffer, useLocalStorageToggle } from "../../../../util/hooks";
|
||||
import Icon from "../../../shared/Icon";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
@@ -12,6 +18,10 @@ import Selector from "../../../shared/menu/Selector";
|
||||
import { useConfig } from "../config/Provider";
|
||||
import s from "./log.module.scss";
|
||||
|
||||
const POLLING_RATE = 5000;
|
||||
|
||||
let interval = -1;
|
||||
|
||||
const Log: Component<{
|
||||
log?: LogType;
|
||||
logTail: number;
|
||||
@@ -57,9 +67,23 @@ const Log: Component<{
|
||||
}
|
||||
};
|
||||
const buffer = useBuffer(scrolled, 250);
|
||||
const [poll, togglePoll] = useLocalStorageToggle(
|
||||
"deployment-log-polling",
|
||||
false
|
||||
);
|
||||
clearInterval(interval);
|
||||
interval = setInterval(() => {
|
||||
if (poll() && deployment()?.state === DockerContainerState.Running) {
|
||||
p.reload();
|
||||
}
|
||||
}, POLLING_RATE);
|
||||
onCleanup(() => clearInterval(interval));
|
||||
return (
|
||||
<Show when={p.log}>
|
||||
<Grid gap="0.5rem" style={{ height: "100%", "grid-template-rows": "auto 1fr" }}>
|
||||
<Grid
|
||||
gap="0.5rem"
|
||||
style={{ height: "100%", "grid-template-rows": "auto 1fr" }}
|
||||
>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
@@ -75,7 +99,7 @@ const Log: Component<{
|
||||
position="bottom right"
|
||||
itemStyle={{ width: "4rem" }}
|
||||
/>
|
||||
<Show when={userCanUpdate()}>
|
||||
{/* <Show when={userCanUpdate()}>
|
||||
<button
|
||||
class="blue"
|
||||
onClick={() =>
|
||||
@@ -89,7 +113,7 @@ const Log: Component<{
|
||||
>
|
||||
download full log
|
||||
</button>
|
||||
</Show>
|
||||
</Show> */}
|
||||
<button
|
||||
class="blue"
|
||||
onClick={async () => {
|
||||
@@ -100,6 +124,9 @@ const Log: Component<{
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
<button class={poll() ? "green" : "red"} onClick={togglePoll}>
|
||||
{poll() ? "" : "don't "}poll
|
||||
</button>
|
||||
</Flex>
|
||||
<div style={{ position: "relative", height: "100%" }}>
|
||||
<div
|
||||
|
||||
@@ -1,90 +1,51 @@
|
||||
import {
|
||||
Component,
|
||||
Match,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { MAX_PAGE_WIDTH } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { combineClasses } from "../../util/helpers";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import SimpleTabs from "../shared/tabs/SimpleTabs";
|
||||
import s from "./home.module.scss";
|
||||
import Summary from "./Summary";
|
||||
import Builds from "./Tree/Builds";
|
||||
import Groups from "./Tree/Groups";
|
||||
import { TreeProvider } from "./Tree/Provider";
|
||||
import Servers from "./Tree/Servers";
|
||||
import Updates from "./Updates/Updates";
|
||||
|
||||
const Home2: Component<{}> = (p) => {
|
||||
// const { width } = useAppDimensions();
|
||||
const { servers } = useAppState();
|
||||
return (
|
||||
<SimpleTabs
|
||||
containerStyle={{ width: "100%" }}
|
||||
localStorageKey="home-groups-servers-tab-v1"
|
||||
tabs={[
|
||||
{
|
||||
title: "groups",
|
||||
element: () => <Groups />,
|
||||
},
|
||||
{
|
||||
title: "servers",
|
||||
element: () => <Servers serverIDs={servers.ids()!} showAdd />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Home: Component<{}> = (p) => {
|
||||
const { width } = useAppDimensions();
|
||||
const { isSemiMobile } = useAppDimensions();
|
||||
const { servers } = useAppState();
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={width() >= 1200}>
|
||||
<Grid class={combineClasses(s.Home)}>
|
||||
<Grid style={{ height: "fit-content" }}>
|
||||
<SimpleTabs
|
||||
localStorageKey="home-groups-servers-tab-v1"
|
||||
tabs={[
|
||||
{
|
||||
title: "groups",
|
||||
element: () => <Groups />,
|
||||
},
|
||||
{
|
||||
title: "servers",
|
||||
element: () => <Servers serverIDs={servers.ids()!} showAdd />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid style={{ height: "fit-content" }}>
|
||||
<Summary />
|
||||
<Updates />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Match>
|
||||
<Match when={width() < 1200}>
|
||||
<Grid class={s.Home}>
|
||||
{/* <Summary /> */}
|
||||
<SimpleTabs
|
||||
localStorageKey="home-groups-servers-tab-v1"
|
||||
tabs={[
|
||||
{
|
||||
title: "groups",
|
||||
element: () => <Groups />,
|
||||
},
|
||||
{
|
||||
title: "servers",
|
||||
element: () => <Servers serverIDs={servers.ids()!} showAdd />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Updates />
|
||||
</Grid>
|
||||
</Match>
|
||||
</Switch>
|
||||
<>
|
||||
<Grid
|
||||
style={{ width: "100%" }}
|
||||
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
|
||||
>
|
||||
<Summary />
|
||||
<Updates />
|
||||
</Grid>
|
||||
<TreeProvider>
|
||||
<SimpleTabs
|
||||
containerStyle={{ width: "100%" }}
|
||||
localStorageKey="home-groups-servers-tab-v1"
|
||||
tabs={[
|
||||
{
|
||||
title: "groups",
|
||||
element: () => <Groups />,
|
||||
},
|
||||
{
|
||||
title: "servers",
|
||||
element: () => <Servers serverIDs={servers.ids()!} showAdd />,
|
||||
},
|
||||
{
|
||||
title: "builds",
|
||||
element: () => <Builds />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</TreeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home2;
|
||||
export default Home;
|
||||
|
||||
@@ -1,172 +1,56 @@
|
||||
import { Component, createMemo, Show } from "solid-js";
|
||||
import { Accessor, Component, createMemo } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { DockerContainerState, ServerStatus } from "../../types";
|
||||
import { combineClasses } from "../../util/helpers";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import s from "./home.module.scss";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import PieChart, { PieChartSection } from "../shared/PieChart";
|
||||
import { COLORS } from "../../style/colors";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
|
||||
const PIE_CHART_SIZE = 250;
|
||||
|
||||
const Summary: Component<{}> = (p) => {
|
||||
const { builds } = useAppState();
|
||||
const { isMobile } = useAppDimensions();
|
||||
const deployentCount = useDeploymentCount();
|
||||
const serverCount = useServerCount();
|
||||
return (
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
class={combineClasses(s.Summary, "card shadow wrap")}
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
<Grid
|
||||
class="card shadow"
|
||||
gridTemplateColumns={isMobile() ? "1fr" : "1fr 1fr"}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
placeItems="center"
|
||||
gap="0"
|
||||
>
|
||||
<h1>summary</h1>
|
||||
<Flex gap="1rem" justifyContent="flex-end" class="wrap">
|
||||
<ServersSummary />
|
||||
<DeploymentsSummary />
|
||||
<BuildsSummary />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<div
|
||||
style={{ width: `${PIE_CHART_SIZE}px`, height: `${PIE_CHART_SIZE}px` }}
|
||||
>
|
||||
<PieChart title="deployments" sections={deployentCount()} />
|
||||
</div>
|
||||
<div
|
||||
style={{ width: `${PIE_CHART_SIZE}px`, height: `${PIE_CHART_SIZE}px` }}
|
||||
>
|
||||
<PieChart title="servers" sections={serverCount()} />
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Summary;
|
||||
|
||||
const BuildsSummary = () => {
|
||||
const { builds } = useAppState();
|
||||
return (
|
||||
<Grid
|
||||
placeItems="start center"
|
||||
class={combineClasses(s.SummaryItem, "shadow")}
|
||||
gap="0.5rem"
|
||||
>
|
||||
<h2>builds</h2>
|
||||
<Grid gap="0.5rem" style={{ width: "100%", height: "100%" }}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>total</div>
|
||||
<h2 class="text-green">{builds.ids()?.length}</h2>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const DeploymentsSummary = () => {
|
||||
const deployentCount = useDeploymentCount();
|
||||
return (
|
||||
<Grid
|
||||
placeItems="start center"
|
||||
class={combineClasses(s.SummaryItem, "shadow")}
|
||||
gap="0.5rem"
|
||||
>
|
||||
<h2>deployments</h2>
|
||||
<Grid gap="0.5rem" style={{ width: "100%", height: "100%" }}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>total</div>
|
||||
<h2 class="text-green">{deployentCount().total}</h2>
|
||||
</Flex>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>running</div>
|
||||
<h2 class="text-green">{deployentCount().running}</h2>
|
||||
</Flex>
|
||||
<Show when={deployentCount().stopped > 0}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>stopped</div>
|
||||
<h2 class="text-red">{deployentCount().stopped}</h2>
|
||||
</Flex>
|
||||
</Show>
|
||||
<Show when={deployentCount().notDeployed > 0}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>not deployed</div>
|
||||
<h2 class="text-blue">{deployentCount().notDeployed}</h2>
|
||||
</Flex>
|
||||
</Show>
|
||||
<Show when={deployentCount().unknown > 0}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>unknown</div>
|
||||
<h2 class="text-orange">{deployentCount().unknown}</h2>
|
||||
</Flex>
|
||||
</Show>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const ServersSummary = () => {
|
||||
const serverCount = useServerCount();
|
||||
return (
|
||||
<Grid
|
||||
placeItems="start center"
|
||||
class={combineClasses(s.SummaryItem, "shadow")}
|
||||
gap="0.5rem"
|
||||
>
|
||||
<h2>servers</h2>
|
||||
<Grid gap="0.5rem" style={{ width: "100%", height: "100%" }}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>total</div>
|
||||
<h2 class="text-green">{serverCount().total}</h2>
|
||||
</Flex>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>healthy</div>
|
||||
<h2 class="text-green">{serverCount().healthy}</h2>
|
||||
</Flex>
|
||||
<Show when={serverCount().unhealthy > 0}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>unhealthy</div>
|
||||
<h2 class="text-red">{serverCount().unhealthy}</h2>
|
||||
</Flex>
|
||||
</Show>
|
||||
<Show when={serverCount().disabled > 0}>
|
||||
<Flex
|
||||
gap="0.4rem"
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<div>disabled</div>
|
||||
<h2 class="text-blue">{serverCount().disabled}</h2>
|
||||
</Flex>
|
||||
</Show>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
function useDeploymentCount() {
|
||||
function useDeploymentCount(): Accessor<PieChartSection[]> {
|
||||
const { deployments } = useAppState();
|
||||
const count = createMemo(() => {
|
||||
const ids = deployments.ids();
|
||||
if (!ids)
|
||||
return { total: 0, running: 0, stopped: 0, notDeployed: 0, unknown: 0 };
|
||||
return [
|
||||
{ title: "running", amount: 0, color: COLORS.textgreen },
|
||||
{ title: "stopped", amount: 0, color: COLORS.textred },
|
||||
{ title: "not deployed", amount: 0, color: COLORS.textblue },
|
||||
{ title: "unknown", amount: 0, color: COLORS.textorange },
|
||||
];
|
||||
let running = 0;
|
||||
let stopped = 0;
|
||||
let notDeployed = 0;
|
||||
@@ -183,16 +67,26 @@ function useDeploymentCount() {
|
||||
unknown++;
|
||||
}
|
||||
}
|
||||
return { total: ids.length, running, stopped, notDeployed, unknown };
|
||||
return [
|
||||
{ title: "running", amount: running, color: COLORS.textgreen },
|
||||
{ title: "stopped", amount: stopped, color: COLORS.textred },
|
||||
{ title: "not deployed", amount: notDeployed, color: COLORS.textblue },
|
||||
{ title: "unknown", amount: unknown, color: COLORS.textorange },
|
||||
];
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
function useServerCount() {
|
||||
function useServerCount(): Accessor<PieChartSection[]> {
|
||||
const { servers } = useAppState();
|
||||
const count = createMemo(() => {
|
||||
const ids = servers.ids();
|
||||
if (!ids) return { total: 0, healthy: 0, unhealthy: 0, disabled: 0 };
|
||||
if (!ids)
|
||||
return [
|
||||
{ title: "healthy", amount: 0, color: COLORS.textgreen },
|
||||
{ title: "unhealthy", amount: 0, color: COLORS.textred },
|
||||
{ title: "disabled", amount: 0, color: COLORS.textblue },
|
||||
];
|
||||
let healthy = 0;
|
||||
let unhealthy = 0;
|
||||
let disabled = 0;
|
||||
@@ -206,7 +100,50 @@ function useServerCount() {
|
||||
unhealthy++;
|
||||
}
|
||||
}
|
||||
return { total: ids.length, healthy, unhealthy, disabled };
|
||||
return [
|
||||
{ title: "healthy", amount: healthy, color: COLORS.textgreen },
|
||||
{ title: "unhealthy", amount: unhealthy, color: COLORS.textred },
|
||||
{ title: "disabled", amount: disabled, color: COLORS.textblue },
|
||||
];
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// const SummaryItem: Component<{
|
||||
// title: string;
|
||||
// metrics: Array<{ title: string; class: string; count?: number }>;
|
||||
// }> = (p) => {
|
||||
// return (
|
||||
// <Flex
|
||||
// class="card light shadow wrap"
|
||||
// justifyContent="space-between"
|
||||
// alignItems="center"
|
||||
// >
|
||||
// <h2>{p.title}</h2>
|
||||
// <Flex class="wrap">
|
||||
// <For each={p.metrics}>
|
||||
// {(metric) => (
|
||||
// <Show when={metric?.count && metric.count > 0}>
|
||||
// <Flex gap="0.4rem" alignItems="center">
|
||||
// <div>{metric.title}</div>
|
||||
// <h2 class={metric.class}>{metric.count}</h2>
|
||||
// </Flex>
|
||||
// </Show>
|
||||
// )}
|
||||
// </For>
|
||||
// </Flex>
|
||||
// </Flex>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const BuildsSummary = () => {
|
||||
// const { builds } = useAppState();
|
||||
// return (
|
||||
// <SummaryItem
|
||||
// title="builds"
|
||||
// metrics={[
|
||||
// { title: "total", class: "text-green", count: builds.ids()?.length },
|
||||
// ]}
|
||||
// />
|
||||
// );
|
||||
// };
|
||||
166
frontend/src/components/home/Summary2.tsx
Normal file
166
frontend/src/components/home/Summary2.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { Component, createMemo, For, Show } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { DockerContainerState, ServerStatus } from "../../types";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
|
||||
const Summary: Component<{}> = (p) => {
|
||||
return (
|
||||
<Grid class="card shadow" gridTemplateRows="auto 1fr 1fr 1fr">
|
||||
<h1>summary</h1>
|
||||
<DeploymentsSummary />
|
||||
<ServersSummary />
|
||||
<BuildsSummary />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Summary;
|
||||
|
||||
const SummaryItem: Component<{
|
||||
title: string;
|
||||
metrics: Array<{ title: string; class: string; count?: number }>;
|
||||
}> = (p) => {
|
||||
return (
|
||||
<Flex
|
||||
class="card light shadow wrap"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{p.title}</h2>
|
||||
<Flex class="wrap">
|
||||
<For each={p.metrics}>
|
||||
{(metric) => (
|
||||
<Show when={metric?.count && metric.count > 0}>
|
||||
<Flex gap="0.4rem" alignItems="center">
|
||||
<div>{metric.title}</div>
|
||||
<h2 class={metric.class}>{metric.count}</h2>
|
||||
</Flex>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const BuildsSummary = () => {
|
||||
const { builds } = useAppState();
|
||||
return (
|
||||
<SummaryItem
|
||||
title="builds"
|
||||
metrics={[
|
||||
{ title: "total", class: "text-green", count: builds.ids()?.length },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const DeploymentsSummary = () => {
|
||||
const deployentCount = useDeploymentCount();
|
||||
return (
|
||||
<SummaryItem
|
||||
title="deployments"
|
||||
metrics={[
|
||||
{
|
||||
title: "total",
|
||||
class: "text-green",
|
||||
count: deployentCount().total,
|
||||
},
|
||||
{
|
||||
title: "running",
|
||||
class: "text-green",
|
||||
count: deployentCount().running,
|
||||
},
|
||||
{
|
||||
title: "stopped",
|
||||
class: "text-red",
|
||||
count: deployentCount().stopped,
|
||||
},
|
||||
{
|
||||
title: "not deployed",
|
||||
class: "text-blue",
|
||||
count: deployentCount().notDeployed,
|
||||
},
|
||||
{
|
||||
title: "unknown",
|
||||
class: "text-blue",
|
||||
count: deployentCount().unknown,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ServersSummary = () => {
|
||||
const serverCount = useServerCount();
|
||||
return (
|
||||
<SummaryItem
|
||||
title="servers"
|
||||
metrics={[
|
||||
{ title: "total", class: "text-green", count: serverCount().total },
|
||||
{ title: "healthy", class: "text-green", count: serverCount().healthy },
|
||||
{
|
||||
title: "unhealthy",
|
||||
class: "text-red",
|
||||
count: serverCount().unhealthy,
|
||||
},
|
||||
{
|
||||
title: "disabled",
|
||||
class: "text-blue",
|
||||
count: serverCount().disabled,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function useDeploymentCount() {
|
||||
const { deployments } = useAppState();
|
||||
const count = createMemo(() => {
|
||||
const ids = deployments.ids();
|
||||
if (!ids)
|
||||
return { total: 0, running: 0, stopped: 0, notDeployed: 0, unknown: 0 };
|
||||
let running = 0;
|
||||
let stopped = 0;
|
||||
let notDeployed = 0;
|
||||
let unknown = 0;
|
||||
for (const id of ids) {
|
||||
const state = deployments.get(id)!.state;
|
||||
if (state === DockerContainerState.NotDeployed) {
|
||||
notDeployed++;
|
||||
} else if (state === DockerContainerState.Running) {
|
||||
running++;
|
||||
} else if (state === DockerContainerState.Exited) {
|
||||
stopped++;
|
||||
} else if (state === DockerContainerState.Unknown) {
|
||||
unknown++;
|
||||
}
|
||||
}
|
||||
return { total: ids.length, running, stopped, notDeployed, unknown };
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
function useServerCount() {
|
||||
const { servers } = useAppState();
|
||||
const count = createMemo(() => {
|
||||
const ids = servers.ids();
|
||||
if (!ids) return { total: 0, healthy: 0, unhealthy: 0, disabled: 0 };
|
||||
let healthy = 0;
|
||||
let unhealthy = 0;
|
||||
let disabled = 0;
|
||||
for (const id of ids) {
|
||||
const server = servers.get(id)!;
|
||||
if (server.status === ServerStatus.Disabled) {
|
||||
disabled++;
|
||||
} else if (server.status === ServerStatus.Ok) {
|
||||
healthy++;
|
||||
} else if (server.status === ServerStatus.NotOk) {
|
||||
unhealthy++;
|
||||
}
|
||||
}
|
||||
return { total: ids.length, healthy, unhealthy, disabled };
|
||||
});
|
||||
return count;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store";
|
||||
import { client, pushNotification } from "../../..";
|
||||
import { CreateServerBody } from "../../../util/client_types";
|
||||
import { useToggle } from "../../../util/hooks";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Input from "../../shared/Input";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import CenterMenu from "../../shared/menu/CenterMenu";
|
||||
@@ -13,10 +14,9 @@ const AddServer: Component<{}> = () => {
|
||||
<CenterMenu
|
||||
show={show}
|
||||
toggleShow={toggleShow}
|
||||
target={<Icon type="plus" />}
|
||||
title="add server"
|
||||
target="add server"
|
||||
targetClass="green shadow"
|
||||
targetStyle={{ width: "100%" }}
|
||||
content={() => <Content close={toggleShow} />}
|
||||
position="center"
|
||||
/>
|
||||
|
||||
175
frontend/src/components/home/Tree/Builds.tsx
Normal file
175
frontend/src/components/home/Tree/Builds.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, createMemo, createSignal, For, Show } from "solid-js";
|
||||
import { client } from "../../..";
|
||||
import { useAppDimensions } from "../../../state/DimensionProvider";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../../types";
|
||||
import { getId, readableMonitorTimestamp } from "../../../util/helpers";
|
||||
import {
|
||||
ActionStateProvider,
|
||||
useActionStates,
|
||||
} from "../../build/ActionStateProvider";
|
||||
import { NewBuild } from "../../New";
|
||||
import ConfirmButton from "../../shared/ConfirmButton";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Input from "../../shared/Input";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import Loading from "../../shared/loading/Loading";
|
||||
import Selector from "../../shared/menu/Selector";
|
||||
import { TreeSortType, TREE_SORTS, useTreeState } from "./Provider";
|
||||
|
||||
const Builds: Component<{}> = (p) => {
|
||||
const { isSemiMobile } = useAppDimensions();
|
||||
const { user } = useUser();
|
||||
const { builds } = useAppState();
|
||||
const { sort, setSort, build_sorter } = useTreeState();
|
||||
const [buildFilter, setBuildFilter] = createSignal("");
|
||||
const buildIDs = createMemo(() => {
|
||||
if (builds.loaded()) {
|
||||
const filters = buildFilter()
|
||||
.split(" ")
|
||||
.filter((term) => term.length > 0)
|
||||
.map((term) => term.toLowerCase());
|
||||
return builds
|
||||
.ids()!
|
||||
.filter((id) => {
|
||||
const name = builds.get(id)!.name;
|
||||
for (const term of filters) {
|
||||
if (!name.includes(term)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort(build_sorter());
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Grid>
|
||||
<Grid gridTemplateColumns="1fr auto auto">
|
||||
<Input
|
||||
placeholder="filter builds"
|
||||
value={buildFilter()}
|
||||
onEdit={setBuildFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
position="bottom right"
|
||||
targetClass="blue"
|
||||
targetStyle={{ height: "100%" }}
|
||||
containerStyle={{ height: "100%" }}
|
||||
/>
|
||||
<Show when={user().admin || user().create_build_permissions}>
|
||||
<NewBuild />
|
||||
</Show>
|
||||
</Grid>
|
||||
<Grid gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}>
|
||||
<For each={buildIDs()}>
|
||||
{(id) => (
|
||||
<ActionStateProvider build_id={id}>
|
||||
<Build id={id} />
|
||||
</ActionStateProvider>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const Build: Component<{ id: string }> = (p) => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { user } = useUser();
|
||||
const { builds } = useAppState();
|
||||
const build = () => builds.get(p.id)!;
|
||||
// const server = () =>
|
||||
// build().server_id ? servers.get(build().server_id!) : undefined;
|
||||
const version = () => {
|
||||
return `v${build().version.major}.${build().version.minor}.${
|
||||
build().version.patch
|
||||
}`;
|
||||
};
|
||||
const lastBuiltAt = () => {
|
||||
if (
|
||||
build().last_built_at === undefined ||
|
||||
build().last_built_at?.length === 0 ||
|
||||
build().last_built_at === "never"
|
||||
) {
|
||||
return "not built";
|
||||
} else {
|
||||
return readableMonitorTimestamp(build().last_built_at!);
|
||||
}
|
||||
};
|
||||
const actions = useActionStates();
|
||||
const userCanExecute = () =>
|
||||
user().admin ||
|
||||
build().permissions![getId(user())] === PermissionLevel.Execute ||
|
||||
build().permissions![getId(user())] === PermissionLevel.Update;
|
||||
// const isAwsBuild = () => build().aws_config ? true : false;
|
||||
return (
|
||||
<A
|
||||
href={`/build/${p.id}`}
|
||||
class="card light shadow hoverable"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "fit-content",
|
||||
"box-sizing": "border-box",
|
||||
"justify-content": "space-between",
|
||||
padding: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ "font-size": "1.25rem" }}>{build().name}</h1>
|
||||
<Flex alignItems="center">
|
||||
{/* <Show when={server()}>
|
||||
<A
|
||||
href={`/server/${build().server_id!}`}
|
||||
style={{ padding: 0, opacity: 0.7 }}
|
||||
>
|
||||
<div class="text-hover">{server()?.server.name}</div>
|
||||
</A>
|
||||
</Show>
|
||||
<Show when={isAwsBuild()}>
|
||||
<div style={{ opacity: 0.7 }}>aws build</div>
|
||||
</Show> */}
|
||||
<h2>{version()}</h2>
|
||||
<Show when={!isMobile()}>
|
||||
<div style={{ opacity: 0.7 }}>{lastBuiltAt()}</div>
|
||||
</Show>
|
||||
<Show when={userCanExecute()}>
|
||||
<Show
|
||||
when={!actions.building}
|
||||
fallback={
|
||||
<button
|
||||
class="green"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Loading type="spinner" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<ConfirmButton
|
||||
class="green"
|
||||
onConfirm={() => {
|
||||
client.build(p.id);
|
||||
}}
|
||||
>
|
||||
<Icon type="build" width="0.9rem" />
|
||||
</ConfirmButton>
|
||||
</Show>
|
||||
</Show>
|
||||
</Flex>
|
||||
</A>
|
||||
);
|
||||
};
|
||||
|
||||
export default Builds;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { combineClasses, deploymentStateClass, getId } from "../../../util/helpers";
|
||||
import Circle from "../../shared/Circle";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import s from "../home.module.scss";
|
||||
|
||||
const Deployment: Component<{ id: string }> = (p) => {
|
||||
const { deployments } = useAppState();
|
||||
const deployment = () => deployments.get(p.id)!;
|
||||
return (
|
||||
<Show when={deployment()}>
|
||||
<A
|
||||
href={`/deployment/${p.id}`}
|
||||
class={combineClasses(
|
||||
s.DropdownItem,
|
||||
)}
|
||||
>
|
||||
<h2>{deployment().deployment.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<div style={{ opacity: 0.7 }}>{deployments.status(p.id)}</div>
|
||||
<Circle
|
||||
size={1}
|
||||
class={deploymentStateClass(deployments.state(p.id))}
|
||||
/>
|
||||
</Flex>
|
||||
</A>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default Deployment;
|
||||
@@ -5,46 +5,64 @@ import Icon from "../../shared/Icon";
|
||||
import Input from "../../shared/Input";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import { NewGroup } from "./New";
|
||||
import { NewGroup } from "../../New";
|
||||
import s from "../home.module.scss";
|
||||
import { combineClasses } from "../../../util/helpers";
|
||||
import Server from "./Server";
|
||||
import Menu from "../../shared/menu/Menu";
|
||||
import { client } from "../../..";
|
||||
import ConfirmButton from "../../shared/ConfirmButton";
|
||||
import { TreeSortType, TREE_SORTS, useTreeState } from "./Provider";
|
||||
import Selector from "../../shared/menu/Selector";
|
||||
|
||||
const Groups: Component<{}> = (p) => {
|
||||
const { groups } = useAppState();
|
||||
const [groupFilter, setGroupFilter] = createSignal("");
|
||||
const { sort, setSort, group_sorter } = useTreeState();
|
||||
const groupIDs = createMemo(() => {
|
||||
if (groups.loaded()) {
|
||||
const filters = groupFilter()
|
||||
.split(" ")
|
||||
.filter((term) => term.length > 0)
|
||||
.map((term) => term.toLowerCase());
|
||||
return groups.ids()?.filter((id) => {
|
||||
const name = groups.get(id)!.name;
|
||||
for (const term of filters) {
|
||||
if (!name.includes(term)) {
|
||||
return false;
|
||||
return groups
|
||||
.ids()
|
||||
?.filter((id) => {
|
||||
const name = groups.get(id)!.name;
|
||||
for (const term of filters) {
|
||||
if (!name.includes(term)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.sort(group_sorter());
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Grid style={{ height: "fit-content" }}>
|
||||
<Input
|
||||
placeholder="filter groups"
|
||||
value={groupFilter()}
|
||||
onEdit={setGroupFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Grid gridTemplateColumns="1fr auto auto">
|
||||
<Input
|
||||
placeholder="filter groups"
|
||||
value={groupFilter()}
|
||||
onEdit={setGroupFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
position="bottom right"
|
||||
targetClass="blue"
|
||||
targetStyle={{ height: "100%" }}
|
||||
containerStyle={{ height: "100%" }}
|
||||
/>
|
||||
<NewGroup />
|
||||
</Grid>
|
||||
<For each={groupIDs()}>{(id) => <Group id={id} />}</For>
|
||||
<NewGroup />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -53,8 +71,9 @@ export default Groups;
|
||||
|
||||
const Group: Component<{ id: string }> = (p) => {
|
||||
const { groups, servers, ungroupedServerIds } = useAppState();
|
||||
const { server_sorter } = useTreeState();
|
||||
const group = () => groups.get(p.id);
|
||||
const serverIDs = () => group()?.servers;
|
||||
const serverIDs = () => group()?.servers.sort(server_sorter());
|
||||
const [open, toggleOpen] = useLocalStorageToggle(p.id + "-group-homeopen-v1");
|
||||
const [showAdd, setShowAdd] = createSignal(false);
|
||||
const [edit, setEdit] = createSignal(false);
|
||||
@@ -69,7 +88,9 @@ const Group: Component<{ id: string }> = (p) => {
|
||||
<h1 style={{ "font-size": "1.25rem" }}>{group()?.name}</h1>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<h2>{serverIDs()!.length} server{serverIDs()!.length > 1 ? "s" : ""}</h2>
|
||||
<h2>
|
||||
{serverIDs()!.length} server{serverIDs()!.length > 1 ? "s" : ""}
|
||||
</h2>
|
||||
<Show when={open()}>
|
||||
<button
|
||||
class="blue"
|
||||
|
||||
88
frontend/src/components/home/Tree/Provider.tsx
Normal file
88
frontend/src/components/home/Tree/Provider.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ParentComponent, createContext, useContext } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useLocalStorage } from "../../../util/hooks";
|
||||
|
||||
export const TREE_SORTS = ["name", "created at"] as const;
|
||||
export type TreeSortType = typeof TREE_SORTS[number];
|
||||
|
||||
const value = () => {
|
||||
const { servers, groups, builds } = useAppState();
|
||||
const [sort, setSort] = useLocalStorage<TreeSortType>(
|
||||
TREE_SORTS[0],
|
||||
"home-sort-v2"
|
||||
);
|
||||
const server_sorter = () => {
|
||||
if (!servers.loaded()) return () => 0;
|
||||
if (sort() === "name") {
|
||||
return (a: string, b: string) => {
|
||||
const sa = servers.get(a)!;
|
||||
const sb = servers.get(b)!;
|
||||
if (sa.server.name < sb.server.name) {
|
||||
return -1;
|
||||
} else if (sa.server.name > sb.server.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
} else {
|
||||
return () => 0;
|
||||
}
|
||||
};
|
||||
const group_sorter = () => {
|
||||
if (!groups.loaded()) return () => 0;
|
||||
if (sort() === "name") {
|
||||
return (a: string, b: string) => {
|
||||
const ga = groups.get(a)!;
|
||||
const gb = groups.get(b)!;
|
||||
if (ga.name < gb.name) {
|
||||
return -1;
|
||||
} else if (ga.name > gb.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
} else {
|
||||
return () => 0;
|
||||
}
|
||||
};
|
||||
const build_sorter = () => {
|
||||
if (!builds.loaded()) return () => 0;
|
||||
if (sort() === "name") {
|
||||
return (a: string, b: string) => {
|
||||
const ba = builds.get(a)!;
|
||||
const bb = builds.get(b)!;
|
||||
if (ba.name < bb.name) {
|
||||
return -1;
|
||||
} else if (ba.name > bb.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
} else {
|
||||
return () => 0;
|
||||
}
|
||||
};
|
||||
return {
|
||||
sort,
|
||||
setSort,
|
||||
server_sorter,
|
||||
group_sorter,
|
||||
build_sorter
|
||||
};
|
||||
}
|
||||
|
||||
export type Value = ReturnType<typeof value>;
|
||||
|
||||
const context = createContext<Value>();
|
||||
|
||||
export const TreeProvider: ParentComponent<{}> = (p) => {
|
||||
return (
|
||||
<context.Provider value={value()}>
|
||||
{p.children}
|
||||
</context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTreeState() {
|
||||
return useContext(context) as Value;
|
||||
}
|
||||
@@ -1,48 +1,21 @@
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { Component, createSignal, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { combineClasses, getId, readableStorageAmount } from "../../../util/helpers";
|
||||
import { combineClasses, readableStorageAmount } from "../../../util/helpers";
|
||||
import { useLocalStorageToggle } from "../../../util/hooks";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import Deployment from "./Deployment";
|
||||
import s from "../home.module.scss";
|
||||
import { NewBuild, NewDeployment } from "./New";
|
||||
import Loading from "../../shared/loading/Loading";
|
||||
import { A } from "@solidjs/router";
|
||||
import { PermissionLevel, ServerStatus } from "../../../types";
|
||||
import { ServerStatus } from "../../../types";
|
||||
import { useAppDimensions } from "../../../state/DimensionProvider";
|
||||
import Build from "./Build";
|
||||
import SimpleTabs from "../../shared/tabs/SimpleTabs";
|
||||
// import StatGraphs from "../../server/StatGraphs/StatGraphs";
|
||||
import ServerChildren from "../../server_children/ServerChildren";
|
||||
|
||||
const Server: Component<{ id: string }> = (p) => {
|
||||
const { servers, serverStats, deployments, builds } = useAppState();
|
||||
const { isSemiMobile } = useAppDimensions();
|
||||
const { user } = useUser();
|
||||
const { servers } = useAppState();
|
||||
const [open, toggleOpen] = useLocalStorageToggle(p.id + "-homeopen");
|
||||
const server = () => servers.get(p.id);
|
||||
const deploymentIDs = createMemo(() => {
|
||||
return (deployments.loaded() &&
|
||||
deployments
|
||||
.ids()!
|
||||
.filter(
|
||||
(id) => deployments.get(id)?.deployment.server_id === p.id
|
||||
)) as string[];
|
||||
});
|
||||
const buildIDs = createMemo(() => {
|
||||
return (builds.loaded() &&
|
||||
builds
|
||||
.ids()!
|
||||
.filter((id) => builds.get(id)?.server_id === p.id)) as string[];
|
||||
});
|
||||
return (
|
||||
<Show when={server()}>
|
||||
<div class={combineClasses(s.Server, "shadow")}>
|
||||
@@ -79,62 +52,7 @@ const Server: Component<{ id: string }> = (p) => {
|
||||
</Flex>
|
||||
</button>
|
||||
<Show when={open()}>
|
||||
<SimpleTabs
|
||||
containerClass="card shadow"
|
||||
localStorageKey={`${p.id}-home-tab`}
|
||||
tabs={[
|
||||
{
|
||||
title: "deployments",
|
||||
element: () => (
|
||||
<Grid
|
||||
gap=".5rem"
|
||||
class={combineClasses(
|
||||
s.Deployments,
|
||||
open() ? s.Enter : s.Exit
|
||||
)}
|
||||
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
|
||||
>
|
||||
<For each={deploymentIDs()}>
|
||||
{(id) => <Deployment id={id} />}
|
||||
</For>
|
||||
<Show
|
||||
when={
|
||||
user().admin ||
|
||||
server()?.server.permissions![getId(user())] ===
|
||||
PermissionLevel.Update
|
||||
}
|
||||
>
|
||||
<NewDeployment serverID={p.id} />
|
||||
</Show>
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "builds",
|
||||
element: () => (
|
||||
<Grid
|
||||
gap=".5rem"
|
||||
class={combineClasses(
|
||||
s.Deployments,
|
||||
open() ? s.Enter : s.Exit
|
||||
)}
|
||||
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
|
||||
>
|
||||
<For each={buildIDs()}>{(id) => <Build id={id} />}</For>
|
||||
<Show
|
||||
when={
|
||||
user().admin ||
|
||||
server()?.server.permissions![getId(user())] ===
|
||||
PermissionLevel.Update
|
||||
}
|
||||
>
|
||||
<NewBuild serverID={p.id} />
|
||||
</Show>
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ServerChildren id={p.id} />
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -154,6 +72,29 @@ const ServerStats: Component<{ id: string }> = (p) => {
|
||||
await serverStats.load(p.id);
|
||||
setReloading(false);
|
||||
};
|
||||
const mem_perc = () => {
|
||||
return stats() && (100 * stats()!.mem_used_gb) / stats()!.mem_total_gb;
|
||||
};
|
||||
const disk_perc = () => {
|
||||
return stats() && (100 * stats()!.disk.used_gb) / stats()!.disk.total_gb;
|
||||
};
|
||||
const cpu_high = () => {
|
||||
return (
|
||||
server() &&
|
||||
stats() &&
|
||||
stats()!.cpu_perc > (server()!.server.cpu_alert || 75)
|
||||
);
|
||||
};
|
||||
const mem_high = () => {
|
||||
return (
|
||||
server() && stats() && mem_perc()! > (server()!.server.mem_alert || 75)
|
||||
);
|
||||
};
|
||||
const disk_high = () => {
|
||||
return (
|
||||
server() && stats() && disk_perc()! > (server()!.server.disk_alert || 75)
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Show when={!isMobile() && server()?.status === ServerStatus.Ok}>
|
||||
<Show when={stats()} fallback={<Loading type="three-dot" />}>
|
||||
@@ -174,7 +115,9 @@ const ServerStats: Component<{ id: string }> = (p) => {
|
||||
justifyContent="center"
|
||||
>
|
||||
<div style={{ opacity: 0.7 }}>cpu:</div>
|
||||
<h2>{stats()!.cpu_perc.toFixed(1)}%</h2>
|
||||
<h2 class={cpu_high() ? "text-red" : undefined}>
|
||||
{stats()!.cpu_perc.toFixed(1)}%
|
||||
</h2>
|
||||
</Flex>
|
||||
<Flex
|
||||
style={{
|
||||
@@ -186,11 +129,8 @@ const ServerStats: Component<{ id: string }> = (p) => {
|
||||
justifyContent="center"
|
||||
>
|
||||
<div style={{ opacity: 0.7 }}>mem:</div>
|
||||
<h2>
|
||||
{((100 * stats()!.mem_used_gb) / stats()!.mem_total_gb).toFixed(
|
||||
1
|
||||
)}
|
||||
%
|
||||
<h2 class={mem_high() ? "text-red" : undefined}>
|
||||
{mem_perc()?.toFixed(1)}%
|
||||
</h2>
|
||||
<Show when={!isSemiMobile()}>
|
||||
<div>{stats()!.mem_total_gb.toFixed()} GiB</div>
|
||||
@@ -206,11 +146,8 @@ const ServerStats: Component<{ id: string }> = (p) => {
|
||||
justifyContent="center"
|
||||
>
|
||||
<div style={{ opacity: 0.7 }}>disk:</div>
|
||||
<h2>
|
||||
{((100 * stats()!.disk.used_gb) / stats()!.disk.total_gb).toFixed(
|
||||
1
|
||||
)}
|
||||
%
|
||||
<h2 class={disk_high() ? "text-red" : undefined}>
|
||||
{disk_perc()?.toFixed(1)}%
|
||||
</h2>
|
||||
<Show when={!isSemiMobile()}>
|
||||
<div>{readableStorageAmount(stats()!.disk.total_gb)}</div>
|
||||
@@ -248,4 +185,4 @@ const ServerStats: Component<{ id: string }> = (p) => {
|
||||
</Show>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@ import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import Input from "../../shared/Input";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import Selector from "../../shared/menu/Selector";
|
||||
import AddServer from "./AddServer";
|
||||
import { TreeSortType, TREE_SORTS, useTreeState } from "./Provider";
|
||||
import Server from "./Server";
|
||||
|
||||
const Servers: Component<{ serverIDs: string[]; showAdd?: boolean }> = (p) => {
|
||||
const { user } = useUser();
|
||||
const { servers } = useAppState();
|
||||
const { sort, setSort, server_sorter } = useTreeState();
|
||||
const [serverFilter, setServerFilter] = createSignal("");
|
||||
const serverIDs = createMemo(() => {
|
||||
if (servers.loaded()) {
|
||||
@@ -24,25 +27,38 @@ const Servers: Component<{ serverIDs: string[]; showAdd?: boolean }> = (p) => {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})
|
||||
.sort(server_sorter());
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Grid style={{ height: "fit-content" }}>
|
||||
<Input
|
||||
placeholder="filter servers"
|
||||
value={serverFilter()}
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Grid gridTemplateColumns="1fr auto auto">
|
||||
<Input
|
||||
placeholder="filter servers"
|
||||
value={serverFilter()}
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
position="bottom right"
|
||||
targetClass="blue"
|
||||
targetStyle={{ height: "100%" }}
|
||||
containerStyle={{ height: "100%" }}
|
||||
/>
|
||||
<Show
|
||||
when={p.showAdd && (user().admin || user().create_server_permissions)}
|
||||
>
|
||||
<AddServer />
|
||||
</Show>
|
||||
</Grid>
|
||||
<For each={serverIDs()}>{(id) => <Server id={id} />}</For>
|
||||
<Show
|
||||
when={p.showAdd && (user().admin || user().create_server_permissions)}
|
||||
>
|
||||
<AddServer />
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { Operation, Update as UpdateType, UpdateStatus } from "../../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
readableMonitorTimestamp,
|
||||
readableVersion,
|
||||
} from "../../../util/helpers";
|
||||
import Icon from "../../shared/Icon";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
@@ -26,9 +28,16 @@ const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
};
|
||||
const operation = () => {
|
||||
if (p.update.operation === Operation.BuildBuild) {
|
||||
return "build";
|
||||
return `build ${readableVersion(p.update.version!)}`;
|
||||
}
|
||||
return p.update.operation.replaceAll("_", " ");
|
||||
return `${p.update.operation.replaceAll("_", " ")}${
|
||||
p.update.version ? " " + readableVersion(p.update.version) : ""
|
||||
}`;
|
||||
};
|
||||
const link_to = () => {
|
||||
return p.update.target.type === "System"
|
||||
? "/"
|
||||
: `/${p.update.target.type.toLowerCase()}/${p.update.target.id}`;
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
@@ -37,7 +46,9 @@ const Update: Component<{ update: UpdateType }> = (p) => {
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid gap="0.5rem" placeItems="center start">
|
||||
<h2>{name()}</h2>
|
||||
<A style={{ padding: 0 }} href={link_to()}>
|
||||
<h2 class="text-hover">{name()}</h2>
|
||||
</A>
|
||||
<Flex gap="0.5rem">
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -1,22 +1,55 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { Component, createEffect, createSignal, For, Show } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { Operation } from "../../../types";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import s from "../home.module.scss";
|
||||
import Loading from "../../shared/loading/Loading";
|
||||
import Selector from "../../shared/menu/Selector";
|
||||
import Update from "./Update";
|
||||
import { combineClasses } from "../../../util/helpers";
|
||||
import { useAppDimensions } from "../../../state/DimensionProvider";
|
||||
|
||||
const OPERATIONS = Object.values(Operation)
|
||||
.filter((e) => e !== "none" && !e.includes("user"))
|
||||
.map((e) => e.replaceAll("_", " "));
|
||||
|
||||
const Updates: Component<{}> = () => {
|
||||
const { updates } = useAppState();
|
||||
const { isMobile } = useAppDimensions();
|
||||
const [operation, setOperation] = createSignal<Operation>();
|
||||
createEffect(() => {
|
||||
if (operation()) {
|
||||
updates.load([operation()!]);
|
||||
} else {
|
||||
updates.load();
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Show when={updates.loaded()}>
|
||||
<Grid
|
||||
class={combineClasses(s.Updates, "card shadow")}
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Grid class="card shadow" style={{ "flex-grow": 1 }}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>updates</h1>
|
||||
<Grid>
|
||||
<Selector
|
||||
selected={operation() ? operation()! : "all"}
|
||||
items={["all", ...OPERATIONS]}
|
||||
onSelect={(o) =>
|
||||
o === "all"
|
||||
? setOperation(undefined)
|
||||
: setOperation(o.replaceAll(" ", "_") as Operation)
|
||||
}
|
||||
targetStyle={{ padding: "0" }}
|
||||
position="bottom right"
|
||||
searchStyle={{ width: "15rem" }}
|
||||
menuClass="scroller"
|
||||
menuStyle={{ "max-height": "50vh" }}
|
||||
useSearch
|
||||
/>
|
||||
</Flex>
|
||||
<Show
|
||||
when={updates.loaded()}
|
||||
fallback={
|
||||
<Flex justifyContent="center">
|
||||
<Loading type="three-dot" />
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Grid class="updates-container-small scroller">
|
||||
<For each={updates.collection()!}>
|
||||
{(update) => <Update update={update} />}
|
||||
</For>
|
||||
@@ -24,14 +57,18 @@ const Updates: Component<{}> = () => {
|
||||
<button
|
||||
class="grey"
|
||||
style={{ width: "100%" }}
|
||||
onClick={updates.loadMore}
|
||||
onClick={() =>
|
||||
operation()
|
||||
? updates.loadMore([operation()!])
|
||||
: updates.loadMore()
|
||||
}
|
||||
>
|
||||
load more
|
||||
</button>
|
||||
</Show>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Show>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
.Update {
|
||||
background-color: c.$lightgrey;
|
||||
padding: 0.75rem;
|
||||
height: 40px;
|
||||
transform-origin: top;
|
||||
animation-name: Enter;
|
||||
animation-duration: 750ms;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ServerButton {
|
||||
@@ -35,9 +34,9 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ServerButton:hover {
|
||||
background-color: rgba(c.$lightblue, 0.5);
|
||||
}
|
||||
// .ServerButton:hover {
|
||||
// background-color: rgba(c.$lightblue, 0.5);
|
||||
// }
|
||||
|
||||
.Deployments {
|
||||
background-color: c.$lightgrey;
|
||||
@@ -60,10 +59,6 @@
|
||||
// max-height: 50vh;
|
||||
// }
|
||||
|
||||
.Summary {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Updates {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -25,25 +25,27 @@ const Actions: Component<{}> = (p) => {
|
||||
<Show
|
||||
when={server() && server().status === ServerStatus.Ok && userCanExecute()}
|
||||
>
|
||||
<Grid class={combineClasses("card shadow")}>
|
||||
<Grid class={combineClasses("card shadow")} gridTemplateRows="auto 1fr">
|
||||
<h1>actions</h1>
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
prune images <PruneImages />
|
||||
</Flex>
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
prune containers <PruneContainers />
|
||||
</Flex>
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
prune networks{" "}
|
||||
<ConfirmButton
|
||||
class="green"
|
||||
onConfirm={() => {
|
||||
client.prune_docker_networks(params.id);
|
||||
}}
|
||||
>
|
||||
<Icon type="cut" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
<Grid style={{ height: "fit-content" }}>
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
prune images <PruneImages />
|
||||
</Flex>
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
prune containers <PruneContainers />
|
||||
</Flex>
|
||||
<Flex class={combineClasses("action shadow")}>
|
||||
prune networks{" "}
|
||||
<ConfirmButton
|
||||
class="green"
|
||||
onConfirm={() => {
|
||||
client.prune_docker_networks(params.id);
|
||||
}}
|
||||
>
|
||||
<Icon type="cut" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Show>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user