mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-15 05:04:58 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b94fcf3da | ||
|
|
9cf03b8b88 | ||
|
|
a288edcf61 | ||
|
|
89cc18ad37 | ||
|
|
ffa3b671e1 | ||
|
|
f32eeb413b | ||
|
|
b5a5103cfc | ||
|
|
c5697e59f3 | ||
|
|
f030667ff4 | ||
|
|
e9fef5d97c | ||
|
|
f5818ac7ea | ||
|
|
c85ab4110d | ||
|
|
9690ea35b8 | ||
|
|
6300c8011b | ||
|
|
97f582b381 | ||
|
|
5135a9c228 | ||
|
|
b7d1212a82 | ||
|
|
7d9d0a9fc4 | ||
|
|
ed9aef4321 | ||
|
|
0aa638bdf4 | ||
|
|
0ec39d793d | ||
|
|
5579ba869c | ||
|
|
210940038c | ||
|
|
98a1a60362 | ||
|
|
86cf9116ba | ||
|
|
8b2defe0d9 | ||
|
|
50b14b3ce5 |
53
Cargo.lock
generated
53
Cargo.lock
generated
@@ -734,7 +734,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "core"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -753,7 +753,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"jwt",
|
||||
"monitor_helpers",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.9",
|
||||
"mungos",
|
||||
"periphery_client",
|
||||
"serde",
|
||||
@@ -987,10 +987,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
|
||||
|
||||
[[package]]
|
||||
name = "db_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.9",
|
||||
"mungos",
|
||||
]
|
||||
|
||||
@@ -1837,11 +1837,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_cli"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async_timing_util",
|
||||
"clap",
|
||||
"colored",
|
||||
"monitor_types 0.2.9",
|
||||
"rand",
|
||||
"run_command",
|
||||
"serde",
|
||||
@@ -1853,12 +1854,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"envy",
|
||||
"futures-util",
|
||||
"monitor_types 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"monitor_types 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -1870,11 +1871,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_helpers"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.9",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1883,7 +1884,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_periphery"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -1895,11 +1896,12 @@ dependencies = [
|
||||
"envy",
|
||||
"futures",
|
||||
"monitor_helpers",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.9",
|
||||
"run_command",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"svi",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"toml",
|
||||
@@ -1908,7 +1910,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_types"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bollard",
|
||||
@@ -1925,9 +1927,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_types"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de46e03ba424cb9f70a57a5dd38b81399cca131cdb2edf8bf7d03f829e02c140"
|
||||
checksum = "7de716e157711aac3646ae9faddf6d48cefc0d3e163cdf1b5a72bb034a353fef"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bollard",
|
||||
@@ -1944,9 +1946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mungos"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fc5132f76fafd19d773c68520ab427659a7b17484f2b4705f323b60f84e9d6c"
|
||||
checksum = "588d78564faa32532f258d8e9080035b36b10f65289d3bbb6bdbbcccf5cbd3cd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -2183,11 +2185,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
"monitor_types 0.2.5",
|
||||
"monitor_types 0.2.9",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2861,6 +2863,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 +3022,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 +3037,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.42.0",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM rust:latest as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY ./periphery ./periphery
|
||||
|
||||
COPY ./lib/types ./lib/types
|
||||
COPY ./lib/helpers ./lib/helpers
|
||||
|
||||
RUN cd periphery && cargo build --release
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
ARG DEPS_INSTALLER
|
||||
|
||||
COPY ./${DEPS_INSTALLER}.sh ./
|
||||
RUN sh ./${DEPS_INSTALLER}.sh
|
||||
|
||||
COPY --from=builder /builder/periphery/target/release/periphery /usr/local/bin/periphery
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD "periphery"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_cli"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor cli | tools to setup monitor system"
|
||||
@@ -13,6 +13,7 @@ path = "src/main.rs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
monitor_types = { path = "../lib/types" }
|
||||
clap = "4.0"
|
||||
async_timing_util = "0.1.14"
|
||||
rand = "0.8"
|
||||
|
||||
@@ -7,15 +7,13 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use async_timing_util::Timelength;
|
||||
use clap::ArgMatches;
|
||||
use colored::Colorize;
|
||||
use monitor_types::{CoreConfig, MongoConfig, PeripheryConfig, RestartMode, Timelength};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use run_command::run_command_pipe_to_terminal;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::types::{CoreConfig, MongoConfig, PeripheryConfig, RestartMode};
|
||||
|
||||
const CORE_IMAGE_NAME: &str = "mbecker2020/monitor_core";
|
||||
const PERIPHERY_IMAGE_NAME: &str = "mbecker2020/monitor_periphery";
|
||||
const PERIPHERY_CRATE: &str = "monitor_periphery";
|
||||
@@ -64,6 +62,7 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
|
||||
.map(|p| p.to_owned());
|
||||
|
||||
let config = CoreConfig {
|
||||
title: String::from("monitor"),
|
||||
host,
|
||||
port,
|
||||
jwt_valid_for,
|
||||
@@ -321,7 +320,9 @@ pub fn gen_periphery_config(sub_matches: &ArgMatches) {
|
||||
.map(|p| p.as_str())
|
||||
.unwrap_or("~/.monitor/repos")
|
||||
.to_string()
|
||||
.replace("~", env::var("HOME").unwrap().as_str());
|
||||
.replace("~", env::var("HOME").unwrap().as_str())
|
||||
.parse()
|
||||
.expect("failed to parse --repo_dir as path");
|
||||
|
||||
let config = PeripheryConfig {
|
||||
port,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use clap::{arg, Arg, Command};
|
||||
|
||||
mod helpers;
|
||||
mod types;
|
||||
|
||||
use helpers::*;
|
||||
|
||||
|
||||
207
cli/src/types.rs
207
cli/src/types.rs
@@ -1,207 +0,0 @@
|
||||
use std::{collections::HashMap, net::IpAddr};
|
||||
|
||||
use async_timing_util::Timelength;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use strum_macros::{Display, EnumString};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CoreConfig {
|
||||
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
|
||||
pub host: String,
|
||||
|
||||
// port the core web server runs on
|
||||
#[serde(default = "default_core_port")]
|
||||
pub port: u16,
|
||||
|
||||
// daily utc offset in hours to run daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight.
|
||||
#[serde(default)]
|
||||
pub daily_offset_hours: u8,
|
||||
|
||||
// number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily
|
||||
#[serde(default)]
|
||||
pub keep_stats_for_days: u64, // 0 means never prune
|
||||
|
||||
pub jwt_secret: String,
|
||||
#[serde(default = "default_jwt_valid_for")]
|
||||
pub jwt_valid_for: Timelength,
|
||||
|
||||
// interval at which to collect server stats and alert for out of bounds
|
||||
pub monitoring_interval: Timelength,
|
||||
|
||||
// used to verify validity from github webhooks
|
||||
pub github_webhook_secret: String,
|
||||
|
||||
// used to form the frontend listener url, if None will use 'host'.
|
||||
pub github_webhook_base_url: Option<String>,
|
||||
|
||||
// sent in auth header with req to periphery
|
||||
pub passkey: String,
|
||||
|
||||
// integration with slack app
|
||||
pub slack_url: Option<String>,
|
||||
|
||||
// enable login with local auth
|
||||
pub local_auth: bool,
|
||||
|
||||
// allowed docker orgs used with monitor. first in this list will be default for build
|
||||
#[serde(default)]
|
||||
pub docker_organizations: Vec<String>,
|
||||
|
||||
pub mongo: MongoConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub github_oauth: OauthCredentials,
|
||||
|
||||
#[serde(default)]
|
||||
pub google_oauth: OauthCredentials,
|
||||
|
||||
#[serde(default)]
|
||||
pub aws: AwsBuilderConfig,
|
||||
}
|
||||
|
||||
fn default_core_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
|
||||
fn default_jwt_valid_for() -> Timelength {
|
||||
Timelength::OneWeek
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct OauthCredentials {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MongoConfig {
|
||||
pub uri: String,
|
||||
#[serde(default = "default_core_mongo_app_name")]
|
||||
pub app_name: String,
|
||||
#[serde(default = "default_core_mongo_db_name")]
|
||||
pub db_name: String,
|
||||
}
|
||||
|
||||
fn default_core_mongo_app_name() -> String {
|
||||
"monitor_core".to_string()
|
||||
}
|
||||
|
||||
fn default_core_mongo_db_name() -> String {
|
||||
"monitor".to_string()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AwsBuilderConfig {
|
||||
#[serde(skip_serializing)]
|
||||
pub access_key_id: String,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub secret_access_key: String,
|
||||
|
||||
pub default_ami_id: String,
|
||||
pub default_subnet_id: String,
|
||||
pub default_key_pair_name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub available_ami_accounts: AvailableAmiAccounts,
|
||||
|
||||
#[serde(default = "default_aws_region")]
|
||||
pub default_region: String,
|
||||
|
||||
#[serde(default = "default_volume_gb")]
|
||||
pub default_volume_gb: i32,
|
||||
|
||||
#[serde(default = "default_instance_type")]
|
||||
pub default_instance_type: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub default_security_group_ids: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub default_assign_public_ip: bool,
|
||||
}
|
||||
|
||||
fn default_aws_region() -> String {
|
||||
String::from("us-east-1")
|
||||
}
|
||||
|
||||
fn default_volume_gb() -> i32 {
|
||||
8
|
||||
}
|
||||
|
||||
fn default_instance_type() -> String {
|
||||
String::from("m5.2xlarge")
|
||||
}
|
||||
|
||||
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_id, AmiAccounts)
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AmiAccounts {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub github: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub docker: Vec<String>,
|
||||
}
|
||||
|
||||
pub type GithubUsername = String;
|
||||
pub type GithubToken = String;
|
||||
pub type GithubAccounts = HashMap<GithubUsername, GithubToken>;
|
||||
|
||||
pub type DockerUsername = String;
|
||||
pub type DockerToken = String;
|
||||
pub type DockerAccounts = HashMap<DockerUsername, DockerToken>;
|
||||
|
||||
pub type SecretsMap = HashMap<String, String>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PeripheryConfig {
|
||||
#[serde(default = "default_periphery_port")]
|
||||
pub port: u16,
|
||||
#[serde(default = "default_repo_dir")]
|
||||
pub repo_dir: String,
|
||||
#[serde(default = "default_stats_refresh_interval")]
|
||||
pub stats_polling_rate: Timelength,
|
||||
#[serde(default)]
|
||||
pub allowed_ips: Vec<IpAddr>,
|
||||
#[serde(default)]
|
||||
pub passkeys: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub secrets: SecretsMap,
|
||||
#[serde(default)]
|
||||
pub github_accounts: GithubAccounts,
|
||||
#[serde(default)]
|
||||
pub docker_accounts: DockerAccounts,
|
||||
}
|
||||
|
||||
fn default_periphery_port() -> u16 {
|
||||
8000
|
||||
}
|
||||
|
||||
fn default_repo_dir() -> String {
|
||||
"/repos".to_string()
|
||||
}
|
||||
|
||||
fn default_stats_refresh_interval() -> Timelength {
|
||||
Timelength::FiveSeconds
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy)]
|
||||
pub enum RestartMode {
|
||||
#[serde(rename = "no")]
|
||||
#[strum(serialize = "no")]
|
||||
NoRestart,
|
||||
#[serde(rename = "on-failure")]
|
||||
#[strum(serialize = "on-failure")]
|
||||
OnFailure,
|
||||
#[serde(rename = "always")]
|
||||
#[strum(serialize = "always")]
|
||||
Always,
|
||||
#[serde(rename = "unless-stopped")]
|
||||
#[strum(serialize = "unless-stopped")]
|
||||
UnlessStopped,
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
# optional. this will be the document title on the web page (shows up as text in the browser tab). default is 'monitor'
|
||||
title = "monitor"
|
||||
|
||||
# this should be the url used to access monitor in browser, potentially behind DNS, eg https://monitor.mogh.tech or http://12.34.56.78:9000
|
||||
host = "https://monitor.mogh.tech"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "core"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
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.8"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -7,7 +7,8 @@ use mungos::{doc, to_bson};
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
Build, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget, Version, AwsBuilderBuildConfig,
|
||||
AwsBuilderBuildConfig, Build, Log, Operation, PermissionLevel, Update, UpdateStatus,
|
||||
UpdateTarget, Version,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -442,10 +443,17 @@ impl State {
|
||||
self.config.aws.secret_access_key.clone(),
|
||||
)
|
||||
.await;
|
||||
let ami_id = aws_config
|
||||
.ami_id
|
||||
let ami_name = aws_config
|
||||
.ami_name
|
||||
.as_ref()
|
||||
.unwrap_or(&self.config.aws.default_ami_id);
|
||||
.unwrap_or(&self.config.aws.default_ami_name);
|
||||
let ami_id = &self
|
||||
.config
|
||||
.aws
|
||||
.available_ami_accounts
|
||||
.get(ami_name)
|
||||
.ok_or(anyhow!("no ami id associated with ami name {ami_name}"))?
|
||||
.ami_id;
|
||||
let instance_type = aws_config
|
||||
.instance_type
|
||||
.as_ref()
|
||||
|
||||
@@ -489,10 +489,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 +503,7 @@ impl State {
|
||||
if let Some(version) = split.get(1) {
|
||||
Ok(version.to_string())
|
||||
} else {
|
||||
Ok("latest".to_string())
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_request, JwtExtension, RequestUser, RequestUserExtension},
|
||||
response,
|
||||
state::{State, StateExtension},
|
||||
ResponseResult,
|
||||
};
|
||||
|
||||
pub mod build;
|
||||
@@ -35,22 +37,33 @@ struct UpdateDescriptionBody {
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/user",
|
||||
get(|jwt, req| async { get_user(jwt, req).await.map_err(handle_anyhow_error) }),
|
||||
"/title",
|
||||
get(|state: StateExtension| async move { state.config.title.clone() }),
|
||||
)
|
||||
.route("/user", get(get_request_user))
|
||||
.nest("/listener", github_listener::router())
|
||||
.nest(
|
||||
"/",
|
||||
Router::new()
|
||||
.route("/user/:id", get(get_user_at_id))
|
||||
.route(
|
||||
"/username/:id",
|
||||
get(|state, user_id| async {
|
||||
get_username(state, user_id)
|
||||
get(|state: StateExtension, Path(UserId { id })| async move {
|
||||
let user = state
|
||||
.db
|
||||
.get_user(&id)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
.context("failed to find user at id")
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(user.username))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
@@ -90,8 +103,11 @@ pub fn router() -> Router {
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::Result<Json<User>> {
|
||||
let mut user = jwt.authenticate(&req).await?;
|
||||
async fn get_request_user(
|
||||
Extension(jwt): JwtExtension,
|
||||
req: Request<Body>,
|
||||
) -> ResponseResult<Json<User>> {
|
||||
let mut user = jwt.authenticate(&req).await.map_err(handle_anyhow_error)?;
|
||||
user.password = None;
|
||||
for secret in &mut user.secrets {
|
||||
secret.hash = String::new();
|
||||
@@ -99,23 +115,10 @@ async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::R
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
async fn get_username(
|
||||
state: StateExtension,
|
||||
Path(UserId { id }): Path<UserId>,
|
||||
) -> anyhow::Result<String> {
|
||||
let user = state.db.get_user(&id).await?;
|
||||
Ok(user.username)
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
) -> Result<Json<Vec<User>>, (StatusCode, String)> {
|
||||
) -> ResponseResult<Json<Vec<User>>> {
|
||||
if user.is_admin {
|
||||
let users = state
|
||||
.db
|
||||
@@ -137,8 +140,33 @@ async fn get_users(
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_at_id(
|
||||
state: StateExtension,
|
||||
Path(UserId { id }): Path<UserId>,
|
||||
user: RequestUserExtension,
|
||||
) -> ResponseResult<Json<User>> {
|
||||
if user.is_admin {
|
||||
let mut user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&id)
|
||||
.await
|
||||
.context("failed at query to get user from mongo")
|
||||
.map_err(handle_anyhow_error)?
|
||||
.ok_or(anyhow!(""))
|
||||
.map_err(handle_anyhow_error)?;
|
||||
user.password = None;
|
||||
for secret in &mut user.secrets {
|
||||
secret.hash = String::new();
|
||||
}
|
||||
Ok(Json(user))
|
||||
} else {
|
||||
Err((StatusCode::UNAUTHORIZED, "user is not admin".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// need to run requested actions in here to prevent them being dropped mid action when user disconnects prematurely
|
||||
pub async fn spawn_request_action<A>(action: A) -> Result<A::Output, (StatusCode, String)>
|
||||
pub async fn spawn_request_action<A>(action: A) -> ResponseResult<A::Output>
|
||||
where
|
||||
A: Future + Send + 'static,
|
||||
A::Output: Send + 'static,
|
||||
|
||||
@@ -284,7 +284,7 @@ async fn modify_user_create_server_permissions(
|
||||
"user does not have permissions for this action (not admin)"
|
||||
));
|
||||
}
|
||||
let user = state
|
||||
let target_user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&user_id)
|
||||
@@ -312,7 +312,7 @@ async fn modify_user_create_server_permissions(
|
||||
"modify user create server permissions",
|
||||
format!(
|
||||
"{update_type} create server permissions for {} (id: {})",
|
||||
user.username, user.id
|
||||
target_user.username, target_user.id
|
||||
),
|
||||
)],
|
||||
start_ts: ts.clone(),
|
||||
@@ -339,7 +339,7 @@ async fn modify_user_create_build_permissions(
|
||||
"user does not have permissions for this action (not admin)"
|
||||
));
|
||||
}
|
||||
let user = state
|
||||
let target_user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&user_id)
|
||||
@@ -367,7 +367,7 @@ async fn modify_user_create_build_permissions(
|
||||
"modify user create build permissions",
|
||||
format!(
|
||||
"{update_type} create build permissions for {} (id: {})",
|
||||
user.username, user.id
|
||||
target_user.username, target_user.id
|
||||
),
|
||||
)],
|
||||
start_ts: ts.clone(),
|
||||
|
||||
@@ -339,6 +339,20 @@ pub fn router() -> Router {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/secrets",
|
||||
get(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(ServerId { id })| async move {
|
||||
let vars = state
|
||||
.get_available_secrets(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(vars))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/action_state",
|
||||
get(
|
||||
@@ -628,6 +642,18 @@ impl State {
|
||||
Ok(docker_accounts)
|
||||
}
|
||||
|
||||
async fn get_available_secrets(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let server = self
|
||||
.get_server_check_permissions(id, user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let vars = self.periphery.get_available_secrets(&server).await?;
|
||||
Ok(vars)
|
||||
}
|
||||
|
||||
async fn get_server_action_states(
|
||||
&self,
|
||||
id: String,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use ::helpers::get_socket_addr;
|
||||
use auth::JwtClient;
|
||||
use axum::Router;
|
||||
use axum::{http::StatusCode, Router};
|
||||
use state::State;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
@@ -16,8 +16,10 @@ mod monitoring;
|
||||
mod state;
|
||||
mod ws;
|
||||
|
||||
type ResponseResult<T> = Result<T, (StatusCode, String)>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (config, spa_router) = config::load();
|
||||
|
||||
println!("starting monitor core on port {}...", config.port);
|
||||
@@ -40,6 +42,7 @@ async fn main() {
|
||||
|
||||
axum::Server::bind(&get_socket_addr(config.port))
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.expect("monitor core axum server crashed");
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -412,8 +412,12 @@ impl State {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let servers = servers.unwrap();
|
||||
if servers.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut blocks = vec![Block::header("INFO | daily update"), Block::divider()];
|
||||
for (server, stats) in servers.unwrap() {
|
||||
for (server, stats) in servers {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" | {region}")
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{ws::Message as AxumMessage, Path, Query, WebSocketUpgrade},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
@@ -13,7 +12,7 @@ use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use types::{traits::Permissioned, PermissionLevel, SystemStatsQuery};
|
||||
|
||||
use crate::{auth::JwtExtension, state::StateExtension};
|
||||
use crate::{auth::JwtExtension, state::StateExtension, ResponseResult};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerId {
|
||||
@@ -26,7 +25,7 @@ pub async fn ws_handler(
|
||||
path: Path<ServerId>,
|
||||
query: Query<SystemStatsQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
) -> ResponseResult<impl IntoResponse> {
|
||||
let server = state
|
||||
.db
|
||||
.get_server(&path.id)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Monitor builds docker images by cloning the source repository from Github, running ```docker build```, and pushing the resulting image to docker hub. Any repo containing a 'Dockerfile' is buildable using this method.
|
||||
|
||||
Build configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](https://github.com/mbecker20/monitor/blob/main/docs/paths.md).
|
||||
|
||||
## repo configuration
|
||||
To specify the github repo to build, just give it the name of the repo and the branch under *repo config*. The name is given like ```mbecker20/monitor```, it includes the username / organization that owns the repo.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Monitor can deploy any docker images that it can access with the configured docker accounts. It works by parsing the deployment configuration into a ```docker run``` command. The configuration is stored on MongoDB, and records of all actions (update config, deploy, stop, etc.) are stored as well.
|
||||
|
||||
Deployment configuration involves passing file / directory paths, for more details about passing file paths, see the [file paths doc](https://github.com/mbecker20/monitor/blob/main/docs/paths.md).
|
||||
|
||||
## configuring the image
|
||||
|
||||
There are two options to configure the deployed image.
|
||||
|
||||
38
docs/paths.md
Normal file
38
docs/paths.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# File Paths
|
||||
|
||||
When working with monitor, you might have to configure file or directory paths.
|
||||
|
||||
## Relative Paths
|
||||
|
||||
Where possible, it is better to use relative file paths. Using relative file paths removes the connection between the process being run and the particular server it runs one, making it easier to move things between servers.
|
||||
|
||||
Where you see relative paths:
|
||||
|
||||
- setting the build directory and path of the Dockerfile
|
||||
- setting a pre build command path
|
||||
- configuring a frontend mount (used for web apps)
|
||||
|
||||
For all of the above, the path can be given relative to the root of the configured repo
|
||||
|
||||
The one exception is the Dockerfile path, which is given relative to the build directory (This is done by Docker itself, and this pattern matches usage of the Docker CLI).
|
||||
|
||||
There are 3 kinds of paths to pass:
|
||||
|
||||
1. to specify the root of the repo, use ```.``` as the path
|
||||
2. to specify a folder in the repo, pass it with **no** preceding ```/```. For example, ```example_folder``` or ```folder1/folder2```
|
||||
3. to specify an absolute path on the servers filesystem, use a preceding slash, eg. ```/home/ubuntu/example```. This way should only be used if absolutely necessary.
|
||||
|
||||
### Implementation
|
||||
|
||||
relative file paths are joined with the path of the repo on the system using a Rust [PathBuf](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push).
|
||||
|
||||
## Docker Volume Paths
|
||||
|
||||
These are passed directly to the Docker CLI using ```--volume /path/on/system:/path/in/container```. So for these, the same rules apply as when using Docker on the command line. Paths here should be given as absolute, don't use ```~``` or even ```$HOME```.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.6.0",
|
||||
"@tanstack/solid-query": "^4.26.0",
|
||||
"axios": "^1.2.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"lightweight-charts": "^3.8.0",
|
||||
|
||||
@@ -8,8 +8,9 @@ const Deployment = lazy(() => import("./components/deployment/Deployment"));
|
||||
const Server = lazy(() => import("./components/server/Server"));
|
||||
const Build = lazy(() => import("./components/build/Build"));
|
||||
const Users = lazy(() => import("./components/users/Users"));
|
||||
const User = lazy(() => import("./components/users/User"));
|
||||
const Stats = lazy(() => import("./components/stats/Stats"));
|
||||
const Account = lazy(() => import("./components/Account"));
|
||||
const Account = lazy(() => import("./components/account/Account"));
|
||||
|
||||
const App: Component = () => {
|
||||
const { user } = useUser();
|
||||
@@ -25,6 +26,7 @@ const App: Component = () => {
|
||||
<Route path="/account" component={Account} />
|
||||
<Show when={user().admin}>
|
||||
<Route path="/users" component={Users} />
|
||||
<Route path="/user/:id" component={User} />
|
||||
</Show>
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { client, pushNotification } from "..";
|
||||
import { useAppState } from "../state/StateProvider";
|
||||
import { UpdateTarget } from "../types";
|
||||
import { useToggle } from "../util/hooks";
|
||||
import ConfirmButton from "./shared/ConfirmButton";
|
||||
import Flex from "./shared/layout/Flex";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import Loading from "./shared/loading/Loading";
|
||||
import CenterMenu from "./shared/menu/CenterMenu";
|
||||
@@ -19,7 +17,8 @@ const Description: Component<{
|
||||
const [show, toggleShow] = useToggle();
|
||||
const description = () => {
|
||||
if (p.description) {
|
||||
return p.description;
|
||||
let [description] = p.description.split("\n");
|
||||
return description;
|
||||
} else {
|
||||
return "add a description";
|
||||
}
|
||||
@@ -110,8 +109,7 @@ const DescriptionMenu: Component<{
|
||||
placeholder="add a description"
|
||||
value={desc()}
|
||||
onEdit={setDesc}
|
||||
onEnter={update_description}
|
||||
style={{ width: "700px", "max-width": "90vw", padding: "1rem" }}
|
||||
style={{ width: "900px", "max-width": "90vw", height: "70vh", padding: "1rem" }}
|
||||
disabled={!p.userCanUpdate}
|
||||
/>
|
||||
<Show when={p.userCanUpdate}>
|
||||
|
||||
41
frontend/src/components/account/Account.tsx
Normal file
41
frontend/src/components/account/Account.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { readableMonitorTimestamp, readableUserType } from "../../util/helpers";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Resources from "./Resources";
|
||||
import Secrets from "./Secrets";
|
||||
|
||||
const Account: Component<{}> = (p) => {
|
||||
const { user, username } = useUser();
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>{username()}</h1>
|
||||
<Flex>
|
||||
<Show when={user().admin}>
|
||||
<div class="dimmed">admin</div>
|
||||
</Show>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">type:</div>
|
||||
<div>{user() ? readableUserType(user()!) : "unknown"}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">created:</div>
|
||||
<div>{readableMonitorTimestamp(user().created_at!)}</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Secrets />
|
||||
<Show when={!user().admin}>
|
||||
<Resources />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Account;
|
||||
124
frontend/src/components/account/Resources.tsx
Normal file
124
frontend/src/components/account/Resources.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, createMemo, createSignal, For } from "solid-js";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../types";
|
||||
import { getId } from "../../util/helpers";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
|
||||
const Resources: Component<{}> = (p) => {
|
||||
const { user, user_id } = useUser();
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { builds, deployments, servers } = useAppState();
|
||||
const [search, setSearch] = createSignal("");
|
||||
const _servers = createMemo(() => {
|
||||
return servers.filterArray((s) => {
|
||||
if (!s.server.name.includes(search())) return false;
|
||||
const p = s.server.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
const _deployments = createMemo(() => {
|
||||
return deployments.filterArray((d) => {
|
||||
if (!d.deployment.name.includes(search())) return false;
|
||||
const p = d.deployment.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
const _builds = createMemo(() => {
|
||||
return builds.filterArray((b) => {
|
||||
if (!b.name.includes(search())) return false;
|
||||
const p = b.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>servers</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_servers()}>
|
||||
{(item) => (
|
||||
<A
|
||||
class="card light shadow"
|
||||
href={`/server/${getId(item.server)}`}
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.server.name}</h2>
|
||||
<div class="dimmed">{item.server.region || "unknown region"}</div>
|
||||
</Grid>
|
||||
<div>{item.server.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>deployments</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_deployments()}>
|
||||
{(item) => (
|
||||
<A
|
||||
href={`/deployment/${getId(item.deployment)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.deployment.name}</h2>
|
||||
<div class="dimmed">
|
||||
{servers.get(item.deployment.server_id)?.server.name ||
|
||||
"unknown"}
|
||||
</div>
|
||||
</Grid>
|
||||
<div>{item.deployment.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>builds</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_builds()}>
|
||||
{(item) => (
|
||||
<A
|
||||
href={`/build/${getId(item)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<h2>{item.name}</h2>
|
||||
<div>{item.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Resources;
|
||||
@@ -1,79 +1,81 @@
|
||||
import { Component, For, Match, Show, Switch } from "solid-js";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { client, pushNotification } from "..";
|
||||
import { useUser } from "../state/UserProvider";
|
||||
import { copyToClipboard, readableMonitorTimestamp } from "../util/helpers";
|
||||
import { useToggle } from "../util/hooks";
|
||||
import ConfirmButton from "./shared/ConfirmButton";
|
||||
import Icon from "./shared/Icon";
|
||||
import Input from "./shared/Input";
|
||||
import Flex from "./shared/layout/Flex";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import Loading from "./shared/loading/Loading";
|
||||
import CenterMenu from "./shared/menu/CenterMenu";
|
||||
import Selector from "./shared/menu/Selector";
|
||||
import { client, pushNotification } from "../..";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { copyToClipboard, readableMonitorTimestamp } from "../../util/helpers";
|
||||
import { useToggle } from "../../util/hooks";
|
||||
import ConfirmButton from "../shared/ConfirmButton";
|
||||
import Icon from "../shared/Icon";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import CenterMenu from "../shared/menu/CenterMenu";
|
||||
import Selector from "../shared/menu/Selector";
|
||||
|
||||
const Account: Component<{}> = (p) => {
|
||||
const { user, reloadUser } = useUser();
|
||||
const Secrets: Component<{}> = (p) => {
|
||||
const { user, reloadUser } = useUser();
|
||||
const [showCreate, toggleShowCreate] = useToggle();
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Flex justifyContent="space-between">
|
||||
<h1>api secrets</h1>
|
||||
<CenterMenu
|
||||
show={showCreate}
|
||||
toggleShow={toggleShowCreate}
|
||||
targetClass="green"
|
||||
title="create secret"
|
||||
target={<Icon type="plus" />}
|
||||
content={() => <CreateNewMenu />}
|
||||
position="center"
|
||||
/>
|
||||
</Flex>
|
||||
<For each={user().secrets}>
|
||||
{(secret) => (
|
||||
<Flex
|
||||
class="card dark shadow wrap"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{secret.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>created:</div>
|
||||
<div>{readableMonitorTimestamp(secret.created_at)}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>expires:</div>
|
||||
<div>{secret.expires ? readableMonitorTimestamp(secret.expires) : "never"}</div>
|
||||
</Flex>
|
||||
<ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() =>
|
||||
client.delete_api_secret(secret.name).then(reloadUser)
|
||||
}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
return (
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Flex justifyContent="space-between">
|
||||
<h1>api secrets</h1>
|
||||
<CenterMenu
|
||||
show={showCreate}
|
||||
toggleShow={toggleShowCreate}
|
||||
targetClass="green"
|
||||
title="create secret"
|
||||
target={<Icon type="plus" />}
|
||||
content={() => <CreateNewSecretMenu />}
|
||||
position="center"
|
||||
/>
|
||||
</Flex>
|
||||
<For each={user().secrets}>
|
||||
{(secret) => (
|
||||
<Flex
|
||||
class="card light shadow wrap"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{secret.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>created:</div>
|
||||
<div>{readableMonitorTimestamp(secret.created_at)}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>expires:</div>
|
||||
<div>
|
||||
{secret.expires
|
||||
? readableMonitorTimestamp(secret.expires)
|
||||
: "never"}
|
||||
</div>
|
||||
</Flex>
|
||||
<ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() =>
|
||||
client.delete_api_secret(secret.name).then(reloadUser)
|
||||
}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Account;
|
||||
export default Secrets;
|
||||
|
||||
const EXPIRE_LENGTHS = ["30 days", "90 days", "1 year", "never"] as const;
|
||||
type ExpireLength = typeof EXPIRE_LENGTHS[number];
|
||||
|
||||
const CreateNewMenu = () => {
|
||||
const CreateNewSecretMenu = () => {
|
||||
const { reloadUser } = useUser();
|
||||
const [info, setInfo] = createStore<{
|
||||
name: string;
|
||||
@@ -167,4 +169,4 @@ function createExpires(length: ExpireLength) {
|
||||
const add_days = length === "30 days" ? 30 : length === "90 days" ? 90 : 365;
|
||||
const add_ms = add_days * 24 * 60 * 60 * 1000;
|
||||
return new Date(Date.now() + add_ms).toISOString();
|
||||
}
|
||||
}
|
||||
@@ -22,22 +22,9 @@ const AwsBuilderConfig: Component<{}> = (p) => {
|
||||
const Ami: Component = () => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const { build, setBuild, userCanUpdate } = useConfig();
|
||||
const default_ami_id = () => aws_builder_config()?.default_ami_id;
|
||||
const get_ami_id = () => {
|
||||
if (build.aws_config?.ami_id) {
|
||||
return build.aws_config.ami_id;
|
||||
} else {
|
||||
return default_ami_id() || "unknown";
|
||||
}
|
||||
};
|
||||
const get_ami_name = (ami_id: string) => {
|
||||
if (aws_builder_config() === undefined || ami_id === "unknown")
|
||||
return "unknown";
|
||||
return (
|
||||
aws_builder_config()!.available_ami_accounts![ami_id]?.name || "unknown"
|
||||
);
|
||||
};
|
||||
const ami_ids = () => {
|
||||
const default_ami_name = () => aws_builder_config()?.default_ami_name;
|
||||
const get_ami_name = () => build.aws_config?.ami_name || aws_builder_config()?.default_ami_name || "unknown";
|
||||
const ami_names = () => {
|
||||
if (aws_builder_config() === undefined) return [];
|
||||
return Object.keys(aws_builder_config()!.available_ami_accounts!);
|
||||
};
|
||||
@@ -50,16 +37,16 @@ const Ami: Component = () => {
|
||||
<h1>ami</h1>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={get_ami_id()}
|
||||
items={ami_ids()}
|
||||
onSelect={(ami_id) => {
|
||||
if (ami_id === default_ami_id()) {
|
||||
setBuild("aws_config", "ami_id", undefined);
|
||||
selected={get_ami_name()}
|
||||
items={ami_names()}
|
||||
onSelect={(ami_name) => {
|
||||
if (ami_name === default_ami_name()) {
|
||||
setBuild("aws_config", "ami_name", undefined);
|
||||
} else {
|
||||
setBuild("aws_config", "ami_id", ami_id);
|
||||
setBuild("aws_config", "ami_name", ami_name);
|
||||
}
|
||||
}}
|
||||
itemMap={get_ami_name}
|
||||
itemMap={(i) => i.replaceAll("_", " ")}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
useSearch
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import { Component, createEffect, 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 +40,10 @@ const BuildArgs: Component<{}> = (p) => {
|
||||
};
|
||||
|
||||
const EditBuildArgs: Component<{}> = (p) => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const [show, toggle] = useToggle();
|
||||
const [buildArgs, setBuildArgs] = createSignal("");
|
||||
const { build, setBuild } = useConfig();
|
||||
const { build, setBuild, server } = useConfig();
|
||||
createEffect(() => {
|
||||
setBuildArgs(
|
||||
parseEnvVarseToDotEnv(
|
||||
@@ -56,6 +61,27 @@ const EditBuildArgs: Component<{}> = (p) => {
|
||||
}
|
||||
toggle();
|
||||
};
|
||||
const [peripherySecrets, setPeripherySecrets] =
|
||||
createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_available_secrets(build.server_id!)
|
||||
.then(setPeripherySecrets);
|
||||
}
|
||||
});
|
||||
const secrets = () => {
|
||||
if (build.server_id) {
|
||||
return peripherySecrets() || [];
|
||||
} 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 +95,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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -26,10 +26,10 @@ const Docker: Component<{}> = (p) => {
|
||||
if (build.server_id) {
|
||||
return peripheryDockerAccounts() || [];
|
||||
} 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 [];
|
||||
};
|
||||
|
||||
@@ -25,10 +25,10 @@ const Repo: Component<{}> = (p) => {
|
||||
if (build.server_id) {
|
||||
return peripheryGithubAccounts() || [];
|
||||
} 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 [];
|
||||
};
|
||||
|
||||
@@ -41,24 +41,36 @@ 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";
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import { Component, createEffect, createSignal, For, Show } from "solid-js";
|
||||
import { client } from "../../../../..";
|
||||
import { ServerStatus } from "../../../../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
parseDotEnvToEnvVars,
|
||||
@@ -38,7 +40,7 @@ const Env: Component<{}> = (p) => {
|
||||
const EditDotEnv: Component<{}> = (p) => {
|
||||
const [show, toggle] = useToggle();
|
||||
const [dotenv, setDotEnv] = createSignal("");
|
||||
const { deployment, setDeployment } = useConfig();
|
||||
const { deployment, setDeployment, server } = useConfig();
|
||||
createEffect(() => {
|
||||
setDotEnv(
|
||||
parseEnvVarseToDotEnv(
|
||||
@@ -56,6 +58,15 @@ const EditDotEnv: Component<{}> = (p) => {
|
||||
}
|
||||
toggle();
|
||||
};
|
||||
const [secrets, setSecrets] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_available_secrets(deployment.server_id)
|
||||
.then(setSecrets);
|
||||
}
|
||||
});
|
||||
let ref: HTMLTextAreaElement;
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
@@ -69,19 +80,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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -58,6 +58,7 @@ const Builds: Component<{}> = (p) => {
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -51,6 +51,7 @@ const Groups: Component<{}> = (p) => {
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { ParentComponent, createContext, useContext } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useLocalStorage } from "../../../util/hooks";
|
||||
|
||||
export const TREE_SORTS = ["name", "created"] as const;
|
||||
export const TREE_SORTS = ["name", "created at"] as const;
|
||||
export type TreeSortType = typeof TREE_SORTS[number];
|
||||
|
||||
const value = () => {
|
||||
const { servers, groups, builds } = useAppState();
|
||||
const [sort, setSort] = useLocalStorage<TreeSortType>(
|
||||
TREE_SORTS[0],
|
||||
"home-sort-v1"
|
||||
"home-sort-v2"
|
||||
);
|
||||
const server_sorter = () => {
|
||||
if (!servers.loaded()) return () => 0;
|
||||
|
||||
@@ -42,7 +42,8 @@ const Servers: Component<{ serverIDs: string[]; showAdd?: boolean }> = (p) => {
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
33
frontend/src/components/shared/CheckBox.tsx
Normal file
33
frontend/src/components/shared/CheckBox.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component, JSX } from "solid-js";
|
||||
import Flex from "./layout/Flex";
|
||||
|
||||
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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import Circle from "../../shared/Circle";
|
||||
import { ControlledTabs } from "../../shared/tabs/Tabs";
|
||||
import { useAppDimensions } from "../../../state/DimensionProvider";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { Build, ServerStatus } from "../../../types";
|
||||
import { A } from "@solidjs/router";
|
||||
import { ServerStatus } from "../../../types";
|
||||
|
||||
const mobileStyle: JSX.CSSProperties = {
|
||||
// position: "fixed",
|
||||
|
||||
330
frontend/src/components/users/User.tsx
Normal file
330
frontend/src/components/users/User.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import {
|
||||
Operation,
|
||||
PermissionLevel,
|
||||
PermissionsTarget,
|
||||
User as UserType,
|
||||
} from "../../types";
|
||||
import {
|
||||
getId,
|
||||
readableMonitorTimestamp,
|
||||
readableUserType,
|
||||
} from "../../util/helpers";
|
||||
import { useToggle } from "../../util/hooks";
|
||||
import CheckBox from "../shared/CheckBox";
|
||||
import Icon from "../shared/Icon";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import Selector from "../shared/menu/Selector";
|
||||
|
||||
const User: Component = () => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { builds, deployments, servers, ws } = useAppState();
|
||||
const params = useParams<{ id: string }>();
|
||||
const [user, { refetch }] = createResource(() =>
|
||||
client.get_user_by_id(params.id)
|
||||
);
|
||||
onCleanup(
|
||||
ws.subscribe(
|
||||
[
|
||||
Operation.ModifyUserEnabled,
|
||||
Operation.ModifyUserCreateServerPermissions,
|
||||
Operation.ModifyUserCreateBuildPermissions,
|
||||
Operation.ModifyUserPermissions,
|
||||
],
|
||||
refetch
|
||||
)
|
||||
);
|
||||
const [showAll, toggleShowAll] = useToggle(false);
|
||||
const [search, setSearch] = createSignal("");
|
||||
const _servers = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return servers.filterArray((s) => s.server.name.includes(search()));
|
||||
} else {
|
||||
return servers.filterArray((s) => {
|
||||
if (!s.server.name.includes(search())) return false;
|
||||
const p = s.server.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
const _deployments = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return deployments.filterArray((d) =>
|
||||
d.deployment.name.includes(search())
|
||||
);
|
||||
} else {
|
||||
return deployments.filterArray((d) => {
|
||||
if (!d.deployment.name.includes(search())) return false;
|
||||
const p = d.deployment.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
const _builds = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return builds.filterArray((b) => b.name.includes(search()));
|
||||
} else {
|
||||
return builds.filterArray((b) => {
|
||||
if (!b.name.includes(search())) return false;
|
||||
const p = b.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Show when={user()} fallback={<Loading type="three-dot" />}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Flex alignItems="center">
|
||||
<A href="/users" class="grey">
|
||||
<Icon type="arrow-left" />
|
||||
</A>
|
||||
<h1>{user()?.username}</h1>
|
||||
<Show when={user()?.admin}>
|
||||
<h2 style={{ opacity: 0.7 }}>admin</h2>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<CheckBox
|
||||
label="show all resources"
|
||||
checked={showAll()}
|
||||
toggle={toggleShowAll}
|
||||
/>
|
||||
<UserPermissionButtons user={user()!} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<Input
|
||||
placeholder="search resources"
|
||||
class="lightgrey"
|
||||
style={{ padding: "0.5rem" }}
|
||||
value={search()}
|
||||
onEdit={setSearch}
|
||||
/>
|
||||
<Flex>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">type:</div>
|
||||
<div>{user() ? readableUserType(user()!) : "unknown"}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">created:</div>
|
||||
<div>
|
||||
{user()?.created_at
|
||||
? readableMonitorTimestamp(user()?.created_at!)
|
||||
: "unknown"}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>servers</h1>
|
||||
<Show when={_servers()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_servers()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.server.name}</h2>
|
||||
<div class="dimmed">
|
||||
{item.server.region || "unknown region"}
|
||||
</div>
|
||||
</Grid>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.server.permissions?.[params.id] || "none") !==
|
||||
"none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={item.server.permissions?.[params.id] || "none"}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Server,
|
||||
target_id: getId(item.server),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>deployments</h1>
|
||||
<Show when={_deployments()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_deployments()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.deployment.name}</h2>
|
||||
<div class="dimmed">
|
||||
{servers.get(item.deployment.server_id)?.server.name ||
|
||||
"unknown"}
|
||||
</div>
|
||||
</Grid>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.deployment.permissions?.[params.id] || "none") !==
|
||||
"none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={
|
||||
item.deployment.permissions?.[params.id] || "none"
|
||||
}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Deployment,
|
||||
target_id: getId(item.deployment),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>builds</h1>
|
||||
<Show when={_builds()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_builds()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h2>{item.name}</h2>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.permissions?.[params.id] || "none") !== "none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={item.permissions?.[params.id] || "none"}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Build,
|
||||
target_id: getId(item),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
export const UserPermissionButtons: Component<{ user: UserType }> = (p) => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
return (
|
||||
<Show when={!p.user.admin}>
|
||||
<Grid
|
||||
placeItems="center end"
|
||||
gridTemplateColumns={!isMobile() ? "auto 1fr 1fr" : undefined}
|
||||
>
|
||||
<button
|
||||
class={p.user.enabled ? "green" : "red"}
|
||||
style={{ width: isMobile() ? "11rem" : "6rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_enabled({
|
||||
user_id: getId(p.user),
|
||||
enabled: !p.user.enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.enabled ? "enabled" : "disabled"}
|
||||
</button>
|
||||
<button
|
||||
class={p.user.create_server_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_create_server_permissions({
|
||||
user_id: getId(p.user),
|
||||
create_server_permissions: !p.user.create_server_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.create_server_permissions
|
||||
? "can create servers"
|
||||
: "cannot create servers"}
|
||||
</button>
|
||||
<button
|
||||
class={p.user.create_build_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_create_build_permissions({
|
||||
user_id: getId(p.user),
|
||||
create_build_permissions: !p.user.create_build_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.create_build_permissions
|
||||
? "can create builds"
|
||||
: "cannot create builds"}
|
||||
</button>
|
||||
</Grid>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
@@ -8,18 +9,16 @@ import {
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { Operation } from "../../types";
|
||||
import { combineClasses, getId } from "../../util/helpers";
|
||||
import { getId } from "../../util/helpers";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import s from "./users.module.scss";
|
||||
import { UserPermissionButtons } from "./User";
|
||||
|
||||
const Users: Component<{}> = (p) => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { ws } = useAppState();
|
||||
const [users, { refetch }] = createResource(() => client.list_users());
|
||||
onCleanup(
|
||||
@@ -60,61 +59,28 @@ const Users: Component<{}> = (p) => {
|
||||
</Flex>
|
||||
<For each={filteredUsers()}>
|
||||
{(user) => (
|
||||
<Flex class={combineClasses(s.User, "shadow")}>
|
||||
<div class={s.Username}>{user.username}</div>
|
||||
<Grid
|
||||
placeItems="center end"
|
||||
gridTemplateColumns={!isMobile() ? "1fr 1fr 1fr" : undefined}
|
||||
<Show
|
||||
when={!user.admin}
|
||||
fallback={
|
||||
<Flex class="card light shadow">
|
||||
<h2>{user.username}</h2>
|
||||
<h2 style={{ opacity: 0.7 }}>admin</h2>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<A
|
||||
href={`/user/${getId(user)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
width: "100%",
|
||||
"justify-content": "space-between",
|
||||
"align-items": "center",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class={user.enabled ? "green" : "red"}
|
||||
style={{ width: isMobile() ? "11rem" : "6rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_enabled({
|
||||
user_id: getId(user),
|
||||
enabled: !user.enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.enabled ? "enabled" : "disabled"}
|
||||
</button>
|
||||
<button
|
||||
class={user.create_server_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_create_server_permissions({
|
||||
user_id: getId(user),
|
||||
create_server_permissions:
|
||||
!user.create_server_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.create_server_permissions
|
||||
? "can create servers"
|
||||
: "cannot create servers"}
|
||||
</button>
|
||||
<button
|
||||
class={user.create_build_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_create_build_permissions({
|
||||
user_id: getId(user),
|
||||
create_build_permissions: !user.create_build_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.create_build_permissions
|
||||
? "can create builds"
|
||||
: "cannot create builds"}
|
||||
</button>
|
||||
{/* <ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() => deleteUser(user._id!)}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton> */}
|
||||
</Grid>
|
||||
</Flex>
|
||||
<h2>{user.username}</h2>
|
||||
<UserPermissionButtons user={user} />
|
||||
</A>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
@use "../../style/colors.scss" as c;
|
||||
|
||||
.UsersContent {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Users {
|
||||
width: fit-content;
|
||||
min-width: 30rem;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.Username {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.User {
|
||||
background-color: c.$lightgrey;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -232,13 +232,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) {
|
||||
|
||||
@@ -314,6 +314,10 @@ svg {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dimmed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// .hoverable {
|
||||
// transition: all 250ms ease-in-out;
|
||||
// }
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Build {
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions?: PermissionsMap;
|
||||
skip_secret_interp?: boolean;
|
||||
server_id?: string;
|
||||
aws_config?: AwsBuilderBuildConfig;
|
||||
version: Version;
|
||||
@@ -65,7 +66,7 @@ export interface BuildVersionsReponse {
|
||||
export interface AwsBuilderBuildConfig {
|
||||
region?: string;
|
||||
instance_type?: string;
|
||||
ami_id?: string;
|
||||
ami_name?: string;
|
||||
volume_gb?: number;
|
||||
subnet_id?: string;
|
||||
security_group_ids?: string[];
|
||||
@@ -76,7 +77,7 @@ export interface AwsBuilderBuildConfig {
|
||||
export interface AwsBuilderConfig {
|
||||
access_key_id: string;
|
||||
secret_access_key: string;
|
||||
default_ami_id: string;
|
||||
default_ami_name: string;
|
||||
default_subnet_id: string;
|
||||
default_key_pair_name: string;
|
||||
available_ami_accounts?: AvailableAmiAccounts;
|
||||
@@ -88,9 +89,10 @@ export interface AwsBuilderConfig {
|
||||
}
|
||||
|
||||
export interface AmiAccounts {
|
||||
name: string;
|
||||
ami_id: string;
|
||||
github?: string[];
|
||||
docker?: string[];
|
||||
secrets?: string[];
|
||||
}
|
||||
|
||||
export interface Deployment {
|
||||
@@ -99,6 +101,7 @@ export interface Deployment {
|
||||
description?: string;
|
||||
server_id: string;
|
||||
permissions?: PermissionsMap;
|
||||
skip_secret_interp?: boolean;
|
||||
docker_run_args: DockerRunArgs;
|
||||
build_id?: string;
|
||||
build_version?: Version;
|
||||
@@ -382,7 +385,7 @@ export interface User {
|
||||
|
||||
export interface ApiSecret {
|
||||
name: string;
|
||||
hash: string;
|
||||
hash?: string;
|
||||
created_at: string;
|
||||
expires?: string;
|
||||
}
|
||||
|
||||
@@ -49,11 +49,18 @@ import { generateQuery, QueryObject } from "./helpers";
|
||||
|
||||
export class Client {
|
||||
loginOptions: LoginOptions | undefined;
|
||||
monitorTitle: string | undefined;
|
||||
|
||||
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 +123,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 +145,10 @@ export class Client {
|
||||
return this.post("/api/update_description", body);
|
||||
}
|
||||
|
||||
get_monitor_title(): Promise<string> {
|
||||
return this.get("/api/title");
|
||||
}
|
||||
|
||||
// deployment
|
||||
|
||||
list_deployments(
|
||||
@@ -244,6 +261,10 @@ export class Client {
|
||||
return this.get(`/api/server/${id}/docker_accounts`);
|
||||
}
|
||||
|
||||
get_server_available_secrets(id: string): Promise<string[]> {
|
||||
return this.get(`/api/server/${id}/secrets`);
|
||||
}
|
||||
|
||||
get_server_version(id: string): Promise<string> {
|
||||
return this.get(`/api/server/${id}/version`);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
EnvironmentVar,
|
||||
ServerStatus,
|
||||
Timelength,
|
||||
User,
|
||||
Version,
|
||||
} from "../types";
|
||||
|
||||
@@ -234,3 +235,13 @@ export function readableStorageAmount(gb: number) {
|
||||
export function readableVersion(version: Version) {
|
||||
return `v${version.major}.${version.minor}.${version.patch}`;
|
||||
}
|
||||
|
||||
export function readableUserType(user: User) {
|
||||
if (user.github_id) {
|
||||
return "github"
|
||||
} else if (user.google_id) {
|
||||
return "google"
|
||||
} else {
|
||||
return "local"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +435,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@solidjs/router/-/router-0.6.0.tgz#27583cd0aa81a99482e0e7eddae5e214bd8bf6b6"
|
||||
integrity sha512-7ug2fzXXhvvDBL4CQyMvMM9o3dgBE6PoRh38T8UTmMnYz4rcCfROqSZc9yq+YEC96qWt5OvJgZ1Uj/4EAQXlfA==
|
||||
|
||||
"@tanstack/query-core@4.26.0":
|
||||
version "4.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.26.0.tgz#fc65c8c117e72baead3a82a1f272a4ec210c7650"
|
||||
integrity sha512-9CRqXmCH82KZDKmezoGU4FOn1Oqbzlp2/zf71n+9nC58e7NSqCIjfSCMpqQWcu9YqUcUykxZEUunOyKHVc6BJA==
|
||||
|
||||
"@tanstack/solid-query@^4.26.0":
|
||||
version "4.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/solid-query/-/solid-query-4.26.0.tgz#77aa1b60e47719075802891a1f29a0a25385bd48"
|
||||
integrity sha512-2+dXfIHy8pU0GlrkxjXi8i7z9Ff1C7dbspAo3t6X6jJK57kesGhpE8AjmgIOUrTH1XsoHkTnBKxmoq/I7ewMCQ==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "4.26.0"
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl gnupg lsb-release make cmake g++ python3 node-gyp build-essential libssl-dev git
|
||||
git config --global pull.rebase false
|
||||
|
||||
# install docker cli
|
||||
# mkdir -p /etc/apt/keyrings
|
||||
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
# echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
# chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
# apt-get update
|
||||
# apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
|
||||
# install nodejs and enable yarn
|
||||
curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs
|
||||
corepack enable
|
||||
@@ -1,11 +0,0 @@
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl gnupg lsb-release git
|
||||
git config --global pull.rebase false
|
||||
|
||||
# install docker cli
|
||||
# mkdir -p /etc/apt/keyrings
|
||||
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
# echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
# apt-get update
|
||||
# apt-get install docker-ce docker-ce-cli
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "db_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
types = { package = "monitor_types", path = "../types" }
|
||||
mungos = "0.3.0"
|
||||
mungos = "0.3.8"
|
||||
anyhow = "1.0"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_helpers"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "helpers used as dependency for mogh tech monitor"
|
||||
|
||||
@@ -133,10 +133,7 @@ pub fn to_monitor_name(name: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal Error: {err:#?}"),
|
||||
)
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#?}"))
|
||||
}
|
||||
|
||||
pub fn generate_secret(length: usize) -> String {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
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.9"
|
||||
# monitor_types = { path = "../types" }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
|
||||
@@ -58,6 +58,17 @@ impl MonitorClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_server_available_secrets(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
self.get(
|
||||
&format!("/api/server/{server_id}/secrets"),
|
||||
Option::<()>::None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_server(&self, name: &str, address: &str) -> anyhow::Result<Server> {
|
||||
self.post(
|
||||
"/api/server/create",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "periphery_client"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -51,6 +51,12 @@ impl PeripheryClient {
|
||||
.context("failed to get docker accounts from periphery")
|
||||
}
|
||||
|
||||
pub async fn get_available_secrets(&self, server: &Server) -> anyhow::Result<Vec<String>> {
|
||||
self.get_json(server, "/secrets")
|
||||
.await
|
||||
.context("failed to get secret variable names from periphery")
|
||||
}
|
||||
|
||||
pub async fn get_system_information(
|
||||
&self,
|
||||
server: &Server,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_types"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "types for the mogh tech monitor"
|
||||
|
||||
@@ -37,6 +37,11 @@ pub struct Build {
|
||||
#[builder(setter(skip))]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
pub server_id: Option<String>, // server which this image should be built on
|
||||
@@ -173,7 +178,7 @@ pub struct AwsBuilderBuildConfig {
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
pub ami_id: Option<String>,
|
||||
pub ami_name: Option<String>,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
|
||||
@@ -17,6 +17,9 @@ pub type SecretsMap = HashMap<String, String>; // these are used for injection i
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CoreConfig {
|
||||
#[serde(default = "default_title")]
|
||||
pub title: String,
|
||||
|
||||
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
|
||||
pub host: String,
|
||||
|
||||
@@ -71,6 +74,10 @@ pub struct CoreConfig {
|
||||
pub aws: AwsBuilderConfig,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
String::from("monitor")
|
||||
}
|
||||
|
||||
fn default_core_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
@@ -115,7 +122,7 @@ pub struct AwsBuilderConfig {
|
||||
#[serde(skip_serializing)]
|
||||
pub secret_access_key: String,
|
||||
|
||||
pub default_ami_id: String,
|
||||
pub default_ami_name: String,
|
||||
pub default_subnet_id: String,
|
||||
pub default_key_pair_name: String,
|
||||
|
||||
@@ -151,16 +158,18 @@ fn default_instance_type() -> String {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_id, AmiAccounts)
|
||||
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_name, AmiAccounts)
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct AmiAccounts {
|
||||
pub name: String,
|
||||
pub ami_id: String,
|
||||
#[serde(default)]
|
||||
pub github: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub docker: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub secrets: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -37,6 +37,11 @@ pub struct Deployment {
|
||||
#[builder(setter(skip))]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "docker_run_args_diff_no_change")]))]
|
||||
pub docker_run_args: DockerRunArgs,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct User {
|
||||
#[diff(attr(#[derive(Debug, Serialize)]))]
|
||||
pub struct ApiSecret {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub hash: String,
|
||||
pub created_at: String,
|
||||
pub expires: Option<String>,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=agent to connect with monitor core
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=max
|
||||
WorkingDirectory=/home/max
|
||||
ExecStart=/bin/bash --login -c 'source /home/max/.bashrc; $HOME/.cargo/bin/periphery --config-path ~/.monitor/periphery.config.toml --home-dir $HOME'
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_periphery"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor periphery binary | run monitor periphery as system daemon"
|
||||
@@ -17,12 +17,10 @@ helpers = { package = "monitor_helpers", path = "../lib/helpers" }
|
||||
types = { package = "monitor_types", path = "../lib/types" }
|
||||
run_command = { version = "0.0.5", features = ["async_tokio"] }
|
||||
async_timing_util = "0.1.14"
|
||||
tokio = { version = "1.25", features = ["full"] }
|
||||
# tokio-util = "0.7"
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
axum = { version = "0.6", features = ["ws"] }
|
||||
tower = { version = "0.4", features = ["full"] }
|
||||
futures = "0.3"
|
||||
# futures-util = "0.3.25"
|
||||
dotenv = "0.15"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
@@ -34,3 +32,4 @@ sysinfo = "0.28"
|
||||
toml = "0.7"
|
||||
daemonize = "0.5.0"
|
||||
clap = { version = "4.1", features = ["derive"] }
|
||||
svi = "0.1.3"
|
||||
|
||||
@@ -26,7 +26,14 @@ async fn build_image(
|
||||
tokio::spawn(async move {
|
||||
let logs = match get_docker_token(&build.docker_account, &config) {
|
||||
Ok(docker_token) => {
|
||||
match docker::build(&build, config.repo_dir.clone(), docker_token).await {
|
||||
match docker::build(
|
||||
&build,
|
||||
config.repo_dir.clone(),
|
||||
docker_token,
|
||||
&config.secrets,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(logs) => logs,
|
||||
Err(e) => vec![Log::error("build", format!("{e:#?}"))],
|
||||
}
|
||||
|
||||
@@ -109,7 +109,13 @@ async fn deploy(
|
||||
) -> anyhow::Result<Json<Log>> {
|
||||
let log = match get_docker_token(&deployment.docker_run_args.docker_account, &config) {
|
||||
Ok(docker_token) => tokio::spawn(async move {
|
||||
docker::deploy(&deployment, &docker_token, config.repo_dir.clone()).await
|
||||
docker::deploy(
|
||||
&deployment,
|
||||
&docker_token,
|
||||
config.repo_dir.clone(),
|
||||
&config.secrets,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.context("failed at spawn thread for deploy")?,
|
||||
|
||||
101
periphery/src/api/guard.rs
Normal file
101
periphery/src/api/guard.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::ConnectInfo,
|
||||
http::{Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
Json, RequestExt,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use types::{monitor_timestamp, PeripheryConfig};
|
||||
|
||||
pub async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.passkeys.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let req_passkey = req.headers().get("authorization");
|
||||
if req_passkey.is_none() {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("request was not sent with passkey"),
|
||||
));
|
||||
}
|
||||
let req_passkey = req_passkey
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("failed to get passkey from authorization header as str: {e:?}"),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
if config.passkeys.contains(&req_passkey) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let ConnectInfo(socket_addr) =
|
||||
req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} (bad passkey) | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp(),
|
||||
);
|
||||
Err((StatusCode::UNAUTHORIZED, format!("request passkey invalid")))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn guard_request_by_ip(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.allowed_ips.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let ConnectInfo(socket_addr) = req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
if config.allowed_ips.contains(&ip) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp()
|
||||
);
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("requesting ip {ip} not allowed"),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,4 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::ConnectInfo,
|
||||
http::{Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::get,
|
||||
Json, RequestExt, Router,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use types::{monitor_timestamp, PeripheryConfig};
|
||||
use axum::{middleware, routing::get, Json, Router};
|
||||
|
||||
use crate::{helpers::docker::DockerClient, HomeDirExtension, PeripheryConfigExtension};
|
||||
|
||||
@@ -21,6 +9,7 @@ mod build;
|
||||
mod command;
|
||||
mod container;
|
||||
mod git;
|
||||
mod guard;
|
||||
mod image;
|
||||
mod network;
|
||||
mod stats;
|
||||
@@ -34,6 +23,7 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
get(|sys: StatsExtension| async move { Json(sys.read().unwrap().info.clone()) }),
|
||||
)
|
||||
.route("/accounts/:account_type", get(accounts::get_accounts))
|
||||
.route("/secrets", get(get_available_secrets))
|
||||
.nest("/command", command::router())
|
||||
.nest("/container", container::router())
|
||||
.nest("/network", network::router())
|
||||
@@ -42,8 +32,8 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
.nest("/build", build::router())
|
||||
.nest("/image", image::router())
|
||||
.layer(DockerClient::extension())
|
||||
.layer(middleware::from_fn(guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey))
|
||||
.layer(middleware::from_fn(guard::guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard::guard_request_by_passkey))
|
||||
.layer(StatsClient::extension(
|
||||
config.stats_polling_rate.to_string().parse().unwrap(),
|
||||
))
|
||||
@@ -51,91 +41,7 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
.layer(home_dir)
|
||||
}
|
||||
|
||||
async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.passkeys.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let req_passkey = req.headers().get("authorization");
|
||||
if req_passkey.is_none() {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("request was not sent with passkey"),
|
||||
));
|
||||
}
|
||||
let req_passkey = req_passkey
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("failed to get passkey from authorization header as str: {e:?}"),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
if config.passkeys.contains(&req_passkey) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let ConnectInfo(socket_addr) =
|
||||
req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} (bad passkey) | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp(),
|
||||
);
|
||||
Err((StatusCode::UNAUTHORIZED, format!("request passkey invalid")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn guard_request_by_ip(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.allowed_ips.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let ConnectInfo(socket_addr) = req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
if config.allowed_ips.contains(&ip) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp()
|
||||
);
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("requesting ip {ip} not allowed"),
|
||||
))
|
||||
}
|
||||
async fn get_available_secrets(config: PeripheryConfigExtension) -> Json<Vec<String>> {
|
||||
let vars: Vec<String> = config.secrets.keys().map(|k| k.clone()).collect();
|
||||
Json(vars)
|
||||
}
|
||||
|
||||
@@ -82,23 +82,28 @@ pub fn load() -> (Args, u16, PeripheryConfigExtension, HomeDirExtension) {
|
||||
|
||||
fn print_startup_log(config_paths: Vec<String>, args: &Args, config: &PeripheryConfig) {
|
||||
println!("\nconfig paths: {config_paths:?}");
|
||||
let mut config = config.clone();
|
||||
config.github_accounts = config
|
||||
let mut config_for_print = config.clone();
|
||||
config_for_print.github_accounts = config_for_print
|
||||
.github_accounts
|
||||
.into_iter()
|
||||
.map(|(a, _)| (a, "<SECRET>".to_string()))
|
||||
.collect();
|
||||
config.docker_accounts = config
|
||||
config_for_print.docker_accounts = config_for_print
|
||||
.docker_accounts
|
||||
.into_iter()
|
||||
.map(|(a, _)| (a, "<SECRET>".to_string()))
|
||||
.collect();
|
||||
config.secrets = config
|
||||
config_for_print.secrets = config_for_print
|
||||
.secrets
|
||||
.into_iter()
|
||||
.map(|(s, _)| (s, "<SECRET>".to_string()))
|
||||
.collect();
|
||||
println!("{config:#?}");
|
||||
config_for_print.passkeys = config_for_print
|
||||
.passkeys
|
||||
.into_iter()
|
||||
.map(|_| "<SECRET>".to_string())
|
||||
.collect();
|
||||
println!("{config_for_print:#?}");
|
||||
if args.daemon {
|
||||
println!("daemon mode enabled");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use helpers::to_monitor_name;
|
||||
@@ -20,10 +20,12 @@ pub async fn build(
|
||||
docker_build_args,
|
||||
docker_account,
|
||||
docker_organization,
|
||||
skip_secret_interp,
|
||||
..
|
||||
}: &Build,
|
||||
mut repo_dir: PathBuf,
|
||||
docker_token: Option<String>,
|
||||
secrets: &HashMap<String, String>,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let mut logs = Vec::new();
|
||||
let DockerBuildArgs {
|
||||
@@ -55,8 +57,19 @@ pub async fn build(
|
||||
"cd {} && docker build {build_args}{image_tags} -f {dockerfile_path} .{docker_push}",
|
||||
build_dir.display()
|
||||
);
|
||||
let build_log = run_monitor_command("docker build", command).await;
|
||||
logs.push(build_log);
|
||||
if *skip_secret_interp {
|
||||
let build_log = run_monitor_command("docker build", command).await;
|
||||
logs.push(build_log);
|
||||
} else {
|
||||
let (command, replacers) =
|
||||
svi::interpolate_variables(&command, secrets, svi::Interpolator::DoubleBrackets)
|
||||
.context("failed to interpolate secrets into docker build command")?;
|
||||
let mut build_log = run_monitor_command("docker build", command).await;
|
||||
build_log.command = svi::replace_in_string(&build_log.command, &replacers);
|
||||
build_log.stdout = svi::replace_in_string(&build_log.stdout, &replacers);
|
||||
build_log.stderr = svi::replace_in_string(&build_log.stderr, &replacers);
|
||||
logs.push(build_log);
|
||||
}
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use helpers::to_monitor_name;
|
||||
@@ -78,6 +78,7 @@ pub async fn deploy(
|
||||
deployment: &Deployment,
|
||||
docker_token: &Option<String>,
|
||||
repo_dir: PathBuf,
|
||||
secrets: &HashMap<String, String>,
|
||||
) -> Log {
|
||||
if let Err(e) = docker_login(&deployment.docker_run_args.docker_account, docker_token).await {
|
||||
return Log::error("docker login", format!("{e:#?}"));
|
||||
@@ -85,7 +86,22 @@ pub async fn deploy(
|
||||
let _ = pull_image(&deployment.docker_run_args.image).await;
|
||||
let _ = stop_and_remove_container(&to_monitor_name(&deployment.name)).await;
|
||||
let command = docker_run_command(deployment, repo_dir);
|
||||
run_monitor_command("docker run", command).await
|
||||
if deployment.skip_secret_interp {
|
||||
run_monitor_command("docker run", command).await
|
||||
} else {
|
||||
let command =
|
||||
svi::interpolate_variables(&command, secrets, svi::Interpolator::DoubleBrackets)
|
||||
.context("failed to interpolate secrets into docker run command");
|
||||
if let Err(e) = command {
|
||||
return Log::error("docker run", format!("{e:?}"));
|
||||
}
|
||||
let (command, replacers) = command.unwrap();
|
||||
let mut log = run_monitor_command("docker run", command).await;
|
||||
log.command = svi::replace_in_string(&log.command, &replacers);
|
||||
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
|
||||
log.stderr = svi::replace_in_string(&log.stderr, &replacers);
|
||||
log
|
||||
}
|
||||
}
|
||||
|
||||
pub fn docker_run_command(
|
||||
|
||||
@@ -99,7 +99,10 @@ async fn clone(
|
||||
let success = output.success();
|
||||
let (command, stderr) = if access_token_at.len() > 0 {
|
||||
let access_token = access_token.unwrap();
|
||||
(command.replace(&access_token, "<TOKEN>"), output.stderr.replace(&access_token, "<TOKEN>"))
|
||||
(
|
||||
command.replace(&access_token, "<TOKEN>"),
|
||||
output.stderr.replace(&access_token, "<TOKEN>"),
|
||||
)
|
||||
} else {
|
||||
(command, output.stderr)
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ mod helpers;
|
||||
type PeripheryConfigExtension = Extension<Arc<PeripheryConfig>>;
|
||||
type HomeDirExtension = Extension<Arc<String>>;
|
||||
|
||||
fn main() {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, port, config, home_dir) = config::load();
|
||||
|
||||
if args.daemon {
|
||||
@@ -29,7 +29,9 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
run_periphery_server(port, config, home_dir)
|
||||
run_periphery_server(port, config, home_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -37,11 +39,12 @@ async fn run_periphery_server(
|
||||
port: u16,
|
||||
config: PeripheryConfigExtension,
|
||||
home_dir: HomeDirExtension,
|
||||
) {
|
||||
) -> anyhow::Result<()> {
|
||||
let app = api::router(config, home_dir);
|
||||
|
||||
axum::Server::bind(&get_socket_addr(port))
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.expect("monitor periphery axum server crashed");
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user