forked from github-starred/komodo
Compare commits
44 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 |
121
Cargo.lock
generated
121
Cargo.lock
generated
@@ -602,7 +602,7 @@ dependencies = [
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"time 0.3.20",
|
||||
"uuid 1.3.0",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -732,9 +732,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "core"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -753,7 +759,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"jwt",
|
||||
"monitor_helpers",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.10",
|
||||
"mungos",
|
||||
"periphery_client",
|
||||
"serde",
|
||||
@@ -987,10 +993,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
|
||||
|
||||
[[package]]
|
||||
name = "db_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.10",
|
||||
"mungos",
|
||||
]
|
||||
|
||||
@@ -1036,6 +1042,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version 0.4.0",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff-struct"
|
||||
version = "0.5.1"
|
||||
@@ -1788,9 +1807,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mongodb"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a1df476ac9541b0e4fdc8e2cc48884e66c92c933cd17a1fd75e68caf75752e"
|
||||
checksum = "a37fe10c1485a0cd603468e284a1a8535b4ecf46808f5f7de3639a1e1252dbf8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
@@ -1798,21 +1817,22 @@ dependencies = [
|
||||
"bson",
|
||||
"chrono",
|
||||
"derivative",
|
||||
"derive_more",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hmac",
|
||||
"lazy_static",
|
||||
"md-5",
|
||||
"os_info",
|
||||
"pbkdf2",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"rustc_version_runtime",
|
||||
"rustls",
|
||||
"rustls-pemfile 0.3.0",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_with 1.14.0",
|
||||
@@ -1830,18 +1850,19 @@ dependencies = [
|
||||
"trust-dns-proto",
|
||||
"trust-dns-resolver",
|
||||
"typed-builder",
|
||||
"uuid 0.8.2",
|
||||
"uuid",
|
||||
"webpki-roots",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monitor_cli"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"async_timing_util",
|
||||
"clap",
|
||||
"colored",
|
||||
"monitor_types 0.2.10",
|
||||
"rand",
|
||||
"run_command",
|
||||
"serde",
|
||||
@@ -1853,12 +1874,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"envy",
|
||||
"futures-util",
|
||||
"monitor_types 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"monitor_types 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -1870,11 +1891,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_helpers"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.10",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1883,7 +1904,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_periphery"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -1895,11 +1916,12 @@ dependencies = [
|
||||
"envy",
|
||||
"futures",
|
||||
"monitor_helpers",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.10",
|
||||
"run_command",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"svi",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"toml",
|
||||
@@ -1908,7 +1930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_types"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bollard",
|
||||
@@ -1925,9 +1947,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_types"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de46e03ba424cb9f70a57a5dd38b81399cca131cdb2edf8bf7d03f829e02c140"
|
||||
checksum = "7bf35db6341431dea9f062f5d676305a213834638410fb9cdc49ca2521635c43"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bollard",
|
||||
@@ -1944,11 +1966,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mungos"
|
||||
version = "0.3.7"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fc5132f76fafd19d773c68520ab427659a7b17484f2b4705f323b60f84e9d6c"
|
||||
checksum = "2b8fabef8c6e29f25a64c58736ab58b191e28aa3bafc3e84a3b0e78a1ba00665"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"envy",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"mongodb",
|
||||
@@ -2121,16 +2144,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c424bc68d15e0778838ac013b5b3449544d8133633d8016319e7e05a820b8c0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.4.1"
|
||||
@@ -2168,9 +2181,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.10.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7"
|
||||
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
@@ -2183,11 +2196,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.10",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2497,20 +2510,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile 1.0.2",
|
||||
"rustls-pemfile",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.2"
|
||||
@@ -2861,6 +2865,15 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "svi"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec1ee5e6cf961310f3b4ba037f6a3680fc264f9077e0b9f16a0d7cc8d0ade140"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -3011,9 +3024,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.25.0"
|
||||
version = "1.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
|
||||
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -3026,7 +3039,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.42.0",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3094,6 +3107,7 @@ checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -3418,15 +3432,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.0"
|
||||
|
||||
@@ -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.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor cli | tools to setup monitor system"
|
||||
@@ -13,6 +13,7 @@ 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"
|
||||
|
||||
@@ -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,
|
||||
@@ -321,7 +320,9 @@ 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,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use clap::{arg, Arg, Command};
|
||||
|
||||
mod helpers;
|
||||
mod types;
|
||||
|
||||
use helpers::*;
|
||||
|
||||
|
||||
207
cli/src/types.rs
207
cli/src/types.rs
@@ -1,207 +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
|
||||
|
||||
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,
|
||||
|
||||
// used to form the frontend listener url, if None will use 'host'.
|
||||
pub github_webhook_base_url: Option<String>,
|
||||
|
||||
// sent in auth header with req to periphery
|
||||
pub passkey: String,
|
||||
|
||||
// integration with slack app
|
||||
pub slack_url: Option<String>,
|
||||
|
||||
// enable login with local auth
|
||||
pub local_auth: bool,
|
||||
|
||||
// allowed docker orgs used with monitor. first in this list will be default for build
|
||||
#[serde(default)]
|
||||
pub docker_organizations: Vec<String>,
|
||||
|
||||
pub mongo: MongoConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub github_oauth: OauthCredentials,
|
||||
|
||||
#[serde(default)]
|
||||
pub google_oauth: OauthCredentials,
|
||||
|
||||
#[serde(default)]
|
||||
pub aws: AwsBuilderConfig,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AwsBuilderConfig {
|
||||
#[serde(skip_serializing)]
|
||||
pub access_key_id: String,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub secret_access_key: String,
|
||||
|
||||
pub default_ami_id: String,
|
||||
pub default_subnet_id: String,
|
||||
pub default_key_pair_name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub available_ami_accounts: AvailableAmiAccounts,
|
||||
|
||||
#[serde(default = "default_aws_region")]
|
||||
pub default_region: String,
|
||||
|
||||
#[serde(default = "default_volume_gb")]
|
||||
pub default_volume_gb: i32,
|
||||
|
||||
#[serde(default = "default_instance_type")]
|
||||
pub default_instance_type: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub default_security_group_ids: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub default_assign_public_ip: bool,
|
||||
}
|
||||
|
||||
fn default_aws_region() -> String {
|
||||
String::from("us-east-1")
|
||||
}
|
||||
|
||||
fn default_volume_gb() -> i32 {
|
||||
8
|
||||
}
|
||||
|
||||
fn default_instance_type() -> String {
|
||||
String::from("m5.2xlarge")
|
||||
}
|
||||
|
||||
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_id, AmiAccounts)
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AmiAccounts {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub github: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub docker: Vec<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 passkeys: Vec<String>,
|
||||
#[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,3 +1,6 @@
|
||||
# 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 = "https://monitor.mogh.tech"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "core"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -11,7 +11,7 @@ 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.25", 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"] }
|
||||
@@ -19,7 +19,7 @@ axum-extra = { version = "0.5.0", features = ["spa"] }
|
||||
tower = { version = "0.4", features = ["full"] }
|
||||
tower-http = { version = "0.4.0", features = ["cors"] }
|
||||
slack = { package = "slack_client_rs", version = "0.0.8" }
|
||||
mungos = "0.3.3"
|
||||
mungos = "0.3.14"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -7,7 +7,8 @@ use mungos::{doc, to_bson};
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
Build, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget, Version, AwsBuilderBuildConfig,
|
||||
AwsBuilderBuildConfig, Build, Log, Operation, PermissionLevel, Update, UpdateStatus,
|
||||
UpdateTarget, Version,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -442,10 +443,17 @@ impl State {
|
||||
self.config.aws.secret_access_key.clone(),
|
||||
)
|
||||
.await;
|
||||
let ami_id = aws_config
|
||||
.ami_id
|
||||
let ami_name = aws_config
|
||||
.ami_name
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_ami_id);
|
||||
.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()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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::{
|
||||
@@ -274,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,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use diff::Diff;
|
||||
use futures_util::future::join_all;
|
||||
use helpers::to_monitor_name;
|
||||
use mungos::doc;
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
@@ -49,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()
|
||||
|
||||
@@ -43,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 {
|
||||
@@ -162,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(
|
||||
@@ -324,7 +348,7 @@ pub fn router() -> Router {
|
||||
}
|
||||
|
||||
impl State {
|
||||
async fn get_deployment_with_container_state(
|
||||
pub async fn get_deployment_with_container_state(
|
||||
&self,
|
||||
user: &RequestUser,
|
||||
id: &str,
|
||||
@@ -489,10 +513,10 @@ impl State {
|
||||
if let Some(version) = update.version {
|
||||
Ok(version.to_string())
|
||||
} else {
|
||||
Ok("latest".to_string())
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok("latest".to_string())
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
} else {
|
||||
let split = deployment
|
||||
@@ -503,7 +527,7 @@ impl State {
|
||||
if let Some(version) = split.get(1) {
|
||||
Ok(version.to_string())
|
||||
} else {
|
||||
Ok("latest".to_string())
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_request, JwtExtension, RequestUser, RequestUserExtension},
|
||||
response,
|
||||
state::{State, StateExtension},
|
||||
ResponseResult,
|
||||
};
|
||||
|
||||
pub mod build;
|
||||
@@ -35,22 +37,33 @@ struct UpdateDescriptionBody {
|
||||
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(
|
||||
@@ -90,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();
|
||||
@@ -99,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
|
||||
@@ -137,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,
|
||||
|
||||
@@ -284,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)
|
||||
@@ -312,7 +312,7 @@ 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(),
|
||||
@@ -339,7 +339,7 @@ async fn modify_user_create_build_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)
|
||||
@@ -367,7 +367,7 @@ async fn modify_user_create_build_permissions(
|
||||
"modify user create build permissions",
|
||||
format!(
|
||||
"{update_type} create build permissions for {} (id: {})",
|
||||
user.username, user.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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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};
|
||||
|
||||
@@ -16,8 +16,10 @@ 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);
|
||||
@@ -40,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(())
|
||||
}
|
||||
|
||||
@@ -412,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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
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.
|
||||
|
||||
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```.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.6.0",
|
||||
"@tanstack/solid-query": "^4.26.0",
|
||||
"axios": "^1.2.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"lightweight-charts": "^3.8.0",
|
||||
|
||||
@@ -8,8 +8,9 @@ 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 User = lazy(() => import("./components/users/User"));
|
||||
const Stats = lazy(() => import("./components/stats/Stats"));
|
||||
const Account = lazy(() => import("./components/Account"));
|
||||
const Account = lazy(() => import("./components/account/Account"));
|
||||
|
||||
const App: Component = () => {
|
||||
const { user } = useUser();
|
||||
@@ -25,6 +26,7 @@ const App: Component = () => {
|
||||
<Route path="/account" component={Account} />
|
||||
<Show when={user().admin}>
|
||||
<Route path="/users" component={Users} />
|
||||
<Route path="/user/:id" component={User} />
|
||||
</Show>
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { client, pushNotification } from "..";
|
||||
import { useAppState } from "../state/StateProvider";
|
||||
import { UpdateTarget } from "../types";
|
||||
import { useToggle } from "../util/hooks";
|
||||
import ConfirmButton from "./shared/ConfirmButton";
|
||||
import Flex from "./shared/layout/Flex";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import Loading from "./shared/loading/Loading";
|
||||
import CenterMenu from "./shared/menu/CenterMenu";
|
||||
@@ -19,7 +17,8 @@ const Description: Component<{
|
||||
const [show, toggleShow] = useToggle();
|
||||
const description = () => {
|
||||
if (p.description) {
|
||||
return p.description;
|
||||
let [description] = p.description.split("\n");
|
||||
return description;
|
||||
} else {
|
||||
return "add a description";
|
||||
}
|
||||
@@ -110,8 +109,7 @@ const DescriptionMenu: Component<{
|
||||
placeholder="add a description"
|
||||
value={desc()}
|
||||
onEdit={setDesc}
|
||||
onEnter={update_description}
|
||||
style={{ width: "700px", "max-width": "90vw", padding: "1rem" }}
|
||||
style={{ width: "900px", "max-width": "90vw", height: "70vh", padding: "1rem" }}
|
||||
disabled={!p.userCanUpdate}
|
||||
/>
|
||||
<Show when={p.userCanUpdate}>
|
||||
|
||||
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;
|
||||
@@ -1,79 +1,81 @@
|
||||
import { Component, For, Match, Show, Switch } from "solid-js";
|
||||
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";
|
||||
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 Account: Component<{}> = (p) => {
|
||||
const { user, reloadUser } = useUser();
|
||||
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={() => <CreateNewMenu />}
|
||||
position="center"
|
||||
/>
|
||||
</Flex>
|
||||
<For each={user().secrets}>
|
||||
{(secret) => (
|
||||
<Flex
|
||||
class="card dark 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>
|
||||
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>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Account;
|
||||
export default Secrets;
|
||||
|
||||
const EXPIRE_LENGTHS = ["30 days", "90 days", "1 year", "never"] as const;
|
||||
type ExpireLength = typeof EXPIRE_LENGTHS[number];
|
||||
|
||||
const CreateNewMenu = () => {
|
||||
const CreateNewSecretMenu = () => {
|
||||
const { reloadUser } = useUser();
|
||||
const [info, setInfo] = createStore<{
|
||||
name: string;
|
||||
@@ -167,4 +169,4 @@ function createExpires(length: ExpireLength) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,10 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
set(...args);
|
||||
set("updated", true);
|
||||
};
|
||||
const server = () => build.server_id ? servers.get(build.server_id) : undefined;
|
||||
const server = () =>
|
||||
builds.get(params.id)?.server_id
|
||||
? servers.get(builds.get(params.id)!.server_id!)
|
||||
: undefined;
|
||||
|
||||
const load = () => {
|
||||
// console.log("load build");
|
||||
|
||||
@@ -22,22 +22,9 @@ const AwsBuilderConfig: Component<{}> = (p) => {
|
||||
const Ami: Component = () => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
const default_ami_id = () => aws_builder_config()?.default_ami_id;
|
||||
const get_ami_id = () => {
|
||||
if (build.aws_config?.ami_id) {
|
||||
return build.aws_config.ami_id;
|
||||
} else {
|
||||
return default_ami_id() || "unknown";
|
||||
}
|
||||
};
|
||||
const get_ami_name = (ami_id: string) => {
|
||||
if (aws_builder_config() === undefined || ami_id === "unknown")
|
||||
return "unknown";
|
||||
return (
|
||||
aws_builder_config()!.available_ami_accounts![ami_id]?.name || "unknown"
|
||||
);
|
||||
};
|
||||
const ami_ids = () => {
|
||||
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!);
|
||||
};
|
||||
@@ -50,16 +37,16 @@ const Ami: Component = () => {
|
||||
<h1>ami</h1>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={get_ami_id()}
|
||||
items={ami_ids()}
|
||||
onSelect={(ami_id) => {
|
||||
if (ami_id === default_ami_id()) {
|
||||
setBuild("aws_config", "ami_id", undefined);
|
||||
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_id", ami_id);
|
||||
setBuild("aws_config", "ami_name", ami_name);
|
||||
}
|
||||
}}
|
||||
itemMap={get_ami_name}
|
||||
itemMap={(i) => i.replaceAll("_", " ")}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
useSearch
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
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";
|
||||
@@ -36,9 +48,11 @@ const BuildArgs: Component<{}> = (p) => {
|
||||
};
|
||||
|
||||
const EditBuildArgs: Component<{}> = (p) => {
|
||||
const { aws_builder_config, builds, serverSecrets } = useAppState();
|
||||
const [show, toggle] = useToggle();
|
||||
const [buildArgs, setBuildArgs] = createSignal("");
|
||||
const { build, setBuild } = useConfig();
|
||||
const params = useParams();
|
||||
const { build, setBuild, server } = useConfig();
|
||||
createEffect(() => {
|
||||
setBuildArgs(
|
||||
parseEnvVarseToDotEnv(
|
||||
@@ -56,6 +70,23 @@ const EditBuildArgs: Component<{}> = (p) => {
|
||||
}
|
||||
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}
|
||||
@@ -69,19 +100,44 @@ const EditBuildArgs: Component<{}> = (p) => {
|
||||
</button>
|
||||
)}
|
||||
content={() => (
|
||||
<TextArea
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={buildArgs()}
|
||||
onEdit={setBuildArgs}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"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={() =>
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Component, createEffect, createResource, createSignal, Show } from "solid-js";
|
||||
import {
|
||||
Component,
|
||||
createResource,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../types";
|
||||
@@ -10,26 +14,16 @@ import Selector from "../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const Docker: Component<{}> = (p) => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const { aws_builder_config, serverDockerAccounts, docker_organizations } = useAppState();
|
||||
const { build, setBuild, server, userCanUpdate } = useConfig();
|
||||
const [dockerOrgs] = createResource(() => client.get_docker_organizations());
|
||||
const [peripheryDockerAccounts, setPeripheryDockerAccounts] =
|
||||
createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_docker_accounts(build.server_id!)
|
||||
.then(setPeripheryDockerAccounts);
|
||||
}
|
||||
});
|
||||
const dockerAccounts = () => {
|
||||
if (build.server_id) {
|
||||
return peripheryDockerAccounts() || [];
|
||||
return serverDockerAccounts.get(build.server_id, server()?.status || ServerStatus.NotOk) || [];
|
||||
} else if (build.aws_config) {
|
||||
const ami_id =
|
||||
build.aws_config?.ami_id || aws_builder_config()?.default_ami_id;
|
||||
return ami_id
|
||||
? aws_builder_config()?.available_ami_accounts![ami_id].docker || []
|
||||
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 [];
|
||||
};
|
||||
@@ -87,7 +81,7 @@ const Docker: Component<{}> = (p) => {
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
</Flex>
|
||||
<Show when={build.docker_organization || (dockerOrgs() || []).length > 0}>
|
||||
<Show when={build.docker_organization || (docker_organizations() || []).length > 0}>
|
||||
<Flex
|
||||
justifyContent={userCanUpdate() ? "space-between" : undefined}
|
||||
alignItems="center"
|
||||
@@ -97,7 +91,7 @@ const Docker: Component<{}> = (p) => {
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={build.docker_organization || "none"}
|
||||
items={["none", ...(dockerOrgs() || [])]}
|
||||
items={["none", ...(docker_organizations() || [])]}
|
||||
onSelect={(account) => {
|
||||
setBuild(
|
||||
"docker_organization",
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
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 { client } from "../../../..";
|
||||
import { ServerStatus } from "../../../../types";
|
||||
import Selector from "../../../shared/menu/Selector";
|
||||
|
||||
const Repo: Component<{}> = (p) => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const { aws_builder_config, serverGithubAccounts } = useAppState();
|
||||
const { build, setBuild, server, userCanUpdate } = useConfig();
|
||||
const [peripheryGithubAccounts, setPeripheryGithubAccounts] =
|
||||
createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_github_accounts(build.server_id!)
|
||||
.then(setPeripheryGithubAccounts);
|
||||
}
|
||||
});
|
||||
const githubAccounts = () => {
|
||||
if (build.server_id) {
|
||||
return peripheryGithubAccounts() || [];
|
||||
return (
|
||||
serverGithubAccounts.get(
|
||||
build.server_id,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || []
|
||||
);
|
||||
} else if (build.aws_config) {
|
||||
const ami_id =
|
||||
build.aws_config?.ami_id || aws_builder_config()?.default_ami_id;
|
||||
return ami_id
|
||||
? aws_builder_config()?.available_ami_accounts![ami_id].github || []
|
||||
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 [];
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -8,13 +9,11 @@ import Loading from "../../../shared/loading/Loading";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const ListenerUrl: Component<{}> = (p) => {
|
||||
const { github_webhook_base_url } = useAppState();
|
||||
const { build } = useConfig();
|
||||
const [github_base_url] = createResource(() =>
|
||||
client.get_github_webhook_base_url()
|
||||
);
|
||||
const listenerUrl = () => {
|
||||
if (github_base_url()) {
|
||||
return `${github_base_url()}/api/listener/build/${getId(build)}`;
|
||||
if (github_webhook_base_url()) {
|
||||
return `${github_webhook_base_url()}/api/listener/build/${getId(build)}`;
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, createResource, Show } from "solid-js";
|
||||
import { Component, createResource, createSignal, Show } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import {
|
||||
@@ -19,6 +19,8 @@ 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, servers, builds } = useAppState();
|
||||
@@ -41,24 +43,38 @@ const Header: Component<{}> = (p) => {
|
||||
const [deployed_version] = createResource(() =>
|
||||
client.get_deployment_deployed_version(params.id)
|
||||
);
|
||||
const image = () => {
|
||||
const derived_image = () => {
|
||||
if (deployment().deployment.build_id) {
|
||||
const build = builds.get(deployment().deployment.build_id!)!;
|
||||
if (deployment().state === DockerContainerState.NotDeployed) {
|
||||
const version = deployment().deployment.build_version
|
||||
? readableVersion(deployment().deployment.build_version!).replaceAll(
|
||||
"v",
|
||||
""
|
||||
)
|
||||
: "latest";
|
||||
return `${build.name}:${version}`;
|
||||
} else {
|
||||
return deployed_version() && `${build.name}:${deployed_version()}`;
|
||||
}
|
||||
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
|
||||
@@ -75,7 +91,34 @@ const Header: Component<{}> = (p) => {
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Flex alignItems="center">
|
||||
<h1>{deployment()!.deployment.name}</h1>
|
||||
<Show
|
||||
when={editingName()}
|
||||
fallback={
|
||||
<button
|
||||
onClick={() => setEditingName(true)}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<h1>{deployment()!.deployment.name}</h1>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<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()}>
|
||||
|
||||
@@ -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, ServerStatus, ServerWithStatus } from "../../../../types";
|
||||
import {
|
||||
Deployment,
|
||||
Operation,
|
||||
PermissionLevel,
|
||||
ServerStatus,
|
||||
ServerWithStatus,
|
||||
} from "../../../../types";
|
||||
import { getId } from "../../../../util/helpers";
|
||||
|
||||
type ConfigDeployment = Deployment & {
|
||||
@@ -28,7 +36,7 @@ type State = {
|
||||
server: () => ServerWithStatus | undefined;
|
||||
reset: () => void;
|
||||
save: () => void;
|
||||
networks: Accessor<any[]>;
|
||||
networks: Resource<any[]>;
|
||||
userCanUpdate: () => boolean;
|
||||
};
|
||||
|
||||
@@ -87,19 +95,20 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
};
|
||||
createEffect(load);
|
||||
|
||||
const [networks, setNetworks] = createSignal<any[]>([]);
|
||||
const server = () => servers.get(deployments.get(params.id)!.deployment.server_id);
|
||||
createEffect(() => {
|
||||
const server = () =>
|
||||
servers.get(deployments.get(params.id)!.deployment.server_id);
|
||||
|
||||
const [networks] = createResource(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_docker_networks(deployments.get(params.id)!.deployment.server_id)
|
||||
.then(setNetworks);
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, createEffect, createSignal } 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";
|
||||
@@ -7,25 +7,21 @@ import Selector from "../../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const DockerAccount: Component<{}> = (p) => {
|
||||
const { serverDockerAccounts } = useAppState();
|
||||
const { deployment, setDeployment, server, userCanUpdate } = useConfig();
|
||||
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_docker_accounts(deployment.server_id)
|
||||
.then(setDockerAccounts);
|
||||
}
|
||||
});
|
||||
const dockerAccounts = () =>
|
||||
serverDockerAccounts.get(
|
||||
deployment.server_id,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || [];
|
||||
const when_none_selected = () => {
|
||||
if (deployment.build_id) {
|
||||
return "same as build"
|
||||
return "same as build";
|
||||
} else {
|
||||
return "none"
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
const accounts = () => {
|
||||
return [when_none_selected(), ...(dockerAccounts() || [])];
|
||||
}
|
||||
};
|
||||
const accounts = () => [when_none_selected(), ...dockerAccounts()];
|
||||
return (
|
||||
<Flex
|
||||
class={combineClasses("config-item shadow")}
|
||||
@@ -37,10 +33,13 @@ const DockerAccount: Component<{}> = (p) => {
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
items={accounts()}
|
||||
selected={deployment.docker_run_args.docker_account || when_none_selected()}
|
||||
selected={
|
||||
deployment.docker_run_args.docker_account || when_none_selected()
|
||||
}
|
||||
onSelect={(account) =>
|
||||
setDeployment("docker_run_args", {
|
||||
docker_account: account === when_none_selected() ? 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,
|
||||
@@ -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,19 +86,44 @@ const EditDotEnv: Component<{}> = (p) => {
|
||||
</button>
|
||||
)}
|
||||
content={() => (
|
||||
<TextArea
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={dotenv()}
|
||||
onEdit={setDotEnv}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
@@ -8,13 +8,11 @@ import Loading from "../../../../shared/loading/Loading";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const WebhookUrl: Component<{}> = (p) => {
|
||||
const { github_webhook_base_url } = useAppState();
|
||||
const { deployment } = useConfig();
|
||||
const [github_base_url] = createResource(() =>
|
||||
client.get_github_webhook_base_url()
|
||||
);
|
||||
const listenerUrl = () => {
|
||||
if (github_base_url()) {
|
||||
return `${github_base_url()}/api/listener/deployment/${getId(
|
||||
if (github_webhook_base_url()) {
|
||||
return `${github_webhook_base_url()}/api/listener/deployment/${getId(
|
||||
deployment
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
@@ -9,15 +9,13 @@ import Selector from "../../../../shared/menu/Selector";
|
||||
import { useConfig } from "../Provider";
|
||||
|
||||
const Git: Component<{}> = (p) => {
|
||||
const { serverGithubAccounts } = useAppState();
|
||||
const { deployment, server, setDeployment, userCanUpdate } = useConfig();
|
||||
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_github_accounts(deployment.server_id)
|
||||
.then(setGithubAccounts);
|
||||
}
|
||||
});
|
||||
const githubAccounts = () =>
|
||||
serverGithubAccounts.get(
|
||||
deployment.server_id,
|
||||
server()?.status || ServerStatus.NotOk
|
||||
) || [];
|
||||
return (
|
||||
<Grid class={combineClasses("config-item shadow")}>
|
||||
<h1>github config</h1>
|
||||
@@ -56,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,126 +1,56 @@
|
||||
import { Component, createMemo, For, Show } from "solid-js";
|
||||
import { Accessor, Component, createMemo } 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";
|
||||
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 { isMobile } = useAppDimensions();
|
||||
const deployentCount = useDeploymentCount();
|
||||
const serverCount = useServerCount();
|
||||
return (
|
||||
<Grid class="card shadow" gridTemplateRows="auto 1fr 1fr 1fr">
|
||||
<h1>summary</h1>
|
||||
<DeploymentsSummary />
|
||||
<ServersSummary />
|
||||
<BuildsSummary />
|
||||
<Grid
|
||||
class="card shadow"
|
||||
gridTemplateColumns={isMobile() ? "1fr" : "1fr 1fr"}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
placeItems="center"
|
||||
gap="0"
|
||||
>
|
||||
<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 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() {
|
||||
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;
|
||||
@@ -137,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;
|
||||
@@ -160,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;
|
||||
}
|
||||
@@ -58,6 +58,7 @@ const Builds: Component<{}> = (p) => {
|
||||
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)}
|
||||
|
||||
@@ -51,6 +51,7 @@ const Groups: Component<{}> = (p) => {
|
||||
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)}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { ParentComponent, createContext, useContext } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useLocalStorage } from "../../../util/hooks";
|
||||
|
||||
export const TREE_SORTS = ["name", "created"] as const;
|
||||
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-v1"
|
||||
"home-sort-v2"
|
||||
);
|
||||
const server_sorter = () => {
|
||||
if (!servers.loaded()) return () => 0;
|
||||
|
||||
@@ -42,7 +42,8 @@ const Servers: Component<{ serverIDs: string[]; showAdd?: boolean }> = (p) => {
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -34,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;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component, createResource, Show } from "solid-js";
|
||||
import { Component, createResource, createSignal, Show } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { combineClasses, getId, serverStatusClass } 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";
|
||||
@@ -15,6 +14,7 @@ import { client } from "../..";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import HoverMenu from "../shared/menu/HoverMenu";
|
||||
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
|
||||
import Input, { AutofocusInput } from "../shared/Input";
|
||||
|
||||
const Header: Component<{}> = (p) => {
|
||||
const { servers } = useAppState();
|
||||
@@ -25,6 +25,8 @@ const Header: Component<{}> = (p) => {
|
||||
const { isMobile, isSemiMobile } = useAppDimensions();
|
||||
const [showUpdates, toggleShowUpdates] =
|
||||
useLocalStorageToggle("show-updates");
|
||||
const [editingName, setEditingName] = createSignal(false);
|
||||
const [updatingName, setUpdatingName] = createSignal(false);
|
||||
const userCanUpdate = () =>
|
||||
user().admin ||
|
||||
server().server.permissions![getId(user())] === PermissionLevel.Update;
|
||||
@@ -50,7 +52,38 @@ const Header: Component<{}> = (p) => {
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>{server().server.name}</h1>
|
||||
<Show
|
||||
when={editingName()}
|
||||
fallback={
|
||||
<button
|
||||
onClick={() => setEditingName(true)}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<h1>{server().server.name}</h1>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={!updatingName()}
|
||||
fallback={<Loading type="three-dot" />}
|
||||
>
|
||||
<AutofocusInput
|
||||
value={server().server.name}
|
||||
placeholder={server().server.name}
|
||||
onEnter={async (new_name) => {
|
||||
setUpdatingName(true);
|
||||
await client.update_server({
|
||||
...server().server,
|
||||
name: new_name,
|
||||
});
|
||||
setEditingName(false);
|
||||
setUpdatingName(false);
|
||||
}}
|
||||
onBlur={() => setEditingName(false)}
|
||||
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={userCanUpdate()}>
|
||||
<Flex alignItems="center">
|
||||
<div class={serverStatusClass(server().status)}>{status()}</div>
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { Component, createResource, Show } from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { DockerContainerState } from "../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
deploymentStateClass,
|
||||
readableVersion,
|
||||
} from "../../util/helpers";
|
||||
import Circle from "../shared/Circle";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import s from "./serverchildren.module.scss";
|
||||
|
||||
const Deployment: Component<{ id: string }> = (p) => {
|
||||
const { deployments, builds } = useAppState();
|
||||
const deployment = () => deployments.get(p.id)!;
|
||||
const [deployed_version] = createResource(() =>
|
||||
client.get_deployment_deployed_version(p.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) {
|
||||
if (deployment().deployment.build_id) {
|
||||
const build = builds.get(deployment().deployment.build_id!);
|
||||
if (build === undefined) return "unknown"
|
||||
const version = deployment().deployment.build_version
|
||||
? readableVersion(deployment().deployment.build_version!).replaceAll(
|
||||
"v",
|
||||
""
|
||||
)
|
||||
: "latest";
|
||||
return `${build.name}:${version}`;
|
||||
} else {
|
||||
return deployment().deployment.docker_run_args.image || "unknown";
|
||||
}
|
||||
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 {
|
||||
@@ -39,7 +49,15 @@ const Deployment: Component<{ id: string }> = (p) => {
|
||||
};
|
||||
return (
|
||||
<Show when={deployment()}>
|
||||
<A href={`/deployment/${p.id}`} class="card hoverable" style={{ width: "100%", "justify-content": "space-between", padding: "0.5rem" }}>
|
||||
<A
|
||||
href={`/deployment/${p.id}`}
|
||||
class="card hoverable"
|
||||
style={{
|
||||
width: "100%",
|
||||
"justify-content": "space-between",
|
||||
padding: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0">
|
||||
<h2>{deployment().deployment.name}</h2>
|
||||
<div style={{ opacity: 0.7 }}>{image()}</div>
|
||||
|
||||
32
frontend/src/components/shared/CheckBox.tsx
Normal file
32
frontend/src/components/shared/CheckBox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Component, JSX } from "solid-js";
|
||||
|
||||
const CheckBox: Component<{
|
||||
label: JSX.Element;
|
||||
checked: boolean;
|
||||
toggle: () => void;
|
||||
}> = (p) => {
|
||||
return (
|
||||
<button
|
||||
class="blue"
|
||||
style={{ gap: "0.5rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
p.toggle();
|
||||
}}
|
||||
>
|
||||
{p.label}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={p.checked}
|
||||
style={{
|
||||
width: "fit-content",
|
||||
margin: 0,
|
||||
appearance: "auto",
|
||||
"-webkit-appearance": "checkbox",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBox;
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, JSX, Show } from "solid-js";
|
||||
import { Component, JSX, onMount, Show } from "solid-js";
|
||||
|
||||
const Input: Component<
|
||||
{
|
||||
onEdit?: (value: string) => void;
|
||||
onConfirm?: (value: string) => void;
|
||||
onEnter?: (value: string) => void;
|
||||
onEsc?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
} & JSX.InputHTMLAttributes<HTMLInputElement> &
|
||||
JSX.HTMLAttributes<HTMLDivElement>
|
||||
@@ -14,17 +15,37 @@ const Input: Component<
|
||||
<input
|
||||
{...p}
|
||||
onInput={(e) => p.onEdit && p.onEdit(e.currentTarget.value)}
|
||||
onBlur={(e) => p.onConfirm && p.onConfirm(e.currentTarget.value)}
|
||||
onKeyDown={p.onKeyDown || ((e) => {
|
||||
if (e.key === "Enter") {
|
||||
p.onEnter
|
||||
? p.onEnter(e.currentTarget.value)
|
||||
: e.currentTarget.blur();
|
||||
}
|
||||
})}
|
||||
onBlur={
|
||||
p.onBlur || ((e) => p.onConfirm && p.onConfirm(e.currentTarget.value))
|
||||
}
|
||||
onKeyDown={
|
||||
p.onKeyDown ||
|
||||
((e) => {
|
||||
if (e.key === "Enter") {
|
||||
p.onEnter && p.onEnter(e.currentTarget.value);
|
||||
} else if (e.key === "Escape") {
|
||||
p.onEsc ? p.onEsc(e.currentTarget.value) : e.currentTarget.blur();
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
||||
|
||||
export const AutofocusInput: Component<
|
||||
{
|
||||
onEdit?: (value: string) => void;
|
||||
onConfirm?: (value: string) => void;
|
||||
onEnter?: (value: string) => void;
|
||||
onEsc?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
} & JSX.InputHTMLAttributes<HTMLInputElement> &
|
||||
JSX.HTMLAttributes<HTMLDivElement>
|
||||
> = (p) => {
|
||||
let ref: HTMLInputElement;
|
||||
onMount(() => setTimeout(() => ref?.focus(), 100));
|
||||
return <Input ref={ref! as any} {...p} />;
|
||||
};
|
||||
|
||||
241
frontend/src/components/shared/PieChart.tsx
Normal file
241
frontend/src/components/shared/PieChart.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
onCleanup,
|
||||
onMount,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import Grid from "./layout/Grid";
|
||||
|
||||
export type PieChartSection = {
|
||||
title: string;
|
||||
amount: number;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const PieChart: Component<{
|
||||
title: string;
|
||||
sections: (PieChartSection | undefined)[];
|
||||
donutProportion?: number;
|
||||
seperation?: number;
|
||||
}> = (p) => {
|
||||
let ref: HTMLDivElement;
|
||||
let canvas: HTMLCanvasElement;
|
||||
const [chart, setChart] = createSignal<PieChartCanvas>();
|
||||
const [selected, setSelected] = createSignal<number>();
|
||||
const sections = createMemo(
|
||||
() =>
|
||||
p.sections
|
||||
.filter((s) => s && s.amount > 0)
|
||||
.sort((a, b) => {
|
||||
if (a!.amount > b!.amount) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}) as PieChartSection[]
|
||||
);
|
||||
const onResize = () =>
|
||||
chart()?.updateCanvasDim(ref.clientWidth, ref.clientHeight);
|
||||
onMount(() => {
|
||||
const chart = new PieChartCanvas(
|
||||
canvas,
|
||||
sections(),
|
||||
setSelected,
|
||||
p.donutProportion,
|
||||
p.seperation
|
||||
);
|
||||
setChart(chart);
|
||||
onResize();
|
||||
window.addEventListener("resize", onResize);
|
||||
});
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
});
|
||||
createEffect(() => {
|
||||
chart()?.updateSections(sections());
|
||||
chart()?.draw();
|
||||
});
|
||||
return (
|
||||
<Grid
|
||||
ref={ref!}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"box-sizing": "border-box",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
placeItems="center"
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "grid", gap: "0.2rem" }}>
|
||||
<h2 style={{ "margin-bottom": "0.5rem" }}>{p.title}</h2>
|
||||
<For each={sections()}>
|
||||
{(section, index) => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
"justify-content": "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: selected() === index() ? 1 : 0.7,
|
||||
}}
|
||||
>
|
||||
{section.title}:
|
||||
</div>
|
||||
<div style={{ color: section.color }}>{section.amount}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<Show when={sections().length === 0}>
|
||||
<div style={{ opacity: 0.7 }}>none</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Grid>
|
||||
<canvas ref={canvas!} style={{ "z-index": 1 }} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default PieChart;
|
||||
|
||||
type InnerPieChartSection = PieChartSection & {
|
||||
startAngle: number;
|
||||
endAngle: number;
|
||||
};
|
||||
|
||||
class PieChartCanvas {
|
||||
sections: InnerPieChartSection[];
|
||||
selected?: number;
|
||||
cx = 0;
|
||||
cy = 0;
|
||||
r = 0;
|
||||
|
||||
constructor(
|
||||
private canvas: HTMLCanvasElement,
|
||||
sections: PieChartSection[],
|
||||
private onSelectedUpdate: (selected: number | undefined) => void,
|
||||
private donutProportion = 0.8,
|
||||
private seperation = 0.02 // private initAngle = -Math.PI / 8
|
||||
) {
|
||||
this.sections = [];
|
||||
this.updateSections(sections);
|
||||
this.canvas.addEventListener("mousemove", (e) => this.onMouseOver(e));
|
||||
this.canvas.addEventListener("mouseout", () => {
|
||||
this.selected = undefined;
|
||||
this.onSelectedUpdate(this.selected);
|
||||
this.draw();
|
||||
});
|
||||
}
|
||||
|
||||
draw() {
|
||||
const ctx = this.canvas.getContext("2d");
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (const segIndex in this.sections) {
|
||||
const seg = this.sections[segIndex];
|
||||
const outerStartAngle = seg.startAngle + this.seperation;
|
||||
const outerEndAngle = seg.endAngle - this.seperation;
|
||||
const innerStartAngle =
|
||||
seg.startAngle + this.seperation / this.donutProportion;
|
||||
const innerEndAngle =
|
||||
seg.endAngle - this.seperation / this.donutProportion;
|
||||
|
||||
ctx.fillStyle =
|
||||
Number(segIndex) === this.selected ? seg.color : `${seg.color}B3`;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
this.cx + this.donutProportion * this.r * Math.cos(innerStartAngle),
|
||||
this.cy + this.donutProportion * this.r * Math.sin(innerStartAngle)
|
||||
);
|
||||
ctx.lineTo(
|
||||
this.cx + this.r * Math.cos(outerStartAngle),
|
||||
this.cy + this.r * Math.sin(outerStartAngle)
|
||||
);
|
||||
ctx.arc(this.cx, this.cy, this.r, outerStartAngle, outerEndAngle);
|
||||
ctx.lineTo(
|
||||
this.cx + this.donutProportion * this.r * Math.cos(innerEndAngle),
|
||||
this.cy + this.donutProportion * this.r * Math.sin(innerEndAngle)
|
||||
);
|
||||
ctx.arc(
|
||||
this.cx,
|
||||
this.cy,
|
||||
this.donutProportion * this.r,
|
||||
innerEndAngle,
|
||||
innerStartAngle,
|
||||
true
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
updateSections(sections: PieChartSection[]) {
|
||||
let startAngle = 0;
|
||||
const total = sections.reduce((prev, curr) => prev + curr.amount, 0);
|
||||
this.sections = sections.map((s) => {
|
||||
const proportion = s.amount / total;
|
||||
const rads = Math.PI * 2 * proportion;
|
||||
startAngle += rads;
|
||||
return {
|
||||
...s,
|
||||
startAngle: startAngle - rads,
|
||||
endAngle: startAngle,
|
||||
};
|
||||
});
|
||||
this.draw();
|
||||
}
|
||||
|
||||
onMouseOver(e: MouseEvent) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = e.x - rect.x - this.cx;
|
||||
const y = e.y - rect.y - this.cy;
|
||||
if (x * x + y * y > this.r * this.r) {
|
||||
this.selected = undefined;
|
||||
this.onSelectedUpdate(this.selected);
|
||||
this.draw();
|
||||
return;
|
||||
}
|
||||
const atan = Math.atan(y / x);
|
||||
const angle =
|
||||
x >= 0 ? (y >= 0 ? atan : 2 * Math.PI + atan) : Math.PI + atan;
|
||||
for (const secIndex in this.sections) {
|
||||
if (angle < this.sections[secIndex].endAngle) {
|
||||
this.selected = Number(secIndex);
|
||||
this.onSelectedUpdate(this.selected);
|
||||
this.draw();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCanvasDim(width: number, height: number) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
this.cx = this.canvas.width / 2;
|
||||
this.cy = this.canvas.height / 2;
|
||||
this.r =
|
||||
this.canvas.width < this.canvas.height
|
||||
? this.canvas.width / 2 - 2
|
||||
: this.canvas.height / 2 - 2;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ const TextArea: Component<
|
||||
onEdit?: (value: string) => void;
|
||||
onConfirm?: (value: string) => void;
|
||||
onEnter?: (value: string) => void;
|
||||
shiftDisablesOnEnter?: boolean;
|
||||
disabled?: boolean;
|
||||
} & JSX.InputHTMLAttributes<HTMLTextAreaElement> &
|
||||
JSX.HTMLAttributes<HTMLDivElement>
|
||||
@@ -16,7 +17,11 @@ const TextArea: Component<
|
||||
onInput={(e) => p.onEdit && p.onEdit(e.currentTarget.value)}
|
||||
onBlur={(e) => p.onConfirm && p.onConfirm(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && p.onEnter) {
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
p.onEnter &&
|
||||
(!p.shiftDisablesOnEnter || !e.shiftKey)
|
||||
) {
|
||||
e.preventDefault();
|
||||
p.onEnter(e.currentTarget.value);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LineData, SingleValueData } from "lightweight-charts";
|
||||
import { Accessor, Component, For, ParentComponent, Show } from "solid-js";
|
||||
import { COLORS } from "../../style/colors";
|
||||
import { SystemStats, SystemStatsRecord } from "../../types";
|
||||
import {
|
||||
convertTsMsToLocalUnixTsInSecs,
|
||||
@@ -9,14 +9,6 @@ import Grid from "../shared/layout/Grid";
|
||||
import LightweightChart, { LightweightValue } from "../shared/LightweightChart";
|
||||
import s from "./stats.module.scss";
|
||||
|
||||
export const COLORS = {
|
||||
blue: "#184e9f",
|
||||
orange: "#ac5c36",
|
||||
purple: "#5A0B4D",
|
||||
green: "#41764c",
|
||||
red: "#952E23",
|
||||
};
|
||||
|
||||
const CHART_HEIGHT = "250px";
|
||||
const SMALL_CHART_HEIGHT = "150px";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { client, MAX_PAGE_WIDTH } from "../..";
|
||||
import { client } from "../..";
|
||||
import { SystemProcess, SystemStats } from "../../types";
|
||||
import { convert_timelength_to_ms } from "../../util/helpers";
|
||||
import { useLocalStorage } from "../../util/hooks";
|
||||
|
||||
@@ -16,8 +16,8 @@ import Circle from "../../shared/Circle";
|
||||
import { ControlledTabs } from "../../shared/tabs/Tabs";
|
||||
import { useAppDimensions } from "../../../state/DimensionProvider";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { Build, ServerStatus } from "../../../types";
|
||||
import { A } from "@solidjs/router";
|
||||
import { ServerStatus } from "../../../types";
|
||||
|
||||
const mobileStyle: JSX.CSSProperties = {
|
||||
// position: "fixed",
|
||||
|
||||
330
frontend/src/components/users/User.tsx
Normal file
330
frontend/src/components/users/User.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import {
|
||||
Operation,
|
||||
PermissionLevel,
|
||||
PermissionsTarget,
|
||||
User as UserType,
|
||||
} from "../../types";
|
||||
import {
|
||||
getId,
|
||||
readableMonitorTimestamp,
|
||||
readableUserType,
|
||||
} from "../../util/helpers";
|
||||
import { useToggle } from "../../util/hooks";
|
||||
import CheckBox from "../shared/CheckBox";
|
||||
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";
|
||||
|
||||
const User: Component = () => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { builds, deployments, servers, ws } = useAppState();
|
||||
const params = useParams<{ id: string }>();
|
||||
const [user, { refetch }] = createResource(() =>
|
||||
client.get_user_by_id(params.id)
|
||||
);
|
||||
onCleanup(
|
||||
ws.subscribe(
|
||||
[
|
||||
Operation.ModifyUserEnabled,
|
||||
Operation.ModifyUserCreateServerPermissions,
|
||||
Operation.ModifyUserCreateBuildPermissions,
|
||||
Operation.ModifyUserPermissions,
|
||||
],
|
||||
refetch
|
||||
)
|
||||
);
|
||||
const [showAll, toggleShowAll] = useToggle(false);
|
||||
const [search, setSearch] = createSignal("");
|
||||
const _servers = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return servers.filterArray((s) => s.server.name.includes(search()));
|
||||
} else {
|
||||
return servers.filterArray((s) => {
|
||||
if (!s.server.name.includes(search())) return false;
|
||||
const p = s.server.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
const _deployments = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return deployments.filterArray((d) =>
|
||||
d.deployment.name.includes(search())
|
||||
);
|
||||
} else {
|
||||
return deployments.filterArray((d) => {
|
||||
if (!d.deployment.name.includes(search())) return false;
|
||||
const p = d.deployment.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
const _builds = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return builds.filterArray((b) => b.name.includes(search()));
|
||||
} else {
|
||||
return builds.filterArray((b) => {
|
||||
if (!b.name.includes(search())) return false;
|
||||
const p = b.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Show when={user()} fallback={<Loading type="three-dot" />}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Flex alignItems="center">
|
||||
<A href="/users" class="grey">
|
||||
<Icon type="arrow-left" />
|
||||
</A>
|
||||
<h1>{user()?.username}</h1>
|
||||
<Show when={user()?.admin}>
|
||||
<h2 style={{ opacity: 0.7 }}>admin</h2>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<CheckBox
|
||||
label="show all resources"
|
||||
checked={showAll()}
|
||||
toggle={toggleShowAll}
|
||||
/>
|
||||
<UserPermissionButtons user={user()!} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<Input
|
||||
placeholder="search resources"
|
||||
class="lightgrey"
|
||||
style={{ padding: "0.5rem" }}
|
||||
value={search()}
|
||||
onEdit={setSearch}
|
||||
/>
|
||||
<Flex>
|
||||
<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>
|
||||
{user()?.created_at
|
||||
? readableMonitorTimestamp(user()?.created_at!)
|
||||
: "unknown"}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>servers</h1>
|
||||
<Show when={_servers()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_servers()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.server.name}</h2>
|
||||
<div class="dimmed">
|
||||
{item.server.region || "unknown region"}
|
||||
</div>
|
||||
</Grid>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.server.permissions?.[params.id] || "none") !==
|
||||
"none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={item.server.permissions?.[params.id] || "none"}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Server,
|
||||
target_id: getId(item.server),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>deployments</h1>
|
||||
<Show when={_deployments()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_deployments()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.deployment.name}</h2>
|
||||
<div class="dimmed">
|
||||
{servers.get(item.deployment.server_id)?.server.name ||
|
||||
"unknown"}
|
||||
</div>
|
||||
</Grid>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.deployment.permissions?.[params.id] || "none") !==
|
||||
"none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={
|
||||
item.deployment.permissions?.[params.id] || "none"
|
||||
}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Deployment,
|
||||
target_id: getId(item.deployment),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>builds</h1>
|
||||
<Show when={_builds()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_builds()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h2>{item.name}</h2>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.permissions?.[params.id] || "none") !== "none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={item.permissions?.[params.id] || "none"}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Build,
|
||||
target_id: getId(item),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
export const UserPermissionButtons: Component<{ user: UserType }> = (p) => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
return (
|
||||
<Show when={!p.user.admin}>
|
||||
<Grid
|
||||
placeItems="center end"
|
||||
gridTemplateColumns={!isMobile() ? "auto 1fr 1fr" : undefined}
|
||||
>
|
||||
<button
|
||||
class={p.user.enabled ? "green" : "red"}
|
||||
style={{ width: isMobile() ? "11rem" : "6rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_enabled({
|
||||
user_id: getId(p.user),
|
||||
enabled: !p.user.enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.enabled ? "enabled" : "disabled"}
|
||||
</button>
|
||||
<button
|
||||
class={p.user.create_server_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_create_server_permissions({
|
||||
user_id: getId(p.user),
|
||||
create_server_permissions: !p.user.create_server_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.create_server_permissions
|
||||
? "can create servers"
|
||||
: "cannot create servers"}
|
||||
</button>
|
||||
<button
|
||||
class={p.user.create_build_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_create_build_permissions({
|
||||
user_id: getId(p.user),
|
||||
create_build_permissions: !p.user.create_build_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.create_build_permissions
|
||||
? "can create builds"
|
||||
: "cannot create builds"}
|
||||
</button>
|
||||
</Grid>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
@@ -8,18 +9,16 @@ import {
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { Operation } from "../../types";
|
||||
import { combineClasses, getId } from "../../util/helpers";
|
||||
import { getId } from "../../util/helpers";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import s from "./users.module.scss";
|
||||
import { UserPermissionButtons } from "./User";
|
||||
|
||||
const Users: Component<{}> = (p) => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { ws } = useAppState();
|
||||
const [users, { refetch }] = createResource(() => client.list_users());
|
||||
onCleanup(
|
||||
@@ -60,61 +59,28 @@ const Users: Component<{}> = (p) => {
|
||||
</Flex>
|
||||
<For each={filteredUsers()}>
|
||||
{(user) => (
|
||||
<Flex class={combineClasses(s.User, "shadow")}>
|
||||
<div class={s.Username}>{user.username}</div>
|
||||
<Grid
|
||||
placeItems="center end"
|
||||
gridTemplateColumns={!isMobile() ? "1fr 1fr 1fr" : undefined}
|
||||
<Show
|
||||
when={!user.admin}
|
||||
fallback={
|
||||
<Flex class="card light shadow">
|
||||
<h2>{user.username}</h2>
|
||||
<h2 style={{ opacity: 0.7 }}>admin</h2>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<A
|
||||
href={`/user/${getId(user)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
width: "100%",
|
||||
"justify-content": "space-between",
|
||||
"align-items": "center",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class={user.enabled ? "green" : "red"}
|
||||
style={{ width: isMobile() ? "11rem" : "6rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_enabled({
|
||||
user_id: getId(user),
|
||||
enabled: !user.enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.enabled ? "enabled" : "disabled"}
|
||||
</button>
|
||||
<button
|
||||
class={user.create_server_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_create_server_permissions({
|
||||
user_id: getId(user),
|
||||
create_server_permissions:
|
||||
!user.create_server_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.create_server_permissions
|
||||
? "can create servers"
|
||||
: "cannot create servers"}
|
||||
</button>
|
||||
<button
|
||||
class={user.create_build_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_create_build_permissions({
|
||||
user_id: getId(user),
|
||||
create_build_permissions: !user.create_build_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.create_build_permissions
|
||||
? "can create builds"
|
||||
: "cannot create builds"}
|
||||
</button>
|
||||
{/* <ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() => deleteUser(user._id!)}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton> */}
|
||||
</Grid>
|
||||
</Flex>
|
||||
<h2>{user.username}</h2>
|
||||
<UserPermissionButtons user={user} />
|
||||
</A>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
@use "../../style/colors.scss" as c;
|
||||
|
||||
.UsersContent {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Users {
|
||||
width: fit-content;
|
||||
min-width: 30rem;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.Username {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.User {
|
||||
background-color: c.$lightgrey;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -6,8 +6,11 @@ import {
|
||||
useDeployments,
|
||||
useGroups,
|
||||
useProcedures,
|
||||
useServerDockerAccounts,
|
||||
useServerGithubAccounts,
|
||||
useServerInfo,
|
||||
useServers,
|
||||
useServerSecrets,
|
||||
useServerStats,
|
||||
useUpdates,
|
||||
useUsernames,
|
||||
@@ -23,6 +26,9 @@ export type State = {
|
||||
getPermissionOnServer: (id: string) => PermissionLevel;
|
||||
serverStats: ReturnType<typeof useServerStats>;
|
||||
serverInfo: ReturnType<typeof useServerInfo>;
|
||||
serverDockerAccounts: ReturnType<typeof useServerDockerAccounts>;
|
||||
serverGithubAccounts: ReturnType<typeof useServerGithubAccounts>;
|
||||
serverSecrets: ReturnType<typeof useServerSecrets>;
|
||||
ungroupedServerIds: () => string[] | undefined;
|
||||
builds: ReturnType<typeof useBuilds>;
|
||||
getPermissionOnBuild: (id: string) => PermissionLevel;
|
||||
@@ -34,6 +40,8 @@ export type State = {
|
||||
getPermissionOnProcedure: (id: string) => PermissionLevel;
|
||||
updates: ReturnType<typeof useUpdates>;
|
||||
aws_builder_config: Resource<AwsBuilderConfig>;
|
||||
docker_organizations: Resource<string[]>;
|
||||
github_webhook_base_url: Resource<string>;
|
||||
};
|
||||
|
||||
const context = createContext<
|
||||
@@ -54,6 +62,8 @@ export const AppStateProvider: ParentComponent = (p) => {
|
||||
const deployments = useDeployments();
|
||||
const usernames = useUsernames();
|
||||
const [aws_builder_config] = createResource(() => client.get_aws_builder_defaults());
|
||||
const [docker_organizations] = createResource(() => client.get_docker_organizations());
|
||||
const [github_webhook_base_url] = createResource(() => client.get_github_webhook_base_url());
|
||||
const state: State = {
|
||||
usernames,
|
||||
servers,
|
||||
@@ -107,6 +117,9 @@ export const AppStateProvider: ParentComponent = (p) => {
|
||||
},
|
||||
serverStats: useServerStats(servers),
|
||||
serverInfo: useServerInfo(servers),
|
||||
serverDockerAccounts: useServerDockerAccounts(servers),
|
||||
serverGithubAccounts: useServerGithubAccounts(servers),
|
||||
serverSecrets: useServerSecrets(servers),
|
||||
groups,
|
||||
getPermissionOnGroup: (id: string) => {
|
||||
const group = groups.get(id)!;
|
||||
@@ -133,6 +146,8 @@ export const AppStateProvider: ParentComponent = (p) => {
|
||||
},
|
||||
updates: useUpdates(),
|
||||
aws_builder_config,
|
||||
docker_organizations,
|
||||
github_webhook_base_url,
|
||||
};
|
||||
|
||||
// createEffect(() => {
|
||||
|
||||
@@ -112,6 +112,106 @@ export function useServerInfo(servers: ReturnType<typeof useServers>) {
|
||||
};
|
||||
}
|
||||
|
||||
export function useServerGithubAccounts(servers: ReturnType<typeof useServers>) {
|
||||
const [accounts, set] = createSignal<
|
||||
Record<string, string[] | undefined>
|
||||
>({});
|
||||
const load = async (serverID: string) => {
|
||||
if (servers.get(serverID)?.status === ServerStatus.Ok) {
|
||||
try {
|
||||
const info = await client.get_server_github_accounts(serverID);
|
||||
set((s) => ({ ...s, [serverID]: info }));
|
||||
} catch (error) {
|
||||
console.log("error getting server github accounts", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const loading: Record<string, boolean> = {};
|
||||
return {
|
||||
get: (serverID: string, serverStatus?: ServerStatus) => {
|
||||
const accts = accounts()[serverID];
|
||||
if (
|
||||
accts === undefined &&
|
||||
!loading[serverID] &&
|
||||
(serverStatus ? serverStatus === ServerStatus.Ok : true)
|
||||
) {
|
||||
loading[serverID] = true;
|
||||
load(serverID);
|
||||
}
|
||||
return accts;
|
||||
},
|
||||
load,
|
||||
};
|
||||
}
|
||||
|
||||
export function useServerDockerAccounts(
|
||||
servers: ReturnType<typeof useServers>
|
||||
) {
|
||||
const [accounts, set] = createSignal<Record<string, string[] | undefined>>(
|
||||
{}
|
||||
);
|
||||
const load = async (serverID: string) => {
|
||||
if (servers.get(serverID)?.status === ServerStatus.Ok) {
|
||||
try {
|
||||
const info = await client.get_server_docker_accounts(serverID);
|
||||
set((s) => ({ ...s, [serverID]: info }));
|
||||
} catch (error) {
|
||||
console.log("error getting server docker accounts", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const loading: Record<string, boolean> = {};
|
||||
return {
|
||||
get: (serverID: string, serverStatus?: ServerStatus) => {
|
||||
const accts = accounts()[serverID];
|
||||
if (
|
||||
accts === undefined &&
|
||||
!loading[serverID] &&
|
||||
(serverStatus ? serverStatus === ServerStatus.Ok : true)
|
||||
) {
|
||||
loading[serverID] = true;
|
||||
load(serverID);
|
||||
}
|
||||
return accts;
|
||||
},
|
||||
load,
|
||||
};
|
||||
}
|
||||
|
||||
export function useServerSecrets(
|
||||
servers: ReturnType<typeof useServers>
|
||||
) {
|
||||
const [accounts, set] = createSignal<Record<string, string[] | undefined>>(
|
||||
{}
|
||||
);
|
||||
const load = async (serverID: string) => {
|
||||
if (servers.get(serverID)?.status === ServerStatus.Ok) {
|
||||
try {
|
||||
const info = await client.get_server_available_secrets(serverID);
|
||||
set((s) => ({ ...s, [serverID]: info }));
|
||||
} catch (error) {
|
||||
console.log("error getting server github_accounts", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const loading: Record<string, boolean> = {};
|
||||
return {
|
||||
get: (serverID: string, serverStatus?: ServerStatus) => {
|
||||
const accts = accounts()[serverID];
|
||||
if (
|
||||
accts === undefined &&
|
||||
!loading[serverID] &&
|
||||
(serverStatus ? serverStatus === ServerStatus.Ok : true)
|
||||
) {
|
||||
loading[serverID] = true;
|
||||
load(serverID);
|
||||
}
|
||||
return accts;
|
||||
},
|
||||
load,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUsernames() {
|
||||
const [usernames, set] = createSignal<Record<string, string | undefined>>({});
|
||||
const load = async (userID: string) => {
|
||||
@@ -232,13 +332,18 @@ export function useArrayWithId<T, O>(
|
||||
idPath: string[],
|
||||
options?: O
|
||||
) {
|
||||
let is_loaded = false;
|
||||
const [collection, set] = createSignal<T[]>();
|
||||
const load = (options?: O) => {
|
||||
query(options).then(set);
|
||||
const load = (_options?: O) => {
|
||||
if (!is_loaded || _options !== options) {
|
||||
query(_options).then((r) => {
|
||||
is_loaded = true;
|
||||
options = _options;
|
||||
set(r);
|
||||
});
|
||||
}
|
||||
};
|
||||
createEffect(() => {
|
||||
load(options);
|
||||
});
|
||||
load(options);
|
||||
const addOrUpdate = (item: T) => {
|
||||
set((items: T[] | undefined) => {
|
||||
if (items) {
|
||||
|
||||
@@ -110,6 +110,11 @@ async function handleMessage(
|
||||
const deployment = await client.get_deployment(update.target.id!);
|
||||
deployments.update(deployment);
|
||||
}
|
||||
} else if (update.operation === Operation.RenameDeployment) {
|
||||
if (update.status === UpdateStatus.Complete) {
|
||||
const deployment = await client.get_deployment(update.target.id!);
|
||||
deployments.update(deployment);
|
||||
}
|
||||
} else if (
|
||||
[
|
||||
Operation.DeployContainer,
|
||||
|
||||
@@ -14,12 +14,12 @@ $lightgreen: #4f8d5c;
|
||||
$green: #41764c;
|
||||
$darkgreen: #2b4f33;
|
||||
|
||||
$textred: #f04633;
|
||||
$textred: #f76858;
|
||||
$lightred: #b13a2d;
|
||||
$red: #952E23;
|
||||
$darkred: #631F17;
|
||||
|
||||
$textorange: #984f2d;
|
||||
$textorange: #e77e4e;
|
||||
$lightorange: #d56b3a;
|
||||
$orange: #ac5c36;
|
||||
$darkorange: #984f2d;
|
||||
|
||||
31
frontend/src/style/colors.tsx
Normal file
31
frontend/src/style/colors.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
export const COLORS = {
|
||||
"app-color": "#fceade",
|
||||
|
||||
lightgrey: "#3f454d",
|
||||
grey: "#25292e",
|
||||
darkgrey: "#16181b",
|
||||
|
||||
textblue: "#5f9af4",
|
||||
lightblue: "#1c63cd",
|
||||
blue: "#184e9f",
|
||||
darkblue: "#12366d",
|
||||
|
||||
textgreen: "#80ea97",
|
||||
lightgreen: "#4f8d5c",
|
||||
green: "#41764c",
|
||||
darkgreen: "#2b4f33",
|
||||
|
||||
textred: "#f76858",
|
||||
lightred: "#b13a2d",
|
||||
red: "#952E23",
|
||||
darkred: "#631F17",
|
||||
|
||||
textorange: "#e77e4e",
|
||||
lightorange: "#d56b3a",
|
||||
orange: "#ac5c36",
|
||||
darkorange: "#984f2d",
|
||||
|
||||
lightpurple: "#720e61",
|
||||
purple: "#5A0B4D",
|
||||
darkpurple: "#3b0732",
|
||||
};
|
||||
@@ -314,6 +314,10 @@ svg {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dimmed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// .hoverable {
|
||||
// transition: all 250ms ease-in-out;
|
||||
// }
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Build {
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions?: PermissionsMap;
|
||||
skip_secret_interp?: boolean;
|
||||
server_id?: string;
|
||||
aws_config?: AwsBuilderBuildConfig;
|
||||
version: Version;
|
||||
@@ -65,7 +66,7 @@ export interface BuildVersionsReponse {
|
||||
export interface AwsBuilderBuildConfig {
|
||||
region?: string;
|
||||
instance_type?: string;
|
||||
ami_id?: string;
|
||||
ami_name?: string;
|
||||
volume_gb?: number;
|
||||
subnet_id?: string;
|
||||
security_group_ids?: string[];
|
||||
@@ -76,7 +77,7 @@ export interface AwsBuilderBuildConfig {
|
||||
export interface AwsBuilderConfig {
|
||||
access_key_id: string;
|
||||
secret_access_key: string;
|
||||
default_ami_id: string;
|
||||
default_ami_name: string;
|
||||
default_subnet_id: string;
|
||||
default_key_pair_name: string;
|
||||
available_ami_accounts?: AvailableAmiAccounts;
|
||||
@@ -88,9 +89,10 @@ export interface AwsBuilderConfig {
|
||||
}
|
||||
|
||||
export interface AmiAccounts {
|
||||
name: string;
|
||||
ami_id: string;
|
||||
github?: string[];
|
||||
docker?: string[];
|
||||
secrets?: string[];
|
||||
}
|
||||
|
||||
export interface Deployment {
|
||||
@@ -99,6 +101,7 @@ export interface Deployment {
|
||||
description?: string;
|
||||
server_id: string;
|
||||
permissions?: PermissionsMap;
|
||||
skip_secret_interp?: boolean;
|
||||
docker_run_args: DockerRunArgs;
|
||||
build_id?: string;
|
||||
build_version?: Version;
|
||||
@@ -126,6 +129,7 @@ export interface DeploymentActionState {
|
||||
pulling: boolean;
|
||||
recloning: boolean;
|
||||
updating: boolean;
|
||||
renaming: boolean;
|
||||
}
|
||||
|
||||
export interface DockerRunArgs {
|
||||
@@ -382,7 +386,7 @@ export interface User {
|
||||
|
||||
export interface ApiSecret {
|
||||
name: string;
|
||||
hash: string;
|
||||
hash?: string;
|
||||
created_at: string;
|
||||
expires?: string;
|
||||
}
|
||||
@@ -419,6 +423,7 @@ export enum Operation {
|
||||
PruneImagesServer = "prune_images_server",
|
||||
PruneContainersServer = "prune_containers_server",
|
||||
PruneNetworksServer = "prune_networks_server",
|
||||
RenameServer = "rename_server",
|
||||
CreateBuild = "create_build",
|
||||
UpdateBuild = "update_build",
|
||||
DeleteBuild = "delete_build",
|
||||
@@ -432,6 +437,7 @@ export enum Operation {
|
||||
RemoveContainer = "remove_container",
|
||||
PullDeployment = "pull_deployment",
|
||||
RecloneDeployment = "reclone_deployment",
|
||||
RenameDeployment = "rename_deployment",
|
||||
CreateProcedure = "create_procedure",
|
||||
UpdateProcedure = "update_procedure",
|
||||
DeleteProcedure = "delete_procedure",
|
||||
|
||||
@@ -43,17 +43,29 @@ import {
|
||||
ModifyUserCreateServerBody,
|
||||
ModifyUserEnabledBody,
|
||||
PermissionsUpdateBody,
|
||||
RenameDeploymentBody,
|
||||
UpdateDescriptionBody,
|
||||
} from "./client_types";
|
||||
import { generateQuery, QueryObject } from "./helpers";
|
||||
|
||||
export class Client {
|
||||
loginOptions: LoginOptions | undefined;
|
||||
monitorTitle: string | undefined;
|
||||
secrets_cache: Record<string, string[]> = {};
|
||||
github_accounts_cache: Record<string, string[]> = {};
|
||||
docker_accounts_cache: Record<string, string[]> = {};
|
||||
server_version_cache: Record<string, string> = {};
|
||||
|
||||
constructor(private baseURL: string, public token: string | null) {}
|
||||
|
||||
async initialize() {
|
||||
this.loginOptions = await this.get_login_options();
|
||||
const [loginOptions, monitorTitle] = await Promise.all([
|
||||
this.get_login_options(),
|
||||
this.get_monitor_title(),
|
||||
]);
|
||||
this.loginOptions = loginOptions;
|
||||
this.monitorTitle = monitorTitle;
|
||||
document.title = monitorTitle;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const exchange_token = params.get("token");
|
||||
if (exchange_token) {
|
||||
@@ -116,10 +128,16 @@ export class Client {
|
||||
return this.get(`/api/username/${user_id}`);
|
||||
}
|
||||
|
||||
// admin only
|
||||
list_users(): Promise<User[]> {
|
||||
return this.get("/api/users");
|
||||
}
|
||||
|
||||
// admin only
|
||||
get_user_by_id(user_id: string): Promise<User> {
|
||||
return this.get(`/api/user/${user_id}`);
|
||||
}
|
||||
|
||||
exchange_for_jwt(exchange_token: string): Promise<string> {
|
||||
return this.post("/auth/exchange", { token: exchange_token });
|
||||
}
|
||||
@@ -132,6 +150,10 @@ export class Client {
|
||||
return this.post("/api/update_description", body);
|
||||
}
|
||||
|
||||
get_monitor_title(): Promise<string> {
|
||||
return this.get("/api/title");
|
||||
}
|
||||
|
||||
// deployment
|
||||
|
||||
list_deployments(
|
||||
@@ -183,6 +205,10 @@ export class Client {
|
||||
return this.patch("/api/deployment/update", deployment);
|
||||
}
|
||||
|
||||
rename_deployment(deployment_id: string, new_name: string) {
|
||||
return this.patch(`/api/deployment/${deployment_id}/rename`, { new_name });
|
||||
}
|
||||
|
||||
reclone_deployment(deployment_id: string): Promise<Update> {
|
||||
return this.post(`/api/deployment/${deployment_id}/reclone`);
|
||||
}
|
||||
@@ -237,14 +263,51 @@ export class Client {
|
||||
}
|
||||
|
||||
get_server_github_accounts(id: string): Promise<string[]> {
|
||||
// if (this.github_accounts_cache[id]) {
|
||||
// return this.github_accounts_cache[id];
|
||||
// } else {
|
||||
// this.github_accounts_cache[id] = [];
|
||||
// }
|
||||
// this.github_accounts_cache[id] = await this.get(
|
||||
// `/api/server/${id}/github_accounts`
|
||||
// );
|
||||
// return this.github_accounts_cache[id];
|
||||
return this.get(`/api/server/${id}/github_accounts`);
|
||||
}
|
||||
|
||||
get_server_docker_accounts(id: string): Promise<string[]> {
|
||||
// if (this.docker_accounts_cache[id]) {
|
||||
// return this.docker_accounts_cache[id];
|
||||
// } else {
|
||||
// this.docker_accounts_cache[id] = [];
|
||||
// };
|
||||
// this.docker_accounts_cache[id] = await this.get(
|
||||
// `/api/server/${id}/docker_accounts`
|
||||
// );
|
||||
// return this.docker_accounts_cache[id];
|
||||
return this.get(`/api/server/${id}/docker_accounts`);
|
||||
}
|
||||
|
||||
get_server_available_secrets(id: string): Promise<string[]> {
|
||||
// if (this.secrets_cache[id]) {
|
||||
// return this.secrets_cache[id];
|
||||
// } else {
|
||||
// this.secrets_cache[id] = [];
|
||||
// };
|
||||
// console.log("loading");
|
||||
// this.secrets_cache[id] = await this.get(`/api/server/${id}/secrets`);
|
||||
// return this.secrets_cache[id];
|
||||
return this.get(`/api/server/${id}/secrets`);
|
||||
}
|
||||
|
||||
get_server_version(id: string): Promise<string> {
|
||||
// if (this.server_version_cache[id]) {
|
||||
// return this.server_version_cache[id];
|
||||
// } else {
|
||||
// this.server_version_cache[id] = "loading...";
|
||||
// };
|
||||
// this.server_version_cache[id] = await this.get(`/api/server/${id}/version`);
|
||||
// return this.server_version_cache[id];
|
||||
return this.get(`/api/server/${id}/version`);
|
||||
}
|
||||
|
||||
@@ -334,7 +397,7 @@ export class Client {
|
||||
get_build_versions(
|
||||
id: string,
|
||||
query?: BuildVersionsQuery
|
||||
): Promise<BuildVersionsReponse> {
|
||||
): Promise<BuildVersionsReponse[]> {
|
||||
return this.get(`/api/build/${id}/versions${generateQuery(query as any)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ export interface CopyDeploymentBody {
|
||||
server_id: string;
|
||||
}
|
||||
|
||||
export interface RenameDeploymentBody {
|
||||
new_name: string;
|
||||
}
|
||||
|
||||
export interface GetContainerLogQuery {
|
||||
tail?: number;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
EnvironmentVar,
|
||||
ServerStatus,
|
||||
Timelength,
|
||||
User,
|
||||
Version,
|
||||
} from "../types";
|
||||
|
||||
@@ -234,3 +235,13 @@ export function readableStorageAmount(gb: number) {
|
||||
export function readableVersion(version: Version) {
|
||||
return `v${version.major}.${version.minor}.${version.patch}`;
|
||||
}
|
||||
|
||||
export function readableUserType(user: User) {
|
||||
if (user.github_id) {
|
||||
return "github"
|
||||
} else if (user.google_id) {
|
||||
return "google"
|
||||
} else {
|
||||
return "local"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +435,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@solidjs/router/-/router-0.6.0.tgz#27583cd0aa81a99482e0e7eddae5e214bd8bf6b6"
|
||||
integrity sha512-7ug2fzXXhvvDBL4CQyMvMM9o3dgBE6PoRh38T8UTmMnYz4rcCfROqSZc9yq+YEC96qWt5OvJgZ1Uj/4EAQXlfA==
|
||||
|
||||
"@tanstack/query-core@4.26.0":
|
||||
version "4.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.26.0.tgz#fc65c8c117e72baead3a82a1f272a4ec210c7650"
|
||||
integrity sha512-9CRqXmCH82KZDKmezoGU4FOn1Oqbzlp2/zf71n+9nC58e7NSqCIjfSCMpqQWcu9YqUcUykxZEUunOyKHVc6BJA==
|
||||
|
||||
"@tanstack/solid-query@^4.26.0":
|
||||
version "4.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/solid-query/-/solid-query-4.26.0.tgz#77aa1b60e47719075802891a1f29a0a25385bd48"
|
||||
integrity sha512-2+dXfIHy8pU0GlrkxjXi8i7z9Ff1C7dbspAo3t6X6jJK57kesGhpE8AjmgIOUrTH1XsoHkTnBKxmoq/I7ewMCQ==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "4.26.0"
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl gnupg lsb-release make cmake g++ python3 node-gyp build-essential libssl-dev git
|
||||
git config --global pull.rebase false
|
||||
|
||||
# install docker cli
|
||||
# mkdir -p /etc/apt/keyrings
|
||||
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
# echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
# chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
# apt-get update
|
||||
# apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
|
||||
# install nodejs and enable yarn
|
||||
curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs
|
||||
corepack enable
|
||||
@@ -1,11 +0,0 @@
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl gnupg lsb-release git
|
||||
git config --global pull.rebase false
|
||||
|
||||
# install docker cli
|
||||
# mkdir -p /etc/apt/keyrings
|
||||
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
# echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
# apt-get update
|
||||
# apt-get install docker-ce docker-ce-cli
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "db_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
types = { package = "monitor_types", path = "../types" }
|
||||
mungos = "0.3.0"
|
||||
mungos = "0.3.8"
|
||||
anyhow = "1.0"
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use collections::{
|
||||
actions_collection, builds_collection, deployments_collection, groups_collection,
|
||||
@@ -29,7 +27,10 @@ pub struct DbClient {
|
||||
impl DbClient {
|
||||
pub async fn new(config: MongoConfig) -> DbClient {
|
||||
let db_name = &config.db_name;
|
||||
let mungos = Mungos::new(&config.uri, &config.app_name, Duration::from_secs(3), None)
|
||||
let mungos = Mungos::builder()
|
||||
.uri(&config.uri)
|
||||
.app_name(&config.app_name)
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to initialize mungos");
|
||||
DbClient {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_helpers"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "helpers used as dependency for mogh tech monitor"
|
||||
|
||||
@@ -133,10 +133,7 @@ pub fn to_monitor_name(name: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal Error: {err:#?}"),
|
||||
)
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#?}"))
|
||||
}
|
||||
|
||||
pub fn generate_secret(length: usize) -> String {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "a client to interact with the monitor system"
|
||||
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
monitor_types = "0.2.5"
|
||||
monitor_types = "0.2.10"
|
||||
# monitor_types = { path = "../types" }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
|
||||
@@ -124,6 +124,15 @@ impl MonitorClient {
|
||||
.context("failed at updating deployment")
|
||||
}
|
||||
|
||||
pub async fn rename_deployment(&self, id: &str, new_name: &str) -> anyhow::Result<Update> {
|
||||
self.patch(
|
||||
&format!("/api/deployment/{id}/rename"),
|
||||
json!({ "new_name": new_name }),
|
||||
)
|
||||
.await
|
||||
.context("failed at renaming deployment")
|
||||
}
|
||||
|
||||
pub async fn reclone_deployment(&self, id: &str) -> anyhow::Result<Update> {
|
||||
self.post::<(), _>(&format!("/api/deployment/{id}/reclone"), None)
|
||||
.await
|
||||
|
||||
@@ -58,6 +58,17 @@ impl MonitorClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_server_available_secrets(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
self.get(
|
||||
&format!("/api/server/{server_id}/secrets"),
|
||||
Option::<()>::None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_server(&self, name: &str, address: &str) -> anyhow::Result<Server> {
|
||||
self.post(
|
||||
"/api/server/create",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "periphery_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -70,6 +70,21 @@ impl PeripheryClient {
|
||||
.context("failed to remove container on periphery")
|
||||
}
|
||||
|
||||
pub async fn container_rename(
|
||||
&self,
|
||||
server: &Server,
|
||||
curr_name: &str,
|
||||
new_name: &str,
|
||||
) -> anyhow::Result<Log> {
|
||||
self.post_json(
|
||||
server,
|
||||
"/container/rename",
|
||||
&json!({ "curr_name": curr_name, "new_name": new_name }),
|
||||
)
|
||||
.await
|
||||
.context("failed to rename container on periphery")
|
||||
}
|
||||
|
||||
pub async fn deploy(&self, server: &Server, deployment: &Deployment) -> anyhow::Result<Log> {
|
||||
self.post_json(server, "/container/deploy", deployment)
|
||||
.await
|
||||
|
||||
@@ -51,6 +51,12 @@ impl PeripheryClient {
|
||||
.context("failed to get docker accounts from periphery")
|
||||
}
|
||||
|
||||
pub async fn get_available_secrets(&self, server: &Server) -> anyhow::Result<Vec<String>> {
|
||||
self.get_json(server, "/secrets")
|
||||
.await
|
||||
.context("failed to get secret variable names from periphery")
|
||||
}
|
||||
|
||||
pub async fn get_system_information(
|
||||
&self,
|
||||
server: &Server,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_types"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "types for the mogh tech monitor"
|
||||
|
||||
@@ -37,6 +37,11 @@ pub struct Build {
|
||||
#[builder(setter(skip))]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
pub server_id: Option<String>, // server which this image should be built on
|
||||
@@ -173,7 +178,7 @@ pub struct AwsBuilderBuildConfig {
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
pub ami_id: Option<String>,
|
||||
pub ami_name: Option<String>,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
|
||||
@@ -17,6 +17,9 @@ pub type SecretsMap = HashMap<String, String>; // these are used for injection i
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CoreConfig {
|
||||
#[serde(default = "default_title")]
|
||||
pub title: String,
|
||||
|
||||
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
|
||||
pub host: String,
|
||||
|
||||
@@ -71,6 +74,10 @@ pub struct CoreConfig {
|
||||
pub aws: AwsBuilderConfig,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
String::from("monitor")
|
||||
}
|
||||
|
||||
fn default_core_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
@@ -115,7 +122,7 @@ pub struct AwsBuilderConfig {
|
||||
#[serde(skip_serializing)]
|
||||
pub secret_access_key: String,
|
||||
|
||||
pub default_ami_id: String,
|
||||
pub default_ami_name: String,
|
||||
pub default_subnet_id: String,
|
||||
pub default_key_pair_name: String,
|
||||
|
||||
@@ -151,16 +158,18 @@ fn default_instance_type() -> String {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_id, AmiAccounts)
|
||||
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_name, AmiAccounts)
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AmiAccounts {
|
||||
pub name: String,
|
||||
pub ami_id: String,
|
||||
#[serde(default)]
|
||||
pub github: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub docker: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub secrets: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -37,6 +37,11 @@ pub struct Deployment {
|
||||
#[builder(setter(skip))]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "docker_run_args_diff_no_change")]))]
|
||||
pub docker_run_args: DockerRunArgs,
|
||||
@@ -102,6 +107,7 @@ pub struct DeploymentActionState {
|
||||
pub pulling: bool,
|
||||
pub recloning: bool,
|
||||
pub updating: bool,
|
||||
pub renaming: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -100,6 +100,7 @@ pub enum Operation {
|
||||
PruneImagesServer,
|
||||
PruneContainersServer,
|
||||
PruneNetworksServer,
|
||||
RenameServer,
|
||||
|
||||
// build
|
||||
CreateBuild,
|
||||
@@ -117,6 +118,7 @@ pub enum Operation {
|
||||
RemoveContainer,
|
||||
PullDeployment,
|
||||
RecloneDeployment,
|
||||
RenameDeployment,
|
||||
|
||||
// procedure
|
||||
CreateProcedure,
|
||||
|
||||
@@ -115,11 +115,11 @@ impl Default for Server {
|
||||
}
|
||||
|
||||
fn default_cpu_alert() -> f32 {
|
||||
50.0
|
||||
95.0
|
||||
}
|
||||
|
||||
fn default_mem_alert() -> f64 {
|
||||
75.0
|
||||
80.0
|
||||
}
|
||||
|
||||
fn default_disk_alert() -> f64 {
|
||||
|
||||
@@ -60,6 +60,7 @@ impl Busy for DeploymentActionState {
|
||||
|| self.starting
|
||||
|| self.stopping
|
||||
|| self.updating
|
||||
|| self.renaming
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct User {
|
||||
#[diff(attr(#[derive(Debug, Serialize)]))]
|
||||
pub struct ApiSecret {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub hash: String,
|
||||
pub created_at: String,
|
||||
pub expires: Option<String>,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=agent to connect with monitor core
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=max
|
||||
WorkingDirectory=/home/max
|
||||
ExecStart=/bin/bash --login -c 'source /home/max/.bashrc; $HOME/.cargo/bin/periphery --config-path ~/.monitor/periphery.config.toml --home-dir $HOME'
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "monitor_periphery"
|
||||
version = "0.2.5"
|
||||
version = "0.2.10"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor periphery binary | run monitor periphery as system daemon"
|
||||
description = "monitor periphery binary"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[[bin]]
|
||||
@@ -17,12 +17,10 @@ helpers = { package = "monitor_helpers", path = "../lib/helpers" }
|
||||
types = { package = "monitor_types", path = "../lib/types" }
|
||||
run_command = { version = "0.0.5", features = ["async_tokio"] }
|
||||
async_timing_util = "0.1.14"
|
||||
tokio = { version = "1.25", features = ["full"] }
|
||||
# tokio-util = "0.7"
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
axum = { version = "0.6", features = ["ws"] }
|
||||
tower = { version = "0.4", features = ["full"] }
|
||||
futures = "0.3"
|
||||
# futures-util = "0.3.25"
|
||||
dotenv = "0.15"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
@@ -34,3 +32,4 @@ sysinfo = "0.28"
|
||||
toml = "0.7"
|
||||
daemonize = "0.5.0"
|
||||
clap = { version = "4.1", features = ["derive"] }
|
||||
svi = "0.1.3"
|
||||
|
||||
@@ -26,7 +26,14 @@ async fn build_image(
|
||||
tokio::spawn(async move {
|
||||
let logs = match get_docker_token(&build.docker_account, &config) {
|
||||
Ok(docker_token) => {
|
||||
match docker::build(&build, config.repo_dir.clone(), docker_token).await {
|
||||
match docker::build(
|
||||
&build,
|
||||
config.repo_dir.clone(),
|
||||
docker_token,
|
||||
&config.secrets,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(logs) => logs,
|
||||
Err(e) => vec![Log::error("build", format!("{e:#?}"))],
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use helpers::{handle_anyhow_error, to_monitor_name};
|
||||
use helpers::handle_anyhow_error;
|
||||
use serde::Deserialize;
|
||||
use types::{Deployment, Log};
|
||||
|
||||
@@ -21,6 +21,12 @@ struct Container {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RenameContainerBody {
|
||||
curr_name: String,
|
||||
new_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetLogQuery {
|
||||
tail: Option<u64>, // default is 1000 if not passed
|
||||
@@ -67,20 +73,26 @@ pub fn router() -> Router {
|
||||
)
|
||||
.route(
|
||||
"/start",
|
||||
post(|Json(container): Json<Container>| async move {
|
||||
Json(docker::start_container(&to_monitor_name(&container.name)).await)
|
||||
post(|container: Json<Container>| async move {
|
||||
Json(docker::start_container(&container.name).await)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/stop",
|
||||
post(|Json(container): Json<Container>| async move {
|
||||
Json(docker::stop_container(&to_monitor_name(&container.name)).await)
|
||||
post(|container: Json<Container>| async move {
|
||||
Json(docker::stop_container(&container.name).await)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/remove",
|
||||
post(|Json(container): Json<Container>| async move {
|
||||
Json(docker::stop_and_remove_container(&to_monitor_name(&container.name)).await)
|
||||
post(|container: Json<Container>| async move {
|
||||
Json(docker::stop_and_remove_container(&container.name).await)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/rename",
|
||||
post(|body: Json<RenameContainerBody>| async move {
|
||||
Json(docker::rename_container(&body.curr_name, &body.new_name).await)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
@@ -109,7 +121,13 @@ async fn deploy(
|
||||
) -> anyhow::Result<Json<Log>> {
|
||||
let log = match get_docker_token(&deployment.docker_run_args.docker_account, &config) {
|
||||
Ok(docker_token) => tokio::spawn(async move {
|
||||
docker::deploy(&deployment, &docker_token, config.repo_dir.clone()).await
|
||||
docker::deploy(
|
||||
&deployment,
|
||||
&docker_token,
|
||||
config.repo_dir.clone(),
|
||||
&config.secrets,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.context("failed at spawn thread for deploy")?,
|
||||
|
||||
101
periphery/src/api/guard.rs
Normal file
101
periphery/src/api/guard.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::ConnectInfo,
|
||||
http::{Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
Json, RequestExt,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use types::{monitor_timestamp, PeripheryConfig};
|
||||
|
||||
pub async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.passkeys.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let req_passkey = req.headers().get("authorization");
|
||||
if req_passkey.is_none() {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("request was not sent with passkey"),
|
||||
));
|
||||
}
|
||||
let req_passkey = req_passkey
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("failed to get passkey from authorization header as str: {e:?}"),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
if config.passkeys.contains(&req_passkey) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let ConnectInfo(socket_addr) =
|
||||
req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} (bad passkey) | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp(),
|
||||
);
|
||||
Err((StatusCode::UNAUTHORIZED, format!("request passkey invalid")))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn guard_request_by_ip(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.allowed_ips.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let ConnectInfo(socket_addr) = req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
if config.allowed_ips.contains(&ip) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp()
|
||||
);
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("requesting ip {ip} not allowed"),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,4 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::ConnectInfo,
|
||||
http::{Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::get,
|
||||
Json, RequestExt, Router,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use types::{monitor_timestamp, PeripheryConfig};
|
||||
use axum::{middleware, routing::get, Json, Router};
|
||||
|
||||
use crate::{helpers::docker::DockerClient, HomeDirExtension, PeripheryConfigExtension};
|
||||
|
||||
@@ -21,6 +9,7 @@ mod build;
|
||||
mod command;
|
||||
mod container;
|
||||
mod git;
|
||||
mod guard;
|
||||
mod image;
|
||||
mod network;
|
||||
mod stats;
|
||||
@@ -34,6 +23,7 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
get(|sys: StatsExtension| async move { Json(sys.read().unwrap().info.clone()) }),
|
||||
)
|
||||
.route("/accounts/:account_type", get(accounts::get_accounts))
|
||||
.route("/secrets", get(get_available_secrets))
|
||||
.nest("/command", command::router())
|
||||
.nest("/container", container::router())
|
||||
.nest("/network", network::router())
|
||||
@@ -42,8 +32,8 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
.nest("/build", build::router())
|
||||
.nest("/image", image::router())
|
||||
.layer(DockerClient::extension())
|
||||
.layer(middleware::from_fn(guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey))
|
||||
.layer(middleware::from_fn(guard::guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard::guard_request_by_passkey))
|
||||
.layer(StatsClient::extension(
|
||||
config.stats_polling_rate.to_string().parse().unwrap(),
|
||||
))
|
||||
@@ -51,91 +41,7 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
.layer(home_dir)
|
||||
}
|
||||
|
||||
async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.passkeys.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let req_passkey = req.headers().get("authorization");
|
||||
if req_passkey.is_none() {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("request was not sent with passkey"),
|
||||
));
|
||||
}
|
||||
let req_passkey = req_passkey
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("failed to get passkey from authorization header as str: {e:?}"),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
if config.passkeys.contains(&req_passkey) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let ConnectInfo(socket_addr) =
|
||||
req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} (bad passkey) | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp(),
|
||||
);
|
||||
Err((StatusCode::UNAUTHORIZED, format!("request passkey invalid")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn guard_request_by_ip(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.allowed_ips.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let ConnectInfo(socket_addr) = req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
if config.allowed_ips.contains(&ip) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp()
|
||||
);
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("requesting ip {ip} not allowed"),
|
||||
))
|
||||
}
|
||||
async fn get_available_secrets(config: PeripheryConfigExtension) -> Json<Vec<String>> {
|
||||
let vars: Vec<String> = config.secrets.keys().map(|k| k.clone()).collect();
|
||||
Json(vars)
|
||||
}
|
||||
|
||||
@@ -82,23 +82,28 @@ pub fn load() -> (Args, u16, PeripheryConfigExtension, HomeDirExtension) {
|
||||
|
||||
fn print_startup_log(config_paths: Vec<String>, args: &Args, config: &PeripheryConfig) {
|
||||
println!("\nconfig paths: {config_paths:?}");
|
||||
let mut config = config.clone();
|
||||
config.github_accounts = config
|
||||
let mut config_for_print = config.clone();
|
||||
config_for_print.github_accounts = config_for_print
|
||||
.github_accounts
|
||||
.into_iter()
|
||||
.map(|(a, _)| (a, "<SECRET>".to_string()))
|
||||
.collect();
|
||||
config.docker_accounts = config
|
||||
config_for_print.docker_accounts = config_for_print
|
||||
.docker_accounts
|
||||
.into_iter()
|
||||
.map(|(a, _)| (a, "<SECRET>".to_string()))
|
||||
.collect();
|
||||
config.secrets = config
|
||||
config_for_print.secrets = config_for_print
|
||||
.secrets
|
||||
.into_iter()
|
||||
.map(|(s, _)| (s, "<SECRET>".to_string()))
|
||||
.collect();
|
||||
println!("{config:#?}");
|
||||
config_for_print.passkeys = config_for_print
|
||||
.passkeys
|
||||
.into_iter()
|
||||
.map(|_| "<SECRET>".to_string())
|
||||
.collect();
|
||||
println!("{config_for_print:#?}");
|
||||
if args.daemon {
|
||||
println!("daemon mode enabled");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use helpers::to_monitor_name;
|
||||
@@ -20,10 +20,12 @@ pub async fn build(
|
||||
docker_build_args,
|
||||
docker_account,
|
||||
docker_organization,
|
||||
skip_secret_interp,
|
||||
..
|
||||
}: &Build,
|
||||
mut repo_dir: PathBuf,
|
||||
docker_token: Option<String>,
|
||||
secrets: &HashMap<String, String>,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let mut logs = Vec::new();
|
||||
let DockerBuildArgs {
|
||||
@@ -55,8 +57,19 @@ pub async fn build(
|
||||
"cd {} && docker build {build_args}{image_tags} -f {dockerfile_path} .{docker_push}",
|
||||
build_dir.display()
|
||||
);
|
||||
let build_log = run_monitor_command("docker build", command).await;
|
||||
logs.push(build_log);
|
||||
if *skip_secret_interp {
|
||||
let build_log = run_monitor_command("docker build", command).await;
|
||||
logs.push(build_log);
|
||||
} else {
|
||||
let (command, replacers) =
|
||||
svi::interpolate_variables(&command, secrets, svi::Interpolator::DoubleBrackets)
|
||||
.context("failed to interpolate secrets into docker build command")?;
|
||||
let mut build_log = run_monitor_command("docker build", command).await;
|
||||
build_log.command = svi::replace_in_string(&build_log.command, &replacers);
|
||||
build_log.stdout = svi::replace_in_string(&build_log.stdout, &replacers);
|
||||
build_log.stderr = svi::replace_in_string(&build_log.stderr, &replacers);
|
||||
logs.push(build_log);
|
||||
}
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user