Compare commits

...

44 Commits

Author SHA1 Message Date
mbecker20
349fc297ce 0.2.10 add renaming functionality 2023-03-22 20:33:26 +00:00
mbecker20
5ad87c03ed show none when none 2023-03-22 07:16:04 +00:00
mbecker20
d16006f28f improve design 2023-03-22 07:03:28 +00:00
beckerinj
7f0452a5f5 improve pie chart home page 2023-03-22 02:59:29 -04:00
mbecker20
c605b2f6fc implement pie chart summary 2023-03-22 06:41:57 +00:00
beckerinj
6c2d8a8494 unnecessary import 2023-03-21 23:10:01 -07:00
mbecker20
874691f729 add a pie chart component 2023-03-21 09:44:00 +00:00
beckerinj
cdf702e17d orange 2023-03-21 00:52:25 -07:00
mbecker20
25fdb32627 rename deployments 2023-03-19 08:14:54 +00:00
mbecker20
e976ea0a3a improve the behavior 2023-03-17 20:55:37 +00:00
mbecker20
34e6b4fc69 rename server working 2023-03-17 20:40:19 +00:00
mbecker20
a2d77567b3 dont need to 'to_monitor_name' servers 2023-03-15 07:35:54 +00:00
mbecker20
ecb460f9b5 add rename deployment to monitor client 2023-03-14 20:15:27 +00:00
mbecker20
63444b089c rename deployment func 2023-03-12 23:36:20 +00:00
mbecker20
c787984b77 initialize mongo with builder 2023-03-12 22:03:31 +00:00
mbecker20
bf3d03e801 fix problem of repeated query for docker accounts, secrets, etc 2023-03-12 05:07:55 +00:00
mbecker20
bc2e69b975 use resource to load stuff 2023-03-12 03:45:49 +00:00
mbecker20
7b94fcf3da 0.2.9 finish implement secret helpers on frontend 2023-03-12 00:48:18 +00:00
mbecker20
9cf03b8b88 add route to get available secret keys 2023-03-12 00:16:03 +00:00
mbecker20
a288edcf61 0.2.8 implement secret interpolation on builds and deployments 2023-03-11 23:34:17 +00:00
mbecker20
89cc18ad37 update tokio version 2023-03-10 19:27:40 +00:00
mbecker20
ffa3b671e1 change default alerting thresholds 2023-03-09 07:08:38 +00:00
beckerinj
f32eeb413b add label to home sort by 2023-03-08 16:10:23 -05:00
mbecker20
b5a5103cfc move core dockerfile 2023-03-08 18:26:42 +00:00
mbecker20
c5697e59f3 delete sample file 2023-03-08 18:24:15 +00:00
mbecker20
f030667ff4 update image in deployment header as well 2023-03-07 17:41:00 +00:00
mbecker20
e9fef5d97c change get_deployment_deployed_version to 'unknown' if not known 2023-03-07 17:39:59 +00:00
beckerinj
f5818ac7ea actually return image 2023-03-07 12:37:45 -05:00
mbecker20
c85ab4110d show derived image is container.image is sha256: 2023-03-07 16:30:59 +00:00
mbecker20
9690ea35b8 make description text area larger 2023-03-07 08:44:31 +00:00
mbecker20
6300c8011b fix modify global user permissions operator - make operator the admin, instead of the target 2023-03-06 17:09:12 +00:00
mbecker20
97f582b381 customizable page title 2023-03-06 02:07:08 +00:00
mbecker20
5135a9c228 show server name under deployment on admin user manage page 2023-03-06 01:46:40 +00:00
mbecker20
b7d1212a82 make resources links in account page 2023-03-05 21:50:15 +00:00
mbecker20
7d9d0a9fc4 add view of resources you can access on account page 2023-03-05 21:42:11 +00:00
beckerinj
ed9aef4321 add resources to account page 2023-03-05 16:33:40 -05:00
mbecker20
0aa638bdf4 only do daily update if servers not empty 2023-03-05 20:19:06 +00:00
mbecker20
0ec39d793d one page to view all permissions for user 2023-03-05 09:24:59 +00:00
mbecker20
5579ba869c v0.2.7 remove passkeys from periphery startup log 2023-03-03 17:27:42 +00:00
mbecker20
210940038c hide passkeys on periphery startup config log 2023-03-03 17:24:28 +00:00
mbecker20
98a1a60362 /home/ubuntu/example 2023-03-03 08:15:17 +00:00
mbecker20
86cf9116ba update builds and deployments docs with link to file paths doc 2023-03-03 08:09:41 +00:00
mbecker20
8b2defe0d9 add doc about file paths 2023-03-03 07:58:09 +00:00
mbecker20
50b14b3ce5 0.2.6 store ami name instead of ami_id (because the id has to change sometimes) 2023-03-03 07:11:55 +00:00
103 changed files with 2510 additions and 1022 deletions

121
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@
use clap::{arg, Arg, Command};
mod helpers;
mod types;
use helpers::*;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 [];
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;

View File

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

View 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();
}
}

View File

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

View File

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

View File

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

View File

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

View 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>
);
};

View File

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

View File

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

View File

@@ -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(() => {

View File

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

View File

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

View File

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

View 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",
};

View File

@@ -314,6 +314,10 @@ svg {
flex-wrap: wrap;
}
.dimmed {
opacity: 0.7;
}
// .hoverable {
// transition: all 250ms ease-in-out;
// }

View File

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

View File

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

View File

@@ -29,6 +29,10 @@ export interface CopyDeploymentBody {
server_id: string;
}
export interface RenameDeploymentBody {
new_name: string;
}
export interface GetContainerLogQuery {
tail?: number;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,6 +60,7 @@ impl Busy for DeploymentActionState {
|| self.starting
|| self.stopping
|| self.updating
|| self.renaming
}
}

View File

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

View File

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

View File

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

View File

@@ -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:#?}"))],
}

View File

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

View File

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

View File

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

View File

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