Compare commits

..

28 Commits

Author SHA1 Message Date
mbecker20
a666df099f use image on deployment container 2023-02-26 06:55:31 +00:00
mbecker20
21dd0ee072 cli should be 0.2.2 2023-02-26 06:48:38 +00:00
mbecker20
bd2a1d4236 v0.2.1 merge multiple config files 2023-02-26 06:25:26 +00:00
mbecker20
7acdbcfd8f improve updates selector - add class 2023-02-25 22:34:37 +00:00
mbecker20
58514c5c93 fix height of builder config when no builder type chosen 2023-02-25 22:07:05 +00:00
mbecker20
580e800923 fix clap args with - 2023-02-23 23:14:02 +00:00
mbecker20
29f6b19f33 cli 0.2.0. fix starting mongo when no existing container present 2023-02-23 22:46:17 +00:00
mbecker20
e090247723 fix error when user doesn't have access to build on deployment 2023-02-23 08:00:55 +00:00
mbecker20
1374c26cd8 0.2.0 cleanup 2023-02-23 07:28:11 +00:00
mbecker20
5467b40b2e fix to git clone <TOKEN> splice 2023-02-23 07:22:32 +00:00
mbecker20
165b9012da improve users responsiveness 2023-02-23 07:17:18 +00:00
mbecker20
22630f665e update the manage users page 2023-02-23 07:09:33 +00:00
mbecker20
3d867084ba log poll default to false 2023-02-23 06:44:55 +00:00
mbecker20
171dd2d9e0 remove menu animation, change builder type to selector 2023-02-23 06:38:36 +00:00
mbecker20
9709239f88 build version h2 2023-02-22 22:45:05 +00:00
mbecker20
60d457b285 improve deployment in tree and display deployed version in header 2023-02-22 22:27:54 +00:00
mbecker20
8b1d4793a7 0.1.17 support building with ec2 instances 2023-02-22 21:05:03 +00:00
mbecker20
f2166c8435 configure aws config on builds 2023-02-22 20:49:56 +00:00
mbecker20
07d723a748 more prog on frontend, some api etc 2023-02-22 06:39:32 +00:00
mbecker20
b36f485287 put server / aws build on build header 2023-02-21 23:05:49 +00:00
mbecker20
a121ae0828 begin frontend refactor for ephemeral build support 2023-02-21 18:11:43 +00:00
mbecker20
e2b5a02008 building works 2023-02-21 05:22:26 +00:00
mbecker20
575aa62625 update versions to 0.1.16 2023-02-21 04:32:41 +00:00
mbecker20
ac88a2c4ed testing and fixes for aws build 2023-02-21 04:27:30 +00:00
mbecker20
f1dcb71a8a poll periphery on build instance to ensure connectivity before moving on 2023-02-20 22:56:22 +00:00
mbecker20
30d04bc201 support building on epheral ec2 2023-02-20 09:41:15 +00:00
mbecker20
33a00bb1a2 poll when instance running 2023-02-20 04:55:44 +00:00
mbecker20
ccca44ea89 start working on build instance spawn on aws 2023-02-20 01:19:07 +00:00
87 changed files with 2922 additions and 1088 deletions

17
.vscode/tasks.json vendored
View File

@@ -92,15 +92,6 @@
"cwd": "${workspaceFolder}/lib/types"
}
},
{
"type": "cargo",
"command": "publish",
"args": ["--allow-dirty"],
"label": "publish monitor helpers",
"options": {
"cwd": "${workspaceFolder}/lib/helpers"
}
},
{
"type": "cargo",
"command": "publish",
@@ -109,14 +100,6 @@
"cwd": "${workspaceFolder}/lib/monitor_client"
}
},
{
"type": "cargo",
"command": "publish",
"label": "publish monitor periphery",
"options": {
"cwd": "${workspaceFolder}/periphery"
}
},
{
"type": "cargo",
"command": "publish",

896
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@ WORKDIR /builder
COPY ./core ./core
# COPY ./lib/types ./lib/types
# COPY ./lib/helpers ./lib/helpers
COPY ./lib/types ./lib/types
COPY ./lib/helpers ./lib/helpers
COPY ./lib/db_client ./lib/db_client
COPY ./lib/periphery_client ./lib/periphery_client

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_cli"
version = "0.1.21"
version = "0.2.2"
edition = "2021"
authors = ["MoghTech"]
description = "monitor cli | tools to setup monitor system"

View File

@@ -74,6 +74,7 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
local_auth: true,
github_oauth: Default::default(),
google_oauth: Default::default(),
aws: Default::default(),
mongo: MongoConfig {
uri: mongo_uri,
db_name: mongo_db_name,
@@ -177,7 +178,9 @@ pub fn start_mongo(sub_matches: &ArgMatches) {
}
}
let command = format!("docker stop {name} && docker container rm {name} && docker run -d --name {name} -p {port}:27017 --network {network} -v {mount}:/data/db{env} --restart {restart} --log-opt max-size=15m --log-opt max-file=3 mongo --quiet");
let stop = run_command_pipe_to_terminal(&format!("docker stop {name} && docker container rm {name}"));
let command = format!("docker run -d --name {name} -p {port}:27017 --network {network} -v {mount}:/data/db{env} --restart {restart} --log-opt max-size=15m --log-opt max-file=3 mongo --quiet");
let output = run_command_pipe_to_terminal(&command);

View File

@@ -36,19 +36,19 @@ fn cli() -> Command {
.required(false)
)
.arg(
arg!(--mongo-uri <URI> "sets the mongo uri to use. default is 'mongodb://monitor-mongo'")
arg!(--"mongo-uri" <URI> "sets the mongo uri to use. default is 'mongodb://monitor-mongo'")
.required(false)
)
.arg(
arg!(--mongo-db-name <NAME> "sets the db name to use. default is 'monitor'")
arg!(--"mongo-db-name" <NAME> "sets the db name to use. default is 'monitor'")
.required(false)
)
.arg(
arg!(--jwt-valid-for <TIMELENGTH> "sets the length of time jwt stays valid for. default is 1-wk (one week)")
arg!(--"jwt-valid-for" <TIMELENGTH> "sets the length of time jwt stays valid for. default is 1-wk (one week)")
.required(false)
)
.arg(
arg!(--slack-url <URL> "sets the slack url to use for slack notifications")
arg!(--"slack-url" <URL> "sets the slack url to use for slack notifications")
.required(false)
),
)
@@ -96,7 +96,7 @@ fn cli() -> Command {
arg!(--name <NAME> "specify the name of the monitor core container. default is monitor-core")
)
.arg(
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/core.config.toml")
arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/core.config.toml")
.required(false)
)
.arg(
@@ -111,7 +111,7 @@ fn cli() -> Command {
arg!(--restart <RESTART> "sets docker restart mode of monitor core container. default is unless-stopped")
)
.arg(
arg!(--add-internal-host "adds the docker flag '--add-host=host.docker.internal:host-gateway'. default is true")
arg!(--"add-internal-host" "adds the docker flag '--add-host=host.docker.internal:host-gateway'. default is true")
)
),
)
@@ -133,15 +133,15 @@ fn cli() -> Command {
.required(false)
)
.arg(
arg!(--stats-polling-rate <INTERVAL> "sets stats polling rate to control granularity of system stats returned. default is 5-sec. options: 1-sec, 5-sec, 10-sec, 30-sec, 1-min")
arg!(--"stats-polling-rate" <INTERVAL> "sets stats polling rate to control granularity of system stats returned. default is 5-sec. options: 1-sec, 5-sec, 10-sec, 30-sec, 1-min")
.required(false)
)
.arg(
arg!(--allowed-ips <IPS> "used to only accept requests from known ips. give ips as comma seperated list, like '--allowed_ips 127.0.0.1,10.20.30.43'. default is empty, which will not block any ip.")
arg!(--"allowed-ips" <IPS> "used to only accept requests from known ips. give ips as comma seperated list, like '--allowed_ips 127.0.0.1,10.20.30.43'. default is empty, which will not block any ip.")
.required(false)
)
.arg(
arg!(--repo-dir <PATH> "if running in container, this should be '/repos'. default is ~/.monitor/repos").required(false)
arg!(--"repo-dir" <PATH> "if running in container, this should be '/repos'. default is ~/.monitor/repos").required(false)
)
)
.subcommand(
@@ -157,7 +157,7 @@ fn cli() -> Command {
arg!(--install "specify this to install periphery from crates.io")
)
.arg(
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
.required(false)
)
)
@@ -171,7 +171,7 @@ fn cli() -> Command {
arg!(--install "specify this to install periphery from crates.io")
)
.arg(
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
.required(false)
)
.arg(
@@ -183,32 +183,32 @@ fn cli() -> Command {
.required(false)
)
)
.subcommand(
Command::new("container")
.about("start up monitor periphery in docker container")
.arg(
arg!(--yes "used in scripts to skip 'enter to continue' step")
)
.arg(
arg!(--name <NAME> "specify the name of the monitor periphery container. default is monitor-periphery")
)
.arg(
arg!(--config-path <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
.required(false)
)
.arg(arg!(--repo-dir <PATH> "specify the folder on host to clone repos into. default is ~/.monitor/repos").required(false))
.arg(
arg!(--port <PORT> "sets port monitor periphery will run on. default is 8000")
.required(false)
)
.arg(
arg!(--network <NETWORK> "sets docker network of monitor periphery container. default is bridge")
.required(false)
)
.arg(
arg!(--restart <RESTART> "sets docker restart mode of monitor periphery container. default is unless-stopped")
)
)
// .subcommand(
// Command::new("container")
// .about("start up monitor periphery in docker container")
// .arg(
// arg!(--yes "used in scripts to skip 'enter to continue' step")
// )
// .arg(
// arg!(--name <NAME> "specify the name of the monitor periphery container. default is monitor-periphery")
// )
// .arg(
// arg!(--"config-path" <PATH> "specify the file path to use for config. default is ~/.monitor/periphery.config.toml")
// .required(false)
// )
// .arg(arg!(--"repo-dir" <PATH> "specify the folder on host to clone repos into. default is ~/.monitor/repos").required(false))
// .arg(
// arg!(--port <PORT> "sets port monitor periphery will run on. default is 8000")
// .required(false)
// )
// .arg(
// arg!(--network <NETWORK> "sets docker network of monitor periphery container. default is bridge")
// .required(false)
// )
// .arg(
// arg!(--restart <RESTART> "sets docker restart mode of monitor periphery container. default is unless-stopped")
// )
// )
),
)
}
@@ -239,7 +239,7 @@ fn main() {
match periphery_start_command {
("systemd", sub_matches) => start_periphery_systemd(sub_matches),
("daemon", sub_matches) => start_periphery_daemon(sub_matches),
("container", sub_matches) => start_periphery_container(sub_matches),
// ("container", sub_matches) => start_periphery_container(sub_matches),
_ => println!("\n❌ invalid call, should be 'monitor periphery start <daemon, container> <flags>' ❌\n")
}
}

View File

@@ -21,7 +21,6 @@ pub struct CoreConfig {
#[serde(default)]
pub keep_stats_for_days: u64, // 0 means never prune
// jwt config
pub jwt_secret: String,
#[serde(default = "default_jwt_valid_for")]
pub jwt_valid_for: Timelength,
@@ -41,14 +40,16 @@ pub struct CoreConfig {
// enable login with local auth
pub local_auth: bool,
// github integration
pub mongo: MongoConfig,
#[serde(default)]
pub github_oauth: OauthCredentials,
// google integration
#[serde(default)]
pub google_oauth: OauthCredentials,
// mongo config
pub mongo: MongoConfig,
#[serde(default)]
pub aws: AwsBuilderConfig,
}
fn default_core_port() -> u16 {
@@ -86,6 +87,60 @@ 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>;

View File

@@ -31,6 +31,21 @@ monitoring_interval = "1-min"
# allow or deny user login with username / password
local_auth = true
[aws]
access_key_id = "your_aws_key_id"
secret_access_key = "your_aws_secret_key"
default_region = "us-east-1"
default_ami_id = "your_periphery_ami"
default_key_pair_name = "your_default_key_pair_name"
default_instance_type = "m5.2xlarge"
default_volume_gb = 8
default_subnet_id = "your_default_subnet_id"
default_security_group_ids = ["sg_id_1", "sg_id_2"]
default_assign_public_ip = false
[aws.available_ami_accounts]
your_periphery_ami = { name = "default ami", github = ["github_username"], docker = ["docker_username"] }
[github_oauth]
enabled = true
id = "your_github_client_id"

View File

@@ -1,15 +1,13 @@
[package]
name = "core"
version = "0.1.15"
version = "0.2.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# helpers = { package = "monitor_helpers", path = "../lib/helpers" }
# types = { package = "monitor_types", path = "../lib/types" }
helpers = { package = "monitor_helpers", version = "0.1.15" }
types = { package = "monitor_types", version = "0.1.15" }
helpers = { package = "monitor_helpers", path = "../lib/helpers" }
types = { package = "monitor_types", path = "../lib/types" }
db = { package = "db_client", path = "../lib/db_client" }
periphery = { package = "periphery_client", path = "../lib/periphery_client" }
axum_oauth2 = { path = "../lib/axum_oauth2" }

View File

@@ -1,6 +1,12 @@
use std::time::Duration;
use anyhow::{anyhow, Context};
use diff::Diff;
use helpers::{all_logs_success, to_monitor_name};
use helpers::{
all_logs_success,
aws::{self, create_ec2_client, create_instance_with_ami, terminate_ec2_instance, Ec2Instance},
to_monitor_name,
};
use mungos::{doc, to_bson};
use types::{
monitor_timestamp,
@@ -8,11 +14,10 @@ use types::{
Build, Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget, Version,
};
use crate::{
auth::RequestUser,
helpers::{any_option_diff_is_some, option_diff_is_some},
state::State,
};
use crate::{auth::RequestUser, state::State};
const BUILDER_POLL_RATE_SECS: u64 = 2;
const BUILDER_POLL_MAX_TRIES: usize = 30;
impl State {
pub async fn get_build_check_permissions(
@@ -39,18 +44,13 @@ impl State {
}
}
pub async fn create_build(
&self,
name: &str,
server_id: String,
user: &RequestUser,
) -> anyhow::Result<Build> {
self.get_server_check_permissions(&server_id, user, PermissionLevel::Update)
.await?;
pub async fn create_build(&self, name: &str, user: &RequestUser) -> anyhow::Result<Build> {
if !user.is_admin && !user.create_build_permissions {
return Err(anyhow!("user does not have permission to create builds"));
}
let start_ts = monitor_timestamp();
let build = Build {
name: to_monitor_name(name),
server_id,
permissions: [(user.id.clone(), PermissionLevel::Update)]
.into_iter()
.collect(),
@@ -84,10 +84,7 @@ impl State {
mut build: Build,
user: &RequestUser,
) -> anyhow::Result<Build> {
build.id = self
.create_build(&build.name, build.server_id.clone(), user)
.await?
.id;
build.id = self.create_build(&build.name, user).await?.id;
let build = self.update_build(build, user).await?;
Ok(build)
}
@@ -96,14 +93,12 @@ impl State {
&self,
target_id: &str,
new_name: String,
new_server_id: String,
user: &RequestUser,
) -> anyhow::Result<Build> {
let mut build = self
.get_build_check_permissions(target_id, user, PermissionLevel::Update)
.await?;
build.name = new_name;
build.server_id = new_server_id;
build.version = Version::default();
let build = self.create_full_build(build, user).await?;
Ok(build)
@@ -117,11 +112,6 @@ impl State {
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
.await?;
let start_ts = monitor_timestamp();
let server = self.db.get_server(&build.server_id).await?;
let delete_repo_log = match self.periphery.delete_repo(&server, &build.name).await {
Ok(log) => log,
Err(e) => Log::error("delete repo", format!("{e:#?}")),
};
self.db.builds.delete_one(build_id).await?;
let update = Update {
target: UpdateTarget::Build(build_id.to_string()),
@@ -129,13 +119,10 @@ impl State {
start_ts,
end_ts: Some(monitor_timestamp()),
operator: user.id.clone(),
logs: vec![
delete_repo_log,
Log::simple(
"delete build",
format!("deleted build {} on server {}", build.name, server.name),
),
],
logs: vec![Log::simple(
"delete build",
format!("deleted build {}", build.name),
)],
success: true,
..Default::default()
};
@@ -171,16 +158,25 @@ impl State {
mut new_build: Build,
user: &RequestUser,
) -> anyhow::Result<Build> {
let start_ts = monitor_timestamp();
let current_build = self
.get_build_check_permissions(&new_build.id, user, PermissionLevel::Update)
.await?;
let start_ts = monitor_timestamp();
if let Some(new_server_id) = &new_build.server_id {
if current_build.server_id.is_none()
|| new_server_id != current_build.server_id.as_ref().unwrap()
{
self.get_server_check_permissions(new_server_id, user, PermissionLevel::Update)
.await
.context("user does not have permission to attach build to this server")?;
}
}
// none of these should be changed through this method
new_build.name = current_build.name.clone();
new_build.permissions = current_build.permissions.clone();
new_build.server_id = current_build.server_id.clone();
new_build.last_built_at = String::new();
new_build.last_built_at = current_build.last_built_at.clone();
new_build.created_at = current_build.created_at.clone();
new_build.updated_at = start_ts.clone();
@@ -192,41 +188,42 @@ impl State {
let diff = current_build.diff(&new_build);
let mut update = Update {
let update = Update {
operation: Operation::UpdateBuild,
target: UpdateTarget::Build(new_build.id.clone()),
start_ts,
status: UpdateStatus::InProgress,
status: UpdateStatus::Complete,
logs: vec![Log::simple(
"build update",
serde_json::to_string_pretty(&diff).unwrap(),
)],
operator: user.id.clone(),
end_ts: Some(monitor_timestamp()),
success: true,
..Default::default()
};
update.id = self.add_update(update.clone()).await?;
// update.id = self.add_update(update.clone()).await?;
if any_option_diff_is_some(&[&diff.repo, &diff.branch, &diff.github_account])
|| option_diff_is_some(&diff.on_clone)
{
let server = self.db.get_server(&current_build.server_id).await?;
match self.periphery.clone_repo(&server, &new_build).await {
Ok(clone_logs) => {
update.logs.extend(clone_logs);
}
Err(e) => update
.logs
.push(Log::error("cloning repo", format!("{e:#?}"))),
}
}
// if any_option_diff_is_some(&[&diff.repo, &diff.branch, &diff.github_account])
// || option_diff_is_some(&diff.on_clone)
// {
// let server = self.db.get_server(&current_build.server_id).await?;
// match self.periphery.clone_repo(&server, &new_build).await {
// Ok(clone_logs) => {
// update.logs.extend(clone_logs);
// }
// Err(e) => update
// .logs
// .push(Log::error("cloning repo", format!("{e:#?}"))),
// }
// }
update.end_ts = Some(monitor_timestamp());
update.success = all_logs_success(&update.logs);
update.status = UpdateStatus::Complete;
// update.end_ts = Some(monitor_timestamp());
// update.success = all_logs_success(&update.logs);
// update.status = UpdateStatus::Complete;
self.update_update(update).await?;
self.add_update(update).await?;
Ok(new_build)
}
@@ -253,10 +250,7 @@ impl State {
let mut build = self
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
.await?;
let server = self.db.get_server(&build.server_id).await?;
build.version.increment();
let mut update = Update {
target: UpdateTarget::Build(build_id.to_string()),
operation: Operation::BuildBuild,
@@ -267,12 +261,95 @@ impl State {
version: build.version.clone().into(),
..Default::default()
};
update.id = self.add_update(update.clone()).await?;
let (server, aws_client) = if let Some(server_id) = &build.server_id {
let server = self.db.get_server(server_id).await;
if let Err(e) = server {
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
update
.logs
.push(Log::error("get build server", format!("{e:#?}")));
self.update_update(update.clone()).await?;
return Err(e);
}
let server = Ec2Instance {
instance_id: String::new(),
server: server.unwrap(),
};
(server, None)
} else if build.aws_config.is_some() {
let start_ts = monitor_timestamp();
let res = self.create_ec2_instance_for_build(&build).await;
if let Err(e) = res {
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
update.logs.push(Log {
stage: "start build server".to_string(),
stderr: format!("{e:#?}"),
success: false,
start_ts,
end_ts: monitor_timestamp(),
..Default::default()
});
self.update_update(update).await?;
return Err(e);
}
let (server, aws_client, logs) = res.unwrap();
update.logs.extend(logs);
self.update_update(update.clone()).await?;
(server, aws_client)
} else {
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
update.logs.push(Log::error(
"start build",
"build has neither server_id nor aws_config attached".to_string(),
));
self.update_update(update).await?;
return Err(anyhow!(
"build has neither server_id or aws_config attached"
));
};
let clone_success = match self.periphery.clone_repo(&server.server, &build).await {
Ok(clone_logs) => {
update.logs.extend(clone_logs);
true
}
Err(e) => {
update
.logs
.push(Log::error("clone repo", format!("{e:#?}")));
false
}
};
if !clone_success {
let _ = self
.periphery
.delete_repo(&server.server, &build.name)
.await;
if let Some(aws_client) = aws_client {
self.terminate_ec2_instance(aws_client, &server, &mut update)
.await;
}
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
update.success = false;
self.update_update(update.clone()).await?;
return Ok(update);
}
self.update_update(update.clone()).await?;
let build_logs = match self
.periphery
.build(&server, &build)
.build(&server.server, &build)
.await
.context("failed at call to periphery to build")
{
@@ -282,9 +359,9 @@ impl State {
match build_logs {
Some(logs) => {
let success = all_logs_success(&logs);
update.logs.extend(logs);
update.success = all_logs_success(&update.logs);
if update.success {
if success {
let _ = self
.db
.builds
@@ -305,73 +382,200 @@ impl State {
.push(Log::error("build", "builder busy".to_string()));
}
}
let _ = self
.periphery
.delete_repo(&server.server, &build.name)
.await;
if let Some(aws_client) = aws_client {
self.terminate_ec2_instance(aws_client, &server, &mut update)
.await;
}
update.success = all_logs_success(&update.logs);
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
self.update_update(update.clone()).await?;
Ok(update)
}
pub async fn reclone_build(
async fn create_ec2_instance_for_build(
&self,
build_id: &str,
user: &RequestUser,
) -> anyhow::Result<Update> {
if self.build_busy(build_id).await {
return Err(anyhow!("build busy"));
build: &Build,
) -> anyhow::Result<(Ec2Instance, Option<aws::Client>, Vec<Log>)> {
if build.aws_config.is_none() {
return Err(anyhow!("build has no aws_config attached"));
}
{
let mut lock = self.build_action_states.lock().await;
let entry = lock.entry(build_id.to_string()).or_default();
entry.recloning = true;
}
let res = self.reclone_build_inner(build_id, user).await;
{
let mut lock = self.build_action_states.lock().await;
let entry = lock.entry(build_id.to_string()).or_default();
entry.recloning = false;
}
res
}
async fn reclone_build_inner(
&self,
build_id: &str,
user: &RequestUser,
) -> anyhow::Result<Update> {
let build = self
.get_build_check_permissions(build_id, user, PermissionLevel::Update)
.await?;
let server = self.db.get_server(&build.server_id).await?;
let mut update = Update {
target: UpdateTarget::Build(build_id.to_string()),
operation: Operation::RecloneBuild,
start_ts: monitor_timestamp(),
status: UpdateStatus::InProgress,
operator: user.id.clone(),
let start_instance_ts = monitor_timestamp();
let aws_config = build.aws_config.as_ref().unwrap();
let region = aws_config
.region
.as_ref()
.unwrap_or(&self.config.aws.default_region)
.to_string();
let aws_client = create_ec2_client(
region,
&self.config.aws.access_key_id,
self.config.aws.secret_access_key.clone(),
)
.await;
let ami_id = aws_config
.ami_id
.as_ref()
.unwrap_or(&self.config.aws.default_ami_id);
let instance_type = aws_config
.instance_type
.as_ref()
.unwrap_or(&self.config.aws.default_instance_type);
let subnet_id = aws_config
.subnet_id
.as_ref()
.unwrap_or(&self.config.aws.default_subnet_id);
let security_group_ids = aws_config
.security_group_ids
.as_ref()
.unwrap_or(&self.config.aws.default_security_group_ids)
.to_owned();
let readable_sec_group_ids = security_group_ids.join(", ");
let volume_size_gb = *aws_config
.volume_gb
.as_ref()
.unwrap_or(&self.config.aws.default_volume_gb);
let key_pair_name = aws_config
.key_pair_name
.as_ref()
.unwrap_or(&self.config.aws.default_key_pair_name);
let assign_public_ip = *aws_config
.assign_public_ip
.as_ref()
.unwrap_or(&self.config.aws.default_assign_public_ip);
let instance = create_instance_with_ami(
&aws_client,
&format!("BUILDER-{}-v{}", build.name, build.version.to_string()),
ami_id,
instance_type,
subnet_id,
security_group_ids,
volume_size_gb,
key_pair_name,
assign_public_ip,
)
.await?;
let instance_id = &instance.instance_id;
let start_log = Log {
stage: "start build instance".to_string(),
success: true,
stdout: format!("instance id: {instance_id}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_size_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}"),
start_ts: start_instance_ts,
end_ts: monitor_timestamp(),
..Default::default()
};
update.id = self.add_update(update.clone()).await?;
update.success = match self.periphery.clone_repo(&server, &build).await {
Ok(clone_logs) => {
update.logs.extend(clone_logs);
true
let start_connect_ts = monitor_timestamp();
let mut res = Ok(String::new());
for _ in 0..BUILDER_POLL_MAX_TRIES {
let status = self.periphery.health_check(&instance.server).await;
if let Ok(_) = status {
let connect_log = Log {
stage: "build instance connected".to_string(),
success: true,
stdout: "established contact with periphery on builder".to_string(),
start_ts: start_connect_ts,
end_ts: monitor_timestamp(),
..Default::default()
};
return Ok((instance, Some(aws_client), vec![start_log, connect_log]));
}
Err(e) => {
update
.logs
.push(Log::error("clone repo", format!("{e:#?}")));
false
}
};
update.status = UpdateStatus::Complete;
update.end_ts = Some(monitor_timestamp());
self.update_update(update.clone()).await?;
Ok(update)
res = status;
tokio::time::sleep(Duration::from_secs(BUILDER_POLL_RATE_SECS)).await;
}
let _ = terminate_ec2_instance(&aws_client, &instance.instance_id).await;
Err(anyhow!(
"unable to reach periphery agent on build server\n{res:#?}"
))
}
async fn terminate_ec2_instance(
&self,
aws_client: aws::Client,
server: &Ec2Instance,
update: &mut Update,
) {
let res = terminate_ec2_instance(&aws_client, &server.instance_id).await;
if let Err(e) = res {
update
.logs
.push(Log::error("terminate instance", format!("{e:#?}")))
} else {
update.logs.push(Log::simple(
"terminate instance",
format!("terminate instance id {}", server.instance_id),
))
}
}
// pub async fn reclone_build(
// &self,
// build_id: &str,
// user: &RequestUser,
// ) -> anyhow::Result<Update> {
// if self.build_busy(build_id).await {
// return Err(anyhow!("build busy"));
// }
// {
// let mut lock = self.build_action_states.lock().await;
// let entry = lock.entry(build_id.to_string()).or_default();
// entry.recloning = true;
// }
// let res = self.reclone_build_inner(build_id, user).await;
// {
// let mut lock = self.build_action_states.lock().await;
// let entry = lock.entry(build_id.to_string()).or_default();
// entry.recloning = false;
// }
// res
// }
// async fn reclone_build_inner(
// &self,
// build_id: &str,
// user: &RequestUser,
// ) -> anyhow::Result<Update> {
// let build = self
// .get_build_check_permissions(build_id, user, PermissionLevel::Update)
// .await?;
// let server = self.db.get_server(&build.server_id).await?;
// let mut update = Update {
// target: UpdateTarget::Build(build_id.to_string()),
// operation: Operation::RecloneBuild,
// start_ts: monitor_timestamp(),
// status: UpdateStatus::InProgress,
// operator: user.id.clone(),
// success: true,
// ..Default::default()
// };
// update.id = self.add_update(update.clone()).await?;
// update.success = match self.periphery.clone_repo(&server, &build).await {
// Ok(clone_logs) => {
// update.logs.extend(clone_logs);
// true
// }
// Err(e) => {
// update
// .logs
// .push(Log::error("clone repo", format!("{e:#?}")));
// false
// }
// };
// update.status = UpdateStatus::Complete;
// update.end_ts = Some(monitor_timestamp());
// self.update_update(update.clone()).await?;
// Ok(update)
// }
}

View File

@@ -125,7 +125,7 @@ impl State {
} in &new_procedure.stages
{
match operation {
BuildBuild | RecloneBuild => {
BuildBuild => {
self.get_build_check_permissions(&target_id, user, PermissionLevel::Execute)
.await?;
}
@@ -253,13 +253,6 @@ impl State {
.context(format!("failed at build (id: {target_id})"))?;
updates.push(update);
}
RecloneBuild => {
let update = self
.reclone_build(&target_id, user)
.await
.context(format!("failed at reclone build (id: {target_id})"))?;
updates.push(update);
}
// server
PruneImagesServer => {
let update = self.prune_images(&target_id, user).await.context(format!(

View File

@@ -7,8 +7,8 @@ use axum::{
use helpers::handle_anyhow_error;
use mungos::{doc, Deserialize, Document, FindOptions, Serialize};
use types::{
traits::Permissioned, Build, BuildActionState, BuildVersionsReponse, Operation,
PermissionLevel, UpdateStatus,
traits::Permissioned, AwsBuilderConfig, Build, BuildActionState, BuildVersionsReponse,
Operation, PermissionLevel, UpdateStatus,
};
use typeshare::typeshare;
@@ -31,14 +31,12 @@ struct BuildId {
#[derive(Serialize, Deserialize)]
struct CreateBuildBody {
name: String,
server_id: String,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
struct CopyBuildBody {
name: String,
server_id: String,
}
#[typeshare]
@@ -88,7 +86,7 @@ pub fn router() -> Router {
Extension(user): RequestUserExtension,
Json(build): Json<CreateBuildBody>| async move {
let build = state
.create_build(&build.name, build.server_id, &user)
.create_build(&build.name, &user)
.await
.map_err(handle_anyhow_error)?;
response!(Json(build))
@@ -121,7 +119,7 @@ pub fn router() -> Router {
Json(build): Json<CopyBuildBody>| async move {
let build = spawn_request_action(async move {
state
.copy_build(&id, build.name, build.server_id, &user)
.copy_build(&id, build.name, &user)
.await
.map_err(handle_anyhow_error)
})
@@ -181,23 +179,6 @@ pub fn router() -> Router {
},
),
)
.route(
"/:id/reclone",
post(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(build_id): Path<BuildId>| async move {
let update = spawn_request_action(async move {
state
.reclone_build(&build_id.id, &user)
.await
.map_err(handle_anyhow_error)
})
.await??;
response!(Json(update))
},
),
)
.route(
"/:id/action_state",
get(
@@ -227,6 +208,16 @@ pub fn router() -> Router {
},
),
)
.route(
"/aws_builder_defaults",
get(|Extension(state): StateExtension| async move {
Json(AwsBuilderConfig {
access_key_id: String::new(),
secret_access_key: String::new(),
..state.config.aws.clone()
})
}),
)
}
impl State {

View File

@@ -8,10 +8,11 @@ use axum::{
};
use futures_util::future::join_all;
use helpers::handle_anyhow_error;
use mungos::{Deserialize, Document, Serialize};
use mungos::{doc, options::FindOneOptions, Deserialize, Document, Serialize};
use types::{
traits::Permissioned, Deployment, DeploymentActionState, DeploymentWithContainerState,
DockerContainerState, DockerContainerStats, Log, PermissionLevel, Server,
DockerContainerState, DockerContainerStats, Log, Operation, PermissionLevel, Server,
UpdateStatus,
};
use typeshare::typeshare;
@@ -297,15 +298,29 @@ pub fn router() -> Router {
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(deployment_id): Path<DeploymentId>| async move {
Path(DeploymentId { id })| async move {
let stats = state
.get_deployment_container_stats(&deployment_id.id, &user)
.get_deployment_container_stats(&id, &user)
.await
.map_err(handle_anyhow_error)?;
response!(Json(stats))
},
),
)
.route(
"/:id/deployed_version",
get(
|Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Path(DeploymentId { id })| async move {
let version = state
.get_deployment_deployed_version(&id, &user)
.await
.map_err(handle_anyhow_error)?;
response!(version)
},
),
)
}
impl State {
@@ -443,4 +458,53 @@ impl State {
.await?;
Ok(stats)
}
async fn get_deployment_deployed_version(
&self,
id: &str,
user: &RequestUser,
) -> anyhow::Result<String> {
let deployment = self
.get_deployment_check_permissions(&id, &user, PermissionLevel::Read)
.await?;
if deployment.build_id.is_some() {
let latest_deploy_update = self
.db
.updates
.find_one(
doc! {
"target": {
"type": "Deployment",
"id": id
},
"operation": Operation::DeployContainer.to_string(),
"status": UpdateStatus::Complete.to_string(),
"success": true,
},
FindOneOptions::builder().sort(doc! { "_id": -1 }).build(),
)
.await
.context("failed at query to get latest deploy update from mongo")?;
if let Some(update) = latest_deploy_update {
if let Some(version) = update.version {
Ok(version.to_string())
} else {
Ok("latest".to_string())
}
} else {
Ok("latest".to_string())
}
} else {
let split = deployment
.docker_run_args
.image
.split(':')
.collect::<Vec<&str>>();
if let Some(version) = split.get(1) {
Ok(version.to_string())
} else {
Ok("latest".to_string())
}
}
}
}

View File

@@ -74,6 +74,7 @@ impl State {
id: String::from(GITHUB_WEBHOOK_USER_ID),
is_admin: true,
create_server_permissions: false,
create_build_permissions: false,
},
)
.await?;
@@ -103,6 +104,7 @@ impl State {
id: String::from(GITHUB_WEBHOOK_USER_ID),
is_admin: true,
create_server_permissions: false,
create_build_permissions: false,
},
)
.await?;
@@ -127,6 +129,7 @@ impl State {
id: String::from(GITHUB_WEBHOOK_USER_ID),
is_admin: true,
create_server_permissions: false,
create_build_permissions: false,
},
)
.await?;

View File

@@ -33,6 +33,13 @@ struct ModifyUserCreateServerBody {
create_server_permissions: bool,
}
#[typeshare]
#[derive(Serialize, Deserialize)]
struct ModifyUserCreateBuildBody {
user_id: String,
create_build_permissions: bool,
}
pub fn router() -> Router {
Router::new()
.route(
@@ -62,6 +69,15 @@ pub fn router() -> Router {
response!(Json(update))
}),
)
.route(
"/modify_create_build",
post(|state, user, body| async {
let update = modify_user_create_build_permissions(state, user, body)
.await
.map_err(handle_anyhow_error)?;
response!(Json(update))
}),
)
}
async fn update_permissions(
@@ -309,3 +325,58 @@ async fn modify_user_create_server_permissions(
update.id = state.add_update(update.clone()).await?;
Ok(update)
}
async fn modify_user_create_build_permissions(
Extension(state): StateExtension,
Extension(user): RequestUserExtension,
Json(ModifyUserCreateBuildBody {
user_id,
create_build_permissions,
}): Json<ModifyUserCreateBuildBody>,
) -> anyhow::Result<Update> {
if !user.is_admin {
return Err(anyhow!(
"user does not have permissions for this action (not admin)"
));
}
let user = state
.db
.users
.find_one_by_id(&user_id)
.await
.context("failed at mongo query to find target user")?
.ok_or(anyhow!("did not find any user with user_id {user_id}"))?;
state
.db
.users
.update_one::<Document>(
&user_id,
mungos::Update::Set(doc! { "create_build_permissions": create_build_permissions }),
)
.await?;
let update_type = if create_build_permissions {
"enabled"
} else {
"disabled"
};
let ts = monitor_timestamp();
let mut update = Update {
target: UpdateTarget::System,
operation: Operation::ModifyUserCreateBuildPermissions,
logs: vec![Log::simple(
"modify user create build permissions",
format!(
"{update_type} create build permissions for {} (id: {})",
user.username, user.id
),
)],
start_ts: ts.clone(),
end_ts: Some(ts),
status: UpdateStatus::Complete,
success: true,
operator: user.id.clone(),
..Default::default()
};
update.id = state.add_update(update.clone()).await?;
Ok(update)
}

View File

@@ -24,6 +24,7 @@ pub struct RequestUser {
pub id: String,
pub is_admin: bool,
pub create_server_permissions: bool,
pub create_build_permissions: bool,
}
#[derive(Serialize, Deserialize)]
@@ -109,6 +110,7 @@ impl JwtClient {
id: claims.id,
is_admin: user.admin,
create_server_permissions: user.create_server_permissions,
create_build_permissions: user.create_build_permissions,
};
Ok(user)
} else {

View File

@@ -1,5 +1,5 @@
import { useNavigate } from "@solidjs/router";
import { Component, createSignal } from "solid-js";
import { Component, createSignal, Show } from "solid-js";
import { client, pushNotification } from "..";
import { useAppState } from "../state/StateProvider";
import { Build, Deployment } from "../types";
@@ -11,6 +11,7 @@ import Input from "./shared/Input";
import Flex from "./shared/layout/Flex";
import Grid from "./shared/layout/Grid";
import CenterMenu from "./shared/menu/CenterMenu";
import HoverMenu from "./shared/menu/HoverMenu";
import Selector from "./shared/menu/Selector";
const CopyMenu: Component<{
@@ -42,12 +43,11 @@ const CopyMenu: Component<{
if (p.type === "build") {
promise = client.copy_build(p.id, {
name: newName(),
server_id: selectedId(),
});
} else {
promise = client.copy_deployment(p.id, {
name: newName(),
server_id: selectedId(),
server_id: selectedId()!,
});
}
toggleShow();
@@ -59,45 +59,53 @@ const CopyMenu: Component<{
}
};
return (
<CenterMenu
show={show}
toggleShow={toggleShow}
title={`copy ${p.type} | ${name()}`}
target={<Icon type="duplicate" />}
targetClass="blue"
content={() => (
<Grid placeItems="center">
<Flex alignItems="center">
<Input
placeholder="copy name"
class="card dark"
style={{ padding: "0.5rem" }}
value={newName()}
onEdit={setNewName}
/>
<Selector
label="target: "
selected={selectedId()}
items={servers.ids()!}
onSelect={setSelected}
itemMap={(id) => servers.get(id)!.server.name}
targetClass="blue"
targetStyle={{ display: "flex", gap: "0.5rem" }}
searchStyle={{ width: "100%" }}
position="bottom right"
useSearch
/>
</Flex>
<ConfirmButton
class="green"
style={{ width: "100%" }}
onConfirm={copy}
>
copy {p.type}
</ConfirmButton>
</Grid>
)}
position="center"
<HoverMenu
target={
<CenterMenu
show={show}
toggleShow={toggleShow}
title={`copy ${p.type} | ${name()}`}
target={<Icon type="duplicate" />}
targetClass="blue"
content={() => (
<Grid placeItems="center">
<Flex alignItems="center">
<Input
placeholder="copy name"
class="card dark"
style={{ padding: "0.5rem" }}
value={newName()}
onEdit={setNewName}
/>
<Show when={p.type === "deployment"}>
<Selector
label="target: "
selected={selectedId()!}
items={servers.ids()!}
onSelect={setSelected}
itemMap={(id) => servers.get(id)!.server.name}
targetClass="blue"
targetStyle={{ display: "flex", gap: "0.5rem" }}
searchStyle={{ width: "100%" }}
position="bottom right"
useSearch
/>
</Show>
</Flex>
<ConfirmButton
class="green"
style={{ width: "100%" }}
onConfirm={copy}
>
copy {p.type}
</ConfirmButton>
</Grid>
)}
position="center"
/>
}
content={`copy ${p.type}`}
position="bottom center"
/>
);
};

View File

@@ -47,10 +47,10 @@ export const NewDeployment: Component<{ serverID: string }> = (p) => {
);
};
export const NewBuild: Component<{ serverID: string }> = (p) => {
export const NewBuild: Component<{}> = (p) => {
const [showNew, toggleShowNew] = useToggle();
const create = (name: string) => {
client.create_build({ name, server_id: p.serverID });
client.create_build({ name });
};
return (
<Show

View File

@@ -11,31 +11,29 @@ type State = {
const context = createContext<State>();
export const ActionStateProvider: ParentComponent<{}> = (p) => {
export const ActionStateProvider: ParentComponent<{ build_id: string }> = (p) => {
const { ws } = useAppState();
const params = useParams();
const [actions, setActions] = createStore<BuildActionState>({
building: false,
recloning: false,
updating: false,
});
createEffect(() => {
client.get_build_action_state(params.id).then(setActions);
client.get_build_action_state(p.build_id).then(setActions);
});
onCleanup(
ws.subscribe([Operation.BuildBuild], (update) => {
if (update.target.id === params.id) {
if (update.target.id === p.build_id) {
setActions("building", update.status !== UpdateStatus.Complete);
}
})
);
onCleanup(
ws.subscribe([Operation.RecloneBuild], (update) => {
if (update.target.id === params.id) {
setActions("recloning", update.status !== UpdateStatus.Complete);
}
})
);
// onCleanup(
// ws.subscribe([Operation.RecloneBuild], (update) => {
// if (update.target.id === params.id) {
// setActions("recloning", update.status !== UpdateStatus.Complete);
// }
// })
// );
// onCleanup(
// ws.subscribe([DELETE_BUILD], ({ complete, buildID }) => {
// if (buildID === selected.id()) {

View File

@@ -10,21 +10,21 @@ import { useActionStates } from "./ActionStateProvider";
import { client } from "../..";
import { combineClasses, getId } from "../../util/helpers";
import { useParams } from "@solidjs/router";
import { PermissionLevel, ServerStatus } from "../../types";
import { PermissionLevel, ServerStatus, ServerWithStatus } from "../../types";
const Actions: Component<{}> = (p) => {
const { user } = useUser();
const params = useParams() as { id: string };
const { builds, servers } = useAppState();
const build = () => builds.get(params.id)!;
const server = () => build() && servers.get(build()!.server_id);
const server = () => (build() && build().server_id) ? servers.get(build()!.server_id!) : undefined;
const actions = useActionStates();
const userCanExecute = () =>
user().admin ||
build().permissions![getId(user())] === PermissionLevel.Execute ||
build().permissions![getId(user())] === PermissionLevel.Update;
return (
<Show when={userCanExecute() && server()?.status === ServerStatus.Ok}>
<Show when={userCanExecute() && (server() ? server()?.status === ServerStatus.Ok : true)}>
<Grid class={combineClasses("card shadow")} gridTemplateRows="auto 1fr">
<h1>actions</h1>
<Grid style={{ height: "fit-content" }}>
@@ -48,7 +48,7 @@ const Actions: Component<{}> = (p) => {
</ConfirmButton>
</Show>
</Flex>
<Flex class={combineClasses("action shadow")}>
{/* <Flex class={combineClasses("action shadow")}>
reclone{" "}
<Show
when={!actions.recloning}
@@ -67,7 +67,7 @@ const Actions: Component<{}> = (p) => {
<Icon type="reset" />
</ConfirmButton>
</Show>
</Flex>
</Flex> */}
</Grid>
</Grid>
</Show>

View File

@@ -2,9 +2,7 @@ import { useNavigate, useParams } from "@solidjs/router";
import { Component, createEffect, onCleanup, Show } from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import { Operation, PermissionLevel } from "../../types";
import { combineClasses, getId } from "../../util/helpers";
import { Operation } from "../../types";
import NotFound from "../NotFound";
import Grid from "../shared/layout/Grid";
import Actions from "./Actions";
@@ -35,7 +33,7 @@ const Build: Component<{}> = (p) => {
onCleanup(() => unsub);
return (
<Show when={build()} fallback={<NotFound type="build" />}>
<ActionStateProvider>
<ActionStateProvider build_id={params.id}>
<Grid
style={{
width: "100%",

View File

@@ -27,7 +27,8 @@ const Header: Component<{}> = (p) => {
const userCanUpdate = () =>
user().admin ||
build().permissions![getId(user())] === PermissionLevel.Update;
const server = () => servers.get(build().server_id);
const server = () =>
build().server_id ? servers.get(build().server_id!) : undefined;
return (
<>
<Grid
@@ -69,13 +70,15 @@ const Header: Component<{}> = (p) => {
</Flex>
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center">
<A
href={`/server/${build().server_id}`}
class="text-hover"
style={{ opacity: 0.7, padding: 0 }}
>
{server()?.server.name}
</A>
<Show when={server()} fallback={<div style={{ opacity: 0.7 }}>{build().aws_config ? "aws build" : ""}</div>}>
<A
href={`/server/${build().server_id}`}
class="text-hover"
style={{ opacity: 0.7, padding: 0 }}
>
{server()?.server.name}
</A>
</Show>
<div style={{ opacity: 0.7 }}>build</div>
</Flex>
<div style={{ opacity: 0.7 }}>

View File

@@ -45,7 +45,7 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
set(...args);
set("updated", true);
};
const server = () => servers.get(builds.get(params.id)!.server_id);
const server = () => build.server_id ? servers.get(build.server_id) : undefined;
const load = () => {
// console.log("load build");
@@ -54,11 +54,11 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
...build,
repo: build.repo,
branch: build.branch,
on_clone: build.on_clone,
pre_build: build.pre_build,
docker_build_args: build.docker_build_args,
docker_account: build.docker_account,
github_account: build.github_account,
aws_config: build.aws_config,
loaded: true,
updated: false,
saving: false,

View File

@@ -2,12 +2,10 @@ import { useParams } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { PermissionLevel } from "../../../types";
import { getId } from "../../../util/helpers";
import SimpleTabs from "../../shared/tabs/SimpleTabs";
import { Tab } from "../../shared/tabs/Tabs";
import BuildConfig from "./build-config/BuildConfig";
import GitConfig from "./git-config/GitConfig";
import BuilderConfig from "./builder/BuilderConfig";
import BuildConfig from "./config/BuildConfig";
import Owners from "./Permissions";
import { ConfigProvider } from "./Provider";
@@ -24,12 +22,12 @@ const BuildTabs: Component<{}> = (p) => {
tabs={
[
{
title: "repo",
element: () => <GitConfig />,
title: "config",
element: () => <BuildConfig />,
},
{
title: "build",
element: () => <BuildConfig />,
title: "builder",
element: () => <BuilderConfig />
},
user().admin && {
title: "collaborators",

View File

@@ -0,0 +1,116 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const AwsBuilderConfig: Component<{}> = (p) => {
const { build } = useConfig();
return (
<>
<Ami />
<InstanceType />
<VolumeSize />
<Show when={!build.updated}>
<div style={{ height: "4rem" }} />
</Show>
</>
);
};
const Ami: Component = () => {
const { aws_builder_config } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
const get_ami_id = () => {
if (build.aws_config?.ami_id) {
return build.aws_config.ami_id;
} else {
return aws_builder_config()?.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 = () => {
if (aws_builder_config() === undefined) return [];
return Object.keys(aws_builder_config()!.available_ami_accounts!);
};
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>ami</h1>
<Selector
targetClass="blue"
selected={get_ami_id()}
items={ami_ids()}
onSelect={(ami_id) => setBuild("aws_config", "ami_id", ami_id)}
itemMap={get_ami_name}
position="bottom right"
disabled={!userCanUpdate()}
useSearch
/>
</Flex>
);
};
const InstanceType: Component = () => {
const { aws_builder_config } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>instance type</h1>
<Input
placeholder={aws_builder_config()?.default_instance_type}
value={build.aws_config?.instance_type}
onEdit={(instance_type) =>
setBuild("aws_config", "instance_type", instance_type)
}
disabled={!userCanUpdate()}
/>
</Flex>
);
};
const VolumeSize: Component = () => {
const { aws_builder_config } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>volume size</h1>
<Flex gap="0.25rem" alignItems="center">
<Input
style={{ width: "4rem" }}
placeholder={aws_builder_config()?.default_volume_gb?.toString()}
value={
build.aws_config?.volume_gb
? build.aws_config.volume_gb.toString()
: ""
}
onEdit={(volume_size) =>
setBuild("aws_config", "volume_gb", Number(volume_size))
}
disabled={!userCanUpdate()}
/>
GB
</Flex>
</Flex>
);
};
export default AwsBuilderConfig;

View File

@@ -1,43 +1,30 @@
import { Component, Show } from "solid-js";
import { pushNotification, URL } from "../../../..";
import { combineClasses, copyToClipboard, getId } from "../../../../util/helpers";
import ConfirmButton from "../../../shared/ConfirmButton";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Loading from "../../../shared/loading/Loading";
import { useConfig } from "../Provider";
import Git from "./Git";
import OnClone from "./OnClone";
import Loading from "../../../shared/loading/Loading";
import BuilderType from "./BuilderType";
import BuilderServer from "./BuilderServer";
import AwsBuilderConfig from "./AwsBuilderConfig";
const GitConfig: Component<{}> = (p) => {
const BuilderConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
const listenerUrl = () => `${URL}/api/listener/build/${getId(build)}`;
return (
<Show when={build.loaded}>
<Grid class="config">
<Grid class="config-items scroller">
<Git />
<OnClone />
<Show when={userCanUpdate()}>
<Grid class={combineClasses("config-item shadow")}>
<h1>webhook url</h1>
<Flex justifyContent="space-between" alignItems="center">
<div class="ellipsis" style={{ width: "250px" }}>
{listenerUrl()}
</div>
<ConfirmButton
class="blue"
onFirstClick={() => {
copyToClipboard(listenerUrl());
pushNotification("good", "copied url to clipboard");
}}
confirm={<Icon type="check" />}
>
<Icon type="clipboard" />
</ConfirmButton>
</Flex>
</Grid>
<BuilderType />
<Show when={build.server_id}>
<BuilderServer />
<div style={{ height: "12rem" }} />
</Show>
<Show when={build.aws_config}>
<AwsBuilderConfig />
</Show>
<Show when={!build.server_id && !build.aws_config}>
<div style={{ height: "12rem" }} />
</Show>
</Grid>
<Show when={userCanUpdate() && build.updated}>
@@ -66,4 +53,4 @@ const GitConfig: Component<{}> = (p) => {
);
};
export default GitConfig;
export default BuilderConfig;

View File

@@ -0,0 +1,46 @@
import { Component } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import { PermissionLevel } from "../../../../types";
import { getId } from "../../../../util/helpers";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const BuilderServer: Component<{}> = (p) => {
const { servers, getPermissionOnServer } = useAppState();
const { setBuild, server, userCanUpdate } = useConfig();
const availableServers = () => {
if (!servers.loaded()) return [];
return servers
.ids()!
.filter((id) => {
return getPermissionOnServer(id) === PermissionLevel.Update;
});
};
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>builder server</h1>
<Selector
targetClass="blue"
selected={server()?.server ? getId(server()!.server) : "select server"}
items={availableServers()}
onSelect={(server_id) => setBuild("server_id", server_id)}
itemMap={(server_id) =>
server_id === "select server"
? "select server"
: servers.get(server_id)!.server.name
}
disabled={!userCanUpdate()}
position="bottom right"
useSearch
/>
</Flex>
);
};
export default BuilderServer;

View File

@@ -0,0 +1,52 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const BuilderType: Component<{}> = (p) => {
const { servers } = useAppState();
const { build, setBuild, userCanUpdate } = useConfig();
const builderType = () => {
if (build.server_id) {
return "server";
} else if (build.aws_config) {
return "aws";
} else {
return undefined;
}
};
return (
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>builder type</h1>
<Show when={userCanUpdate()} fallback={<h2>{builderType()}</h2>}>
<Selector
targetClass="blue"
selected={builderType() || "select type"}
items={["aws", "server"]}
position="bottom right"
onSelect={(type) => {
if (type !== builderType()) {
if (type === "server") {
const server_id =
servers.ids()?.length || 0 > 0
? servers.ids()![0]
: undefined;
setBuild({ server_id, aws_config: undefined });
} else if (type === "aws") {
setBuild({ server_id: undefined, aws_config: {} });
}
}
}}
/>
</Show>
</Flex>
);
};
export default BuilderType;

View File

@@ -1,12 +1,10 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import {
combineClasses,
parseDotEnvToEnvVars,
parseEnvVarseToDotEnv,
} from "../../../../util/helpers";
import { useToggle } from "../../../../util/hooks";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import CenterMenu from "../../../shared/menu/CenterMenu";
import TextArea from "../../../shared/TextArea";
import { useConfig } from "../Provider";
@@ -14,24 +12,26 @@ import { useConfig } from "../Provider";
const BuildArgs: Component<{}> = (p) => {
const { build, userCanUpdate } = useConfig();
return (
<Grid class={combineClasses("config-item shadow")}>
<Flex alignItems="center" justifyContent="space-between">
<h1>build args</h1>
<Flex alignItems="center" gap="0.2rem">
<Show
when={
!build.docker_build_args?.build_args ||
build.docker_build_args.build_args.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<EditBuildArgs />
</Show>
</Flex>
<Flex
class="config-item shadow"
alignItems="center"
justifyContent="space-between"
>
<h1>build args</h1>
<Flex alignItems="center" gap="0.2rem">
<Show
when={
!build.docker_build_args?.build_args ||
build.docker_build_args.build_args.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<EditBuildArgs />
</Show>
</Flex>
</Grid>
</Flex>
);
};

View File

@@ -9,6 +9,8 @@ import { useConfig } from "../Provider";
import Loading from "../../../shared/loading/Loading";
import BuildArgs from "./BuildArgs";
import Version from "./Version";
import Repo from "./Repo";
import ListenerUrl from "./ListenerUrl";
const BuildConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
@@ -17,9 +19,11 @@ const BuildConfig: Component<{}> = (p) => {
<Grid class="config">
<Grid class="config-items scroller">
<Version />
<Repo />
<Docker />
<BuildArgs />
<CliBuild />
<ListenerUrl />
</Grid>
<Show when={userCanUpdate() && build.updated}>
<Show

View File

@@ -10,15 +10,28 @@ import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const Docker: Component<{}> = (p) => {
const { aws_builder_config } = useAppState();
const { build, setBuild, server, userCanUpdate } = useConfig();
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
const [peripheryDockerAccounts, setPeripheryDockerAccounts] =
createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_server_docker_accounts(build.server_id)
.then(setDockerAccounts);
.get_server_docker_accounts(build.server_id!)
.then(setPeripheryDockerAccounts);
}
});
const dockerAccounts = () => {
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 || []
: [];
} else return [];
};
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>docker build</h1> {/* checkbox here? */}
@@ -62,7 +75,7 @@ const Docker: Component<{}> = (p) => {
<Selector
targetClass="blue"
selected={build.docker_account || "none"}
items={["none", ...dockerAccounts()!]}
items={["none", ...dockerAccounts()]}
onSelect={(account) => {
setBuild(
"docker_account",

View File

@@ -0,0 +1,37 @@
import { Component, Show } from "solid-js";
import { pushNotification, URL } from "../../../..";
import { copyToClipboard, getId } from "../../../../util/helpers";
import ConfirmButton from "../../../shared/ConfirmButton";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
const ListenerUrl: Component<{}> = (p) => {
const { build, userCanUpdate } = useConfig();
const listenerUrl = () => `${URL}/api/listener/build/${getId(build)}`;
return (
<Show when={userCanUpdate()}>
<Grid class="config-item shadow">
<h1>webhook url</h1>
<Flex justifyContent="space-between" alignItems="center">
<div class="ellipsis" style={{ width: "250px" }}>
{listenerUrl()}
</div>
<ConfirmButton
class="blue"
onFirstClick={() => {
copyToClipboard(listenerUrl());
pushNotification("good", "copied url to clipboard");
}}
confirm={<Icon type="check" />}
>
<Icon type="clipboard" />
</ConfirmButton>
</Flex>
</Grid>
</Show>
);
}
export default ListenerUrl;

View File

@@ -0,0 +1,90 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
import Input from "../../../shared/Input";
import { combineClasses } from "../../../../util/helpers";
import { useAppState } from "../../../../state/StateProvider";
import { client } from "../../../..";
import { ServerStatus } from "../../../../types";
import Selector from "../../../shared/menu/Selector";
const Repo: Component<{}> = (p) => {
const { aws_builder_config } = useAppState();
const { build, setBuild, server, userCanUpdate } = useConfig();
const [peripheryGithubAccounts, setPeripheryGithubAccounts] =
createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client
.get_server_github_accounts(build.server_id!)
.then(setPeripheryGithubAccounts);
}
});
const githubAccounts = () => {
if (build.server_id) {
return peripheryGithubAccounts() || [];
} 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 || []
: [];
} else return [];
};
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>repo config</h1>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>repo: </h2>
<Input
placeholder="ie. solidjs/solid"
value={build.repo || ""}
onEdit={(value) => setBuild("repo", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>branch: </h2>
<Input
placeholder="defaults to main"
value={build.branch || (userCanUpdate() ? "" : "main")}
onEdit={(value) => setBuild("branch", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={githubAccounts()}>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>github account: </h2>
<Selector
targetClass="blue"
selected={build.github_account || "none"}
items={["none", ...githubAccounts()]}
onSelect={(account) => {
setBuild(
"github_account",
account === "none" ? undefined : account
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Show>
</Grid>
);
};
export default Repo;

View File

@@ -1,72 +0,0 @@
import { Component, createEffect, createSignal } from "solid-js";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
import Input from "../../../shared/Input";
import Selector from "../../../shared/menu/Selector";
import { combineClasses } from "../../../../util/helpers";
import { client } from "../../../..";
import { ServerStatus } from "../../../../types";
const Git: Component<{}> = (p) => {
const { build, setBuild, server, userCanUpdate } = useConfig();
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
createEffect(() => {
if (server()?.status === ServerStatus.Ok) {
client.get_server_github_accounts(build.server_id).then(setGithubAccounts);
}
});
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>github config</h1>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>repo: </h2>
<Input
placeholder="ie. solidjs/solid"
value={build.repo || ""}
onEdit={(value) => setBuild("repo", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>branch: </h2>
<Input
placeholder="defaults to main"
value={build.branch || (userCanUpdate() ? "" : "main")}
onEdit={(value) => setBuild("branch", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>github account: </h2>
<Selector
targetClass="blue"
selected={build.github_account || "none"}
items={["none", ...githubAccounts()!]}
onSelect={(account) => {
setBuild(
"github_account",
account === "none" ? undefined : account
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>
);
};
export default Git;

View File

@@ -1,63 +0,0 @@
import { Component } from "solid-js";
import { combineClasses } from "../../../../util/helpers";
import Input from "../../../shared/Input";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
const OnClone: Component = () => {
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>on clone</h1>
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
style={{ "flex-wrap": "wrap" }}
>
<h2>path:</h2>
<Input
placeholder="relative to repo"
value={build.on_clone?.path || ""}
onEdit={(path) => {
if (
path.length === 0 &&
(!build.on_clone ||
!build.on_clone.command ||
build.on_clone.command.length === 0)
) {
setBuild("on_clone", undefined);
}
setBuild("on_clone", { path });
}}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
style={{ "flex-wrap": "wrap" }}
>
<h2>command:</h2>
<Input
placeholder="command"
value={build.on_clone?.command || ""}
onEdit={(command) => {
if (
command.length === 0 &&
(!build.on_clone ||
!build.on_clone.path ||
build.on_clone.path.length === 0)
) {
setBuild("on_clone", undefined);
}
setBuild("on_clone", { command });
}}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>
);
};
export default OnClone;

View File

@@ -1,10 +1,11 @@
import { Component, Show } from "solid-js";
import { Component, createResource, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import {
combineClasses,
deploymentHeaderStateClass,
getId,
readableVersion,
} from "../../util/helpers";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
@@ -20,7 +21,7 @@ import CopyMenu from "../CopyMenu";
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
const Header: Component<{}> = (p) => {
const { deployments, servers } = useAppState();
const { deployments, servers, builds } = useAppState();
const params = useParams();
const deployment = () => deployments.get(params.id)!;
const { user } = useUser();
@@ -37,6 +38,27 @@ const Header: Component<{}> = (p) => {
deployment().deployment.permissions![getId(user())] ===
PermissionLevel.Update;
const server = () => servers.get(deployment().deployment.server_id);
const [deployed_version] = createResource(() =>
client.get_deployment_deployed_version(params.id)
);
const 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()}`;
}
} else {
return deployment().deployment.docker_run_args.image || "unknown";
}
};
return (
<>
<Grid
@@ -52,7 +74,10 @@ const Header: Component<{}> = (p) => {
}}
>
<Flex alignItems="center" justifyContent="space-between">
<h1>{deployment()!.deployment.name}</h1>
<Flex alignItems="center">
<h1>{deployment()!.deployment.name}</h1>
<div style={{ opacity: 0.7 }}>{image()}</div>
</Flex>
<Show when={userCanUpdate()}>
<Flex alignItems="center">
<CopyMenu type="deployment" id={params.id} />

View File

@@ -49,9 +49,9 @@ const Config: Component<{}> = () => {
</Show>
<Network />
<Restart />
<Env />
<Ports />
<Mounts />
<Env />
<ExtraArgs />
<PostImage />
<Show when={isMobile()}>

View File

@@ -17,7 +17,7 @@ const Env: Component<{}> = (p) => {
<Grid class={combineClasses("config-item shadow")}>
<Flex alignItems="center" justifyContent="space-between">
<h1>environment</h1>
<Flex alignItems="center" gap="0.2rem">
<Flex alignItems="center">
<Show
when={
!deployment.docker_run_args.environment ||

View File

@@ -22,21 +22,11 @@ const ExtraArgs: Component<{}> = (p) => {
<Grid class="config-item shadow">
<Flex justifyContent="space-between" alignItems="center">
<h1>extra args</h1>
<Flex alignItems="center">
<Show
when={
!deployment.docker_run_args.extra_args ||
deployment.docker_run_args.extra_args.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<For each={[...deployment.docker_run_args.extra_args!.keys()]}>
{(_, index) => (

View File

@@ -20,6 +20,7 @@ const Network: Component<{}> = (p) => {
onSelect={(network) => setDeployment("docker_run_args", { network })}
position="bottom right"
disabled={!userCanUpdate()}
searchStyle={{ width: "100%", "min-width": "12rem" }}
useSearch
/>
</Flex>

View File

@@ -23,21 +23,11 @@ const Volumes: Component<{}> = (p) => {
<Grid class={combineClasses("config-item shadow")}>
<Flex justifyContent="space-between" alignItems="center">
<h1>volumes</h1>
<Flex alignItems="center">
<Show
when={
!deployment.docker_run_args.volumes ||
deployment.docker_run_args.volumes.length === 0
}
>
<div>none</div>
</Show>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<Show when={userCanUpdate()}>
<button class="green" onClick={onAdd}>
<Icon type="plus" />
</button>
</Show>
</Flex>
<For each={deployment.docker_run_args.volumes}>
{({ local, container }, index) => (

View File

@@ -70,7 +70,7 @@ const Log: Component<{
const buffer = useBuffer(scrolled, 250);
const [poll, togglePoll] = useLocalStorageToggle(
"deployment-log-polling",
true
false
);
clearInterval(interval);
interval = setInterval(() => {

View File

@@ -6,6 +6,7 @@ import { useAppState } from "../../state/StateProvider";
import Grid from "../shared/layout/Grid";
import SimpleTabs from "../shared/tabs/SimpleTabs";
import Summary from "./Summary";
import Builds from "./Tree/Builds";
import Groups from "./Tree/Groups";
import { TreeProvider } from "./Tree/Provider";
import Servers from "./Tree/Servers";
@@ -36,6 +37,10 @@ const Home: Component<{}> = (p) => {
title: "servers",
element: () => <Servers serverIDs={servers.ids()!} showAdd />,
},
{
title: "builds",
element: () => <Builds />
}
]}
/>
</TreeProvider>

View File

@@ -0,0 +1,171 @@
import { A } from "@solidjs/router";
import { Component, createMemo, createSignal, For, Show } from "solid-js";
import { client } from "../../..";
import { useAppDimensions } from "../../../state/DimensionProvider";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { PermissionLevel } from "../../../types";
import { getId, readableMonitorTimestamp } from "../../../util/helpers";
import {
ActionStateProvider,
useActionStates,
} from "../../build/ActionStateProvider";
import { NewBuild } from "../../New";
import ConfirmButton from "../../shared/ConfirmButton";
import Icon from "../../shared/Icon";
import Input from "../../shared/Input";
import Flex from "../../shared/layout/Flex";
import Grid from "../../shared/layout/Grid";
import Loading from "../../shared/loading/Loading";
import Selector from "../../shared/menu/Selector";
import { TreeSortType, TREE_SORTS, useTreeState } from "./Provider";
const Builds: Component<{}> = (p) => {
const { user } = useUser();
const { builds } = useAppState();
const { sort, setSort, build_sorter } = useTreeState();
const [buildFilter, setBuildFilter] = createSignal("");
const buildIDs = createMemo(() => {
if (builds.loaded()) {
const filters = buildFilter()
.split(" ")
.filter((term) => term.length > 0)
.map((term) => term.toLowerCase());
return builds
.ids()!
.filter((id) => {
const name = builds.get(id)!.name;
for (const term of filters) {
if (!name.includes(term)) {
return false;
}
}
return true;
})
.sort(build_sorter());
} else {
return undefined;
}
});
return (
<Grid>
<Grid gridTemplateColumns="1fr auto auto">
<Input
placeholder="filter builds"
value={buildFilter()}
onEdit={setBuildFilter}
style={{ width: "100%", padding: "0.5rem" }}
/>
<Selector
selected={sort()}
items={TREE_SORTS as any as string[]}
onSelect={(mode) => setSort(mode as TreeSortType)}
position="bottom right"
targetClass="blue"
targetStyle={{ height: "100%" }}
containerStyle={{ height: "100%" }}
/>
<Show when={user().admin || user().create_build_permissions}>
<NewBuild />
</Show>
</Grid>
<For each={buildIDs()}>
{(id) => (
<ActionStateProvider build_id={id}>
<Build id={id} />
</ActionStateProvider>
)}
</For>
</Grid>
);
};
const Build: Component<{ id: string }> = (p) => {
const { isMobile } = useAppDimensions();
const { user } = useUser();
const { builds, servers } = useAppState();
const build = () => builds.get(p.id)!;
const server = () =>
build().server_id ? servers.get(build().server_id!) : undefined;
const version = () => {
return `v${build().version.major}.${build().version.minor}.${
build().version.patch
}`;
};
const lastBuiltAt = () => {
if (
build().last_built_at === undefined ||
build().last_built_at?.length === 0 ||
build().last_built_at === "never"
) {
return "not built";
} else {
return readableMonitorTimestamp(build().last_built_at!);
}
};
const actions = useActionStates();
const userCanExecute = () =>
user().admin ||
build().permissions![getId(user())] === PermissionLevel.Execute ||
build().permissions![getId(user())] === PermissionLevel.Update;
const isAwsBuild = () => build().aws_config ? true : false;
return (
<A
href={`/build/${p.id}`}
class="card light shadow"
style={{
width: "100%",
height: "fit-content",
"box-sizing": "border-box",
"justify-content": "space-between",
padding: "0.5rem",
}}
>
<h1 style={{ "font-size": "1.25rem" }}>{build().name}</h1>
<Flex alignItems="center">
<Show when={server()}>
<A
href={`/server/${build().server_id!}`}
style={{ padding: 0, opacity: 0.7 }}
>
<div class="text-hover">{server()?.server.name}</div>
</A>
</Show>
<Show when={isAwsBuild()}>
<div style={{ opacity: 0.7 }}>aws build</div>
</Show>
<h2>{version()}</h2>
<Show when={!isMobile()}>
<div style={{ opacity: 0.7 }}>{lastBuiltAt()}</div>
</Show>
<Show when={userCanExecute()}>
<Show
when={!actions.building}
fallback={
<button
class="green"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<Loading type="spinner" />
</button>
}
>
<ConfirmButton
class="green"
onConfirm={() => {
client.build(p.id);
}}
>
<Icon type="build" width="0.9rem" />
</ConfirmButton>
</Show>
</Show>
</Flex>
</A>
);
};
export default Builds;

View File

@@ -6,7 +6,7 @@ export const TREE_SORTS = ["name", "created"] as const;
export type TreeSortType = typeof TREE_SORTS[number];
const value = () => {
const { servers, groups } = useAppState();
const { servers, groups, builds } = useAppState();
const [sort, setSort] = useLocalStorage<TreeSortType>(
TREE_SORTS[0],
"home-sort-v1"
@@ -29,7 +29,7 @@ const value = () => {
}
};
const group_sorter = () => {
if (!groups.loaded) return () => 0;
if (!groups.loaded()) return () => 0;
if (sort() === "name") {
return (a: string, b: string) => {
const ga = groups.get(a)!;
@@ -44,12 +44,30 @@ const value = () => {
} else {
return () => 0;
}
};
const build_sorter = () => {
if (!builds.loaded()) return () => 0;
if (sort() === "name") {
return (a: string, b: string) => {
const ba = builds.get(a)!;
const bb = builds.get(b)!;
if (ba.name < bb.name) {
return -1;
} else if (ba.name > bb.name) {
return 1;
}
return 0;
};
} else {
return () => 0;
}
};
return {
sort,
setSort,
server_sorter,
group_sorter,
build_sorter
};
}

View File

@@ -33,6 +33,7 @@ const Updates: Component<{}> = () => {
? setOperation(undefined)
: setOperation(o.replaceAll(" ", "_") as Operation)
}
targetClass="blue"
targetStyle={{ padding: "0" }}
position="bottom right"
searchStyle={{ width: "15rem" }}

View File

@@ -1,23 +1,46 @@
import { A } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { combineClasses, deploymentStateClass, getId } from "../../util/helpers";
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 } = useAppState();
const { deployments, builds } = useAppState();
const deployment = () => deployments.get(p.id)!;
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";
}
} else {
return deployment().container?.image || "unknown"
}
};
return (
<Show when={deployment()}>
<A
href={`/deployment/${p.id}`}
class={combineClasses(
s.DropdownItem,
)}
>
<h2>{deployment().deployment.name}</h2>
<A href={`/deployment/${p.id}`} class={combineClasses(s.DropdownItem)}>
<Grid gap="0">
<h2>{deployment().deployment.name}</h2>
<div style={{ opacity: 0.7 }}>{image()}</div>
</Grid>
<Flex alignItems="center">
<div style={{ opacity: 0.7 }}>{deployments.status(p.id)}</div>
<Circle

View File

@@ -6,15 +6,14 @@ import SimpleTabs from "../shared/tabs/SimpleTabs";
import s from "./serverchildren.module.scss";
import { useUser } from "../../state/UserProvider";
import { PermissionLevel } from "../../types";
import { NewBuild, NewDeployment } from "../New";
import { NewDeployment } from "../New";
import Deployment from "./Deployment";
import Build from "./Build";
import { useAppState } from "../../state/StateProvider";
const ServerChildren: Component<{ id: string }> = (p) => {
const { user } = useUser();
const { isSemiMobile } = useAppDimensions();
const { servers, deployments, builds } = useAppState();
const { servers, deployments } = useAppState();
const server = () => servers.get(p.id);
const deploymentIDs = createMemo(() => {
return (deployments.loaded() &&
@@ -24,61 +23,79 @@ const ServerChildren: Component<{ id: string }> = (p) => {
(id) => deployments.get(id)?.deployment.server_id === p.id
)) as string[];
});
const buildIDs = createMemo(() => {
return (builds.loaded() &&
builds
.ids()!
.filter((id) => builds.get(id)?.server_id === p.id)) as string[];
});
// const buildIDs = createMemo(() => {
// return (builds.loaded() &&
// builds
// .ids()!
// .filter((id) => builds.get(id)?.server_id === p.id)) as string[];
// });
return (
<SimpleTabs
containerClass="card shadow"
localStorageKey={`${p.id}-home-tab`}
tabs={[
{
title: "deployments",
element: () => (
<Grid
gap=".5rem"
class={combineClasses(s.Deployments)}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
<Show
when={
user().admin ||
server()?.server.permissions![getId(user())] ===
PermissionLevel.Update
}
>
<NewDeployment serverID={p.id} />
</Show>
</Grid>
),
},
{
title: "builds",
element: () => (
<Grid
gap=".5rem"
class={combineClasses(s.Deployments)}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<For each={buildIDs()}>{(id) => <Build id={id} />}</For>
<Show
when={
user().admin ||
server()?.server.permissions![getId(user())] ===
PermissionLevel.Update
}
>
<NewBuild serverID={p.id} />
</Show>
</Grid>
),
},
]}
/>
<div class="card shadow">
<Grid
gap=".5rem"
class={combineClasses(s.Deployments)}
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
>
<For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
<Show
when={
user().admin ||
server()?.server.permissions![getId(user())] ===
PermissionLevel.Update
}
>
<NewDeployment serverID={p.id} />
</Show>
</Grid>
</div>
// <SimpleTabs
// containerClass="card shadow"
// localStorageKey={`${p.id}-home-tab`}
// tabs={[
// {
// title: "deployments",
// element: () => (
// <Grid
// gap=".5rem"
// class={combineClasses(s.Deployments)}
// gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
// >
// <For each={deploymentIDs()}>{(id) => <Deployment id={id} />}</For>
// <Show
// when={
// user().admin ||
// server()?.server.permissions![getId(user())] ===
// PermissionLevel.Update
// }
// >
// <NewDeployment serverID={p.id} />
// </Show>
// </Grid>
// ),
// },
// // {
// // title: "builds",
// // element: () => (
// // <Grid
// // gap=".5rem"
// // class={combineClasses(s.Deployments)}
// // gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
// // >
// // <For each={buildIDs()}>{(id) => <Build id={id} />}</For>
// // <Show
// // when={
// // user().admin ||
// // server()?.server.permissions![getId(user())] ===
// // PermissionLevel.Update
// // }
// // >
// // <NewBuild serverID={p.id} />
// // </Show>
// // </Grid>
// // ),
// // },
// ]}
// />
);
};

View File

@@ -17,6 +17,7 @@ const ConfirmButton: Component<{
onBlur={() => set(false)}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
if (confirm()) {
p.onConfirm && p.onConfirm();
} else {

View File

@@ -1,8 +1,6 @@
import {
Accessor,
Component,
createEffect,
createSignal,
JSX,
JSXElement,
Show,
@@ -27,16 +25,16 @@ const CenterMenu: Component<{
style?: JSX.CSSProperties;
position?: "top" | "center";
}> = (p) => {
const [buffer, set] = createSignal(p.show());
createEffect(() => {
if (p.show()) {
set(true);
} else {
setTimeout(() => {
set(false);
}, 350);
}
});
// const [buffer, set] = createSignal(p.show());
// createEffect(() => {
// if (p.show()) {
// set(true);
// } else {
// setTimeout(() => {
// set(false);
// }, 350);
// }
// });
return (
<>
<button
@@ -49,7 +47,7 @@ const CenterMenu: Component<{
>
{p.target}
</button>
<Show when={buffer()}>
<Show when={p.show()}>
<Child {...p} show={p.show} toggleShow={p.toggleShow} />
</Show>
</>
@@ -69,7 +67,7 @@ const Child: Component<{
useKeyDown("Escape", p.toggleShow);
return (
<Grid
class={combineClasses(s.CenterMenuContainer, p.show() ? s.Enter : s.Exit)}
class={combineClasses(s.CenterMenuContainer)}
onClick={(e) => {
e.stopPropagation();
p.toggleShow();

View File

@@ -22,18 +22,18 @@ const HoverMenu: Component<{
containerStyle?: JSX.CSSProperties;
}> = (p) => {
const [show, set] = createSignal(false);
const [buffer, setBuffer] = createSignal(false);
let timeout: number;
createEffect(() => {
clearTimeout(timeout);
if (show()) {
setBuffer(true);
} else {
timeout = setTimeout(() => {
setBuffer(false);
}, 350);
}
});
// const [buffer, setBuffer] = createSignal(false);
// let timeout: number;
// createEffect(() => {
// clearTimeout(timeout);
// if (show()) {
// setBuffer(true);
// } else {
// timeout = setTimeout(() => {
// setBuffer(false);
// }, 350);
// }
// });
return (
<Flex
class={s.HoverMenuTarget}
@@ -44,13 +44,13 @@ const HoverMenu: Component<{
alignItems="center"
>
{p.target}
<Show when={buffer()}>
<Show when={show()}>
<div
class={combineClasses(
p.contentClass,
getPositionClass(p.position),
s.HoverMenu,
show() ? s.Enter : s.Exit,
// show() ? s.Enter : s.Exit,
)}
onMouseOut={() => {
set(false);
@@ -59,7 +59,7 @@ const HoverMenu: Component<{
set(false)
e.stopPropagation();
}}
style={{ ...p.contentStyle, padding: p.padding }}
style={{ ...p.contentStyle, padding: p.padding || "0.5rem" }}
>
{p.content}
</div>

View File

@@ -1,7 +1,5 @@
import {
Component,
createEffect,
createSignal,
JSX,
JSXElement,
Show,
@@ -22,20 +20,20 @@ const Menu: Component<{
containerStyle?: JSX.CSSProperties;
backgroundColor?: string;
}> = (p) => {
const [buffer, set] = createSignal(p.show);
createEffect(() => {
if (p.show) {
set(true);
} else {
setTimeout(() => {
set(false);
}, 350);
}
});
// const [buffer, set] = createSignal(p.show);
// createEffect(() => {
// if (p.show) {
// set(true);
// } else {
// setTimeout(() => {
// set(false);
// }, 350);
// }
// });
return (
<div class={s.MenuContainer} style={p.containerStyle}>
{p.target}
<Show when={buffer()}>
<Show when={p.show}>
<div
class={s.MenuBackground}
style={{ "background-color": p.backgroundColor }}
@@ -47,7 +45,7 @@ const Menu: Component<{
s.Menu,
"shadow",
getPositionClass(p.position),
p.show ? s.Enter : s.Exit
// p.show ? s.Enter : s.Exit
)}
style={{ padding: p.padding as any, ...p.menuStyle }}
onClick={(e) => e.stopPropagation()}

View File

@@ -16,7 +16,6 @@ const Account: Component<{ close: () => void }> = (p) => {
<Show when={isMobile()}>
<Flex justifyContent="center">{user().username}</Flex>
</Show>
<Flex justifyContent="center">admin: {user().admin.toString()}</Flex>
<Show when={user().admin}>
<A
href="/users"
@@ -27,12 +26,12 @@ const Account: Component<{ close: () => void }> = (p) => {
manage users
</A>
</Show>
<Show when={!user().admin}>
{/* <Show when={!user().admin}>
<Flex justifyContent="center">
create server permissions:{" "}
{user().create_server_permissions.toString()}
{user().create_server_permissions?.toString()}
</Flex>
</Show>
</Show> */}
<A
href="/account"
class="grey"

View File

@@ -17,7 +17,7 @@ import { ControlledTabs } from "../../shared/tabs/Tabs";
import { useAppDimensions } from "../../../state/DimensionProvider";
import Grid from "../../shared/layout/Grid";
import { A, useNavigate } from "@solidjs/router";
import { ServerStatus } from "../../../types";
import { Build, ServerStatus } from "../../../types";
const mobileStyle: JSX.CSSProperties = {
// position: "fixed",
@@ -191,8 +191,10 @@ const Builds: Component<{ close: () => void }> = (p) => {
gap="0.2rem"
style={{ opacity: 0.6, "font-size": "0.9rem" }}
>
{servers.get(build.server_id)?.server.name}
<Icon type="caret-right" width="0.7rem" />
<Show when={build.server_id}>
{build.server_id && servers.get(build.server_id)?.server.name}
<Icon type="caret-right" width="0.7rem" />
</Show>
build
</Flex>
</Grid>

View File

@@ -8,6 +8,7 @@ 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";
@@ -18,9 +19,19 @@ import Loading from "../shared/loading/Loading";
import s from "./users.module.scss";
const Users: Component<{}> = (p) => {
const { isMobile } = useAppDimensions();
const { ws } = useAppState();
const [users, { refetch }] = createResource(() => client.list_users());
onCleanup(ws.subscribe([Operation.ModifyUserEnabled], refetch));
onCleanup(
ws.subscribe(
[
Operation.ModifyUserEnabled,
Operation.ModifyUserCreateServerPermissions,
Operation.ModifyUserCreateBuildPermissions,
],
refetch
)
);
const [search, setSearch] = createSignal("");
const filteredUsers = createMemo(() =>
users()?.filter((user) => user.username.includes(search()))
@@ -34,55 +45,78 @@ const Users: Component<{}> = (p) => {
</Grid>
}
>
<Grid class={s.UsersContent}>
<Grid class={combineClasses(s.Users, "card shadow")}>
<Flex justifyContent="space-between">
<h1>users</h1>
<Input
class="lightgrey"
placeholder="search"
value={search()}
onEdit={setSearch}
/>
</Flex>
<For each={filteredUsers()}>
{(user) => (
<Flex class={combineClasses(s.User, "shadow")}>
<div class={s.Username}>{user.username}</div>
<Flex alignItems="center">
<button
class={user.enabled ? "green" : "red"}
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"}
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>
{/* <ConfirmButton
<Grid
class="card shadow"
style={{ width: "100%", "box-sizing": "border-box" }}
>
<Flex justifyContent="space-between">
<h1>users</h1>
<Input
class="lightgrey"
placeholder="search"
value={search()}
onEdit={setSearch}
/>
</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}
>
<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> */}
</Flex>
</Flex>
)}
</For>
</Grid>
</Grid>
</Flex>
)}
</For>
</Grid>
</Show>
);

View File

@@ -1,5 +1,5 @@
import { useNavigate } from "@solidjs/router";
import { createContext, ParentComponent, useContext } from "solid-js";
import { createContext, createResource, ParentComponent, Resource, useContext } from "solid-js";
import { useWindowKeyDown } from "../util/hooks";
import {
useBuilds,
@@ -14,7 +14,8 @@ import {
} from "./hooks";
import connectToWs from "./ws";
import { useUser } from "./UserProvider";
import { PermissionLevel } from "../types";
import { AwsBuilderConfig, PermissionLevel } from "../types";
import { client } from "..";
export type State = {
usernames: ReturnType<typeof useUsernames>;
@@ -32,6 +33,7 @@ export type State = {
procedures: ReturnType<typeof useProcedures>;
getPermissionOnProcedure: (id: string) => PermissionLevel;
updates: ReturnType<typeof useUpdates>;
aws_builder_config: Resource<AwsBuilderConfig>;
};
const context = createContext<
@@ -51,6 +53,7 @@ export const AppStateProvider: ParentComponent = (p) => {
const procedures = useProcedures();
const deployments = useDeployments();
const usernames = useUsernames();
const [aws_builder_config] = createResource(() => client.get_aws_builder_defaults());
const state: State = {
usernames,
servers,
@@ -129,6 +132,7 @@ export const AppStateProvider: ParentComponent = (p) => {
}
},
updates: useUpdates(),
aws_builder_config,
};
// createEffect(() => {

View File

@@ -2,6 +2,8 @@
Generated by typeshare 1.0.0
*/
export type AvailableAmiAccounts = Record<string, AmiAccounts>;
export type PermissionsMap = Record<string, PermissionLevel>;
export interface Action {
@@ -21,12 +23,12 @@ export interface Build {
_id?: string;
name: string;
permissions?: PermissionsMap;
server_id: string;
server_id?: string;
aws_config?: AwsBuilderBuildConfig;
version: Version;
repo?: string;
branch?: string;
github_account?: string;
on_clone?: Command;
pre_build?: Command;
docker_build_args?: DockerBuildArgs;
docker_account?: string;
@@ -37,7 +39,6 @@ export interface Build {
export interface BuildActionState {
building: boolean;
recloning: boolean;
updating: boolean;
}
@@ -58,6 +59,37 @@ export interface BuildVersionsReponse {
ts: string;
}
export interface AwsBuilderBuildConfig {
region?: string;
instance_type?: string;
ami_id?: string;
volume_gb?: number;
subnet_id?: string;
security_group_ids?: string[];
key_pair_name?: string;
assign_public_ip?: boolean;
}
export interface AwsBuilderConfig {
access_key_id: string;
secret_access_key: string;
default_ami_id: string;
default_subnet_id: string;
default_key_pair_name: string;
available_ami_accounts?: AvailableAmiAccounts;
default_region?: string;
default_volume_gb?: number;
default_instance_type?: string;
default_security_group_ids?: string[];
default_assign_public_ip?: boolean;
}
export interface AmiAccounts {
name: string;
github?: string[];
docker?: string[];
}
export interface Deployment {
_id?: string;
name: string;
@@ -108,6 +140,7 @@ export interface DockerRunArgs {
export interface BasicContainerInfo {
name: string;
id: string;
image: string;
state: DockerContainerState;
status?: string;
}
@@ -327,9 +360,10 @@ export interface Log {
export interface User {
_id?: string;
username: string;
enabled: boolean;
admin: boolean;
create_server_permissions: boolean;
enabled?: boolean;
admin?: boolean;
create_server_permissions?: boolean;
create_build_permissions?: boolean;
avatar?: string;
secrets?: ApiSecret[];
password?: string;
@@ -382,7 +416,6 @@ export enum Operation {
UpdateBuild = "update_build",
DeleteBuild = "delete_build",
BuildBuild = "build_build",
RecloneBuild = "reclone_build",
CreateDeployment = "create_deployment",
UpdateDeployment = "update_deployment",
DeleteDeployment = "delete_deployment",
@@ -400,6 +433,7 @@ export enum Operation {
DeleteGroup = "delete_group",
ModifyUserEnabled = "modify_user_enabled",
ModifyUserCreateServerPermissions = "modify_user_create_server_permissions",
ModifyUserCreateBuildPermissions = "modify_user_create_build_permissions",
ModifyUserPermissions = "modify_user_permissions",
AutoBuild = "auto_build",
AutoPull = "auto_pull",
@@ -449,7 +483,6 @@ export enum ProcedureOperation {
PruneContainersServer = "prune_containers_server",
PruneNetworksServer = "prune_networks_server",
BuildBuild = "build_build",
RecloneBuild = "reclone_build",
DeployContainer = "deploy_container",
StopContainer = "stop_container",
StartContainer = "start_container",

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import fileDownload from "js-file-download";
import { URL } from "..";
import {
AwsBuilderConfig,
BasicContainerInfo,
Build,
BuildActionState,
@@ -38,6 +39,7 @@ import {
CreateSecretBody,
CreateServerBody,
LoginOptions,
ModifyUserCreateBuildBody,
ModifyUserCreateServerBody,
ModifyUserEnabledBody,
PermissionsUpdateBody,
@@ -145,6 +147,10 @@ export class Client {
return this.get(`/api/deployment/${id}/stats`);
}
get_deployment_deployed_version(id: string): Promise<string> {
return this.get(`/api/deployment/${id}/deployed_version`);
}
create_deployment(body: CreateDeploymentBody): Promise<Deployment> {
return this.post("/api/deployment/create", body);
}
@@ -351,6 +357,10 @@ export class Client {
return this.post(`/api/build/${id}/reclone`);
}
get_aws_builder_defaults(): Promise<AwsBuilderConfig> {
return this.get("/api/build/aws_builder_defaults");
}
// procedure
list_procedures(query?: QueryObject): Promise<Procedure[]> {
@@ -454,6 +464,12 @@ export class Client {
return this.post("/api/permissions/modify_create_server", body);
}
modify_user_create_build_permissions(
body: ModifyUserCreateBuildBody
): Promise<Update> {
return this.post("/api/permissions/modify_create_build", body);
}
async get<R = any>(url: string): Promise<R> {
return await axios({
method: "get",

View File

@@ -6,12 +6,10 @@ import { PermissionLevel, PermissionsTarget } from "../types";
export interface CreateBuildBody {
name: string;
server_id: string;
}
export interface CopyBuildBody {
name: string;
server_id: string;
}
export interface BuildVersionsQuery {
@@ -56,6 +54,11 @@ export interface ModifyUserCreateServerBody {
create_server_permissions: boolean;
}
export interface ModifyUserCreateBuildBody {
user_id: string;
create_build_permissions: boolean;
}
export interface CreateProcedureBody {
name: string;
}

View File

@@ -1,12 +1,11 @@
[package]
name = "db_client"
version = "0.1.15"
version = "0.2.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
types = { package = "monitor_types", version = "0.1.15" }
# types = { package = "monitor_types", path = "../types" }
types = { package = "monitor_types", path = "../types" }
mungos = "0.3.0"
anyhow = "1.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_helpers"
version = "0.1.15"
version = "0.2.1"
edition = "2021"
authors = ["MoghTech"]
description = "helpers used as dependency for mogh tech monitor"
@@ -9,10 +9,11 @@ license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# types = { package = "monitor_types", path = "../types" }
types = { package = "monitor_types", version = "0.1.15" }
tokio = "1.25"
types = { package = "monitor_types", path = "../types" }
periphery_client = { path = "../periphery_client" }
async_timing_util = "0.1.14"
bollard = "0.13"
bollard = "0.14.0"
anyhow = "1.0"
axum = { version = "0.6", features = ["ws", "json"] }
serde = "1.0"
@@ -23,3 +24,5 @@ run_command = { version = "0.0.5", features = ["async_tokio"] }
rand = "0.8"
futures = "0.3"
futures-util = "0.3.25"
aws-config = "0.54"
aws-sdk-ec2 = "0.24"

199
lib/helpers/src/aws/mod.rs Normal file
View File

@@ -0,0 +1,199 @@
use std::time::Duration;
use anyhow::{anyhow, Context};
use aws_sdk_ec2::model::{
BlockDeviceMapping, EbsBlockDevice, InstanceNetworkInterfaceSpecification, InstanceStateChange,
InstanceStateName, InstanceStatus, ResourceType, Tag, TagSpecification,
};
pub use aws_sdk_ec2::{
model::InstanceType,
output::{DescribeInstanceStatusOutput, TerminateInstancesOutput},
Client, Region,
};
use types::Server;
pub async fn create_ec2_client(
region: String,
access_key_id: &str,
secret_access_key: String,
) -> Client {
// There may be a better way to pass these keys to client
std::env::set_var("AWS_ACCESS_KEY_ID", access_key_id);
std::env::set_var("AWS_SECRET_ACCESS_KEY", secret_access_key);
let region = Region::new(region);
let config = aws_config::from_env().region(region).load().await;
let client = Client::new(&config);
client
}
pub struct Ec2Instance {
pub instance_id: String,
pub server: Server,
}
const POLL_RATE_SECS: u64 = 2;
const MAX_POLL_TRIES: usize = 30;
/// this will only resolve after the instance is running
/// should still poll the periphery agent after creation
pub async fn create_instance_with_ami(
client: &Client,
instance_name: &str,
ami_id: &str,
instance_type: &str,
subnet_id: &str,
security_group_ids: Vec<String>,
volume_size_gb: i32,
key_pair_name: &str,
assign_public_ip: bool,
) -> anyhow::Result<Ec2Instance> {
let instance_type = InstanceType::from(instance_type);
if let InstanceType::Unknown(t) = instance_type {
return Err(anyhow!("unknown instance type {t:?}"));
}
let res = client
.run_instances()
.image_id(ami_id)
.instance_type(instance_type)
.block_device_mappings(
BlockDeviceMapping::builder()
.set_device_name(String::from("/dev/sda1").into())
.set_ebs(
EbsBlockDevice::builder()
.volume_size(volume_size_gb)
.build()
.into(),
)
.build(),
)
.network_interfaces(
InstanceNetworkInterfaceSpecification::builder()
.subnet_id(subnet_id)
.associate_public_ip_address(assign_public_ip)
.set_groups(security_group_ids.into())
.device_index(0)
.build(),
)
.key_name(key_pair_name)
.tag_specifications(
TagSpecification::builder()
.tags(Tag::builder().key("Name").value(instance_name).build())
.resource_type(ResourceType::Instance)
.build(),
)
.min_count(1)
.max_count(1)
.send()
.await
.context("failed to start builder ec2 instance")?;
let instance = res
.instances()
.ok_or(anyhow!("got None for created instances"))?
.get(0)
.ok_or(anyhow!("instances array is empty"))?;
let instance_id = instance
.instance_id()
.ok_or(anyhow!("instance does not have instance_id"))?
.to_string();
for _ in 0..MAX_POLL_TRIES {
let state_name = get_ec2_instance_state_name(&client, &instance_id).await?;
if state_name == Some(InstanceStateName::Running) {
let ip = if assign_public_ip {
get_ec2_instance_public_ip(client, &instance_id).await?
} else {
instance
.private_ip_address()
.ok_or(anyhow!("instance does not have private ip"))?
.to_string()
};
let server = Server {
address: format!("http://{ip}:8000"),
..Default::default()
};
return Ok(Ec2Instance {
instance_id,
server,
});
}
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
}
Err(anyhow!("instance not running after polling"))
}
pub async fn get_ec2_instance_status(
client: &Client,
instance_id: &str,
) -> anyhow::Result<Option<InstanceStatus>> {
let status = client
.describe_instance_status()
.instance_ids(instance_id)
.send()
.await
.context("failed to get instance status from aws")?
.instance_statuses()
.ok_or(anyhow!("instance statuses is None"))?
.get(0)
.map(|s| s.to_owned());
Ok(status)
}
pub async fn get_ec2_instance_state_name(
client: &Client,
instance_id: &str,
) -> anyhow::Result<Option<InstanceStateName>> {
let status = get_ec2_instance_status(client, instance_id).await?;
if status.is_none() {
return Ok(None);
}
let state = status
.unwrap()
.instance_state()
.ok_or(anyhow!("instance state is None"))?
.name()
.ok_or(anyhow!("instance state name is None"))?
.to_owned();
Ok(Some(state))
}
pub async fn get_ec2_instance_public_ip(
client: &Client,
instance_id: &str,
) -> anyhow::Result<String> {
let ip = client
.describe_instances()
.instance_ids(instance_id)
.send()
.await
.context("failed to get instance status from aws")?
.reservations()
.ok_or(anyhow!("instance reservations is None"))?
.get(0)
.ok_or(anyhow!("instance reservations is empty"))?
.instances()
.ok_or(anyhow!("instances is None"))?
.get(0)
.ok_or(anyhow!("instances is empty"))?
.public_ip_address()
.ok_or(anyhow!("instance has no public ip"))?
.to_string();
Ok(ip)
}
pub async fn terminate_ec2_instance(
client: &Client,
instance_id: &str,
) -> anyhow::Result<InstanceStateChange> {
let res = client
.terminate_instances()
.instance_ids(instance_id)
.send()
.await
.context("failed to terminate instance from aws")?
.terminating_instances()
.ok_or(anyhow!("terminating instances is None"))?
.get(0)
.ok_or(anyhow!("terminating instances is empty"))?
.to_owned();
Ok(res)
}

View File

@@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::{anyhow, Context};
use types::{Build, DockerBuildArgs, EnvironmentVar, Log, Version};
use crate::{all_logs_success, git, run_monitor_command, to_monitor_name};
use crate::{run_monitor_command, to_monitor_name};
use super::docker_login;
@@ -17,9 +17,7 @@ pub async fn build(
name,
version,
docker_build_args,
branch,
docker_account,
pre_build,
..
}: &Build,
mut repo_dir: PathBuf,
@@ -38,25 +36,25 @@ pub async fn build(
.await
.context("failed to login to docker")?;
repo_dir.push(&name);
let pull_logs = git::pull(repo_dir.clone(), branch, &None).await;
if !all_logs_success(&pull_logs) {
logs.extend(pull_logs);
return Ok(logs);
}
logs.extend(pull_logs);
if let Some(command) = pre_build {
let dir = repo_dir.join(&command.path);
let pre_build_log = run_monitor_command(
"pre build",
format!("cd {} && {}", dir.display(), command.command),
)
.await;
if !pre_build_log.success {
logs.push(pre_build_log);
return Ok(logs);
}
logs.push(pre_build_log);
}
// let pull_logs = git::pull(repo_dir.clone(), branch, &None).await;
// if !all_logs_success(&pull_logs) {
// logs.extend(pull_logs);
// return Ok(logs);
// }
// logs.extend(pull_logs);
// if let Some(command) = pre_build {
// let dir = repo_dir.join(&command.path);
// let pre_build_log = run_monitor_command(
// "pre build",
// format!("cd {} && {}", dir.display(), command.command),
// )
// .await;
// if !pre_build_log.success {
// logs.push(pre_build_log);
// return Ok(logs);
// }
// logs.push(pre_build_log);
// }
let build_dir = repo_dir.join(build_path);
let dockerfile_path = match dockerfile_path {
Some(dockerfile_path) => dockerfile_path.to_owned(),

View File

@@ -45,6 +45,7 @@ impl DockerClient {
.pop()
.ok_or(anyhow!("no names on container (empty vec)"))?
.replace("/", ""),
image: s.image.unwrap_or(String::from("unknown")),
state: s.state.unwrap().parse().unwrap(),
status: s.status,
};

View File

@@ -2,47 +2,10 @@ use std::path::PathBuf;
use ::run_command::async_run_command;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use types::{monitor_timestamp, Build, Command, Deployment, GithubToken, GithubUsername, Log};
use types::{monitor_timestamp, CloneArgs, Command, GithubToken, Log};
use crate::{run_monitor_command, to_monitor_name};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CloneArgs {
name: String,
repo: Option<String>,
branch: Option<String>,
on_clone: Option<Command>,
on_pull: Option<Command>,
pub github_account: Option<GithubUsername>,
}
impl From<&Deployment> for CloneArgs {
fn from(d: &Deployment) -> Self {
CloneArgs {
name: d.name.clone(),
repo: d.repo.clone(),
branch: d.branch.clone(),
on_clone: d.on_clone.clone(),
on_pull: d.on_pull.clone(),
github_account: d.github_account.clone(),
}
}
}
impl From<&Build> for CloneArgs {
fn from(b: &Build) -> Self {
CloneArgs {
name: b.name.clone(),
repo: b.repo.clone(),
branch: b.branch.clone(),
on_clone: b.on_clone.clone(),
on_pull: None,
github_account: b.github_account.clone(),
}
}
}
pub async fn pull(
mut path: PathBuf,
branch: &Option<String>,
@@ -120,7 +83,7 @@ async fn clone(
access_token: Option<GithubToken>,
) -> Log {
let _ = std::fs::remove_dir_all(destination);
let access_token = match access_token {
let access_token_at = match &access_token {
Some(token) => format!("{token}@"),
None => String::new(),
};
@@ -128,12 +91,12 @@ async fn clone(
Some(branch) => format!(" -b {branch}"),
None => String::new(),
};
let repo_url = format!("https://{access_token}github.com/{repo}.git");
let repo_url = format!("https://{access_token_at}github.com/{repo}.git");
let command = format!("git clone {repo_url} {destination}{branch}");
let start_ts = monitor_timestamp();
let output = async_run_command(&command).await;
let command = if access_token.len() > 0 {
command.replace(&access_token, "<TOKEN>")
let command = if access_token_at.len() > 0 {
command.replace(&access_token.unwrap(), "<TOKEN>")
} else {
command
};

View File

@@ -1,15 +1,35 @@
use std::{fs::File, io::Read, net::SocketAddr, str::FromStr};
use std::{borrow::Borrow, fs::File, io::Read, net::SocketAddr, str::FromStr};
use anyhow::Context;
use anyhow::{anyhow, Context};
use axum::http::StatusCode;
use rand::{distributions::Alphanumeric, Rng};
use run_command::{async_run_command, CommandOutput};
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
use types::{monitor_timestamp, Log};
pub mod aws;
pub mod docker;
pub mod git;
pub fn parse_config_files<'a, T: DeserializeOwned>(
paths: impl IntoIterator<Item = impl Borrow<String>>,
merge_nested: bool,
extend_array: bool,
) -> anyhow::Result<T> {
let mut target = Map::new();
for path in paths {
target = merge_objects(
target,
parse_config_file(path.borrow())?,
merge_nested,
extend_array,
)?;
}
serde_json::from_str(&serde_json::to_string(&target)?)
.context("failed to parse final config into expected type")
}
pub fn parse_config_file<T: DeserializeOwned>(path: &str) -> anyhow::Result<T> {
let mut file = File::open(&path).expect(&format!("failed to find config at {path}"));
let config = if path.ends_with("toml") {
@@ -25,6 +45,90 @@ pub fn parse_config_file<T: DeserializeOwned>(path: &str) -> anyhow::Result<T> {
Ok(config)
}
/// object is serde_json::Map<String, serde_json::Value>
/// source will overide target
/// will recurse when field is object if merge_object = true, otherwise object will be replaced
/// will extend when field is array if extend_array = true, otherwise array will be replaced
/// will return error when types on source and target fields do not match
fn merge_objects(
mut target: Map<String, Value>,
source: Map<String, Value>,
merge_nested: bool,
extend_array: bool,
) -> anyhow::Result<Map<String, Value>> {
for (key, value) in source {
let curr = target.remove(&key);
if curr.is_none() {
target.insert(key, value);
continue;
}
let curr = curr.unwrap();
match curr {
Value::Object(target_obj) => {
if !merge_nested {
target.insert(key, value);
continue;
}
match value {
Value::Object(source_obj) => {
target.insert(
key,
Value::Object(merge_objects(
target_obj,
source_obj,
merge_nested,
extend_array,
)?),
);
}
_ => {
return Err(anyhow!(
"types on field {key} do not match. got {value:?}, expected object"
))
}
}
}
Value::Array(mut target_arr) => {
if !extend_array {
target.insert(key, value);
continue;
}
match value {
Value::Array(source_arr) => {
target_arr.extend(source_arr);
target.insert(key, Value::Array(target_arr));
}
_ => {
return Err(anyhow!(
"types on field {key} do not match. got {value:?}, expected array"
))
}
}
}
_ => {
target.insert(key, value);
}
}
}
Ok(target)
}
pub fn parse_comma_seperated_list<T: FromStr>(
comma_sep_list: impl Borrow<str>,
) -> anyhow::Result<Vec<T>> {
comma_sep_list
.borrow()
.split(",")
.filter(|item| item.len() > 0)
.map(|item| {
let item = item
.parse()
.map_err(|_| anyhow!("error parsing string {item} into type T"))?;
Ok::<T, anyhow::Error>(item)
})
.collect()
}
pub fn output_into_log(
stage: &str,
command: String,

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_client"
version = "0.1.15"
version = "0.2.1"
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.1.15"
monitor_types = "0.2.1"
# monitor_types = { path = "../types" }
reqwest = { version = "0.11", features = ["json"] }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }

View File

@@ -1,5 +1,5 @@
use anyhow::Context;
use monitor_types::{Build, BuildActionState, BuildVersionsReponse, Update};
use monitor_types::{AwsBuilderConfig, Build, BuildActionState, BuildVersionsReponse, Update};
use serde_json::{json, Value};
use crate::MonitorClient;
@@ -14,6 +14,7 @@ impl MonitorClient {
pub async fn get_build(&self, build_id: &str) -> anyhow::Result<Build> {
self.get(&format!("/api/build/{build_id}"), Option::<()>::None)
.await
.context(format!("failed at getting build {build_id}"))
}
pub async fn get_build_action_state(&self, build_id: &str) -> anyhow::Result<BuildActionState> {
@@ -22,6 +23,9 @@ impl MonitorClient {
Option::<()>::None,
)
.await
.context(format!(
"failed at getting action state for build {build_id}"
))
}
pub async fn get_build_versions(
@@ -37,6 +41,7 @@ impl MonitorClient {
json!({ "page": page, "major": major.into(), "minor": minor.into(), "patch": patch.into() }),
)
.await
.context("failed at getting build versions")
}
pub async fn create_build(&self, name: &str, server_id: &str) -> anyhow::Result<Build> {
@@ -53,18 +58,16 @@ impl MonitorClient {
pub async fn create_full_build(&self, build: &Build) -> anyhow::Result<Build> {
self.post::<&Build, _>("/api/build/create_full", build)
.await
.context(format!("failed at creating full build"))
.context(format!(
"failed at creating full build with name {}",
build.name
))
}
pub async fn copy_build(
&self,
id: &str,
new_name: &str,
new_server_id: &str,
) -> anyhow::Result<Build> {
pub async fn copy_build(&self, id: &str, new_name: &str) -> anyhow::Result<Build> {
self.post(
&format!("/api/build/{id}/copy"),
json!({ "name": new_name, "server_id": new_server_id }),
json!({ "name": new_name }),
)
.await
.context(format!("failed at copying build {id}"))
@@ -88,9 +91,15 @@ impl MonitorClient {
.context(format!("failed at building build {build_id}"))
}
pub async fn reclone_build(&self, id: &str) -> anyhow::Result<Update> {
self.post::<(), _>(&format!("/api/build/{id}/reclone"), None)
pub async fn get_aws_builder_defaults(&self) -> anyhow::Result<AwsBuilderConfig> {
self.get("/api/build/aws_builder_defaults", Option::<()>::None)
.await
.context(format!("failed at recloning build {id}"))
.context("failed at getting aws builder defaults")
}
// pub async fn reclone_build(&self, id: &str) -> anyhow::Result<Update> {
// self.post::<(), _>(&format!("/api/build/{id}/reclone"), None)
// .await
// .context(format!("failed at recloning build {id}"))
// }
}

View File

@@ -35,6 +35,7 @@ impl MonitorClient {
Option::<()>::None,
)
.await
.context("failed at get_deployment_action_state")
}
pub async fn get_deployment_container_log(
@@ -56,7 +57,21 @@ impl MonitorClient {
Option::<()>::None,
)
.await
.context("failed at get_deployment_container_log")
.context("failed at get_deployment_container_stats")
}
pub async fn get_deployment_deployed_version(
&self,
deployment_id: &str,
) -> anyhow::Result<String> {
self.get(
&format!("/api/deployment/{deployment_id}/deployed_version"),
Option::<()>::None,
)
.await
.context(format!(
"failed at get_deployment_deployed_version for id {deployment_id}"
))
}
pub async fn create_deployment(

View File

@@ -1,15 +1,12 @@
[package]
name = "periphery_client"
version = "0.1.15"
version = "0.2.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helpers = { package = "monitor_helpers", version = "0.1.15" }
types = { package = "monitor_types", version = "0.1.15" }
# types = { package = "monitor_types", path = "../types" }
# helpers = { package = "monitor_helpers", path = "../helpers" }
types = { package = "monitor_types", path = "../types" }
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
tokio = "1.25"
reqwest = { version = "0.11", features = ["json"] }

View File

@@ -1,7 +1,6 @@
use anyhow::Context;
use helpers::git::CloneArgs;
use serde_json::json;
use types::{Command, Log, Server};
use types::{CloneArgs, Command, Log, Server};
use crate::PeripheryClient;

View File

@@ -23,7 +23,7 @@ impl PeripheryClient {
pub fn new(passkey: String) -> PeripheryClient {
PeripheryClient {
http_client: Default::default(),
passkey
passkey,
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_types"
version = "0.1.15"
version = "0.2.1"
edition = "2021"
authors = ["MoghTech"]
description = "types for the mogh tech monitor"
@@ -15,7 +15,7 @@ bson = "2.4"
strum = "0.24"
strum_macros = "0.24"
diff-struct = "0.5"
bollard = "0.13"
bollard = "0.14.0"
derive_builder = "0.12"
typeshare = "1.0.0"
chrono = "0.4"

View File

@@ -32,8 +32,13 @@ pub struct Build {
#[builder(setter(skip))]
pub permissions: PermissionsMap,
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub server_id: String, // server which this image should be built on
#[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
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub aws_config: Option<AwsBuilderBuildConfig>,
#[builder(default)]
pub version: Version,
@@ -51,10 +56,6 @@ pub struct Build {
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub github_account: Option<String>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub on_clone: Option<Command>,
// build related
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
@@ -68,7 +69,7 @@ pub struct Build {
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub docker_account: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "String::is_empty")]
#[diff(attr(#[serde(skip)]))]
#[builder(setter(skip))]
pub last_built_at: String,
@@ -87,7 +88,6 @@ pub struct Build {
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BuildActionState {
pub building: bool,
pub recloning: bool,
pub updating: bool,
}
@@ -149,3 +149,40 @@ pub struct BuildVersionsReponse {
pub version: Version,
pub ts: String,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, Diff, Builder)]
#[diff(attr(#[derive(Debug, Serialize, PartialEq)]))]
pub struct AwsBuilderBuildConfig {
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub region: Option<String>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub instance_type: Option<String>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub ami_id: Option<String>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub volume_gb: Option<i32>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub subnet_id: Option<String>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub security_group_ids: Option<Vec<String>>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub key_pair_name: Option<String>,
#[builder(default)]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub assign_public_ip: Option<bool>,
}

View File

@@ -1,6 +1,7 @@
use std::{collections::HashMap, net::IpAddr, path::PathBuf};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::Timelength;
@@ -23,8 +24,8 @@ pub struct CoreConfig {
#[serde(default = "default_core_port")]
pub port: u16,
// jwt config
pub jwt_secret: String,
#[serde(default = "default_jwt_valid_for")]
pub jwt_valid_for: Timelength,
@@ -51,14 +52,16 @@ pub struct CoreConfig {
// enable login with local auth
pub local_auth: bool,
// github integration
pub mongo: MongoConfig,
#[serde(default)]
pub github_oauth: OauthCredentials,
// google integration
#[serde(default)]
pub google_oauth: OauthCredentials,
// mongo config
pub mongo: MongoConfig,
#[serde(default)]
pub aws: AwsBuilderConfig,
}
fn default_core_port() -> u16 {
@@ -96,6 +99,63 @@ fn default_core_mongo_db_name() -> String {
"monitor".to_string()
}
#[typeshare]
#[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")
}
#[typeshare]
pub type AvailableAmiAccounts = HashMap<String, AmiAccounts>; // (ami_id, AmiAccounts)
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AmiAccounts {
pub name: String,
#[serde(default)]
pub github: Vec<String>,
#[serde(default)]
pub docker: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PeripheryConfig {
#[serde(default = "default_periphery_port")]

View File

@@ -176,6 +176,7 @@ fn default_network() -> String {
pub struct BasicContainerInfo {
pub name: String,
pub id: String,
pub image: String,
pub state: DockerContainerState,
pub status: Option<String>,
}

View File

@@ -38,6 +38,16 @@ pub const GITHUB_WEBHOOK_USER_ID: &str = "github";
#[typeshare]
pub type PermissionsMap = HashMap<String, PermissionLevel>;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CloneArgs {
pub name: String,
pub repo: Option<String>,
pub branch: Option<String>,
pub on_clone: Option<Command>,
pub on_pull: Option<Command>,
pub github_account: Option<GithubUsername>,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Diff)]
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
@@ -96,7 +106,6 @@ pub enum Operation {
UpdateBuild,
DeleteBuild,
BuildBuild,
RecloneBuild,
// deployment
CreateDeployment,
@@ -122,6 +131,7 @@ pub enum Operation {
// user
ModifyUserEnabled,
ModifyUserCreateServerPermissions,
ModifyUserCreateBuildPermissions,
ModifyUserPermissions,
// github webhook automation
@@ -272,24 +282,3 @@ pub fn unix_from_monitor_ts(ts: &str) -> anyhow::Result<i64> {
.context("failed to parse rfc3339 timestamp")?
.timestamp_millis())
}
// pub mod i64_to_str {
// use serde::{Deserializer, Serializer};
// pub fn serialize<S>(t: &i64, s: S) -> Result<S::Ok, S::Error> where S: Serializer {
// s.serialize_str(&t.to_string())
// }
// pub fn deserialize<'de, D>(d: D) -> Result<i64, D::Error> where D: Deserializer<'de> {
// let str = d.deserialize_str(StrVisitor)
// }
// }
// struct StrVisitor;
// impl<'de> Visitor<'de> for StrVisitor {
// type Value = &'de str;
// fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
// formatter.write_str("a json string value")
// }
// }

View File

@@ -74,7 +74,6 @@ pub enum ProcedureOperation {
// build
BuildBuild,
RecloneBuild,
// deployment
DeployContainer,

View File

@@ -1,5 +1,5 @@
use crate::{
Build, BuildActionState, Deployment, DeploymentActionState, Group, PermissionLevel,
Build, BuildActionState, CloneArgs, Deployment, DeploymentActionState, Group, PermissionLevel,
PermissionsMap, Procedure, Server, ServerActionState,
};
@@ -65,6 +65,32 @@ impl Busy for DeploymentActionState {
impl Busy for BuildActionState {
fn busy(&self) -> bool {
self.building || self.recloning || self.updating
self.building || self.updating
}
}
impl From<&Deployment> for CloneArgs {
fn from(d: &Deployment) -> Self {
CloneArgs {
name: d.name.clone(),
repo: d.repo.clone(),
branch: d.branch.clone(),
on_clone: d.on_clone.clone(),
on_pull: d.on_pull.clone(),
github_account: d.github_account.clone(),
}
}
}
impl From<&Build> for CloneArgs {
fn from(b: &Build) -> Self {
CloneArgs {
name: b.name.clone(),
repo: b.repo.clone(),
branch: b.branch.clone(),
on_clone: b.pre_build.clone(),
on_pull: None,
github_account: b.github_account.clone(),
}
}
}

View File

@@ -21,15 +21,22 @@ pub struct User {
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub username: String,
#[serde(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub enabled: bool,
#[serde(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub admin: bool,
#[serde(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub create_server_permissions: bool,
#[serde(default)]
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
pub create_build_permissions: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
pub avatar: Option<String>,

View File

@@ -1,6 +1,6 @@
[package]
name = "monitor_periphery"
version = "0.1.15"
version = "0.2.1"
edition = "2021"
authors = ["MoghTech"]
description = "monitor periphery binary | run monitor periphery as system daemon"
@@ -13,10 +13,8 @@ path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# helpers = { package = "monitor_helpers", path = "../lib/helpers" }
# types = { package = "monitor_types", path = "../lib/types" }
helpers = { package = "monitor_helpers", version = "0.1.15" }
types = { package = "monitor_types", version = "0.1.15" }
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"] }
@@ -26,12 +24,12 @@ dotenv = "0.15"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
bollard = "0.13"
bollard = "0.14.0"
anyhow = "1.0"
envy = "0.4"
sysinfo = "0.27.7"
sysinfo = "0.28"
toml = "0.7"
daemonize = "0.4"
clap = { version = "4.0", features = ["derive"] }
daemonize = "0.5.0"
clap = { version = "4.1", features = ["derive"] }
futures-util = "0.3"
tokio-util = "0.7"

View File

@@ -1,10 +1,7 @@
use axum::{routing::post, Extension, Json, Router};
use helpers::{
git::{self, CloneArgs},
handle_anyhow_error, to_monitor_name,
};
use helpers::{git, handle_anyhow_error, to_monitor_name};
use serde::Deserialize;
use types::{Command, Log};
use types::{CloneArgs, Command, Log};
use crate::{helpers::get_github_token, PeripheryConfigExtension};

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use axum::Extension;
use clap::Parser;
use dotenv::dotenv;
use helpers::parse_config_file;
use helpers::{parse_comma_seperated_list, parse_config_files};
use serde::Deserialize;
use types::PeripheryConfig;
@@ -25,9 +25,12 @@ pub struct Args {
#[arg(long, default_value = "~/.monitor/periphery.log.err")]
pub stderr: String,
/// Sets the path of config file to use
/// Sets the path of a config file to use. can use multiple times
#[arg(short, long)]
pub config_path: Option<String>,
pub config_path: Option<Vec<String>>,
#[arg(short, long)]
pub merge_nested_config: bool,
#[arg(short, long)]
pub home_dir: Option<String>,
@@ -39,7 +42,7 @@ pub struct Args {
#[derive(Deserialize)]
struct Env {
#[serde(default = "default_config_path")]
config_path: String,
config_paths: String,
}
pub fn load() -> (Args, u16, PeripheryConfigExtension, HomeDirExtension) {
@@ -51,15 +54,24 @@ pub fn load() -> (Args, u16, PeripheryConfigExtension, HomeDirExtension) {
std::process::exit(0)
}
let home_dir = get_home_dir(&args.home_dir);
let config_path = args
let config_paths = args
.config_path
.as_ref()
.unwrap_or(&env.config_path)
.replace("~", &home_dir);
let config =
parse_config_file::<PeripheryConfig>(&config_path).expect("failed to parse config file");
.unwrap_or(
&parse_comma_seperated_list(env.config_paths)
.expect("failed to parse config paths on environment into comma seperated list"),
)
.into_iter()
.map(|p| p.replace("~", &home_dir))
.collect();
let config = parse_config_files::<PeripheryConfig>(
&config_paths,
args.merge_nested_config,
args.merge_nested_config,
)
.expect("failed at parsing config");
let _ = std::fs::create_dir(&config.repo_dir);
print_startup_log(&config_path, &args, &config);
print_startup_log(config_paths, &args, &config);
(
args,
config.port,
@@ -68,8 +80,8 @@ pub fn load() -> (Args, u16, PeripheryConfigExtension, HomeDirExtension) {
)
}
fn print_startup_log(config_path: &str, args: &Args, config: &PeripheryConfig) {
println!("\nconfig path: {config_path}");
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
.github_accounts
@@ -94,7 +106,7 @@ fn print_startup_log(config_path: &str, args: &Args, config: &PeripheryConfig) {
}
fn default_config_path() -> String {
"/config/periphery.config.toml".to_string()
"~/.monitor/periphery.config.toml".to_string()
}
fn get_home_dir(home_dir_arg: &Option<String>) -> String {

View File

@@ -30,11 +30,13 @@ async fn main() -> anyhow::Result<()> {
// container.container
// );
let update = test_build(&monitor).await?;
println!("build update:\n{update:#?}");
// let update = test_build(&monitor).await?;
// println!("build update:\n{update:#?}");
// test_updates(&monitor).await.unwrap();
let update = test_aws_build(&monitor).await?;
let end_ts = unix_timestamp_ms();
let finished_in = (end_ts - start_ts) as f64 / 1000.0;
println!("\nfinished in {finished_in} s");

View File

@@ -3,8 +3,9 @@ use monitor_client::{
futures_util::StreamExt,
tokio_tungstenite::tungstenite::Message,
types::{
Build, Command, Conversion, Deployment, DeploymentWithContainerState, DockerBuildArgs,
Server, SystemStats, Update,
AwsBuilderBuildConfig, AwsBuilderConfig, Build, BuildBuilder, Command, Conversion,
Deployment, DeploymentWithContainerState, DockerBuildArgs, DockerBuildArgsBuilder, Server,
SystemStats, Update,
},
MonitorClient,
};
@@ -101,13 +102,9 @@ pub async fn test_build(monitor: &MonitorClient) -> anyhow::Result<Update> {
println!("created build. updating...");
build.repo = Some("mbecker20/monitor".to_string());
// build.branch = Some("");
build.on_clone = Some(Command {
path: ".".to_string(),
command: "yarn".to_string(),
});
build.pre_build = Some(Command {
path: "periphery".to_string(),
command: "yarn build".to_string(),
path: ".".to_string(),
command: "yarn && cd periphery && yarn build".to_string(),
});
build.docker_build_args = Some(DockerBuildArgs {
build_path: "periphery".to_string(),
@@ -129,3 +126,27 @@ pub async fn test_updates(monitor: &MonitorClient) -> anyhow::Result<()> {
println!("{build_updates:#?}");
Ok(())
}
pub async fn test_aws_build(monitor: &MonitorClient) -> anyhow::Result<()> {
let build = BuildBuilder::default()
.name("test_monitor".to_string())
.repo(Some(String::from("mbecker20/monitor")))
.branch(Some(String::from("main")))
.docker_account(Some(String::from("mbecker2020")))
.docker_build_args(
DockerBuildArgsBuilder::default()
.build_path(".".to_string())
.dockerfile_path("Dockerfile.core".to_string().into())
.build()
.context("failed to construct DockerBuildArgs struct")?
.into(),
)
.aws_config(AwsBuilderBuildConfig::default().into())
.build()
.context("failed to construct Build struct")?;
let build = monitor.create_full_build(&build).await?;
println!("{build:#?}\n");
let update = monitor.build(&build.id).await?;
println!("{update:#?}");
Ok(())
}