mirror of
https://github.com/moghtech/komodo.git
synced 2026-10-30 21:26:18 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0be07ae1 | ||
|
|
ab945aadde | ||
|
|
c1c461c273 | ||
|
|
336742ee69 | ||
|
|
405dacce1c | ||
|
|
9acd45aa93 | ||
|
|
c889c2cc03 | ||
|
|
7ac91ef416 | ||
|
|
8e28669aa1 | ||
|
|
6cdb91f8b8 | ||
|
|
e892474713 | ||
|
|
abdae98816 | ||
|
|
ab4fe49f33 | ||
|
|
1ace35103b | ||
|
|
dbee729eee | ||
|
|
792576ce59 | ||
|
|
a07624e9b9 | ||
|
|
bb8054af8a | ||
|
|
7738f3e066 | ||
|
|
5dee16a100 | ||
|
|
35f3bcdf2f | ||
|
|
130ca8e1f1 | ||
|
|
ced4c21688 | ||
|
|
6ec7078024 | ||
|
|
b28d8f2506 | ||
|
|
c88a9291a0 | ||
|
|
1e82d19306 | ||
|
|
dd87e50cb2 | ||
|
|
4c8f96a30f | ||
|
|
c4f45e05f1 | ||
|
|
6aa382c7c1 | ||
|
|
ccb9f059e6 | ||
|
|
1cdcea0771 | ||
|
|
88dda0de80 | ||
|
|
30ed99e2b0 | ||
|
|
e5953b7541 | ||
|
|
1f9d01c59f | ||
|
|
cc5210a3d8 | ||
|
|
26559e2d3b | ||
|
|
7eeddb300f | ||
|
|
1e01bae16b | ||
|
|
87c03924e5 | ||
|
|
f0998b1d43 | ||
|
|
1995a04244 | ||
|
|
420fe6bcd5 | ||
|
|
d4e26c0553 | ||
|
|
5f5e7cb45e | ||
|
|
8aa0304738 | ||
|
|
8ec98c33a4 | ||
|
|
2667182ca3 |
Generated
+301
-171
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_cli"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor cli | tools to setup monitor system"
|
||||
|
||||
@@ -10,19 +10,16 @@ port = 9000
|
||||
# daily utc offset in hours to send daily update. eg 8:00 eastern time is 13:00 UTC, so offset should be 13. default of 0 runs at UTC midnight.
|
||||
daily_offset_hours = 13
|
||||
|
||||
# number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily
|
||||
keep_stats_for_days = 120
|
||||
|
||||
# secret used to generate the jwt. should be some randomly generated hash.
|
||||
jwt_secret = "your_jwt_secret"
|
||||
|
||||
# can be 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk, 30-day
|
||||
jwt_valid_for = "1-wk"
|
||||
|
||||
# webhook url given by slack app
|
||||
# webhook url given by slack app that monitor will send alerts and a daily update to
|
||||
slack_url = "your_slack_app_webhook_url"
|
||||
|
||||
# token that has to be given to github during webhook config as the secret
|
||||
# token that has to be given to github during repo webhook config as the secret
|
||||
github_webhook_secret = "your_random_webhook_secret"
|
||||
|
||||
# optional. an alternate base url that is used to recieve github webhook requests. if not provided, will use 'host' address as base
|
||||
@@ -31,30 +28,20 @@ github_webhook_base_url = "https://monitor-github-webhook.mogh.tech"
|
||||
# token used to authenticate core requests to periphery
|
||||
passkey = "your_random_passkey"
|
||||
|
||||
# can be 30-sec, 1-min, 2-min, 5-min
|
||||
# controls the granularity of the system stats collection by monitor core
|
||||
# can be 15-sec, 30-sec, 1-min, 2-min, 5-min
|
||||
monitoring_interval = "1-min"
|
||||
|
||||
# number of days to keep stats around, or 0 to disable pruning. stats older than this number of days are deleted daily
|
||||
keep_stats_for_days = 14
|
||||
|
||||
# these will be used by the GUI to attach to builds. New build docker orgs will default to first org (or none if empty).
|
||||
# when attached to build, image will be pushed to repo under the specified organization
|
||||
docker_organizations = ["your_docker_org1", "your_docker_org_2"]
|
||||
|
||||
# allow or deny user login with username / password
|
||||
local_auth = true
|
||||
|
||||
# these will be given in the GUI to attach to builds. New build docker orgs will default to first org (or none if empty).
|
||||
docker_organizations = ["your_docker_org1", "your_docker_org_2"]
|
||||
|
||||
[aws]
|
||||
access_key_id = "your_aws_key_id"
|
||||
secret_access_key = "your_aws_secret_key"
|
||||
default_region = "us-east-1"
|
||||
default_ami_id = "your_periphery_ami"
|
||||
default_key_pair_name = "your_default_key_pair_name"
|
||||
default_instance_type = "m5.2xlarge"
|
||||
default_volume_gb = 8
|
||||
default_subnet_id = "your_default_subnet_id"
|
||||
default_security_group_ids = ["sg_id_1", "sg_id_2"]
|
||||
default_assign_public_ip = false
|
||||
|
||||
[aws.available_ami_accounts]
|
||||
your_periphery_ami = { name = "default ami", github = ["github_username"], docker = ["docker_username"] }
|
||||
|
||||
[github_oauth]
|
||||
enabled = true
|
||||
id = "your_github_client_id"
|
||||
@@ -68,4 +55,19 @@ secret = "your_google_client_secret"
|
||||
[mongo]
|
||||
uri = "your_mongo_uri"
|
||||
app_name = "monitor_core"
|
||||
db_name = "monitor"
|
||||
db_name = "monitor" # this is the name of the mongo database that monitor will create its collections in.
|
||||
|
||||
[aws]
|
||||
access_key_id = "your_aws_key_id"
|
||||
secret_access_key = "your_aws_secret_key"
|
||||
default_region = "us-east-1"
|
||||
default_ami_name = "your_ami_name" # must be defined below in [aws.available_ami_accounts]
|
||||
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_key_pair_name = "your_default_key_pair_name"
|
||||
default_assign_public_ip = false
|
||||
|
||||
[aws.available_ami_accounts]
|
||||
your_ami_name = { ami_id = "ami-1234567890", github = ["github_username"], docker = ["docker_username"] }
|
||||
|
||||
+13
-14
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "core"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -11,17 +11,16 @@ types = { package = "monitor_types", path = "../lib/types" }
|
||||
db = { package = "db_client", path = "../lib/db_client" }
|
||||
periphery = { package = "periphery_client", path = "../lib/periphery_client" }
|
||||
axum_oauth2 = { path = "../lib/axum_oauth2" }
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
tokio-util = "0.7"
|
||||
tokio = { version = "1.28", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.19", features=["native-tls"] }
|
||||
tokio-util = { version = "0.7"}
|
||||
axum = { version = "0.6", features = ["ws", "json"] }
|
||||
axum-extra = { version = "0.5.0", features = ["spa"] }
|
||||
tower = { version = "0.4", features = ["full"] }
|
||||
tower-http = { version = "0.4.0", features = ["cors"] }
|
||||
tower = { version = "0.4", features = ["timeout"] }
|
||||
tower-http = { version = "0.4", features = ["fs", "cors"] }
|
||||
slack = { package = "slack_client_rs", version = "0.0.8" }
|
||||
mungos = "0.3.14"
|
||||
futures-util = "0.3"
|
||||
mungos = "0.3.19"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
dotenv = "0.15"
|
||||
envy = "0.4"
|
||||
@@ -31,10 +30,10 @@ jwt = "0.16"
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
async_timing_util = "0.1.14"
|
||||
futures-util = "0.3"
|
||||
diff-struct = "0.5"
|
||||
typeshare = "1.0.0"
|
||||
typeshare = "1.0.1"
|
||||
hex = "0.4"
|
||||
aws-config = "0.55"
|
||||
aws-sdk-ec2 = "0.26"
|
||||
merge_config_files = "0.1.3"
|
||||
aws-config = "0.55.2"
|
||||
aws-sdk-ec2 = "0.27.0"
|
||||
merge_config_files = "0.1.3"
|
||||
termination_signal = "0.1.2"
|
||||
+110
-35
@@ -3,13 +3,12 @@ use std::time::Duration;
|
||||
use anyhow::{anyhow, Context};
|
||||
use aws_sdk_ec2::Client;
|
||||
use diff::Diff;
|
||||
use futures_util::future::join_all;
|
||||
use helpers::{all_logs_success, to_monitor_name};
|
||||
use mungos::{doc, to_bson};
|
||||
use mungos::mongodb::bson::{doc, to_bson};
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
AwsBuilderBuildConfig, Build, Log, Operation, PermissionLevel, Update, UpdateStatus,
|
||||
UpdateTarget, Version,
|
||||
monitor_timestamp, traits::Permissioned, AwsBuilderBuildConfig, Build, DockerContainerState,
|
||||
Log, Operation, PermissionLevel, Update, UpdateStatus, UpdateTarget, Version,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -42,13 +41,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_busy(&self, id: &str) -> bool {
|
||||
match self.build_action_states.lock().await.get(id) {
|
||||
Some(a) => a.busy(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
@@ -116,7 +108,7 @@ impl State {
|
||||
}
|
||||
|
||||
pub async fn delete_build(&self, build_id: &str, user: &RequestUser) -> anyhow::Result<Build> {
|
||||
if self.build_busy(build_id).await {
|
||||
if self.build_action_states.busy(build_id).await {
|
||||
return Err(anyhow!("build busy"));
|
||||
}
|
||||
let build = self
|
||||
@@ -146,21 +138,23 @@ impl State {
|
||||
new_build: Build,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Build> {
|
||||
if self.build_busy(&new_build.id).await {
|
||||
if self.build_action_states.busy(&new_build.id).await {
|
||||
return Err(anyhow!("build busy"));
|
||||
}
|
||||
let id = new_build.id.clone();
|
||||
{
|
||||
let mut lock = self.build_action_states.lock().await;
|
||||
let entry = lock.entry(id.clone()).or_default();
|
||||
entry.updating = true;
|
||||
}
|
||||
self.build_action_states
|
||||
.update_entry(id.clone(), |entry| {
|
||||
entry.updating = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.update_build_inner(new_build, user).await;
|
||||
{
|
||||
let mut lock = self.build_action_states.lock().await;
|
||||
let entry = lock.entry(id).or_default();
|
||||
entry.updating = false;
|
||||
}
|
||||
|
||||
self.build_action_states
|
||||
.update_entry(id.clone(), |entry| {
|
||||
entry.updating = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -251,20 +245,20 @@ impl State {
|
||||
}
|
||||
|
||||
pub async fn build(&self, build_id: &str, user: &RequestUser) -> anyhow::Result<Update> {
|
||||
if self.build_busy(build_id).await {
|
||||
if self.build_action_states.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.building = true;
|
||||
}
|
||||
self.build_action_states
|
||||
.update_entry(build_id.to_string(), |entry| {
|
||||
entry.building = true;
|
||||
})
|
||||
.await;
|
||||
let res = self.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.building = false;
|
||||
}
|
||||
self.build_action_states
|
||||
.update_entry(build_id.to_string(), |entry| {
|
||||
entry.building = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -415,6 +409,8 @@ impl State {
|
||||
.await;
|
||||
}
|
||||
|
||||
self.handle_post_build_redeploy(build_id, &mut update).await;
|
||||
|
||||
update.success = all_logs_success(&update.logs);
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
@@ -424,6 +420,85 @@ impl State {
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
async fn handle_post_build_redeploy(&self, build_id: &str, update: &mut Update) {
|
||||
let redeploy_deployments = self
|
||||
.db
|
||||
.deployments
|
||||
.get_some(
|
||||
doc! { "build_id": build_id, "redeploy_on_build": true },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(deployments) = redeploy_deployments {
|
||||
let futures = deployments.into_iter().map(|d| async move {
|
||||
let request_user = RequestUser {
|
||||
id: "auto redeploy".to_string(),
|
||||
is_admin: true,
|
||||
..Default::default()
|
||||
};
|
||||
let state = self
|
||||
.get_deployment_with_container_state(&request_user, &d.id)
|
||||
.await
|
||||
.map(|r| r.state)
|
||||
.unwrap_or_default();
|
||||
if state == DockerContainerState::Running {
|
||||
Some((
|
||||
d.id.clone(),
|
||||
self.deploy_container(
|
||||
&d.id,
|
||||
&RequestUser {
|
||||
id: "auto redeploy".to_string(),
|
||||
is_admin: true,
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let redeploy_results = join_all(futures).await;
|
||||
|
||||
let mut redeploys = Vec::<String>::new();
|
||||
let mut redeploy_failures = Vec::<String>::new();
|
||||
|
||||
for res in redeploy_results {
|
||||
if res.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (id, res) = res.unwrap();
|
||||
match res {
|
||||
Ok(_) => redeploys.push(id),
|
||||
Err(e) => redeploy_failures.push(format!("{id}: {e:#?}")),
|
||||
}
|
||||
}
|
||||
|
||||
if redeploys.len() > 0 {
|
||||
update.logs.push(Log::simple(
|
||||
"redeploy",
|
||||
format!("redeployed deployments: {}", redeploys.join(", ")),
|
||||
))
|
||||
}
|
||||
|
||||
if redeploy_failures.len() > 0 {
|
||||
update.logs.push(Log::simple(
|
||||
"redeploy failures",
|
||||
redeploy_failures.join("\n"),
|
||||
))
|
||||
}
|
||||
} else if let Err(e) = redeploy_deployments {
|
||||
update.logs.push(Log::simple(
|
||||
"redeploys failed",
|
||||
format!("failed to get deployments to redeploy: {e:#?}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_ec2_instance_for_build(
|
||||
&self,
|
||||
build: &Build,
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use diff::Diff;
|
||||
use helpers::all_logs_success;
|
||||
use types::{
|
||||
monitor_timestamp, traits::Permissioned, Log, Operation, PeripheryCommand,
|
||||
PeripheryCommandBuilder, PermissionLevel, Update, UpdateStatus, UpdateTarget,
|
||||
};
|
||||
|
||||
use crate::{auth::RequestUser, state::State};
|
||||
|
||||
impl State {
|
||||
pub async fn get_command_check_permissions(
|
||||
&self,
|
||||
command_id: &str,
|
||||
user: &RequestUser,
|
||||
permission_level: PermissionLevel,
|
||||
) -> anyhow::Result<PeripheryCommand> {
|
||||
let command = self.db.get_command(command_id).await?;
|
||||
let permissions = command.get_user_permissions(&user.id);
|
||||
if user.is_admin || permissions >= permission_level {
|
||||
Ok(command)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"user does not have required permissions on this command"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_command(
|
||||
&self,
|
||||
name: &str,
|
||||
server_id: String,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<PeripheryCommand> {
|
||||
self.get_server_check_permissions(&server_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let command = PeripheryCommandBuilder::default()
|
||||
.name(name.to_string())
|
||||
.server_id(server_id)
|
||||
.build()
|
||||
.context("failed to build command")?;
|
||||
let command_id = self
|
||||
.db
|
||||
.commands
|
||||
.create_one(command)
|
||||
.await
|
||||
.context("failed at adding command to db")?;
|
||||
let command = self.db.get_command(&command_id).await?;
|
||||
let update = Update {
|
||||
target: UpdateTarget::Command(command_id),
|
||||
operation: Operation::CreateCommand,
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
self.add_update(update).await?;
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
pub async fn create_full_command(
|
||||
&self,
|
||||
mut command: PeripheryCommand,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<PeripheryCommand> {
|
||||
command.id = self
|
||||
.create_command(&command.name, command.server_id.clone(), user)
|
||||
.await?
|
||||
.id;
|
||||
let command = self.update_command(command, user).await?;
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
pub async fn copy_command(
|
||||
&self,
|
||||
target_id: &str,
|
||||
new_name: String,
|
||||
new_server_id: String,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<PeripheryCommand> {
|
||||
let mut command = self
|
||||
.get_command_check_permissions(target_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
command.name = new_name;
|
||||
command.server_id = new_server_id;
|
||||
let command = self.create_full_command(command, user).await?;
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
pub async fn delete_command(
|
||||
&self,
|
||||
command_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<PeripheryCommand> {
|
||||
if self.command_action_states.busy(command_id).await {
|
||||
return Err(anyhow!("command busy"));
|
||||
}
|
||||
let command = self
|
||||
.get_command_check_permissions(command_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
self.db.commands.delete_one(command_id).await?;
|
||||
let update = Update {
|
||||
target: UpdateTarget::Command(command_id.to_string()),
|
||||
operation: Operation::DeleteCommand,
|
||||
start_ts,
|
||||
end_ts: Some(monitor_timestamp()),
|
||||
operator: user.id.clone(),
|
||||
logs: vec![Log::simple(
|
||||
"delete command",
|
||||
format!("deleted command {}", command.name),
|
||||
)],
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
self.add_update(update).await?;
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
pub async fn update_command(
|
||||
&self,
|
||||
mut new_command: PeripheryCommand,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<PeripheryCommand> {
|
||||
let current_command = self
|
||||
.get_command_check_permissions(&new_command.id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
|
||||
// none of these should be changed through this method
|
||||
new_command.permissions = current_command.permissions.clone();
|
||||
new_command.server_id = current_command.server_id.clone();
|
||||
new_command.created_at = current_command.created_at.clone();
|
||||
new_command.updated_at = start_ts.clone();
|
||||
|
||||
self.db
|
||||
.commands
|
||||
.update_one(
|
||||
&new_command.id,
|
||||
mungos::Update::Regular(new_command.clone()),
|
||||
)
|
||||
.await
|
||||
.context("failed at update one command")?;
|
||||
|
||||
let diff = current_command.diff(&new_command);
|
||||
|
||||
let update = Update {
|
||||
operation: Operation::UpdateCommand,
|
||||
target: UpdateTarget::Command(new_command.id.clone()),
|
||||
start_ts,
|
||||
status: UpdateStatus::Complete,
|
||||
logs: vec![Log::simple(
|
||||
"command update",
|
||||
serde_json::to_string_pretty(&diff).unwrap(),
|
||||
)],
|
||||
operator: user.id.clone(),
|
||||
success: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.add_update(update.clone()).await?;
|
||||
|
||||
self.update_update(update).await?;
|
||||
|
||||
Ok(new_command)
|
||||
}
|
||||
|
||||
pub async fn run_command(
|
||||
&self,
|
||||
command_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.command_action_states.busy(command_id).await {
|
||||
return Err(anyhow!("command busy"));
|
||||
}
|
||||
self.command_action_states
|
||||
.update_entry(command_id.to_string(), |entry| {
|
||||
entry.running = true;
|
||||
})
|
||||
.await;
|
||||
let res = self.run_command_inner(command_id, user).await;
|
||||
self.command_action_states
|
||||
.update_entry(command_id.to_string(), |entry| {
|
||||
entry.running = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
async fn run_command_inner(
|
||||
&self,
|
||||
command_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
let start_ts = monitor_timestamp();
|
||||
let command = self
|
||||
.get_command_check_permissions(command_id, user, PermissionLevel::Execute)
|
||||
.await?;
|
||||
|
||||
if command.command.path.is_empty() || command.command.command.is_empty() {
|
||||
return Err(anyhow!("command or path is empty, aborting"));
|
||||
}
|
||||
|
||||
let server = self.db.get_server(&command.server_id).await?;
|
||||
|
||||
let mut update = Update {
|
||||
target: UpdateTarget::Command(command_id.to_string()),
|
||||
operation: Operation::RunCommand,
|
||||
start_ts,
|
||||
status: UpdateStatus::InProgress,
|
||||
success: true,
|
||||
operator: user.id.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
match self.periphery.run_command(&server, &command.command).await {
|
||||
Ok(log) => {
|
||||
update.logs.push(log);
|
||||
}
|
||||
Err(e) => {
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("clone repo", format!("{e:#?}")));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+185
-123
@@ -1,12 +1,11 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use diff::Diff;
|
||||
use helpers::{all_logs_success, to_monitor_name};
|
||||
use mungos::doc;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
Deployment, DeploymentWithContainerState, DockerContainerState, Log, Operation,
|
||||
PermissionLevel, ServerStatus, ServerWithStatus, Update, UpdateStatus, UpdateTarget,
|
||||
monitor_timestamp, traits::Permissioned, Deployment, DeploymentBuilder,
|
||||
DeploymentWithContainerState, DockerContainerState, Log, Operation, PermissionLevel,
|
||||
ServerStatus, ServerWithStatus, TerminationSignal, Update, UpdateStatus, UpdateTarget,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -33,13 +32,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn deployment_busy(&self, id: &str) -> bool {
|
||||
match self.deployment_action_states.lock().await.get(id) {
|
||||
Some(a) => a.busy(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_deployment(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -49,16 +41,18 @@ impl State {
|
||||
self.get_server_check_permissions(&server_id, user, PermissionLevel::Update)
|
||||
.await?;
|
||||
let start_ts = monitor_timestamp();
|
||||
let deployment = Deployment {
|
||||
name: to_monitor_name(name),
|
||||
server_id,
|
||||
permissions: [(user.id.clone(), PermissionLevel::Update)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
created_at: start_ts.clone(),
|
||||
updated_at: start_ts.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut deployment = DeploymentBuilder::default()
|
||||
.name(to_monitor_name(name))
|
||||
.server_id(server_id)
|
||||
.build()
|
||||
.context("failed to build deployment")?;
|
||||
deployment.permissions = [(user.id.clone(), PermissionLevel::Update)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
deployment.created_at = start_ts.clone();
|
||||
deployment.updated_at = start_ts.clone();
|
||||
|
||||
let deployment_id = self
|
||||
.db
|
||||
.deployments
|
||||
@@ -112,8 +106,10 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Deployment> {
|
||||
if self.deployment_busy(deployment_id).await {
|
||||
if self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
let deployment = self
|
||||
@@ -123,7 +119,7 @@ impl State {
|
||||
let server = self.db.get_server(&deployment.server_id).await?;
|
||||
let log = match self
|
||||
.periphery
|
||||
.container_remove(&server, &deployment.name)
|
||||
.container_remove(&server, &deployment.name, stop_signal, stop_time)
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
@@ -164,21 +160,25 @@ impl State {
|
||||
new_deployment: Deployment,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Deployment> {
|
||||
if self.deployment_busy(&new_deployment.id).await {
|
||||
if self.deployment_action_states.busy(&new_deployment.id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
let id = new_deployment.id.clone();
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(id.clone()).or_default();
|
||||
entry.updating = true;
|
||||
}
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(id.clone(), |entry| {
|
||||
entry.updating = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.update_deployment_inner(new_deployment, user).await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(id).or_default();
|
||||
entry.updating = false;
|
||||
}
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(id.clone(), |entry| {
|
||||
entry.updating = false;
|
||||
})
|
||||
.await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
@@ -282,22 +282,25 @@ impl State {
|
||||
new_name: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(&deployment_id).await {
|
||||
if self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.renaming = true;
|
||||
}
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.renaming = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self
|
||||
.rename_deployment_inner(deployment_id, new_name, user)
|
||||
.await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.renaming = false;
|
||||
}
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.renaming = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -367,7 +370,9 @@ impl State {
|
||||
self.update_update(update).await?;
|
||||
return Err(deployment_state.err().unwrap());
|
||||
}
|
||||
let DeploymentWithContainerState { state, .. } = deployment_state.unwrap();
|
||||
let DeploymentWithContainerState {
|
||||
deployment, state, ..
|
||||
} = deployment_state.unwrap();
|
||||
if state != DockerContainerState::NotDeployed {
|
||||
let log = self
|
||||
.periphery
|
||||
@@ -406,7 +411,6 @@ impl State {
|
||||
)
|
||||
.await
|
||||
.context("failed to update deployment name on mongo");
|
||||
|
||||
if let Err(e) = res {
|
||||
update
|
||||
.logs
|
||||
@@ -418,6 +422,20 @@ impl State {
|
||||
))
|
||||
}
|
||||
|
||||
if deployment.repo.is_some() {
|
||||
let res = self.reclone_deployment(deployment_id, user, false).await;
|
||||
if let Err(e) = res {
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("reclone repo", format!("{e:?}")));
|
||||
} else {
|
||||
update.logs.push(Log::simple(
|
||||
"reclone repo",
|
||||
"deployment repo cloned with new name".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
update.end_ts = monitor_timestamp().into();
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.success = all_logs_success(&update.logs);
|
||||
@@ -431,21 +449,25 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
check_deployment_busy: bool,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(deployment_id).await {
|
||||
if check_deployment_busy && self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.recloning = true;
|
||||
}
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.recloning = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.reclone_deployment_inner(deployment_id, user).await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.recloning = false;
|
||||
}
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.recloning = false;
|
||||
})
|
||||
.await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
@@ -469,19 +491,18 @@ impl State {
|
||||
};
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
update.success = match self.periphery.clone_repo(&server, &deployment).await {
|
||||
match self.periphery.clone_repo(&server, &deployment).await {
|
||||
Ok(clone_logs) => {
|
||||
update.logs.extend(clone_logs);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
update
|
||||
.logs
|
||||
.push(Log::error("clone repo", format!("{e:#?}")));
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
update.success = all_logs_success(&update.logs);
|
||||
update.status = UpdateStatus::Complete;
|
||||
update.end_ts = Some(monitor_timestamp());
|
||||
|
||||
@@ -494,21 +515,27 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(deployment_id).await {
|
||||
if self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.deploying = true;
|
||||
}
|
||||
let res = self.deploy_container_inner(deployment_id, user).await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.deploying = false;
|
||||
}
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.deploying = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self
|
||||
.deploy_container_inner(deployment_id, user, stop_signal, stop_time)
|
||||
.await;
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.deploying = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -516,6 +543,8 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Update> {
|
||||
let start_ts = monitor_timestamp();
|
||||
let mut deployment = self
|
||||
@@ -553,7 +582,14 @@ impl State {
|
||||
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
let deploy_log = match self.periphery.deploy(&server, &deployment).await {
|
||||
let stop_signal = stop_signal.unwrap_or(deployment.termination_signal).into();
|
||||
let stop_time = stop_time.unwrap_or(deployment.termination_timeout).into();
|
||||
|
||||
let deploy_log = match self
|
||||
.periphery
|
||||
.deploy(&server, &deployment, stop_signal, stop_time)
|
||||
.await
|
||||
{
|
||||
Ok(log) => log,
|
||||
Err(e) => Log::error("deploy container", format!("{e:#?}")),
|
||||
};
|
||||
@@ -573,20 +609,22 @@ impl State {
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(deployment_id).await {
|
||||
if self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.starting = true;
|
||||
}
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.starting = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.start_container_inner(deployment_id, user).await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.starting = false;
|
||||
}
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.starting = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -642,21 +680,27 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(deployment_id).await {
|
||||
if self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.stopping = true;
|
||||
}
|
||||
let res = self.stop_container_inner(deployment_id, user).await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.stopping = false;
|
||||
}
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.stopping = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self
|
||||
.stop_container_inner(deployment_id, user, stop_signal, stop_time)
|
||||
.await;
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.stopping = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -664,6 +708,8 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Update> {
|
||||
let start_ts = monitor_timestamp();
|
||||
let deployment = self
|
||||
@@ -681,9 +727,12 @@ impl State {
|
||||
};
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
let stop_signal = stop_signal.unwrap_or(deployment.termination_signal).into();
|
||||
let stop_time = stop_time.unwrap_or(deployment.termination_timeout).into();
|
||||
|
||||
let log = self
|
||||
.periphery
|
||||
.container_stop(&server, &deployment.name)
|
||||
.container_stop(&server, &deployment.name, stop_signal, stop_time)
|
||||
.await;
|
||||
|
||||
update.success = match log {
|
||||
@@ -712,21 +761,27 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(deployment_id).await {
|
||||
if self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.removing = true;
|
||||
}
|
||||
let res = self.remove_container_inner(deployment_id, user).await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.removing = false;
|
||||
}
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.removing = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self
|
||||
.remove_container_inner(deployment_id, user, stop_signal, stop_time)
|
||||
.await;
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.removing = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -734,6 +789,8 @@ impl State {
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Update> {
|
||||
let start_ts = monitor_timestamp();
|
||||
let deployment = self
|
||||
@@ -751,9 +808,12 @@ impl State {
|
||||
};
|
||||
update.id = self.add_update(update.clone()).await?;
|
||||
|
||||
let stop_signal = stop_signal.unwrap_or(deployment.termination_signal).into();
|
||||
let stop_time = stop_time.unwrap_or(deployment.termination_timeout).into();
|
||||
|
||||
let log = self
|
||||
.periphery
|
||||
.container_remove(&server, &deployment.name)
|
||||
.container_remove(&server, &deployment.name, stop_signal, stop_time)
|
||||
.await;
|
||||
|
||||
update.success = match log {
|
||||
@@ -783,20 +843,22 @@ impl State {
|
||||
deployment_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.deployment_busy(deployment_id).await {
|
||||
if self.deployment_action_states.busy(deployment_id).await {
|
||||
return Err(anyhow!("deployment busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.pulling = true;
|
||||
}
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.pulling = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.pull_deployment_repo_inner(deployment_id, user).await;
|
||||
{
|
||||
let mut lock = self.deployment_action_states.lock().await;
|
||||
let entry = lock.entry(deployment_id.to_string()).or_default();
|
||||
entry.pulling = false;
|
||||
}
|
||||
|
||||
self.deployment_action_states
|
||||
.update_entry(deployment_id.to_string(), |entry| {
|
||||
entry.pulling = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use types::Update;
|
||||
use crate::state::State;
|
||||
|
||||
mod build;
|
||||
mod command;
|
||||
mod deployment;
|
||||
mod group;
|
||||
mod procedure;
|
||||
|
||||
@@ -209,7 +209,7 @@ impl State {
|
||||
}
|
||||
StopContainer => {
|
||||
let update = self
|
||||
.stop_container(&target_id, user)
|
||||
.stop_container(&target_id, user, Option::None, Option::None)
|
||||
.await
|
||||
.context(format!(
|
||||
"failed at stop container for deployment (id: {target_id})"
|
||||
@@ -218,7 +218,7 @@ impl State {
|
||||
}
|
||||
RemoveContainer => {
|
||||
let update = self
|
||||
.remove_container(&target_id, user)
|
||||
.remove_container(&target_id, user, Option::None, Option::None)
|
||||
.await
|
||||
.context(format!(
|
||||
"failed at remove container for deployment (id: {target_id})"
|
||||
@@ -227,7 +227,7 @@ impl State {
|
||||
}
|
||||
DeployContainer => {
|
||||
let update = self
|
||||
.deploy_container(&target_id, user)
|
||||
.deploy_container(&target_id, user, Option::None, Option::None)
|
||||
.await
|
||||
.context(format!(
|
||||
"failed at deploy container for deployment (id: {target_id})"
|
||||
@@ -236,14 +236,18 @@ impl State {
|
||||
}
|
||||
RecloneDeployment => {
|
||||
let update = self
|
||||
.reclone_deployment(&target_id, user)
|
||||
.reclone_deployment(&target_id, user, true)
|
||||
.await
|
||||
.context(format!("failed at reclone deployment (id: {target_id})"))?;
|
||||
updates.push(update);
|
||||
}
|
||||
PullDeployment => {
|
||||
// implement this one
|
||||
// let update = self.pull
|
||||
let update = self
|
||||
.pull_deployment_repo(&target_id, user)
|
||||
.await
|
||||
.context(format!("failed at pull deployment (id: {target_id})"))?;
|
||||
updates.push(update);
|
||||
}
|
||||
// build
|
||||
BuildBuild => {
|
||||
|
||||
+46
-48
@@ -1,11 +1,10 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use diff::Diff;
|
||||
use futures_util::future::join_all;
|
||||
use mungos::doc;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use types::{
|
||||
monitor_timestamp,
|
||||
traits::{Busy, Permissioned},
|
||||
Log, Operation, PermissionLevel, Server, Update, UpdateStatus, UpdateTarget,
|
||||
monitor_timestamp, traits::Permissioned, Log, Operation, PermissionLevel, Server, Update,
|
||||
UpdateStatus, UpdateTarget,
|
||||
};
|
||||
|
||||
use crate::{auth::RequestUser, state::State};
|
||||
@@ -28,13 +27,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn server_busy(&self, id: &str) -> bool {
|
||||
match self.server_action_states.lock().await.get(id) {
|
||||
Some(a) => a.busy(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_server(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -96,7 +88,7 @@ impl State {
|
||||
server_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Server> {
|
||||
if self.server_busy(server_id).await {
|
||||
if self.server_action_states.busy(server_id).await {
|
||||
return Err(anyhow!("server busy"));
|
||||
}
|
||||
let server = self
|
||||
@@ -121,14 +113,14 @@ impl State {
|
||||
.get_some(doc! { "server_id": server_id }, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|d| async move { self.delete_deployment(&d.id, user).await });
|
||||
.map(|d| async move { self.delete_deployment(&d.id, user, None, None).await });
|
||||
let delete_builds = self
|
||||
.db
|
||||
.builds
|
||||
.get_some(doc! { "server_id": server_id }, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|d| async move { self.delete_deployment(&d.id, user).await });
|
||||
.map(|d| async move { self.delete_deployment(&d.id, user, None, None).await });
|
||||
let update_groups = self
|
||||
.db
|
||||
.groups
|
||||
@@ -164,7 +156,7 @@ impl State {
|
||||
mut new_server: Server,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Server> {
|
||||
if self.server_busy(&new_server.id).await {
|
||||
if self.server_action_states.busy(&new_server.id).await {
|
||||
return Err(anyhow!("server busy"));
|
||||
}
|
||||
let current_server = self
|
||||
@@ -208,20 +200,22 @@ impl State {
|
||||
server_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.server_busy(server_id).await {
|
||||
if self.server_action_states.busy(server_id).await {
|
||||
return Err(anyhow!("server busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.server_action_states.lock().await;
|
||||
let entry = lock.entry(server_id.to_string()).or_default();
|
||||
entry.pruning_networks = true;
|
||||
}
|
||||
self.server_action_states
|
||||
.update_entry(server_id.to_string(), |entry| {
|
||||
entry.pruning_networks = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.prune_networks_inner(server_id, user).await;
|
||||
{
|
||||
let mut lock = self.server_action_states.lock().await;
|
||||
let entry = lock.entry(server_id.to_string()).or_default();
|
||||
entry.pruning_networks = false;
|
||||
}
|
||||
|
||||
self.server_action_states
|
||||
.update_entry(server_id.to_string(), |entry| {
|
||||
entry.pruning_networks = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -269,20 +263,22 @@ impl State {
|
||||
server_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.server_busy(server_id).await {
|
||||
if self.server_action_states.busy(server_id).await {
|
||||
return Err(anyhow!("server busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.server_action_states.lock().await;
|
||||
let entry = lock.entry(server_id.to_string()).or_default();
|
||||
entry.pruning_images = true;
|
||||
}
|
||||
self.server_action_states
|
||||
.update_entry(server_id.to_string(), |entry| {
|
||||
entry.pruning_images = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.prune_images_inner(server_id, user).await;
|
||||
{
|
||||
let mut lock = self.server_action_states.lock().await;
|
||||
let entry = lock.entry(server_id.to_string()).or_default();
|
||||
entry.pruning_images = false;
|
||||
}
|
||||
|
||||
self.server_action_states
|
||||
.update_entry(server_id.to_string(), |entry| {
|
||||
entry.pruning_images = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -331,20 +327,22 @@ impl State {
|
||||
server_id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Update> {
|
||||
if self.server_busy(server_id).await {
|
||||
if self.server_action_states.busy(server_id).await {
|
||||
return Err(anyhow!("server busy"));
|
||||
}
|
||||
{
|
||||
let mut lock = self.server_action_states.lock().await;
|
||||
let entry = lock.entry(server_id.to_string()).or_default();
|
||||
entry.pruning_containers = true;
|
||||
}
|
||||
self.server_action_states
|
||||
.update_entry(server_id.to_string(), |entry| {
|
||||
entry.pruning_containers = true;
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = self.prune_containers_inner(server_id, user).await;
|
||||
{
|
||||
let mut lock = self.server_action_states.lock().await;
|
||||
let entry = lock.entry(server_id.to_string()).or_default();
|
||||
entry.pruning_containers = false;
|
||||
}
|
||||
|
||||
self.server_action_states
|
||||
.update_entry(server_id.to_string(), |entry| {
|
||||
entry.pruning_containers = false;
|
||||
})
|
||||
.await;
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
+138
-23
@@ -1,18 +1,27 @@
|
||||
use std::{cmp::Ordering, collections::HashMap};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
routing::{delete, get, patch, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use futures_util::TryStreamExt;
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize, Document, FindOptions, Serialize};
|
||||
use mungos::mongodb::{
|
||||
bson::{doc, Document},
|
||||
options::FindOptions,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::{
|
||||
traits::Permissioned, AwsBuilderConfig, Build, BuildActionState, BuildVersionsReponse,
|
||||
Operation, PermissionLevel, UpdateStatus,
|
||||
monitor_ts_from_unix, traits::Permissioned, unix_from_monitor_ts, AwsBuilderConfig, Build,
|
||||
BuildActionState, BuildVersionsReponse, Operation, PermissionLevel, UpdateStatus,
|
||||
};
|
||||
use typeshare::typeshare;
|
||||
|
||||
const NUM_VERSIONS_PER_PAGE: u64 = 10;
|
||||
const ONE_DAY_MS: i64 = 86400000;
|
||||
|
||||
use crate::{
|
||||
auth::{RequestUser, RequestUserExtension},
|
||||
@@ -49,12 +58,35 @@ pub struct BuildVersionsQuery {
|
||||
patch: Option<i32>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BuildStatsQuery {
|
||||
#[serde(default)]
|
||||
page: u32,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BuildStatsResponse {
|
||||
pub total_time: f64, // in hours
|
||||
pub total_count: f64, // number of builds
|
||||
pub days: Vec<BuildStatsDay>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct BuildStatsDay {
|
||||
pub time: f64,
|
||||
pub count: f64,
|
||||
pub ts: f64,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/:id",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(build_id): Path<BuildId>| async move {
|
||||
let build = state
|
||||
@@ -68,7 +100,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/list",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Query(query): Query<Document>| async move {
|
||||
let builds = state
|
||||
@@ -82,7 +114,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/create",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Json(build): Json<CreateBuildBody>| async move {
|
||||
let build = state
|
||||
@@ -96,7 +128,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/create_full",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Json(build): Json<Build>| async move {
|
||||
let build = spawn_request_action(async move {
|
||||
@@ -113,7 +145,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/copy",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(BuildId { id }): Path<BuildId>,
|
||||
Json(build): Json<CopyBuildBody>| async move {
|
||||
@@ -131,7 +163,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/delete",
|
||||
delete(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(build_id): Path<BuildId>| async move {
|
||||
let build = spawn_request_action(async move {
|
||||
@@ -148,7 +180,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/update",
|
||||
patch(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Json(build): Json<Build>| async move {
|
||||
let build = spawn_request_action(async move {
|
||||
@@ -165,7 +197,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/build",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(build_id): Path<BuildId>| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
@@ -182,7 +214,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/action_state",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(BuildId { id }): Path<BuildId>| async move {
|
||||
let action_state = state
|
||||
@@ -196,7 +228,7 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/versions",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
|state: StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(BuildId { id }),
|
||||
Query(query): Query<BuildVersionsQuery>| async move {
|
||||
@@ -210,7 +242,7 @@ pub fn router() -> Router {
|
||||
)
|
||||
.route(
|
||||
"/aws_builder_defaults",
|
||||
get(|Extension(state): StateExtension| async move {
|
||||
get(|state: StateExtension| async move {
|
||||
Json(AwsBuilderConfig {
|
||||
access_key_id: String::new(),
|
||||
secret_access_key: String::new(),
|
||||
@@ -220,10 +252,17 @@ pub fn router() -> Router {
|
||||
)
|
||||
.route(
|
||||
"/docker_organizations",
|
||||
get(|Extension(state): StateExtension| async move {
|
||||
get(|state: StateExtension| async move {
|
||||
Json(state.config.docker_organizations.clone())
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/stats",
|
||||
get(|state: StateExtension, query: Query<BuildStatsQuery>| async move {
|
||||
let stats = state.get_build_stats(query.page).await.map_err(handle_anyhow_error)?;
|
||||
response!(Json(stats))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -258,17 +297,11 @@ impl State {
|
||||
) -> anyhow::Result<BuildActionState> {
|
||||
self.get_build_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let action_state = self
|
||||
.build_action_states
|
||||
.lock()
|
||||
.await
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.clone();
|
||||
let action_state = self.build_action_states.get_or_default(id).await;
|
||||
Ok(action_state)
|
||||
}
|
||||
|
||||
pub async fn get_build_versions(
|
||||
async fn get_build_versions(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &RequestUser,
|
||||
@@ -317,4 +350,86 @@ impl State {
|
||||
.collect();
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
async fn get_build_stats(&self, page: u32) -> anyhow::Result<BuildStatsResponse> {
|
||||
let curr_ts = unix_timestamp_ms() as i64;
|
||||
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
|
||||
|
||||
let close_ts = next_day - page as i64 * 30 * ONE_DAY_MS;
|
||||
let open_ts = close_ts - 30 * ONE_DAY_MS;
|
||||
|
||||
let mut build_updates = self
|
||||
.db
|
||||
.updates
|
||||
.collection
|
||||
.find(
|
||||
doc! {
|
||||
"start_ts": {
|
||||
"$gte": monitor_ts_from_unix(open_ts)
|
||||
.context("open_ts out of bounds")?,
|
||||
"$lt": monitor_ts_from_unix(close_ts)
|
||||
.context("close_ts out of bounds")?
|
||||
},
|
||||
"operation": Operation::BuildBuild.to_string(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut days = HashMap::<i64, BuildStatsDay>::with_capacity(32);
|
||||
|
||||
let mut curr = open_ts;
|
||||
|
||||
while curr < close_ts {
|
||||
let stats = BuildStatsDay {
|
||||
ts: curr as f64,
|
||||
..Default::default()
|
||||
};
|
||||
days.insert(curr, stats);
|
||||
curr += ONE_DAY_MS;
|
||||
}
|
||||
|
||||
while let Some(update) = build_updates.try_next().await? {
|
||||
if let Some(end_ts) = update.end_ts {
|
||||
let start_ts = unix_from_monitor_ts(&update.start_ts)
|
||||
.context("failed to parse update start_ts")?;
|
||||
let end_ts =
|
||||
unix_from_monitor_ts(&end_ts).context("failed to parse update end_ts")?;
|
||||
let day = start_ts - start_ts % ONE_DAY_MS;
|
||||
let mut entry = days.entry(day).or_default();
|
||||
entry.count += 1.0;
|
||||
entry.time += ms_to_hour(end_ts - start_ts);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BuildStatsResponse::new(days.into_values().collect()))
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildStatsResponse {
|
||||
fn new(mut days: Vec<BuildStatsDay>) -> BuildStatsResponse {
|
||||
days.sort_by(|a, b| {
|
||||
if a.ts < b.ts {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}
|
||||
});
|
||||
let mut total_time = 0.0;
|
||||
let mut total_count = 0.0;
|
||||
for day in &days {
|
||||
total_time += day.time;
|
||||
total_count += day.count;
|
||||
}
|
||||
BuildStatsResponse {
|
||||
total_time,
|
||||
total_count,
|
||||
days,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0;
|
||||
fn ms_to_hour(duration: i64) -> f64 {
|
||||
duration as f64 / MS_TO_HOUR_DIVISOR
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
routing::{delete, get, patch, post},
|
||||
Json, Router,
|
||||
};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::mongodb::bson::Document;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::{traits::Permissioned, CommandActionState, PeripheryCommand, PermissionLevel};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
api::spawn_request_action,
|
||||
auth::{RequestUser, RequestUserExtension},
|
||||
response,
|
||||
state::{State, StateExtension},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CommandId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateCommandBody {
|
||||
name: String,
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CopyCommandBody {
|
||||
name: String,
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/:id",
|
||||
get(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(CommandId { id })| async move {
|
||||
let command = state
|
||||
.get_command_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(command))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/list",
|
||||
get(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Query(query): Query<Document>| async move {
|
||||
let commands = state
|
||||
.list_commands(&user, query)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(commands))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/create",
|
||||
post(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Json(command): Json<CreateCommandBody>| async move {
|
||||
let command = state
|
||||
.create_command(&command.name, command.server_id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(command))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/create_full",
|
||||
post(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Json(command): Json<PeripheryCommand>| async move {
|
||||
let command = spawn_request_action(async move {
|
||||
state
|
||||
.create_full_command(command, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
.await??;
|
||||
response!(Json(command))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/copy",
|
||||
post(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(CommandId { id }),
|
||||
Json(command): Json<CopyCommandBody>| async move {
|
||||
let command = spawn_request_action(async move {
|
||||
state
|
||||
.copy_command(&id, command.name, command.server_id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
.await??;
|
||||
response!(Json(command))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/delete",
|
||||
delete(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(CommandId { id })| async move {
|
||||
let build = spawn_request_action(async move {
|
||||
state
|
||||
.delete_command(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
.await??;
|
||||
response!(Json(build))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/update",
|
||||
patch(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Json(command): Json<PeripheryCommand>| async move {
|
||||
let command = spawn_request_action(async move {
|
||||
state
|
||||
.update_command(command, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
.await??;
|
||||
response!(Json(command))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/action_state",
|
||||
get(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(CommandId { id })| async move {
|
||||
let action_state = state
|
||||
.get_command_action_states(id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(action_state))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/run",
|
||||
post(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(CommandId { id })| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.run_command(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
.await??;
|
||||
response!(Json(update))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
impl State {
|
||||
async fn list_commands(
|
||||
&self,
|
||||
user: &RequestUser,
|
||||
query: impl Into<Option<Document>>,
|
||||
) -> anyhow::Result<Vec<PeripheryCommand>> {
|
||||
let commands: Vec<PeripheryCommand> = self
|
||||
.db
|
||||
.commands
|
||||
.get_some(query, None)
|
||||
.await
|
||||
.context("failed at get all commands query")?
|
||||
.into_iter()
|
||||
.filter(|s| {
|
||||
if user.is_admin {
|
||||
true
|
||||
} else {
|
||||
let permissions = s.get_user_permissions(&user.id);
|
||||
permissions != PermissionLevel::None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
async fn get_command_action_states(
|
||||
&self,
|
||||
id: String,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<CommandActionState> {
|
||||
self.get_command_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let action_state = self.command_action_states.get_or_default(id).await;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
+72
-63
@@ -4,15 +4,19 @@ use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
routing::{delete, get, patch, post},
|
||||
Extension, Json, Router,
|
||||
Json, Router,
|
||||
};
|
||||
use futures_util::future::join_all;
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, options::FindOneOptions, Deserialize, Document, Serialize};
|
||||
use mungos::mongodb::{
|
||||
bson::{doc, Document},
|
||||
options::FindOneOptions,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::{
|
||||
traits::Permissioned, Deployment, DeploymentActionState, DeploymentWithContainerState,
|
||||
DockerContainerState, DockerContainerStats, Log, Operation, PermissionLevel, Server,
|
||||
UpdateStatus,
|
||||
TerminationSignal, UpdateStatus,
|
||||
};
|
||||
use typeshare::typeshare;
|
||||
|
||||
@@ -55,16 +59,23 @@ pub struct GetContainerLogQuery {
|
||||
tail: Option<u32>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Deserialize)]
|
||||
pub struct StopContainerQuery {
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/:id",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id })| async move {
|
||||
let res = state
|
||||
.get_deployment_with_container_state(&user, &deployment_id.id)
|
||||
.get_deployment_with_container_state(&user, &id)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(res))
|
||||
@@ -74,8 +85,8 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/list",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Query(query): Query<Document>| async move {
|
||||
let deployments = state
|
||||
.list_deployments_with_container_state(&user, query)
|
||||
@@ -88,8 +99,8 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/create",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Json(deployment): Json<CreateDeploymentBody>| async move {
|
||||
let deployment = state
|
||||
.create_deployment(&deployment.name, deployment.server_id, &user)
|
||||
@@ -102,8 +113,8 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/create_full",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Json(full_deployment): Json<Deployment>| async move {
|
||||
let deployment = spawn_request_action(async move {
|
||||
state
|
||||
@@ -119,9 +130,9 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/copy",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(DeploymentId { id }): Path<DeploymentId>,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id }),
|
||||
Json(deployment): Json<CopyDeploymentBody>| async move {
|
||||
let deployment = spawn_request_action(async move {
|
||||
state
|
||||
@@ -137,12 +148,13 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/delete",
|
||||
delete(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId{ id }),
|
||||
Query(StopContainerQuery { stop_signal, stop_time })| async move {
|
||||
let deployment = spawn_request_action(async move {
|
||||
state
|
||||
.delete_deployment(&deployment_id.id, &user)
|
||||
.delete_deployment(&id, &user, stop_signal, stop_time)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -154,8 +166,8 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/update",
|
||||
patch(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Json(deployment): Json<Deployment>| async move {
|
||||
let deployment = spawn_request_action(async move {
|
||||
state
|
||||
@@ -189,12 +201,12 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/reclone",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id })| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.reclone_deployment(&deployment_id.id, &user)
|
||||
.reclone_deployment(&id, &user, true)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -206,12 +218,13 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/deploy",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id }),
|
||||
Query(StopContainerQuery { stop_signal, stop_time })| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.deploy_container(&deployment_id.id, &user)
|
||||
.deploy_container(&id, &user, stop_signal, stop_time)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -223,12 +236,12 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/start_container",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id })| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.start_container(&deployment_id.id, &user)
|
||||
.start_container(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -240,12 +253,13 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/stop_container",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id }),
|
||||
Query(StopContainerQuery { stop_signal, stop_time })| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.stop_container(&deployment_id.id, &user)
|
||||
.stop_container(&id, &user, stop_signal, stop_time)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -257,12 +271,13 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/remove_container",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id }),
|
||||
Query(StopContainerQuery { stop_signal, stop_time })| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.remove_container(&deployment_id.id, &user)
|
||||
.remove_container(&id, &user, stop_signal, stop_time)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -274,12 +289,12 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/pull",
|
||||
post(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>| async move {
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id })| async move {
|
||||
let update = spawn_request_action(async move {
|
||||
state
|
||||
.pull_deployment_repo(&deployment_id.id, &user)
|
||||
.pull_deployment_repo(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
})
|
||||
@@ -291,8 +306,8 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/action_state",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id }): Path<DeploymentId>| async move {
|
||||
let action_state = state
|
||||
.get_deployment_action_states(id, &user)
|
||||
@@ -305,12 +320,12 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/log",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
Path(deployment_id): Path<DeploymentId>,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id }),
|
||||
Query(query): Query<GetContainerLogQuery>| async move {
|
||||
let log = state
|
||||
.get_deployment_container_log(&deployment_id.id, &user, query.tail)
|
||||
.get_deployment_container_log(&id, &user, query.tail)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(log))
|
||||
@@ -320,8 +335,8 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/stats",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id })| async move {
|
||||
let stats = state
|
||||
.get_deployment_container_stats(&id, &user)
|
||||
@@ -334,8 +349,8 @@ pub fn router() -> Router {
|
||||
.route(
|
||||
"/:id/deployed_version",
|
||||
get(
|
||||
|Extension(state): StateExtension,
|
||||
Extension(user): RequestUserExtension,
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(DeploymentId { id })| async move {
|
||||
let version = state
|
||||
.get_deployment_deployed_version(&id, &user)
|
||||
@@ -440,13 +455,7 @@ impl State {
|
||||
) -> anyhow::Result<DeploymentActionState> {
|
||||
self.get_deployment_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let action_state = self
|
||||
.deployment_action_states
|
||||
.lock()
|
||||
.await
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.clone();
|
||||
let action_state = self.deployment_action_states.get_or_default(id).await;
|
||||
Ok(action_state)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum_oauth2::random_duration;
|
||||
use helpers::handle_anyhow_error;
|
||||
use hex::ToHex;
|
||||
use hmac::{Hmac, Mac};
|
||||
use mungos::Deserialize;
|
||||
use serde::Deserialize;
|
||||
use sha2::Sha256;
|
||||
use types::GITHUB_WEBHOOK_USER_ID;
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{Deserialize, Document, Serialize};
|
||||
use mungos::mongodb::bson::Document;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::{traits::Permissioned, Group, PermissionLevel};
|
||||
use typeshare::typeshare;
|
||||
|
||||
|
||||
+12
-9
@@ -9,7 +9,8 @@ use axum::{
|
||||
};
|
||||
use futures_util::Future;
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use serde::Deserialize;
|
||||
use types::{PermissionLevel, UpdateTarget, User};
|
||||
use typeshare::typeshare;
|
||||
|
||||
@@ -20,15 +21,16 @@ use crate::{
|
||||
ResponseResult,
|
||||
};
|
||||
|
||||
pub mod build;
|
||||
pub mod deployment;
|
||||
mod build;
|
||||
mod command;
|
||||
mod deployment;
|
||||
mod github_listener;
|
||||
pub mod group;
|
||||
pub mod permissions;
|
||||
pub mod procedure;
|
||||
pub mod secret;
|
||||
pub mod server;
|
||||
pub mod update;
|
||||
mod group;
|
||||
mod permissions;
|
||||
mod procedure;
|
||||
mod secret;
|
||||
mod server;
|
||||
mod update;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Deserialize)]
|
||||
@@ -94,6 +96,7 @@ pub fn router() -> Router {
|
||||
.nest("/build", build::router())
|
||||
.nest("/deployment", deployment::router())
|
||||
.nest("/server", server::router())
|
||||
.nest("/command", command::router())
|
||||
.nest("/procedure", procedure::router())
|
||||
.nest("/group", group::router())
|
||||
.nest("/update", update::router())
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{routing::post, Extension, Json, Router};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize, Document, Serialize};
|
||||
use mungos::mongodb::bson::{doc, Document};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::{
|
||||
monitor_timestamp, Build, Deployment, Group, Log, Operation, PermissionLevel,
|
||||
PermissionsTarget, Procedure, Server, Update, UpdateStatus, UpdateTarget,
|
||||
|
||||
@@ -5,7 +5,8 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{Deserialize, Document, Serialize};
|
||||
use mungos::mongodb::bson::Document;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::{traits::Permissioned, PermissionLevel, Procedure};
|
||||
use typeshare::typeshare;
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use helpers::{generate_secret, handle_anyhow_error};
|
||||
use mungos::{doc, to_bson, Deserialize, Document, Serialize, Update};
|
||||
use mungos::{
|
||||
mongodb::bson::{doc, to_bson, Document},
|
||||
Update,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::{monitor_timestamp, ApiSecret};
|
||||
use typeshare::typeshare;
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ use axum::{
|
||||
};
|
||||
use futures_util::{future::join_all, SinkExt, StreamExt};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize, Document, FindOptions};
|
||||
use mungos::mongodb::{
|
||||
bson::{doc, Document},
|
||||
options::FindOptions,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::select;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -676,13 +680,7 @@ impl State {
|
||||
) -> anyhow::Result<ServerActionState> {
|
||||
self.get_server_check_permissions(&id, &user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let action_state = self
|
||||
.server_action_states
|
||||
.lock()
|
||||
.await
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.clone();
|
||||
let action_state = self.server_action_states.get_or_default(id).await;
|
||||
Ok(action_state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{extract::Query, routing::get, Extension, Json, Router};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, to_bson};
|
||||
use mungos::mongodb::bson::{doc, to_bson};
|
||||
use serde_json::Value;
|
||||
use types::{PermissionLevel, Update, UpdateTarget};
|
||||
|
||||
@@ -92,6 +92,10 @@ impl State {
|
||||
.get_group_check_permissions(id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.map(|_| ()),
|
||||
UpdateTarget::Command(id) => self
|
||||
.get_command_check_permissions(id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.map(|_| ()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use anyhow::{anyhow, Context};
|
||||
use axum::{extract::Query, response::Redirect, routing::get, Extension, Router};
|
||||
use axum_oauth2::github::{GithubOauthClient, GithubOauthExtension};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use serde::Deserialize;
|
||||
use types::{monitor_timestamp, CoreConfig, User};
|
||||
|
||||
use crate::{response, state::StateExtension};
|
||||
@@ -68,10 +69,15 @@ async fn callback(
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = monitor_timestamp();
|
||||
let no_users_exist = state.db.users.find_one(None, None).await?.is_none();
|
||||
let user = User {
|
||||
username: github_user.login,
|
||||
avatar: github_user.avatar_url.into(),
|
||||
github_id: github_id.into(),
|
||||
enabled: no_users_exist,
|
||||
admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
created_at: ts.clone(),
|
||||
updated_at: ts,
|
||||
..Default::default()
|
||||
|
||||
@@ -4,7 +4,8 @@ use anyhow::{anyhow, Context};
|
||||
use axum::{extract::Query, response::Redirect, routing::get, Extension, Router};
|
||||
use axum_oauth2::google::{GoogleOauthClient, GoogleOauthExtension};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize};
|
||||
use mungos::mongodb::bson::doc;
|
||||
use serde::Deserialize;
|
||||
use types::{monitor_timestamp, CoreConfig, User};
|
||||
|
||||
use crate::{response, state::StateExtension};
|
||||
@@ -85,6 +86,7 @@ async fn callback(
|
||||
.context("failed to generate jwt")?,
|
||||
None => {
|
||||
let ts = monitor_timestamp();
|
||||
let no_users_exist = state.db.users.find_one(None, None).await?.is_none();
|
||||
let user = User {
|
||||
username: google_user
|
||||
.email
|
||||
@@ -95,6 +97,10 @@ async fn callback(
|
||||
.to_string(),
|
||||
avatar: google_user.picture.into(),
|
||||
google_id: google_id.into(),
|
||||
enabled: no_users_exist,
|
||||
admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
created_at: ts.clone(),
|
||||
updated_at: ts,
|
||||
..Default::default()
|
||||
|
||||
@@ -9,7 +9,7 @@ use axum::{body::Body, http::Request, Extension};
|
||||
use axum_oauth2::random_string;
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::{SignWithKey, VerifyWithKey};
|
||||
use mungos::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use types::{CoreConfig, User};
|
||||
|
||||
@@ -20,6 +20,7 @@ pub type RequestUserExtension = Extension<Arc<RequestUser>>;
|
||||
|
||||
type ExchangeTokenMap = Mutex<HashMap<String, (String, u128)>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RequestUser {
|
||||
pub id: String,
|
||||
pub is_admin: bool,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{extract::Json, routing::post, Extension, Router};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::doc;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use types::{monitor_timestamp, User, UserCredentials};
|
||||
|
||||
use crate::state::StateExtension;
|
||||
@@ -47,6 +47,7 @@ async fn create_user_handler(
|
||||
enabled: no_users_exist,
|
||||
admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
created_at: ts.clone(),
|
||||
updated_at: ts,
|
||||
..Default::default()
|
||||
|
||||
@@ -9,7 +9,7 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::CoreConfig;
|
||||
use typeshare::typeshare;
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ use anyhow::{anyhow, Context};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{routing::post, Extension, Json, Router};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::{doc, Deserialize, Document, Update};
|
||||
use mungos::{
|
||||
mongodb::bson::{doc, Document},
|
||||
Update,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use types::unix_from_monitor_ts;
|
||||
|
||||
use crate::state::StateExtension;
|
||||
|
||||
+13
-5
@@ -1,9 +1,12 @@
|
||||
use axum_extra::routing::SpaRouter;
|
||||
use axum::Router;
|
||||
use dotenv::dotenv;
|
||||
use merge_config_files::parse_config_file;
|
||||
use mungos::Deserialize;
|
||||
use serde::Deserialize;
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use types::CoreConfig;
|
||||
|
||||
type SpaRouter = Router;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Env {
|
||||
#[serde(default = "default_config_path")]
|
||||
@@ -12,12 +15,17 @@ struct Env {
|
||||
pub frontend_path: String,
|
||||
}
|
||||
|
||||
pub fn load() -> (CoreConfig, SpaRouter) {
|
||||
pub fn load() -> (CoreConfig, SpaRouter, ServeFile) {
|
||||
dotenv().ok();
|
||||
let env: Env = envy::from_env().expect("failed to parse environment variables");
|
||||
let config = parse_config_file(env.config_path).expect("failed to parse config");
|
||||
let spa_router = SpaRouter::new("/assets", env.frontend_path);
|
||||
(config, spa_router)
|
||||
let spa_router = Router::new().nest_service(
|
||||
"/assets",
|
||||
ServeDir::new(&env.frontend_path)
|
||||
.not_found_service(ServeFile::new(format!("{}/index.html", env.frontend_path))),
|
||||
);
|
||||
let index_html_service = ServeFile::new(format!("{}/index.html", env.frontend_path));
|
||||
(config, spa_router, index_html_service)
|
||||
}
|
||||
|
||||
pub fn default_config_path() -> String {
|
||||
|
||||
+37
-2
@@ -1,9 +1,10 @@
|
||||
use std::str::FromStr;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use diff::{Diff, OptionDiff};
|
||||
use helpers::to_monitor_name;
|
||||
use types::Build;
|
||||
use tokio::sync::RwLock;
|
||||
use types::{traits::Busy, Build};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! response {
|
||||
@@ -66,3 +67,37 @@ pub fn empty_or_only_spaces(word: &str) -> bool {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cache<T: Clone + Default> {
|
||||
cache: RwLock<HashMap<String, T>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Default> Cache<T> {
|
||||
pub async fn get(&self, key: &str) -> Option<T> {
|
||||
self.cache.read().await.get(key).map(|e| e.clone())
|
||||
}
|
||||
|
||||
pub async fn get_or_default(&self, key: String) -> T {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.entry(key).or_default().clone()
|
||||
}
|
||||
|
||||
pub async fn update_entry(&self, key: String, handler: impl Fn(&mut T) -> ()) {
|
||||
let mut cache = self.cache.write().await;
|
||||
handler(cache.entry(key).or_default());
|
||||
}
|
||||
|
||||
pub async fn clear(&self) {
|
||||
self.cache.write().await.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Default + Busy> Cache<T> {
|
||||
pub async fn busy(&self, id: &str) -> bool {
|
||||
match self.get(id).await {
|
||||
Some(state) => state.busy(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
-19
@@ -4,6 +4,7 @@ use ::helpers::get_socket_addr;
|
||||
use auth::JwtClient;
|
||||
use axum::{http::StatusCode, Router};
|
||||
use state::State;
|
||||
use termination_signal::tokio::immediate_term_handle;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
mod actions;
|
||||
@@ -20,29 +21,43 @@ type ResponseResult<T> = Result<T, (StatusCode, String)>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (config, spa_router) = config::load();
|
||||
println!("version: v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
println!("starting monitor core on port {}...", config.port);
|
||||
let term_signal = immediate_term_handle()?;
|
||||
|
||||
let app = Router::new()
|
||||
.merge(spa_router)
|
||||
.nest("/api", api::router())
|
||||
.nest("/auth", auth::router(&config))
|
||||
.nest("/ws", ws::router())
|
||||
.layer(JwtClient::extension(&config))
|
||||
.layer(State::extension(config.clone()).await)
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
);
|
||||
let app = tokio::spawn(async move {
|
||||
let (config, spa_router, index_html_service) = config::load();
|
||||
|
||||
println!("started monitor core on port {}", config.port);
|
||||
println!("starting monitor core on port {}...", config.port);
|
||||
|
||||
axum::Server::bind(&get_socket_addr(config.port))
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
let app = Router::new()
|
||||
.nest("/api", api::router())
|
||||
.nest("/auth", auth::router(&config))
|
||||
.nest("/ws", ws::router())
|
||||
.layer(JwtClient::extension(&config))
|
||||
.layer(State::extension(config.clone()).await)
|
||||
.merge(spa_router)
|
||||
.fallback_service(index_html_service)
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
);
|
||||
|
||||
println!("started monitor core on port {}", config.port);
|
||||
|
||||
axum::Server::bind(&get_socket_addr(config.port))
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
res = app => return res?,
|
||||
_ = term_signal => {},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+35
-27
@@ -4,13 +4,13 @@ use async_timing_util::{
|
||||
unix_timestamp_ms, wait_until_timelength, Timelength, ONE_DAY_MS, ONE_HOUR_MS,
|
||||
};
|
||||
use futures_util::future::join_all;
|
||||
use mungos::doc;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use slack::types::Block;
|
||||
use types::{Server, SystemStats, SystemStatsQuery, SystemStatsRecord};
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct AlertStatus {
|
||||
cpu_alert: bool,
|
||||
mem_alert: bool,
|
||||
@@ -100,16 +100,16 @@ impl State {
|
||||
}
|
||||
|
||||
async fn check_cpu(&self, server: &Server, stats: &SystemStats) {
|
||||
let server_alert_status = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none()
|
||||
|| server_alert_status
|
||||
|| self
|
||||
.server_alert_status
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| s.cpu_alert)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
drop(server_alert_status);
|
||||
if stats.cpu_perc > server.cpu_alert {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" ({region})")
|
||||
@@ -171,24 +171,26 @@ impl State {
|
||||
server.name, stats.cpu_perc
|
||||
)
|
||||
} else {
|
||||
let mut lock = self.server_alert_status.lock().await;
|
||||
let entry = lock.entry(server.id.clone()).or_default();
|
||||
entry.cpu_alert = true;
|
||||
self.server_alert_status
|
||||
.update_entry(server.id.clone(), |entry| {
|
||||
entry.cpu_alert = true;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_mem(&self, server: &Server, stats: &SystemStats) {
|
||||
let server_alert_status = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none()
|
||||
|| server_alert_status
|
||||
|| self
|
||||
.server_alert_status
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| s.mem_alert)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
drop(server_alert_status);
|
||||
let usage_perc = (stats.mem_used_gb / stats.mem_total_gb) * 100.0;
|
||||
if usage_perc > server.mem_alert {
|
||||
let region = if let Some(region) = &server.region {
|
||||
@@ -254,25 +256,27 @@ impl State {
|
||||
server.name, stats.mem_used_gb, stats.mem_total_gb,
|
||||
)
|
||||
} else {
|
||||
let mut lock = self.server_alert_status.lock().await;
|
||||
let entry = lock.entry(server.id.clone()).or_default();
|
||||
entry.mem_alert = true;
|
||||
self.server_alert_status
|
||||
.update_entry(server.id.clone(), |entry| {
|
||||
entry.mem_alert = true;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_disk(&self, server: &Server, stats: &SystemStats) {
|
||||
for disk in &stats.disk.disks {
|
||||
let server_alert_status = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none()
|
||||
|| server_alert_status
|
||||
|| self
|
||||
.server_alert_status
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| *s.disk_alert.get(&disk.mount).unwrap_or(&false))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
drop(server_alert_status);
|
||||
let usage_perc = (disk.used_gb / disk.total_gb) * 100.0;
|
||||
if usage_perc > server.disk_alert {
|
||||
let region = if let Some(region) = &server.region {
|
||||
@@ -315,25 +319,27 @@ impl State {
|
||||
server.name, stats.disk.used_gb, stats.disk.total_gb,
|
||||
)
|
||||
} else {
|
||||
let mut lock = self.server_alert_status.lock().await;
|
||||
let entry = lock.entry(server.id.clone()).or_default();
|
||||
entry.disk_alert.insert(disk.mount.clone(), true);
|
||||
self.server_alert_status
|
||||
.update_entry(server.id.clone(), |entry| {
|
||||
entry.disk_alert.insert(disk.mount.clone(), true);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_components(&self, server: &Server, stats: &SystemStats) {
|
||||
let lock = self.server_alert_status.lock().await;
|
||||
if self.slack.is_none()
|
||||
|| lock
|
||||
|| self
|
||||
.server_alert_status
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| s.component_alert)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
drop(lock);
|
||||
let info = stats
|
||||
.components
|
||||
.iter()
|
||||
@@ -393,9 +399,11 @@ impl State {
|
||||
info.join(" | "),
|
||||
)
|
||||
} else {
|
||||
let mut lock = self.server_alert_status.lock().await;
|
||||
let entry = lock.entry(server.id.clone()).or_default();
|
||||
entry.component_alert = true;
|
||||
self.server_alert_status
|
||||
.update_entry(server.id.clone(), |entry| {
|
||||
entry.component_alert = true;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,7 +495,7 @@ impl State {
|
||||
);
|
||||
}
|
||||
{
|
||||
self.server_alert_status.lock().await.clear();
|
||||
self.server_alert_status.clear().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-11
@@ -1,30 +1,31 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_timing_util::{unix_timestamp_ms, wait_until_timelength, Timelength, ONE_HOUR_MS};
|
||||
use axum::Extension;
|
||||
use db::DbClient;
|
||||
use futures_util::future::join_all;
|
||||
use mungos::doc;
|
||||
use mungos::mongodb::bson::doc;
|
||||
use periphery::PeripheryClient;
|
||||
use tokio::sync::Mutex;
|
||||
use types::{BuildActionState, CoreConfig, DeploymentActionState, ServerActionState};
|
||||
use types::{
|
||||
BuildActionState, CommandActionState, CoreConfig, DeploymentActionState, ServerActionState,
|
||||
};
|
||||
|
||||
use crate::{monitoring::AlertStatus, ws::update::UpdateWsChannel};
|
||||
use crate::{helpers::Cache, monitoring::AlertStatus, ws::update::UpdateWsChannel};
|
||||
|
||||
pub type StateExtension = Extension<Arc<State>>;
|
||||
|
||||
pub type ActionStateMap<T> = Mutex<HashMap<String, T>>;
|
||||
|
||||
// pub type Cache<T> = RwLock<HashMap<String, T>>;
|
||||
pub struct State {
|
||||
pub config: CoreConfig,
|
||||
pub db: DbClient,
|
||||
pub update: UpdateWsChannel,
|
||||
pub periphery: PeripheryClient,
|
||||
pub slack: Option<slack::Client>,
|
||||
pub build_action_states: ActionStateMap<BuildActionState>,
|
||||
pub deployment_action_states: ActionStateMap<DeploymentActionState>,
|
||||
pub server_action_states: ActionStateMap<ServerActionState>,
|
||||
pub server_alert_status: Mutex<HashMap<String, AlertStatus>>, // (server_id, AlertStatus)
|
||||
pub build_action_states: Cache<BuildActionState>,
|
||||
pub deployment_action_states: Cache<DeploymentActionState>,
|
||||
pub server_action_states: Cache<ServerActionState>,
|
||||
pub command_action_states: Cache<CommandActionState>,
|
||||
pub server_alert_status: Cache<AlertStatus>, // (server_id, AlertStatus)
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -38,6 +39,7 @@ impl State {
|
||||
build_action_states: Default::default(),
|
||||
deployment_action_states: Default::default(),
|
||||
server_action_states: Default::default(),
|
||||
command_action_states: Default::default(),
|
||||
server_alert_status: Default::default(),
|
||||
};
|
||||
let state = Arc::new(state);
|
||||
|
||||
@@ -6,9 +6,9 @@ use axum::{
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use helpers::handle_anyhow_error;
|
||||
use mungos::Deserialize;
|
||||
use serde::Deserialize;
|
||||
use tokio::select;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use types::{traits::Permissioned, PermissionLevel, SystemStatsQuery};
|
||||
|
||||
@@ -67,7 +67,7 @@ pub async fn ws_handler(
|
||||
},
|
||||
stats = stats_recv.next() => stats,
|
||||
};
|
||||
if let Some(Ok(Message::Text(msg))) = stats {
|
||||
if let Some(Ok(TungsteniteMessage::Text(msg))) = stats {
|
||||
let _ = ws_sender.send(AxumMessage::Text(msg)).await;
|
||||
} else {
|
||||
let _ = stats_recv.close(None).await;
|
||||
|
||||
@@ -140,6 +140,12 @@ async fn user_can_see_update(
|
||||
.await?;
|
||||
(permissions, "group")
|
||||
}
|
||||
UpdateTarget::Command(command_id) => {
|
||||
let permissions = db_client
|
||||
.get_user_permission_on_command(user_id, command_id)
|
||||
.await?;
|
||||
(permissions, "command")
|
||||
}
|
||||
UpdateTarget::System => {
|
||||
return Err(anyhow!("user not admin, can't recieve system updates"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# core setup
|
||||
|
||||
setting up monitor core is fairly simple. there are some requirements to run monitor core:
|
||||
|
||||
- a valid configuration file
|
||||
- an instance of MongoDB to which monitor core can connect
|
||||
- docker must be installed on the host
|
||||
|
||||
## 1. create the configuration file
|
||||
|
||||
create a configuration file on the system, for example at `~/.monitor/core.config.toml`, and copy the [example config](https://github.com/mbecker20/monitor/blob/main/config_example/core.config.example.toml). fill in all the necessary information before continuing.
|
||||
|
||||
:::note
|
||||
to enable OAuth2 login, you must create a client on the respective OAuth provider,
|
||||
for example [google](https://developers.google.com/identity/protocols/oauth2)
|
||||
or [github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps).
|
||||
monitor uses the `web application` login flow.
|
||||
the redirect uri is `<base_url>/auth/google/callback` for google and `<base_url>/auth/github/callback` for github.
|
||||
:::
|
||||
|
||||
## 2. start monitor core
|
||||
|
||||
monitor core is distributed via dockerhub under the public repo [mbecker2020/monitor_core](https://hub.docker.com/r/mbecker2020/monitor_core).
|
||||
|
||||
```sh
|
||||
docker run -d --name monitor-core \
|
||||
-v $HOME/.monitor/core.config.toml:/config/config.toml \
|
||||
-p 9000:9000 \
|
||||
mbecker2020/monitor_core
|
||||
```
|
||||
|
||||
## first login
|
||||
|
||||
monitor core should now be accessible on the specified port, so navigating to `http://<address>:<port>` will display the login page.
|
||||
|
||||
the first user to log in will be auto enabled and made admin. any additional users to create accounts will be disabled by default.
|
||||
|
||||
## https
|
||||
|
||||
monitor core itself only supports http, so a reverse proxy like [caddy](https://caddyserver.com/) should be used for https
|
||||
@@ -19,6 +19,7 @@ const sidebars = {
|
||||
// But you can create a sidebar manually
|
||||
docs: [
|
||||
"intro",
|
||||
"core-setup",
|
||||
{
|
||||
type: "category",
|
||||
label: "connecting servers",
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
+33
-11
@@ -17,22 +17,44 @@ function HomepageHeader() {
|
||||
<div style={{ display: "flex", gap: "1rem", justifyContent: "center" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<MonitorLogo width="600px" />
|
||||
<h1 className="hero__title" style={{ margin: 0, position: "absolute", top: "40%", left: "50%", transform: "translate(-50%, -50%)" }}>
|
||||
<h1
|
||||
className="hero__title"
|
||||
style={{
|
||||
margin: 0,
|
||||
position: "absolute",
|
||||
top: "40%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
monitor
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link className="button button--secondary button--lg" to="/intro">
|
||||
docs
|
||||
</Link>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="https://github.com/mbecker20/monitor"
|
||||
>
|
||||
github
|
||||
</Link>
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<div className={styles.buttons}>
|
||||
<Link className="button button--secondary button--lg" to="/intro">
|
||||
docs
|
||||
</Link>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="https://github.com/mbecker20/monitor"
|
||||
>
|
||||
github
|
||||
</Link>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="https://github.com/mbecker20/monitor#readme"
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
gridColumn: "span 2",
|
||||
}}
|
||||
>
|
||||
screenshots
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"license": "GPL v3.0",
|
||||
"devDependencies": {
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"sass": "^1.57.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.3",
|
||||
@@ -19,10 +20,12 @@
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.6.0",
|
||||
"@tanstack/solid-query": "^4.26.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.2.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"lightweight-charts": "^3.8.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"solid-js": "^1.6.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ export const NewGroup: Component<{}> = (p) => {
|
||||
<Show
|
||||
when={showNew()}
|
||||
fallback={
|
||||
<button class="green" onClick={toggleShowNew} style={{ height: "100%" }}>
|
||||
<button
|
||||
class="green"
|
||||
onClick={toggleShowNew}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
}
|
||||
@@ -33,8 +37,12 @@ export const NewDeployment: Component<{ serverID: string }> = (p) => {
|
||||
<Show
|
||||
when={showNew()}
|
||||
fallback={
|
||||
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
|
||||
<Icon type="plus" />
|
||||
<button
|
||||
class="green"
|
||||
onClick={toggleShowNew}
|
||||
style={{ width: "100%", height: "fit-content" }}
|
||||
>
|
||||
<Icon type="plus" width="1.2rem" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
@@ -56,8 +64,12 @@ export const NewBuild: Component<{}> = (p) => {
|
||||
<Show
|
||||
when={showNew()}
|
||||
fallback={
|
||||
<button class="green" onClick={toggleShowNew} style={{ width: "100%" }}>
|
||||
<Icon type="plus" />
|
||||
<button
|
||||
class="green"
|
||||
onClick={toggleShowNew}
|
||||
style={{ width: "100%", height: "fit-content" }}
|
||||
>
|
||||
<Icon type="plus" width="1.2rem" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
@@ -87,14 +99,14 @@ const New: Component<{
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex justifyContent="space-between">
|
||||
<Flex justifyContent="space-between" style={{ height: "fit-content", width: "100%" }}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder={p.placeholder}
|
||||
value={name()}
|
||||
onEdit={setName}
|
||||
onEnter={create}
|
||||
style={{ width: "20rem" }}
|
||||
style={{ width: "100%", "min-width": "20rem" }}
|
||||
/>
|
||||
<Flex gap="0.4rem">
|
||||
<button class="green" onClick={create}>
|
||||
|
||||
@@ -33,7 +33,7 @@ const Build: Component<{}> = (p) => {
|
||||
}
|
||||
});
|
||||
});
|
||||
onCleanup(() => unsub);
|
||||
onCleanup(() => unsub());
|
||||
const userCanUpdate = () =>
|
||||
user().admin ||
|
||||
build()?.permissions![user_id()] === PermissionLevel.Update;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { createStore, SetStoreFunction } from "solid-js/store";
|
||||
import { client } from "../../..";
|
||||
import { client, pushNotification } from "../../..";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useUser } from "../../../state/UserProvider";
|
||||
import { Build, Operation, PermissionLevel, ServerWithStatus } from "../../../types";
|
||||
@@ -55,10 +55,17 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
client.get_build(params.id).then((build) => {
|
||||
set({
|
||||
...build,
|
||||
_id: { $oid: params.id } as any,
|
||||
repo: build.repo,
|
||||
branch: build.branch,
|
||||
pre_build: build.pre_build,
|
||||
docker_build_args: build.docker_build_args,
|
||||
docker_build_args: {
|
||||
build_path: build.docker_build_args?.build_path!,
|
||||
dockerfile_path: build.docker_build_args?.dockerfile_path,
|
||||
build_args: build.docker_build_args?.build_args,
|
||||
extra_args: build.docker_build_args?.extra_args,
|
||||
use_buildx: build.docker_build_args?.use_buildx,
|
||||
},
|
||||
docker_account: build.docker_account,
|
||||
github_account: build.github_account,
|
||||
aws_config: build.aws_config,
|
||||
@@ -72,7 +79,13 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
|
||||
const save = () => {
|
||||
setBuild("saving", true);
|
||||
client.update_build(build)
|
||||
client
|
||||
.update_build(build)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
pushNotification("bad", "update build failed");
|
||||
setBuild("saving", false);
|
||||
});
|
||||
};
|
||||
|
||||
let update_unsub = () => {};
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Component, Match, Show, Switch } from "solid-js";
|
||||
import {
|
||||
Component,
|
||||
Match,
|
||||
Setter,
|
||||
Show,
|
||||
Signal,
|
||||
Switch,
|
||||
createSignal,
|
||||
} from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
@@ -13,10 +21,15 @@ import { combineClasses } from "../../util/helpers";
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import {
|
||||
DockerContainerState,
|
||||
Operation,
|
||||
PermissionLevel,
|
||||
ServerStatus,
|
||||
TerminationSignal,
|
||||
TerminationSignalLabel,
|
||||
UpdateStatus,
|
||||
} from "../../types";
|
||||
import ConfirmMenuButton from "../shared/ConfirmMenuButton";
|
||||
import Selector from "../shared/menu/Selector";
|
||||
|
||||
const Actions: Component<{}> = (p) => {
|
||||
const { deployments, builds, servers, getPermissionOnDeployment } =
|
||||
@@ -161,12 +174,12 @@ const Build: Component = () => {
|
||||
};
|
||||
|
||||
const Deploy: Component<{ redeploy?: boolean }> = (p) => {
|
||||
// const { deployments } = useAppState();
|
||||
const params = useParams();
|
||||
// const deployment = () => deployments.get(params.id)!;
|
||||
const actions = useActionStates();
|
||||
const { deployments } = useAppState();
|
||||
const name = () => deployments.get(params.id)?.deployment.name;
|
||||
const deployment = () => deployments.get(params.id);
|
||||
const name = () => deployment()?.deployment.name;
|
||||
const [termSignalLabel, setTermSignalLabel] = useTermSignalLabel();
|
||||
return (
|
||||
<Show
|
||||
when={!actions.deploying}
|
||||
@@ -194,9 +207,19 @@ const Deploy: Component<{ redeploy?: boolean }> = (p) => {
|
||||
<ConfirmMenuButton
|
||||
class="green"
|
||||
onConfirm={() => {
|
||||
client.deploy_container(params.id);
|
||||
client.deploy_container(params.id, {
|
||||
stop_signal: ((termSignalLabel().signal as any) === "default"
|
||||
? undefined
|
||||
: termSignalLabel().signal) as TerminationSignal,
|
||||
});
|
||||
}}
|
||||
title="redeploy container"
|
||||
configs={
|
||||
<TermSignalSelector
|
||||
termSignalLabel={termSignalLabel()}
|
||||
setTermSignalLabel={setTermSignalLabel}
|
||||
/>
|
||||
}
|
||||
match={name()!}
|
||||
>
|
||||
<Icon type={"reset"} />
|
||||
@@ -216,6 +239,7 @@ const RemoveContainer = () => {
|
||||
const actions = useActionStates();
|
||||
const { deployments } = useAppState();
|
||||
const name = () => deployments.get(params.id)?.deployment.name;
|
||||
const [termSignalLabel, setTermSignalLabel] = useTermSignalLabel();
|
||||
return (
|
||||
<Show
|
||||
when={!actions.removing}
|
||||
@@ -230,9 +254,19 @@ const RemoveContainer = () => {
|
||||
<ConfirmMenuButton
|
||||
class="red"
|
||||
onConfirm={() => {
|
||||
client.remove_container(params.id);
|
||||
client.remove_container(params.id, {
|
||||
stop_signal: ((termSignalLabel().signal as any) === "default"
|
||||
? undefined
|
||||
: termSignalLabel().signal) as TerminationSignal,
|
||||
});
|
||||
}}
|
||||
title="destroy container"
|
||||
configs={
|
||||
<TermSignalSelector
|
||||
termSignalLabel={termSignalLabel()}
|
||||
setTermSignalLabel={setTermSignalLabel}
|
||||
/>
|
||||
}
|
||||
match={name()!}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
@@ -282,6 +316,7 @@ const Stop = () => {
|
||||
const actions = useActionStates();
|
||||
const { deployments } = useAppState();
|
||||
const name = () => deployments.get(params.id)?.deployment.name;
|
||||
const [termSignalLabel, setTermSignalLabel] = useTermSignalLabel();
|
||||
return (
|
||||
<Show
|
||||
when={!actions.stopping}
|
||||
@@ -296,9 +331,17 @@ const Stop = () => {
|
||||
<ConfirmMenuButton
|
||||
class="orange"
|
||||
onConfirm={() => {
|
||||
client.stop_container(params.id);
|
||||
client.stop_container(params.id, {
|
||||
stop_signal: termSignalLabel().signal,
|
||||
});
|
||||
}}
|
||||
title="stop container"
|
||||
configs={
|
||||
<TermSignalSelector
|
||||
termSignalLabel={termSignalLabel()}
|
||||
setTermSignalLabel={setTermSignalLabel}
|
||||
/>
|
||||
}
|
||||
match={name()!}
|
||||
>
|
||||
<Icon type="pause" />
|
||||
@@ -374,4 +417,68 @@ const Reclone = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const TermSignalSelector: Component<{
|
||||
termSignalLabel: TerminationSignalLabel;
|
||||
setTermSignalLabel: Setter<TerminationSignalLabel>;
|
||||
}> = (p) => {
|
||||
const params = useParams();
|
||||
const { deployments } = useAppState();
|
||||
const deployment = () => deployments.get(params.id);
|
||||
return (
|
||||
<Show
|
||||
when={
|
||||
deployment()?.state === DockerContainerState.Running &&
|
||||
(deployment()?.deployment.term_signal_labels?.length || 0) > 1
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
class="full-width wrap"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<div class="dimmed">termination signal: </div>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={p.termSignalLabel}
|
||||
items={deployment()?.deployment.term_signal_labels || []}
|
||||
itemMap={({ signal, label }) => (
|
||||
<Flex gap="0.5rem" alignItems="center">
|
||||
<div>{signal}</div>
|
||||
<Show when={label.length > 0}>
|
||||
<div class="dimmed">{label}</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
)}
|
||||
onSelect={(signal) => p.setTermSignalLabel(signal)}
|
||||
position="bottom right"
|
||||
/>
|
||||
</Flex>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
function useTermSignalLabel(): Signal<TerminationSignalLabel> {
|
||||
const params = useParams();
|
||||
const { deployments, ws } = useAppState();
|
||||
const deployment = () => deployments.get(params.id)?.deployment;
|
||||
const term_signal = () =>
|
||||
deployment()?.termination_signal || TerminationSignal.SigTerm;
|
||||
const default_term_signal_label = () => ({
|
||||
signal: term_signal(),
|
||||
label:
|
||||
deployment()?.term_signal_labels?.find(
|
||||
({ signal }) => signal === term_signal()
|
||||
)?.label || "",
|
||||
});
|
||||
const [label, setLabel] = createSignal<TerminationSignalLabel>(
|
||||
default_term_signal_label()
|
||||
);
|
||||
ws.subscribe([Operation.UpdateDeployment], (update) => {
|
||||
if (update.status === UpdateStatus.Complete) {
|
||||
setTimeout(() => setLabel(default_term_signal_label()), 100);
|
||||
}
|
||||
});
|
||||
return [label, setLabel];
|
||||
}
|
||||
|
||||
export default Actions;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Component, Show } from "solid-js";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../types";
|
||||
import { PermissionLevel, TerminationSignal } from "../../types";
|
||||
import Description from "../Description";
|
||||
import NotFound from "../NotFound";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
@@ -16,6 +16,13 @@ import Updates from "./Updates";
|
||||
const POLLING_RATE = 10000;
|
||||
// let interval = -1;
|
||||
|
||||
export const TERM_SIGNALS = [
|
||||
TerminationSignal.SigTerm,
|
||||
TerminationSignal.SigInt,
|
||||
TerminationSignal.SigQuit,
|
||||
TerminationSignal.SigHup,
|
||||
];
|
||||
|
||||
const Deployment: Component<{}> = (p) => {
|
||||
const { user, user_id } = useUser();
|
||||
const { servers, deployments } = useAppState();
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component, createResource, createSignal, Show } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import {
|
||||
combineClasses,
|
||||
deploymentHeaderStateClass,
|
||||
getId,
|
||||
readableVersion,
|
||||
@@ -79,7 +78,7 @@ const Header: Component<{}> = (p) => {
|
||||
<>
|
||||
<Grid
|
||||
gap="0.5rem"
|
||||
class={combineClasses("card shadow")}
|
||||
class="card shadow"
|
||||
style={{
|
||||
position: "relative",
|
||||
cursor: isSemiMobile() ? "pointer" : undefined,
|
||||
@@ -173,7 +172,7 @@ const Header: Component<{}> = (p) => {
|
||||
{server()?.server.name || "unknown"}
|
||||
</A>
|
||||
<div class={deploymentHeaderStateClass(deployment().state)}>
|
||||
{deployment().state}
|
||||
{deployment().state.replaceAll("_", " ")}
|
||||
</div>
|
||||
</Flex>
|
||||
<Show when={status()}>
|
||||
|
||||
@@ -21,6 +21,8 @@ import { useAppDimensions } from "../../../../state/DimensionProvider";
|
||||
import SimpleTabs from "../../../shared/tabs/SimpleTabs";
|
||||
import ExtraArgs from "./container/ExtraArgs";
|
||||
import WebhookUrl from "./container/WebhookUrl";
|
||||
import RedeployOnBuild from "./container/RedeployOnBuild";
|
||||
import TerminationSignals, { DefaultTerminationSignal, DefaultTerminationTimeout } from "./termination/TerminationSignals";
|
||||
|
||||
const Config: Component<{}> = () => {
|
||||
const { deployment, reset, save, userCanUpdate } = useConfig();
|
||||
@@ -47,6 +49,20 @@ const Config: Component<{}> = () => {
|
||||
<Mounts />
|
||||
<ExtraArgs />
|
||||
<PostImage />
|
||||
<RedeployOnBuild />
|
||||
<Show when={isMobile()}>
|
||||
<div style={{ height: "1rem" }} />
|
||||
</Show>
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "termination",
|
||||
element: () => (
|
||||
<Grid class="config-items" placeItems="start center" style={{ "margin-bottom": "" }}>
|
||||
<TerminationSignals />
|
||||
<DefaultTerminationSignal />
|
||||
<DefaultTerminationTimeout />
|
||||
<Show when={isMobile()}>
|
||||
<div style={{ height: "1rem" }} />
|
||||
</Show>
|
||||
|
||||
@@ -62,6 +62,8 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
// console.log("loading deployment");
|
||||
client.get_deployment(params.id).then((deployment) =>
|
||||
set({
|
||||
...deployment,
|
||||
_id: { $oid: params.id } as any,
|
||||
name: deployment.deployment.name,
|
||||
server_id: deployment.deployment.server_id,
|
||||
permissions: deployment.deployment.permissions,
|
||||
@@ -87,6 +89,10 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
repo_mount: deployment.deployment.repo_mount,
|
||||
created_at: deployment.deployment.created_at,
|
||||
updated_at: deployment.deployment.updated_at,
|
||||
redeploy_on_build: deployment.deployment.redeploy_on_build,
|
||||
term_signal_labels: deployment.deployment.term_signal_labels,
|
||||
termination_signal: deployment.deployment.termination_signal,
|
||||
termination_timeout: deployment.deployment.termination_timeout,
|
||||
loaded: true,
|
||||
updated: false,
|
||||
updating: false,
|
||||
@@ -108,11 +114,13 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
|
||||
const save = () => {
|
||||
setDeployment("updating", true);
|
||||
client.update_deployment(deployment).catch((e) => {
|
||||
console.error(e);
|
||||
pushNotification("bad", "update deployment failed");
|
||||
setDeployment("updating", false);
|
||||
});
|
||||
client
|
||||
.update_deployment(deployment)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
pushNotification("bad", "update deployment failed");
|
||||
setDeployment("updating", false);
|
||||
});
|
||||
};
|
||||
|
||||
let update_unsub = () => {};
|
||||
|
||||
@@ -3,8 +3,8 @@ import { client } from "../../../../..";
|
||||
import { useAppState } from "../../../../../state/StateProvider";
|
||||
import {
|
||||
combineClasses,
|
||||
string_to_version,
|
||||
version_to_string,
|
||||
readableVersion,
|
||||
readableMonitorTimestamp,
|
||||
} from "../../../../../util/helpers";
|
||||
import Input from "../../../../shared/Input";
|
||||
import Flex from "../../../../shared/layout/Flex";
|
||||
@@ -14,11 +14,6 @@ import { useConfig } from "../Provider";
|
||||
const Image: Component<{}> = (p) => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
const { builds } = useAppState();
|
||||
const [versions] = createResource(() => {
|
||||
if (deployment.build_id) {
|
||||
return client.get_build_versions(deployment.build_id);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Flex
|
||||
class={combineClasses("config-item shadow")}
|
||||
@@ -41,7 +36,8 @@ const Image: Component<{}> = (p) => {
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={
|
||||
(deployment.build_id && (builds.get(deployment.build_id)?.name || "unknown")) ||
|
||||
(deployment.build_id &&
|
||||
(builds.get(deployment.build_id)?.name || "unknown")) ||
|
||||
"custom image"
|
||||
}
|
||||
items={[
|
||||
@@ -65,33 +61,7 @@ const Image: Component<{}> = (p) => {
|
||||
useSearch
|
||||
/>
|
||||
<Show when={deployment.build_id}>
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={
|
||||
deployment.build_version
|
||||
? `v${version_to_string(deployment.build_version)}`
|
||||
: "latest"
|
||||
}
|
||||
items={[
|
||||
"latest",
|
||||
...(versions()?.map(
|
||||
(v) => `v${version_to_string(v.version)}`
|
||||
) || []),
|
||||
]}
|
||||
onSelect={(version) => {
|
||||
if (version === "latest") {
|
||||
setDeployment("build_version", undefined);
|
||||
} else {
|
||||
setDeployment(
|
||||
"build_version",
|
||||
string_to_version(version.replace("v", ""))
|
||||
);
|
||||
}
|
||||
}}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
useSearch
|
||||
/>
|
||||
<VersionSelector />
|
||||
</Show>
|
||||
</Show>
|
||||
</Flex>
|
||||
@@ -100,3 +70,54 @@ const Image: Component<{}> = (p) => {
|
||||
};
|
||||
|
||||
export default Image;
|
||||
|
||||
|
||||
const VersionSelector: Component<{}> = (p) => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
const [versions] = createResource(() => {
|
||||
if (deployment.build_id) {
|
||||
return client.get_build_versions(deployment.build_id);
|
||||
}
|
||||
});
|
||||
const selected = () => ({
|
||||
version: deployment.build_version || {
|
||||
major: 0,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
},
|
||||
ts: "",
|
||||
});
|
||||
return (
|
||||
<Selector
|
||||
targetClass="blue"
|
||||
selected={selected()}
|
||||
items={[
|
||||
{ version: { major: 0, minor: 0, patch: 0 }, ts: "" },
|
||||
...(versions() || []),
|
||||
]}
|
||||
itemMap={({ version, ts }) => (
|
||||
<>
|
||||
<div>
|
||||
{version.major === 0 && version.minor === 0 && version.patch === 0
|
||||
? "latest"
|
||||
: readableVersion(version)}
|
||||
</div>
|
||||
<Show when={ts.length > 0}>
|
||||
<div class="dimmed">{readableMonitorTimestamp(ts)}</div>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
searchItemMap={({ version }) => readableVersion(version)}
|
||||
onSelect={({ version, ts }) => {
|
||||
if (ts.length === 0) {
|
||||
setDeployment("build_version", undefined);
|
||||
} else {
|
||||
setDeployment("build_version", version);
|
||||
}
|
||||
}}
|
||||
position="bottom right"
|
||||
disabled={!userCanUpdate()}
|
||||
useSearch
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useConfig } from "../Provider";
|
||||
import Flex from "../../../../shared/layout/Flex";
|
||||
|
||||
const RedeployOnBuild: Component<{}> = (p) => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
return (
|
||||
<Show when={deployment.build_id}>
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h1>redeploy on build</h1>
|
||||
<Show
|
||||
when={userCanUpdate()}
|
||||
fallback={<h2>{deployment.redeploy_on_build ? "yes" : "no"}</h2>}
|
||||
>
|
||||
<button
|
||||
class={deployment.redeploy_on_build ? "green" : "red"}
|
||||
onClick={() => setDeployment("redeploy_on_build", (v) => !v)}
|
||||
>
|
||||
{deployment.redeploy_on_build ? "yes" : "no"}
|
||||
</button>
|
||||
</Show>
|
||||
</Flex>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default RedeployOnBuild;
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Component, For, Show, createSignal } from "solid-js";
|
||||
import { useConfig } from "../Provider";
|
||||
import Grid from "../../../../shared/layout/Grid";
|
||||
import Flex from "../../../../shared/layout/Flex";
|
||||
import Icon from "../../../../shared/Icon";
|
||||
import { TERM_SIGNALS } from "../../../Deployment";
|
||||
import { TerminationSignal } from "../../../../../types";
|
||||
import Input from "../../../../shared/Input";
|
||||
import Menu from "../../../../shared/menu/Menu";
|
||||
import Selector from "../../../../shared/menu/Selector";
|
||||
import { pushNotification } from "../../../../..";
|
||||
|
||||
const TerminationSignals: Component<{}> = (p) => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
const signals_to_add = () =>
|
||||
TERM_SIGNALS.filter(
|
||||
(sig) =>
|
||||
!deployment.term_signal_labels
|
||||
?.map(({ signal }) => signal)
|
||||
.includes(sig)
|
||||
);
|
||||
const onAdd = (signal: TerminationSignal) => {
|
||||
setDeployment("term_signal_labels", (term_signals: any) => [
|
||||
...term_signals,
|
||||
{ signal, label: "" },
|
||||
]);
|
||||
};
|
||||
const onRemove = (index: number) => {
|
||||
setDeployment("term_signal_labels", (term_signals) =>
|
||||
term_signals?.filter((_, i) => i !== index)
|
||||
);
|
||||
};
|
||||
const [menuOpen, setMenuOpen] = createSignal(false);
|
||||
return (
|
||||
<Grid class="config-item shadow">
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<h1>termination signals</h1>
|
||||
<Show when={userCanUpdate() && signals_to_add().length > 0}>
|
||||
<Menu
|
||||
show={menuOpen()}
|
||||
close={() => setMenuOpen(false)}
|
||||
target={
|
||||
<button class="green" onClick={() => setMenuOpen(true)}>
|
||||
<Icon type="plus" />
|
||||
</button>
|
||||
}
|
||||
content={
|
||||
<For each={signals_to_add()}>
|
||||
{(signal) => (
|
||||
<button
|
||||
class="grey"
|
||||
onClick={() => {
|
||||
onAdd(signal);
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<h2>{signal}</h2>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Show when={(deployment.term_signal_labels?.length || 0) > 0}>
|
||||
<Grid gridTemplateColumns="auto 1fr auto" placeItems="center start">
|
||||
<For each={deployment.term_signal_labels}>
|
||||
{({ signal, label }, index) => (
|
||||
<>
|
||||
<h2>{signal}</h2>
|
||||
<Input
|
||||
class="full-width"
|
||||
placeholder="label this termination signal"
|
||||
value={label}
|
||||
onConfirm={(value) =>
|
||||
setDeployment("term_signal_labels", index(), "label", value)
|
||||
}
|
||||
disabled={!userCanUpdate()}
|
||||
/>
|
||||
<Show when={userCanUpdate()}>
|
||||
<button class="red" onClick={() => onRemove(index())}>
|
||||
<Icon type="minus" />
|
||||
</button>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default TerminationSignals;
|
||||
|
||||
export const DefaultTerminationSignal: Component<{}> = () => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
const term_signal = () =>
|
||||
deployment.termination_signal || TerminationSignal.SigTerm;
|
||||
const selected = () => ({
|
||||
signal: term_signal(),
|
||||
label:
|
||||
deployment.term_signal_labels?.find(
|
||||
({ signal }) => signal === term_signal()
|
||||
)?.label || "",
|
||||
});
|
||||
return (
|
||||
<Show when={deployment.term_signal_labels?.length || 0 > 0}>
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>default termination signal</h1>
|
||||
<Selector
|
||||
disabled={!userCanUpdate()}
|
||||
targetClass="blue"
|
||||
selected={selected()}
|
||||
items={deployment.term_signal_labels || []}
|
||||
onSelect={({ signal }) => setDeployment("termination_signal", signal)}
|
||||
itemMap={({ signal }) => signal}
|
||||
/>
|
||||
</Flex>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultTerminationTimeout: Component<{}> = () => {
|
||||
const { deployment, setDeployment, userCanUpdate } = useConfig();
|
||||
return (
|
||||
<Flex
|
||||
class="config-item shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>termination timeout</h1>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Input
|
||||
disabled={!userCanUpdate()}
|
||||
style={{ width: "10rem" }}
|
||||
placeholder="10"
|
||||
value={deployment.termination_timeout}
|
||||
onConfirm={(value) => {
|
||||
const val = Number(value);
|
||||
if (!isNaN(val)) {
|
||||
setDeployment("termination_timeout", val);
|
||||
} else {
|
||||
pushNotification("bad", "timeout must be number");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="dimmed" style={{ position: "absolute", right: "1rem", top: "50%", transform: "translateY(-50%)" }}>seconds</div>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -6,22 +6,24 @@ import {
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import Convert from "ansi-to-html";
|
||||
import { pushNotification } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { DockerContainerState, Log as LogType } from "../../../../types";
|
||||
import { combineClasses } from "../../../../util/helpers";
|
||||
import { combineClasses, sanitizeLog } from "../../../../util/helpers";
|
||||
import { useBuffer, useLocalStorageToggle } from "../../../../util/hooks";
|
||||
import Icon from "../../../shared/Icon";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import Selector from "../../../shared/menu/Selector";
|
||||
import { useConfig } from "../config/Provider";
|
||||
import s from "./log.module.scss";
|
||||
|
||||
const POLLING_RATE = 5000;
|
||||
|
||||
let interval = -1;
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
const Log: Component<{
|
||||
log?: LogType;
|
||||
logTail: number;
|
||||
@@ -31,7 +33,6 @@ const Log: Component<{
|
||||
}> = (p) => {
|
||||
const { deployments } = useAppState();
|
||||
const params = useParams();
|
||||
const { userCanUpdate } = useConfig();
|
||||
const deployment = () => deployments.get(params.id);
|
||||
let ref: HTMLDivElement | undefined;
|
||||
let ignore = false;
|
||||
@@ -56,14 +57,15 @@ const Log: Component<{
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
/// the return of this closure is used as the innerHTML of the <pre /> element
|
||||
/// need to make sure this can't be used to inject some script into the page
|
||||
const log = () => {
|
||||
if (deployment()?.state === DockerContainerState.NotDeployed) {
|
||||
return "not deployed";
|
||||
} else {
|
||||
return (
|
||||
(p.error ? p.log?.stderr : p.log?.stdout) ||
|
||||
`no${p.error ? " error" : ""} log`
|
||||
);
|
||||
const log = p.error ? p.log?.stderr : p.log?.stdout;
|
||||
const sanitized = log && sanitizeLog(log);
|
||||
return sanitized ? convert.toHtml(sanitized) : `no${p.error ? " error" : ""} log`;
|
||||
}
|
||||
};
|
||||
const buffer = useBuffer(scrolled, 250);
|
||||
@@ -145,7 +147,7 @@ const Log: Component<{
|
||||
}
|
||||
}}
|
||||
>
|
||||
<pre class={s.Log}>{log()}</pre>
|
||||
<pre class={s.Log} innerHTML={log()} />
|
||||
</div>
|
||||
<Show when={buffer()}>
|
||||
<button
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
}
|
||||
|
||||
.Log {
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
tab-size: 2;
|
||||
padding: 1rem;
|
||||
margin-bottom: 40vh;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Component, Show, createMemo } from "solid-js";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import LightweightChart from "../shared/LightweightChart";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import { COLORS } from "../../style/colors";
|
||||
import { BuildStatsResponse } from "../../util/client_types";
|
||||
import { useLocalStorage } from "../../util/hooks";
|
||||
|
||||
const BuildSummary: Component<{}> = (p) => {
|
||||
const { build_stats } = useAppState();
|
||||
return (
|
||||
<Grid class="full-size card" gridTemplateRows="auto 1fr" style={{ "padding-bottom": "0.6rem" }}>
|
||||
<Show
|
||||
when={build_stats.get()}
|
||||
fallback={
|
||||
<Grid class="full-size" placeItems="center">
|
||||
<Loading type="three-dot" />
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
style={{ height: "fit-content" }}
|
||||
>
|
||||
<h2>last 30 days</h2>
|
||||
<Flex>
|
||||
<Flex alignItems="center" gap="0.5rem">
|
||||
<div class="dimmed">build time: </div>
|
||||
<h2>{build_stats.get()?.total_time.toFixed(1)} hrs</h2>
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap="0.5rem">
|
||||
<div class="dimmed">build count: </div>
|
||||
<h2>{build_stats.get()?.total_count.toFixed()}</h2>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<BuildStatsChart build_stats={build_stats.get()!} />
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuildSummary;
|
||||
|
||||
const BuildStatsChart: Component<{ build_stats: BuildStatsResponse }> = (p) => {
|
||||
const [mode, setMode] = useLocalStorage<"time" | "count">(
|
||||
"time",
|
||||
"build-stats-chart-mode-v2"
|
||||
);
|
||||
const max = createMemo(() => {
|
||||
return p.build_stats.days.reduce((max, day) => {
|
||||
if (mode() === "count") {
|
||||
if (day.count > max) {
|
||||
return day.count;
|
||||
} else return max;
|
||||
} else {
|
||||
if (day.time > max) {
|
||||
return day.time;
|
||||
} else return max;
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
return (
|
||||
<div class="full-size" style={{ position: "relative" }}>
|
||||
<LightweightChart
|
||||
class="full-size"
|
||||
style={{ "min-height": "200px" }}
|
||||
histograms={[
|
||||
{
|
||||
line: p.build_stats.days.map((day) => ({
|
||||
value: mode() === "count" ? day.count : (day.time * 60),
|
||||
time: day.ts / 1000,
|
||||
color:
|
||||
mode() === "count"
|
||||
? day.count > max() * 0.7
|
||||
? COLORS.red
|
||||
: day.count > max() * 0.35
|
||||
? COLORS.orange
|
||||
: COLORS.green
|
||||
: day.time > max() * 0.7
|
||||
? COLORS.red
|
||||
: day.time > max() * 0.35
|
||||
? COLORS.orange
|
||||
: COLORS.green,
|
||||
})),
|
||||
priceLineVisible: false,
|
||||
// priceFormat:
|
||||
// mode() === "count"
|
||||
// ? {
|
||||
// minMove: 1,
|
||||
// }
|
||||
// : undefined,
|
||||
priceFormat: {
|
||||
minMove: 1,
|
||||
}
|
||||
},
|
||||
]}
|
||||
timeVisible={false}
|
||||
options={{
|
||||
grid: {
|
||||
horzLines: { visible: false },
|
||||
vertLines: { visible: false },
|
||||
},
|
||||
}}
|
||||
disableScroll
|
||||
/>
|
||||
<button
|
||||
class="blue opaque"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
"z-index": 20,
|
||||
padding: "0.3rem",
|
||||
}}
|
||||
onClick={() => setMode(mode => {
|
||||
if (mode === "count") {
|
||||
return "time"
|
||||
} else {
|
||||
return "count"
|
||||
}
|
||||
})}
|
||||
>
|
||||
{mode()}{mode() === "time" ? " (min)" : ""}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +1,44 @@
|
||||
import {
|
||||
Component,
|
||||
Component, Match, Show, Switch,
|
||||
} from "solid-js";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import SimpleTabs from "../shared/tabs/SimpleTabs";
|
||||
import { ControlledSimpleTabs } from "../shared/tabs/SimpleTabs";
|
||||
import Summary from "./Summary";
|
||||
import Builds from "./Tree/Builds";
|
||||
import Groups from "./Tree/Groups";
|
||||
import { TreeProvider } from "./Tree/Provider";
|
||||
import Updates from "./Updates/Updates";
|
||||
import { useLocalStorage } from "../../util/hooks";
|
||||
import BuildSummary from "./BuildSummary";
|
||||
|
||||
const Home: Component<{}> = (p) => {
|
||||
const { isSemiMobile } = useAppDimensions();
|
||||
const [selectedTab, setTab] = useLocalStorage<"servers" | "builds">(
|
||||
"servers",
|
||||
"home-groups-servers-tab-v2"
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
style={{ width: "100%" }}
|
||||
gridTemplateColumns={isSemiMobile() ? "1fr" : "1fr 1fr"}
|
||||
>
|
||||
<Summary />
|
||||
<Switch>
|
||||
<Match when={selectedTab() === "servers"}>
|
||||
<Summary />
|
||||
</Match>
|
||||
<Match when={selectedTab() === "builds"}>
|
||||
<BuildSummary />
|
||||
</Match>
|
||||
</Switch>
|
||||
<Updates />
|
||||
</Grid>
|
||||
<TreeProvider>
|
||||
<SimpleTabs
|
||||
<ControlledSimpleTabs
|
||||
selected={selectedTab}
|
||||
set={setTab as any}
|
||||
containerStyle={{ width: "100%" }}
|
||||
localStorageKey="home-groups-servers-tab-v1"
|
||||
tabs={[
|
||||
{
|
||||
title: "servers",
|
||||
@@ -32,8 +46,8 @@ const Home: Component<{}> = (p) => {
|
||||
},
|
||||
{
|
||||
title: "builds",
|
||||
element: () => <Builds />
|
||||
}
|
||||
element: () => <Builds />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TreeProvider>
|
||||
|
||||
@@ -74,7 +74,11 @@ const Groups: Component<{}> = (p) => {
|
||||
/>
|
||||
<Flex alignItems="center" style={{ width: "fit-content" }}>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
label={
|
||||
<div class="dimmed" style={{ "white-space": "nowrap" }}>
|
||||
sort by:
|
||||
</div>
|
||||
}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, For, Show, createEffect, createResource, onCleanup } from "solid-js";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { client } from "../../..";
|
||||
import Flex from "../../shared/layout/Flex";
|
||||
import { readableBytes, readableImageNameTag, readableTimestamp } from "../../../util/helpers";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { Operation } from "../../../types";
|
||||
|
||||
const Images: Component<{}> = (p) => {
|
||||
const params = useParams();
|
||||
const { ws } = useAppState();
|
||||
const [images, { refetch }] = createResource(() => client.get_docker_images(params.id));
|
||||
let unsub = () => {};
|
||||
createEffect(() => {
|
||||
unsub();
|
||||
unsub = ws.subscribe([Operation.PruneImagesServer], (update) => {
|
||||
if (update.target.id === params.id) {
|
||||
refetch()
|
||||
}
|
||||
});
|
||||
});
|
||||
onCleanup(() => unsub());
|
||||
createEffect(() => console.log(images()));
|
||||
return (
|
||||
<Grid class="config">
|
||||
<Grid class="config-items">
|
||||
<Show when={images()}>
|
||||
<For each={images()}>
|
||||
{(image) => {
|
||||
const [name, tag] = readableImageNameTag(image.RepoTags, image.RepoDigests);
|
||||
return (
|
||||
<Flex
|
||||
class="card light hover shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<h2>{name}</h2>
|
||||
<h2 class="dimmed">{tag}</h2>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<div>{readableBytes(image.Size)}</div>
|
||||
<div class="dimmed">
|
||||
{readableTimestamp(image.Created * 1000)}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Images;
|
||||
@@ -9,6 +9,7 @@ import Config from "./config/Config";
|
||||
import { ConfigProvider } from "./config/Provider";
|
||||
import Info from "./Info";
|
||||
import Permissions from "./Permissions";
|
||||
import Images from "./Images";
|
||||
|
||||
const ServerTabs: Component<{}> = (p) => {
|
||||
const { servers } = useAppState();
|
||||
@@ -31,6 +32,10 @@ const ServerTabs: Component<{}> = (p) => {
|
||||
title: "info",
|
||||
element: () => <Info />
|
||||
},
|
||||
server()?.status === ServerStatus.Ok && {
|
||||
title: "images",
|
||||
element: () => <Images />
|
||||
},
|
||||
user().admin && {
|
||||
title: "permissions",
|
||||
element: () => <Permissions />,
|
||||
|
||||
@@ -9,10 +9,15 @@ import {
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { createStore, SetStoreFunction } from "solid-js/store";
|
||||
import { client } from "../../../..";
|
||||
import { client, pushNotification } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { useUser } from "../../../../state/UserProvider";
|
||||
import { Server, Operation, PermissionLevel, ServerStatus } from "../../../../types";
|
||||
import {
|
||||
Server,
|
||||
Operation,
|
||||
PermissionLevel,
|
||||
ServerStatus,
|
||||
} from "../../../../types";
|
||||
import { getId } from "../../../../util/helpers";
|
||||
|
||||
type ConfigServer = Server & { loaded: boolean; updated: boolean };
|
||||
@@ -49,6 +54,7 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
client.get_server(params.id).then((server) => {
|
||||
set({
|
||||
...server.server,
|
||||
_id: { $oid: params.id } as any,
|
||||
loaded: true,
|
||||
updated: false,
|
||||
});
|
||||
@@ -66,10 +72,15 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
createEffect(loadNetworks);
|
||||
|
||||
const save = () => {
|
||||
client.update_server(server);
|
||||
client
|
||||
.update_server(server)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
pushNotification("bad", "update server failed");
|
||||
});
|
||||
};
|
||||
|
||||
let unsub = () => {}
|
||||
let unsub = () => {};
|
||||
createEffect(() => {
|
||||
unsub();
|
||||
unsub = ws.subscribe([Operation.UpdateServer], (update) => {
|
||||
@@ -94,7 +105,8 @@ export const ConfigProvider: ParentComponent<{}> = (p) => {
|
||||
|
||||
const userCanUpdate = () =>
|
||||
user().admin ||
|
||||
servers.get(params.id)!.server.permissions![getId(user())] === PermissionLevel.Update;
|
||||
servers.get(params.id)!.server.permissions![getId(user())] ===
|
||||
PermissionLevel.Update;
|
||||
|
||||
const state = {
|
||||
server,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PermissionLevel } from "../../types";
|
||||
import { NewDeployment } from "../New";
|
||||
import Deployment from "./Deployment";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
|
||||
const ServerChildren: Component<{ id: string }> = (p) => {
|
||||
const { user } = useUser();
|
||||
@@ -35,7 +36,9 @@ const ServerChildren: Component<{ id: string }> = (p) => {
|
||||
PermissionLevel.Update
|
||||
}
|
||||
>
|
||||
<NewDeployment serverID={p.id} />
|
||||
<Flex class="full-width" alignItems="center">
|
||||
<NewDeployment serverID={p.id} />
|
||||
</Flex>
|
||||
</Show>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ const ConfirmMenuButton: Component<{
|
||||
title: string;
|
||||
match: string;
|
||||
info?: JSX.Element;
|
||||
configs?: JSX.Element;
|
||||
children: JSX.Element;
|
||||
}> = (p) => {
|
||||
const [show, toggleShow] = useToggle();
|
||||
@@ -38,6 +39,7 @@ const ConfirmMenuButton: Component<{
|
||||
title={p.title}
|
||||
match={p.match}
|
||||
info={p.info}
|
||||
configs={p.configs}
|
||||
onConfirm={p.onConfirm}
|
||||
/>
|
||||
)}
|
||||
@@ -52,6 +54,7 @@ const ConfirmMenuContent: Component<{
|
||||
match: string;
|
||||
onConfirm?: () => void;
|
||||
info?: JSX.Element;
|
||||
configs?: JSX.Element;
|
||||
}> = (p) => {
|
||||
const [input, setInput] = createSignal("");
|
||||
return (
|
||||
@@ -71,6 +74,7 @@ const ConfirmMenuContent: Component<{
|
||||
value={input()}
|
||||
autofocus
|
||||
/>
|
||||
{p.configs}
|
||||
<ConfirmButton
|
||||
class={p.class}
|
||||
style={{ width: "100%" }}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import {
|
||||
AreaSeriesPartialOptions,
|
||||
BarSeriesPartialOptions,
|
||||
ChartOptions,
|
||||
ColorType,
|
||||
createChart,
|
||||
DeepPartial,
|
||||
HistogramSeriesPartialOptions,
|
||||
IChartApi,
|
||||
ISeriesApi,
|
||||
LineSeriesPartialOptions,
|
||||
@@ -18,6 +22,7 @@ import {
|
||||
export type LightweightValue = {
|
||||
time: number | string;
|
||||
value: number;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export type LightweightLine = {
|
||||
@@ -28,9 +33,14 @@ export type LightweightArea = {
|
||||
line: LightweightValue[];
|
||||
} & AreaSeriesPartialOptions;
|
||||
|
||||
export type LightweightHistogram = {
|
||||
line: LightweightValue[];
|
||||
} & HistogramSeriesPartialOptions;
|
||||
|
||||
const LightweightChart: Component<{
|
||||
lines?: LightweightLine[];
|
||||
areas?: LightweightArea[];
|
||||
histograms?: LightweightHistogram[];
|
||||
class?: string;
|
||||
style?: JSX.CSSProperties;
|
||||
width?: string;
|
||||
@@ -38,11 +48,15 @@ const LightweightChart: Component<{
|
||||
disableScroll?: boolean;
|
||||
onCreateLineSeries?: (series: ISeriesApi<"Line">) => void;
|
||||
onCreateAreaSeries?: (series: ISeriesApi<"Area">) => void;
|
||||
onCreateHistogramSeries?: (series: ISeriesApi<"Histogram">) => void;
|
||||
timeVisible?: boolean;
|
||||
options?: DeepPartial<ChartOptions>;
|
||||
}> = (p) => {
|
||||
let el: HTMLDivElement;
|
||||
const [chart, setChart] = createSignal<IChartApi>();
|
||||
let lineSeries: ISeriesApi<"Line">[] = [];
|
||||
let areaSeries: ISeriesApi<"Area">[] = [];
|
||||
let histogramSeries: ISeriesApi<"Histogram">[] = [];
|
||||
const [loaded, setLoaded] = createSignal(false);
|
||||
onMount(() => {
|
||||
if (loaded()) return;
|
||||
@@ -58,9 +72,10 @@ const LightweightChart: Component<{
|
||||
horzLines: { color: "#3f454d" },
|
||||
vertLines: { color: "#3f454d" },
|
||||
},
|
||||
timeScale: { timeVisible: true },
|
||||
timeScale: { timeVisible: p.timeVisible ?? true },
|
||||
handleScroll: p.disableScroll ? false : true,
|
||||
handleScale: p.disableScroll ? false : true,
|
||||
...p.options
|
||||
});
|
||||
chart.timeScale().fitContent();
|
||||
setChart(chart);
|
||||
@@ -95,6 +110,20 @@ const LightweightChart: Component<{
|
||||
});
|
||||
areaSeries = series;
|
||||
}
|
||||
for (const series of histogramSeries) {
|
||||
chart()!.removeSeries(series);
|
||||
}
|
||||
if (p.histograms) {
|
||||
const series = p.histograms.map((line) => {
|
||||
const series = chart()!.addHistogramSeries(line);
|
||||
series.setData(line.line as any);
|
||||
if (p.onCreateHistogramSeries) {
|
||||
p.onCreateHistogramSeries(series);
|
||||
}
|
||||
return series;
|
||||
});
|
||||
histogramSeries = series;
|
||||
}
|
||||
chart()!.timeScale().fitContent();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,10 +15,10 @@ import { Position } from "./helpers";
|
||||
import Menu from "./Menu";
|
||||
import s from "./menu.module.scss";
|
||||
|
||||
const Selector: Component<{
|
||||
selected: string;
|
||||
items: string[];
|
||||
onSelect?: (item: string, index: number) => void;
|
||||
const Selector = <T,>(p: {
|
||||
selected: T;
|
||||
items: T[];
|
||||
onSelect?: (item: T, index: number) => void;
|
||||
position?: Position;
|
||||
targetClass?: string;
|
||||
targetStyle?: JSX.CSSProperties;
|
||||
@@ -33,12 +33,13 @@ const Selector: Component<{
|
||||
itemClass?: string;
|
||||
itemStyle?: JSX.CSSProperties;
|
||||
label?: JSXElement;
|
||||
itemMap?: (item: string) => string;
|
||||
}> = (p) => {
|
||||
itemMap?: (item: T) => JSXElement;
|
||||
searchItemMap?: (item: T) => string;
|
||||
}) => {
|
||||
const [show, toggle] = useToggle();
|
||||
const [search, setSearch] = createSignal("");
|
||||
let search_ref: HTMLInputElement | undefined;
|
||||
const current = () => (p.itemMap ? p.itemMap(p.selected) : p.selected);
|
||||
const current = () => (p.itemMap ? p.itemMap(p.selected) : p.selected as JSXElement);
|
||||
createEffect(() => {
|
||||
if (show()) setTimeout(() => search_ref?.focus(), 200);
|
||||
});
|
||||
@@ -87,9 +88,11 @@ const Selector: Component<{
|
||||
each={
|
||||
p.useSearch
|
||||
? p.items.filter((item) =>
|
||||
p.itemMap
|
||||
? p.itemMap(item).includes(search())
|
||||
: item.includes(search())
|
||||
p.searchItemMap
|
||||
? p.searchItemMap(item).includes(search())
|
||||
: p.itemMap && typeof p.itemMap(item) === "string"
|
||||
? (p.itemMap(item) as string).includes(search())
|
||||
: (item as string).includes(search())
|
||||
)
|
||||
: p.items
|
||||
}
|
||||
@@ -107,7 +110,7 @@ const Selector: Component<{
|
||||
}}
|
||||
class={combineClasses(p.itemClass, s.SelectorItem)}
|
||||
>
|
||||
{p.itemMap ? p.itemMap(item) : item}
|
||||
{p.itemMap ? p.itemMap(item) : item as string}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -9,7 +9,7 @@ export type Position =
|
||||
| "bottom right"
|
||||
| "bottom center";
|
||||
|
||||
export function getPositionClass(position: Position = "bottom") {
|
||||
export function getPositionClass(position: Position = "bottom right") {
|
||||
switch (position) {
|
||||
case "left":
|
||||
return s.Left;
|
||||
|
||||
@@ -28,19 +28,19 @@ const SimpleTabs: Component<{
|
||||
containerClass?: string;
|
||||
containerStyle?: JSX.CSSProperties;
|
||||
}> = (p) => {
|
||||
const def = p.defaultSelected ? p.defaultSelected : p.tabs[0].title;
|
||||
const defaultSelected = p.defaultSelected ? p.defaultSelected : p.tabs[0].title;
|
||||
const [selected, set] = p.localStorageKey
|
||||
? useLocalStorage(def, p.localStorageKey)
|
||||
: createSignal(def);
|
||||
? useLocalStorage(defaultSelected, p.localStorageKey)
|
||||
: createSignal(defaultSelected);
|
||||
createEffect(() => {
|
||||
if (p.tabs.filter((tab) => tab.title === selected())[0] === undefined) {
|
||||
set(p.tabs[0].title);
|
||||
}
|
||||
});
|
||||
return <ControlledTabs selected={selected} set={set} {...p} />;
|
||||
return <ControlledSimpleTabs selected={selected} set={set} {...p} />;
|
||||
};
|
||||
|
||||
export const ControlledTabs: Component<{
|
||||
export const ControlledSimpleTabs: Component<{
|
||||
tabs: Tab[];
|
||||
selected: Accessor<string>;
|
||||
set: LocalStorageSetter<string>;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
convertTsMsToLocalUnixTsInSecs,
|
||||
get_to_one_sec_divisor,
|
||||
} from "../../util/helpers";
|
||||
import { useLocalStorage, useLocalStorageToggle } from "../../util/hooks";
|
||||
import { useLocalStorageToggle } from "../../util/hooks";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import LightweightChart, { LightweightValue } from "../shared/LightweightChart";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createContext, createResource, ParentComponent, Resource, useContext }
|
||||
import { useWindowKeyDown } from "../util/hooks";
|
||||
import {
|
||||
useBuilds,
|
||||
useBuildStats,
|
||||
useDeployments,
|
||||
useGroups,
|
||||
useProcedures,
|
||||
@@ -19,6 +20,7 @@ import connectToWs from "./ws";
|
||||
import { useUser } from "./UserProvider";
|
||||
import { AwsBuilderConfig, PermissionLevel, UpdateTarget } from "../types";
|
||||
import { client } from "..";
|
||||
import { BuildStatsResponse } from "../util/client_types";
|
||||
|
||||
export type State = {
|
||||
usernames: ReturnType<typeof useUsernames>;
|
||||
@@ -43,6 +45,7 @@ export type State = {
|
||||
docker_organizations: Resource<string[]>;
|
||||
github_webhook_base_url: Resource<string>;
|
||||
name_from_update_target: (target: UpdateTarget) => string;
|
||||
build_stats: ReturnType<typeof useBuildStats>;
|
||||
};
|
||||
|
||||
const context = createContext<
|
||||
@@ -93,6 +96,7 @@ export const AppStateProvider: ParentComponent = (p) => {
|
||||
});
|
||||
},
|
||||
builds,
|
||||
build_stats: useBuildStats(),
|
||||
getPermissionOnBuild: (id: string) => {
|
||||
const build = builds.get(id)!;
|
||||
const permissions = build.permissions![userId] as
|
||||
@@ -161,7 +165,7 @@ export const AppStateProvider: ParentComponent = (p) => {
|
||||
} else {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// createEffect(() => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
intoCollection,
|
||||
keepOnlyInObj,
|
||||
} from "../util/helpers";
|
||||
import { BuildStatsResponse } from "../util/client_types";
|
||||
|
||||
type Collection<T> = Record<string, T>;
|
||||
|
||||
@@ -217,6 +218,8 @@ export function useUsernames() {
|
||||
const load = async (userID: string) => {
|
||||
if (userID === "github") {
|
||||
set((s) => ({ ...s, [userID]: "github" }));
|
||||
} else if (userID === "auto redeploy") {
|
||||
set((s) => ({ ...s, [userID]: "auto redeploy" }));
|
||||
} else {
|
||||
const username = await client.get_username(userID);
|
||||
set((s) => ({ ...s, [userID]: username }));
|
||||
@@ -245,6 +248,26 @@ export function useBuilds() {
|
||||
);
|
||||
}
|
||||
|
||||
let build_stats_loading = false;
|
||||
export function useBuildStats() {
|
||||
const [stats, set] = createSignal<BuildStatsResponse>();
|
||||
const reload = () => {
|
||||
client.get_build_stats().then(set);
|
||||
};
|
||||
const get = () => {
|
||||
if (stats()) {
|
||||
return stats();
|
||||
} else if (!build_stats_loading) {
|
||||
build_stats_loading = true;
|
||||
reload()
|
||||
}
|
||||
}
|
||||
return {
|
||||
get,
|
||||
reload,
|
||||
};
|
||||
}
|
||||
|
||||
const deploymentIdPath = ["deployment", "_id", "$oid"];
|
||||
|
||||
export function useDeployments() {
|
||||
|
||||
@@ -63,7 +63,15 @@ function connectToWs(state: State) {
|
||||
}
|
||||
|
||||
async function handleMessage(
|
||||
{ deployments, builds, servers, groups, procedures, updates }: State,
|
||||
{
|
||||
deployments,
|
||||
builds,
|
||||
servers,
|
||||
groups,
|
||||
procedures,
|
||||
updates,
|
||||
build_stats,
|
||||
}: State,
|
||||
update: Update
|
||||
) {
|
||||
updates.addOrUpdate(update);
|
||||
@@ -135,13 +143,16 @@ async function handleMessage(
|
||||
if (update.status === UpdateStatus.Complete) {
|
||||
builds.delete(update.target.id!);
|
||||
}
|
||||
} else if (
|
||||
[Operation.UpdateBuild, Operation.BuildBuild].includes(update.operation)
|
||||
) {
|
||||
} else if (update.operation === Operation.UpdateBuild) {
|
||||
if (update.status === UpdateStatus.Complete) {
|
||||
const build = await client.get_build(update.target.id!);
|
||||
builds.update(build);
|
||||
}
|
||||
} else if (update.operation === Operation.BuildBuild) {
|
||||
if (update.status === UpdateStatus.Complete) {
|
||||
build_stats.reload();
|
||||
client.get_build(update.target.id!).then((build) => builds.update(build));
|
||||
}
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
@@ -26,6 +26,9 @@ code {
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -180,6 +183,10 @@ svg {
|
||||
background-color: rgba(c.$blue, 0.8);
|
||||
}
|
||||
|
||||
.blue.opaque {
|
||||
background-color: c.$blue;
|
||||
}
|
||||
|
||||
.blue:hover {
|
||||
background-color: c.$lightblue;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,11 @@ export interface Deployment {
|
||||
permissions?: PermissionsMap;
|
||||
skip_secret_interp?: boolean;
|
||||
docker_run_args: DockerRunArgs;
|
||||
term_signal_labels?: TerminationSignalLabel[];
|
||||
termination_signal?: TerminationSignal;
|
||||
termination_timeout?: number;
|
||||
build_id?: string;
|
||||
redeploy_on_build?: boolean;
|
||||
build_version?: Version;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
@@ -134,6 +138,11 @@ export interface DeploymentActionState {
|
||||
renaming: boolean;
|
||||
}
|
||||
|
||||
export interface TerminationSignalLabel {
|
||||
signal: TerminationSignal;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface DockerRunArgs {
|
||||
image: string;
|
||||
ports?: Conversion[];
|
||||
@@ -412,6 +421,13 @@ export enum RestartMode {
|
||||
UnlessStopped = "unless-stopped",
|
||||
}
|
||||
|
||||
export enum TerminationSignal {
|
||||
SigHup = "SIGHUP",
|
||||
SigInt = "SIGINT",
|
||||
SigQuit = "SIGQUIT",
|
||||
SigTerm = "SIGTERM",
|
||||
}
|
||||
|
||||
export enum AccountType {
|
||||
Github = "github",
|
||||
Docker = "docker",
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
UserCredentials,
|
||||
} from "../types";
|
||||
import {
|
||||
BuildStatsQuery,
|
||||
BuildStatsResponse,
|
||||
BuildVersionsQuery,
|
||||
CopyBuildBody,
|
||||
CopyDeploymentBody,
|
||||
@@ -43,7 +45,7 @@ import {
|
||||
ModifyUserCreateServerBody,
|
||||
ModifyUserEnabledBody,
|
||||
PermissionsUpdateBody,
|
||||
RenameDeploymentBody,
|
||||
StopContainerQuery,
|
||||
UpdateDescriptionBody,
|
||||
} from "./client_types";
|
||||
import { generateQuery, QueryObject } from "./helpers";
|
||||
@@ -217,20 +219,39 @@ export class Client {
|
||||
return this.post(`/api/deployment/${deployment_id}/pull`);
|
||||
}
|
||||
|
||||
deploy_container(deployment_id: string): Promise<Update> {
|
||||
return this.post(`/api/deployment/${deployment_id}/deploy`);
|
||||
deploy_container(
|
||||
deployment_id: string,
|
||||
query?: StopContainerQuery
|
||||
): Promise<Update> {
|
||||
return this.post(
|
||||
`/api/deployment/${deployment_id}/deploy${generateQuery(query as any)}`
|
||||
);
|
||||
}
|
||||
|
||||
start_container(deployment_id: string): Promise<Update> {
|
||||
return this.post(`/api/deployment/${deployment_id}/start_container`);
|
||||
}
|
||||
|
||||
stop_container(deployment_id: string): Promise<Update> {
|
||||
return this.post(`/api/deployment/${deployment_id}/stop_container`);
|
||||
stop_container(
|
||||
deployment_id: string,
|
||||
query?: StopContainerQuery
|
||||
): Promise<Update> {
|
||||
return this.post(
|
||||
`/api/deployment/${deployment_id}/stop_container${generateQuery(
|
||||
query as any
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
remove_container(deployment_id: string): Promise<Update> {
|
||||
return this.post(`/api/deployment/${deployment_id}/remove_container`);
|
||||
remove_container(
|
||||
deployment_id: string,
|
||||
query?: StopContainerQuery
|
||||
): Promise<Update> {
|
||||
return this.post(
|
||||
`/api/deployment/${deployment_id}/remove_container${generateQuery(
|
||||
query as any
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
async download_container_log(
|
||||
@@ -364,7 +385,15 @@ export class Client {
|
||||
return this.post(`/api/server/${server_id}/networks/prune`);
|
||||
}
|
||||
|
||||
get_docker_images(server_id: string): Promise<any[]> {
|
||||
get_docker_images(server_id: string): Promise<
|
||||
{
|
||||
RepoTags: string[];
|
||||
RepoDigests: string[];
|
||||
Size: number;
|
||||
Created: number;
|
||||
Id: string;
|
||||
}[]
|
||||
> {
|
||||
return this.get(`/api/server/${server_id}/images`);
|
||||
}
|
||||
|
||||
@@ -401,6 +430,10 @@ export class Client {
|
||||
return this.get(`/api/build/${id}/versions${generateQuery(query as any)}`);
|
||||
}
|
||||
|
||||
get_build_stats(query?: BuildStatsQuery): Promise<BuildStatsResponse> {
|
||||
return this.get(`/api/build/stats${generateQuery(query as any)}`);
|
||||
}
|
||||
|
||||
create_build(body: CreateBuildBody): Promise<Build> {
|
||||
return this.post("/api/build/create", body);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
import axios from "axios";
|
||||
import {
|
||||
BuildStatsQuery,
|
||||
BuildStatsResponse,
|
||||
BuildVersionsQuery,
|
||||
CopyBuildBody,
|
||||
CopyDeploymentBody,
|
||||
CreateBuildBody,
|
||||
CreateDeploymentBody,
|
||||
CreateGroupBody,
|
||||
CreateProcedureBody,
|
||||
CreateSecretBody,
|
||||
CreateServerBody,
|
||||
LoginOptions,
|
||||
ModifyUserCreateBuildBody,
|
||||
ModifyUserCreateServerBody,
|
||||
ModifyUserEnabledBody,
|
||||
PermissionsUpdateBody,
|
||||
StopContainerQuery,
|
||||
UpdateDescriptionBody,
|
||||
} from "./client_types";
|
||||
import { MONITOR_BASE_URL } from "..";
|
||||
import {
|
||||
AwsBuilderConfig,
|
||||
BasicContainerInfo,
|
||||
Build,
|
||||
BuildActionState,
|
||||
BuildVersionsReponse,
|
||||
Deployment,
|
||||
DeploymentActionState,
|
||||
DeploymentWithContainerState,
|
||||
DockerContainerStats,
|
||||
Group,
|
||||
HistoricalStatsQuery,
|
||||
Log,
|
||||
Operation,
|
||||
Procedure,
|
||||
Server,
|
||||
ServerActionState,
|
||||
ServerWithStatus,
|
||||
SystemInformation,
|
||||
SystemStats,
|
||||
SystemStatsQuery,
|
||||
SystemStatsRecord,
|
||||
Update,
|
||||
UpdateTarget,
|
||||
User,
|
||||
UserCredentials,
|
||||
} from "../types";
|
||||
import fileDownload from "js-file-download";
|
||||
import { QueryObject, generateQuery } from "./helpers";
|
||||
|
||||
export function monitor_client(base_url: string, token: string | null) {
|
||||
const state = {
|
||||
base_url,
|
||||
token: token as string | null,
|
||||
login_options: undefined as LoginOptions | undefined,
|
||||
monitor_title: undefined as string | undefined,
|
||||
secrets_cache: {} as Record<string, string[]>,
|
||||
github_accounts_cache: {} as Record<string, string[]>,
|
||||
docker_accounts_cache: {} as Record<string, string[]>,
|
||||
server_version_cache: {} as Record<string, string>,
|
||||
};
|
||||
|
||||
const request = request_client(state);
|
||||
|
||||
const client = {
|
||||
// ===========
|
||||
// AUTH / USER
|
||||
// ===========
|
||||
|
||||
async initialize() {
|
||||
const [loginOptions, monitorTitle] = await Promise.all([
|
||||
client.get_login_options(),
|
||||
client.get_monitor_title(),
|
||||
]);
|
||||
state.login_options = loginOptions;
|
||||
state.monitor_title = monitorTitle;
|
||||
document.title = monitorTitle;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const exchange_token = params.get("token");
|
||||
if (exchange_token) {
|
||||
history.replaceState({}, "", MONITOR_BASE_URL);
|
||||
try {
|
||||
const jwt = await client.exchange_for_jwt(exchange_token);
|
||||
token = jwt;
|
||||
localStorage.setItem("access_token", jwt);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get_login_options(): Promise<LoginOptions> {
|
||||
return request.get("/auth/options");
|
||||
},
|
||||
|
||||
async login(credentials: UserCredentials) {
|
||||
const jwt: string = await request.post("/auth/local/login", credentials);
|
||||
state.token = jwt;
|
||||
localStorage.setItem("access_token", state.token);
|
||||
return await client.get_user();
|
||||
},
|
||||
|
||||
async signup(credentials: UserCredentials) {
|
||||
const jwt: string = await request.post(
|
||||
"/auth/local/create_user",
|
||||
credentials
|
||||
);
|
||||
state.token = jwt;
|
||||
localStorage.setItem("access_token", state.token);
|
||||
return await client.get_user();
|
||||
},
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem("access_token");
|
||||
state.token = null;
|
||||
},
|
||||
|
||||
async get_user(): Promise<User | false> {
|
||||
if (state.token) {
|
||||
try {
|
||||
return await request.get("/api/user");
|
||||
} catch (error: any) {
|
||||
client.logout();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
get_username(user_id: string): Promise<string> {
|
||||
return request.get(`/api/username/${user_id}`);
|
||||
},
|
||||
|
||||
// admin only
|
||||
list_users(): Promise<User[]> {
|
||||
return request.get("/api/users");
|
||||
},
|
||||
|
||||
// admin only
|
||||
get_user_by_id(user_id: string): Promise<User> {
|
||||
return request.get(`/api/user/${user_id}`);
|
||||
},
|
||||
|
||||
exchange_for_jwt(exchange_token: string): Promise<string> {
|
||||
return request.post("/auth/exchange", { token: exchange_token });
|
||||
},
|
||||
|
||||
create_api_secret(body: CreateSecretBody): Promise<string> {
|
||||
return request.post("/api/secret/create", body);
|
||||
},
|
||||
|
||||
delete_api_secret(name: string): Promise<undefined> {
|
||||
return request.delete(`/api/secret/delete/${name}`);
|
||||
},
|
||||
|
||||
// ====
|
||||
// MISC
|
||||
// ====
|
||||
|
||||
get_github_webhook_base_url(): Promise<string> {
|
||||
return request.get("/api/github_webhook_base_url");
|
||||
},
|
||||
|
||||
update_description(body: UpdateDescriptionBody): Promise<undefined> {
|
||||
return request.post("/api/update_description", body);
|
||||
},
|
||||
|
||||
get_monitor_title(): Promise<string> {
|
||||
return request.get("/api/title");
|
||||
},
|
||||
|
||||
list_updates(
|
||||
offset: number,
|
||||
target?: UpdateTarget,
|
||||
show_builds?: boolean,
|
||||
operations?: Operation[]
|
||||
): Promise<Update[]> {
|
||||
return request.get(
|
||||
`/api/update/list${generateQuery({
|
||||
offset,
|
||||
type: target && target.type,
|
||||
id: target && target.id,
|
||||
show_builds,
|
||||
operations: operations?.join(","),
|
||||
})}`
|
||||
);
|
||||
},
|
||||
|
||||
// ==========
|
||||
// DEPLOYMENT
|
||||
// ==========
|
||||
|
||||
list_deployments(
|
||||
query?: QueryObject
|
||||
): Promise<DeploymentWithContainerState[]> {
|
||||
return request.get("/api/deployment/list" + generateQuery(query));
|
||||
},
|
||||
|
||||
get_deployment(id: string): Promise<DeploymentWithContainerState> {
|
||||
return request.get(`/api/deployment/${id}`);
|
||||
},
|
||||
|
||||
get_deployment_action_state(id: string): Promise<DeploymentActionState> {
|
||||
return request.get(`/api/deployment/${id}/action_state`);
|
||||
},
|
||||
|
||||
get_deployment_container_log(id: string, tail?: number): Promise<Log> {
|
||||
return request.get(`/api/deployment/${id}/log${generateQuery({ tail })}`);
|
||||
},
|
||||
|
||||
get_deployment_container_stats(id: string): Promise<DockerContainerStats> {
|
||||
return request.get(`/api/deployment/${id}/stats`);
|
||||
},
|
||||
|
||||
get_deployment_deployed_version(id: string): Promise<string> {
|
||||
return request.get(`/api/deployment/${id}/deployed_version`);
|
||||
},
|
||||
|
||||
create_deployment(body: CreateDeploymentBody): Promise<Deployment> {
|
||||
return request.post("/api/deployment/create", body);
|
||||
},
|
||||
|
||||
create_full_deployment(deployment: Deployment): Promise<Deployment> {
|
||||
return request.post("/api/deployment/create_full", deployment);
|
||||
},
|
||||
|
||||
copy_deployment(
|
||||
target_id: string,
|
||||
body: CopyDeploymentBody
|
||||
): Promise<Deployment> {
|
||||
return request.post(`/api/deployment/${target_id}/copy`, body);
|
||||
},
|
||||
|
||||
delete_deployment(id: string): Promise<Deployment> {
|
||||
return request.delete(`/api/deployment/${id}/delete`);
|
||||
},
|
||||
|
||||
update_deployment(deployment: Deployment): Promise<Deployment> {
|
||||
return request.patch("/api/deployment/update", deployment);
|
||||
},
|
||||
|
||||
rename_deployment(deployment_id: string, new_name: string) {
|
||||
return request.patch(`/api/deployment/${deployment_id}/rename`, {
|
||||
new_name,
|
||||
});
|
||||
},
|
||||
|
||||
reclone_deployment(deployment_id: string): Promise<Update> {
|
||||
return request.post(`/api/deployment/${deployment_id}/reclone`);
|
||||
},
|
||||
|
||||
pull_deployment(deployment_id: string): Promise<Update> {
|
||||
return request.post(`/api/deployment/${deployment_id}/pull`);
|
||||
},
|
||||
|
||||
deploy_container(
|
||||
deployment_id: string,
|
||||
query?: StopContainerQuery
|
||||
): Promise<Update> {
|
||||
return request.post(
|
||||
`/api/deployment/${deployment_id}/deploy${generateQuery(query as any)}`
|
||||
);
|
||||
},
|
||||
|
||||
start_container(deployment_id: string): Promise<Update> {
|
||||
return request.post(`/api/deployment/${deployment_id}/start_container`);
|
||||
},
|
||||
|
||||
stop_container(
|
||||
deployment_id: string,
|
||||
query?: StopContainerQuery
|
||||
): Promise<Update> {
|
||||
return request.post(
|
||||
`/api/deployment/${deployment_id}/stop_container${generateQuery(
|
||||
query as any
|
||||
)}`
|
||||
);
|
||||
},
|
||||
|
||||
remove_container(
|
||||
deployment_id: string,
|
||||
query?: StopContainerQuery
|
||||
): Promise<Update> {
|
||||
return request.post(
|
||||
`/api/deployment/${deployment_id}/remove_container${generateQuery(
|
||||
query as any
|
||||
)}`
|
||||
);
|
||||
},
|
||||
|
||||
async download_container_log(
|
||||
id: string,
|
||||
name: string,
|
||||
error?: boolean | undefined
|
||||
) {
|
||||
const log = await client.get_deployment_container_log(id, 5000);
|
||||
const date = new Date();
|
||||
fileDownload(
|
||||
(error ? log.stderr : log.stdout) || "no log",
|
||||
`${name}-${error ? "error-" : ""}log-${date
|
||||
.toLocaleDateString()
|
||||
.replaceAll("/", "-")}.txt`
|
||||
);
|
||||
},
|
||||
|
||||
// ======
|
||||
// SERVER
|
||||
// ======
|
||||
|
||||
list_servers(query?: QueryObject): Promise<ServerWithStatus[]> {
|
||||
return request.get("/api/server/list" + generateQuery(query));
|
||||
},
|
||||
|
||||
get_server(server_id: string): Promise<ServerWithStatus> {
|
||||
return request.get(`/api/server/${server_id}`);
|
||||
},
|
||||
|
||||
get_server_action_state(id: string): Promise<ServerActionState> {
|
||||
return request.get(`/api/server/${id}/action_state`);
|
||||
},
|
||||
|
||||
get_server_github_accounts(id: string): Promise<string[]> {
|
||||
return request.get(`/api/server/${id}/github_accounts`);
|
||||
},
|
||||
|
||||
get_server_docker_accounts(id: string): Promise<string[]> {
|
||||
return request.get(`/api/server/${id}/docker_accounts`);
|
||||
},
|
||||
|
||||
get_server_available_secrets(id: string): Promise<string[]> {
|
||||
return request.get(`/api/server/${id}/secrets`);
|
||||
},
|
||||
|
||||
get_server_version(id: string): Promise<string> {
|
||||
return request.get(`/api/server/${id}/version`);
|
||||
},
|
||||
|
||||
get_server_system_info(id: string): Promise<SystemInformation> {
|
||||
return request.get(`/api/server/${id}/system_information`);
|
||||
},
|
||||
|
||||
create_server(body: CreateServerBody): Promise<Server> {
|
||||
return request.post("/api/server/create", body);
|
||||
},
|
||||
|
||||
create_full_server(server: Server): Promise<Server> {
|
||||
return request.post("/api/server/create_full", server);
|
||||
},
|
||||
|
||||
delete_server(id: string): Promise<Server> {
|
||||
return request.delete(`/api/server/${id}/delete`);
|
||||
},
|
||||
|
||||
update_server(server: Server): Promise<Server> {
|
||||
return request.patch("/api/server/update", server);
|
||||
},
|
||||
|
||||
get_server_stats(
|
||||
server_id: string,
|
||||
query?: SystemStatsQuery
|
||||
): Promise<SystemStats> {
|
||||
return request.get(
|
||||
`/api/server/${server_id}/stats${generateQuery(query as any)}`
|
||||
);
|
||||
},
|
||||
|
||||
get_server_stats_history(
|
||||
server_id: string,
|
||||
query?: HistoricalStatsQuery
|
||||
): Promise<SystemStatsRecord[]> {
|
||||
return request.get(
|
||||
`/api/server/${server_id}/stats/history${generateQuery(query as any)}`
|
||||
);
|
||||
},
|
||||
|
||||
get_server_stats_at_ts(
|
||||
server_id: string,
|
||||
ts: number
|
||||
): Promise<SystemStatsRecord> {
|
||||
return request.get(`/api/server/${server_id}/stats/at_ts?ts=${ts}`);
|
||||
},
|
||||
|
||||
get_docker_networks(server_id: string): Promise<any[]> {
|
||||
return request.get(`/api/server/${server_id}/networks`);
|
||||
},
|
||||
|
||||
prune_docker_networks(server_id: string): Promise<Log> {
|
||||
return request.post(`/api/server/${server_id}/networks/prune`);
|
||||
},
|
||||
|
||||
get_docker_images(server_id: string): Promise<
|
||||
{
|
||||
RepoTags: string[];
|
||||
RepoDigests: string[];
|
||||
Size: number;
|
||||
Created: number;
|
||||
Id: string;
|
||||
}[]
|
||||
> {
|
||||
return request.get(`/api/server/${server_id}/images`);
|
||||
},
|
||||
|
||||
prune_docker_images(server_id: string): Promise<Log> {
|
||||
return request.post(`/api/server/${server_id}/images/prune`);
|
||||
},
|
||||
|
||||
get_docker_containers(server_id: string): Promise<BasicContainerInfo[]> {
|
||||
return request.get(`/api/server/${server_id}/containers`);
|
||||
},
|
||||
|
||||
prune_docker_containers(server_id: string): Promise<Log> {
|
||||
return request.post(`/api/server/${server_id}/containers/prune`);
|
||||
},
|
||||
|
||||
// =====
|
||||
// BUILD
|
||||
// =====
|
||||
|
||||
list_builds(query?: QueryObject): Promise<Build[]> {
|
||||
return request.get("/api/build/list" + generateQuery(query));
|
||||
},
|
||||
|
||||
get_build(build_id: string): Promise<Build> {
|
||||
return request.get(`/api/build/${build_id}`);
|
||||
},
|
||||
|
||||
get_build_action_state(id: string): Promise<BuildActionState> {
|
||||
return request.get(`/api/build/${id}/action_state`);
|
||||
},
|
||||
|
||||
get_build_versions(
|
||||
id: string,
|
||||
query?: BuildVersionsQuery
|
||||
): Promise<BuildVersionsReponse[]> {
|
||||
return request.get(
|
||||
`/api/build/${id}/versions${generateQuery(query as any)}`
|
||||
);
|
||||
},
|
||||
|
||||
get_build_stats(query?: BuildStatsQuery): Promise<BuildStatsResponse> {
|
||||
return request.get(`/api/build/stats${generateQuery(query as any)}`);
|
||||
},
|
||||
|
||||
create_build(body: CreateBuildBody): Promise<Build> {
|
||||
return request.post("/api/build/create", body);
|
||||
},
|
||||
|
||||
create_full_build(build: Build): Promise<Build> {
|
||||
return request.post("/api/build/create_full", build);
|
||||
},
|
||||
|
||||
copy_build(target_id: string, body: CopyBuildBody): Promise<Build> {
|
||||
return request.post(`/api/build/${target_id}/copy`, body);
|
||||
},
|
||||
|
||||
delete_build(id: string): Promise<Build> {
|
||||
return request.delete(`/api/build/${id}/delete`);
|
||||
},
|
||||
|
||||
update_build(build: Build): Promise<Build> {
|
||||
return request.patch("/api/build/update", build);
|
||||
},
|
||||
|
||||
build(build_id: string): Promise<Update> {
|
||||
return request.post(`/api/build/${build_id}/build`);
|
||||
},
|
||||
|
||||
reclone_build(id: string): Promise<Update> {
|
||||
return request.post(`/api/build/${id}/reclone`);
|
||||
},
|
||||
|
||||
get_aws_builder_defaults(): Promise<AwsBuilderConfig> {
|
||||
return request.get("/api/build/aws_builder_defaults");
|
||||
},
|
||||
|
||||
get_docker_organizations(): Promise<string[]> {
|
||||
return request.get("/api/build/docker_organizations");
|
||||
},
|
||||
|
||||
// =========
|
||||
// PROCEDURE
|
||||
// =========
|
||||
|
||||
list_procedures(query?: QueryObject): Promise<Procedure[]> {
|
||||
return request.get("/api/procedure/list" + generateQuery(query));
|
||||
},
|
||||
|
||||
get_procedure(procedure_id: string): Promise<Procedure> {
|
||||
return request.get(`/api/procedure/${procedure_id}`);
|
||||
},
|
||||
|
||||
create_procedure(body: CreateProcedureBody): Promise<Procedure> {
|
||||
return request.post("/api/procedure/create", body);
|
||||
},
|
||||
|
||||
create_full_procedure(procedure: Procedure): Promise<Procedure> {
|
||||
return request.post("/api/procedure/create_full", procedure);
|
||||
},
|
||||
|
||||
delete_procedure(id: string): Promise<Procedure> {
|
||||
return request.delete(`/api/procedure/${id}/delete`);
|
||||
},
|
||||
|
||||
update_procedure(procedure: Procedure): Promise<Procedure> {
|
||||
return request.patch("/api/procedure/update", procedure);
|
||||
},
|
||||
|
||||
run_procedure(id: string): Promise<Update> {
|
||||
return request.post(`/api/procedure/${id}/run`);
|
||||
},
|
||||
|
||||
// =====
|
||||
// GROUP
|
||||
// =====
|
||||
|
||||
list_groups(query?: QueryObject): Promise<Group[]> {
|
||||
return request.get("/api/group/list" + generateQuery(query));
|
||||
},
|
||||
|
||||
get_group(group_id: string): Promise<Group> {
|
||||
return request.get(`/api/group/${group_id}`);
|
||||
},
|
||||
|
||||
create_group(body: CreateGroupBody): Promise<Group> {
|
||||
return request.post("/api/group/create", body);
|
||||
},
|
||||
|
||||
create_full_group(group: Group): Promise<Group> {
|
||||
return request.post("/api/group/create_full", group);
|
||||
},
|
||||
|
||||
delete_group(id: string): Promise<Group> {
|
||||
return request.delete(`/api/group/${id}/delete`);
|
||||
},
|
||||
|
||||
update_group(group: Group): Promise<Group> {
|
||||
return request.patch("/api/group/update", group);
|
||||
},
|
||||
|
||||
// ===========
|
||||
// PERMISSIONS
|
||||
// ===========
|
||||
|
||||
update_user_permissions_on_target(
|
||||
body: PermissionsUpdateBody
|
||||
): Promise<Update> {
|
||||
return request.post("/api/permissions/update", body);
|
||||
},
|
||||
|
||||
modify_user_enabled(body: ModifyUserEnabledBody): Promise<Update> {
|
||||
return request.post("/api/permissions/modify_enabled", body);
|
||||
},
|
||||
|
||||
modify_user_create_server_permissions(
|
||||
body: ModifyUserCreateServerBody
|
||||
): Promise<Update> {
|
||||
return request.post("/api/permissions/modify_create_server", body);
|
||||
},
|
||||
|
||||
modify_user_create_build_permissions(
|
||||
body: ModifyUserCreateBuildBody
|
||||
): Promise<Update> {
|
||||
return request.post("/api/permissions/modify_create_build", body);
|
||||
},
|
||||
};
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
function request_client(state: { base_url: string; token: string | null }) {
|
||||
return {
|
||||
async get<R = any>(url: string): Promise<R> {
|
||||
return await axios({
|
||||
method: "get",
|
||||
url: state.base_url + url,
|
||||
headers: {
|
||||
authorization: state.token ? `Bearer ${state.token}` : undefined,
|
||||
},
|
||||
}).then(({ data }) => data);
|
||||
},
|
||||
async post<B = any, R = any>(url: string, body?: B): Promise<R> {
|
||||
return await axios({
|
||||
method: "post",
|
||||
url: state.base_url + url,
|
||||
headers: {
|
||||
authorization: `Bearer ${state.token}`,
|
||||
},
|
||||
data: body,
|
||||
}).then(({ data }) => data);
|
||||
},
|
||||
async patch<B = any, R = any>(url: string, body: B): Promise<R> {
|
||||
return await axios({
|
||||
method: "patch",
|
||||
url: state.base_url + url,
|
||||
headers: {
|
||||
authorization: `Bearer ${state.token}`,
|
||||
},
|
||||
data: body,
|
||||
}).then(({ data }) => data);
|
||||
},
|
||||
async delete<R = any>(url: string): Promise<R> {
|
||||
return await axios({
|
||||
method: "delete",
|
||||
url: state.base_url + url,
|
||||
headers: {
|
||||
authorization: `Bearer ${state.token}`,
|
||||
},
|
||||
}).then(({ data }) => data);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function login_with_github() {
|
||||
location.replace(`${MONITOR_BASE_URL}/auth/github/login`);
|
||||
}
|
||||
|
||||
export function login_with_google() {
|
||||
location.replace(`${MONITOR_BASE_URL}/auth/google/login`);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
Generated by typeshare 1.0.0
|
||||
*/
|
||||
|
||||
import { PermissionLevel, PermissionsTarget, UpdateTarget } from "../types";
|
||||
import { PermissionLevel, PermissionsTarget, TerminationSignal, UpdateTarget } from "../types";
|
||||
|
||||
export interface CreateBuildBody {
|
||||
name: string;
|
||||
@@ -19,6 +19,22 @@ export interface BuildVersionsQuery {
|
||||
patch?: number;
|
||||
}
|
||||
|
||||
export interface BuildStatsQuery {
|
||||
page?: number;
|
||||
}
|
||||
|
||||
export interface BuildStatsResponse {
|
||||
total_time: number;
|
||||
total_count: number;
|
||||
days: BuildStatsDay[];
|
||||
}
|
||||
|
||||
export interface BuildStatsDay {
|
||||
time: number;
|
||||
count: number;
|
||||
ts: number;
|
||||
}
|
||||
|
||||
export interface CreateDeploymentBody {
|
||||
name: string;
|
||||
server_id: string;
|
||||
@@ -37,6 +53,11 @@ export interface GetContainerLogQuery {
|
||||
tail?: number;
|
||||
}
|
||||
|
||||
export interface StopContainerQuery {
|
||||
stop_signal?: TerminationSignal;
|
||||
stop_time?: number;
|
||||
}
|
||||
|
||||
export interface CreateGroupBody {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import {
|
||||
Build,
|
||||
Deployment,
|
||||
DeploymentWithContainerState,
|
||||
DockerContainerState,
|
||||
EnvironmentVar,
|
||||
Server,
|
||||
ServerStatus,
|
||||
ServerWithStatus,
|
||||
Timelength,
|
||||
UpdateTarget,
|
||||
User,
|
||||
Version,
|
||||
} from "../types";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
export function combineClasses(...classes: (string | false | undefined)[]) {
|
||||
return classes.filter((c) => (c ? true : false)).join(" ");
|
||||
@@ -23,6 +18,15 @@ export function inPx(num: number) {
|
||||
|
||||
export type QueryObject = Record<string, string | number | boolean | undefined>;
|
||||
|
||||
export function sanitizeLog(log: string) {
|
||||
return sanitizeHtml(log, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.filter(
|
||||
(tag) => tag !== "script"
|
||||
),
|
||||
allowedAttributes: sanitizeHtml.defaults.allowedAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
export function generateQuery(query?: QueryObject) {
|
||||
if (query) {
|
||||
const q = Object.keys(query)
|
||||
@@ -251,3 +255,33 @@ export function readableUserType(user: User) {
|
||||
return "local";
|
||||
}
|
||||
}
|
||||
|
||||
const KB = 1024;
|
||||
const MB = 1024 ** 2;
|
||||
const GB = 1024 ** 3;
|
||||
|
||||
export function readableBytes(bytes: number) {
|
||||
if (bytes > GB) {
|
||||
return `${(bytes / GB).toFixed(1)} GiB`;
|
||||
} else if (bytes > MB) {
|
||||
return `${(bytes / MB).toFixed(1)} MiB`;
|
||||
} else {
|
||||
return `${(bytes / KB).toFixed(1)} KiB`;
|
||||
}
|
||||
}
|
||||
|
||||
export function readableImageId(id: string) {
|
||||
return id.replaceAll("sha256:", "").slice(0, 12);
|
||||
}
|
||||
|
||||
export function readableImageNameTag(repoTags?: string[], repoDigests?: string[]): [string, string] {
|
||||
if (repoTags && repoTags.length > 0) {
|
||||
const [name, tag] = repoTags[0].split(":");
|
||||
return [name, tag];
|
||||
} else if (repoDigests && repoDigests.length > 0) {
|
||||
const [name] = repoDigests[0].split("@");
|
||||
return [name, "none"];
|
||||
} else {
|
||||
return ["none", "none"]
|
||||
}
|
||||
}
|
||||
@@ -447,6 +447,13 @@
|
||||
dependencies:
|
||||
"@tanstack/query-core" "4.26.0"
|
||||
|
||||
"@types/sanitize-html@^2.9.0":
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.9.0.tgz#5b609f7592de22ef80a0930c39670329753dca1b"
|
||||
integrity sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==
|
||||
dependencies:
|
||||
htmlparser2 "^8.0.0"
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
@@ -454,6 +461,13 @@ ansi-styles@^3.2.1:
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
ansi-to-html@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.7.2.tgz#a92c149e4184b571eb29a0135ca001a8e2d710cb"
|
||||
integrity sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==
|
||||
dependencies:
|
||||
entities "^2.2.0"
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||
@@ -580,16 +594,61 @@ debug@^4.1.0:
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
deepmerge@^4.2.2:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
|
||||
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
|
||||
electron-to-chromium@^1.4.251:
|
||||
version "1.4.284"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
|
||||
integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
|
||||
|
||||
entities@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
|
||||
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
|
||||
|
||||
entities@^4.2.0, entities@^4.4.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
esbuild@^0.16.3:
|
||||
version "0.16.10"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.10.tgz#d485c28f1626a3f9c1796c952e4cd0561f0031bb"
|
||||
@@ -628,6 +687,11 @@ escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
||||
|
||||
escape-string-regexp@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
fancy-canvas@0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/fancy-canvas/-/fancy-canvas-0.2.2.tgz#33fd4976724169a1eda5015f515a2a1302d1ec91"
|
||||
@@ -698,6 +762,16 @@ html-entities@2.3.2:
|
||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488"
|
||||
integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==
|
||||
|
||||
htmlparser2@^8.0.0:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
entities "^4.4.0"
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.0.tgz#c91f09108ed7504c2f0faec7222f40178ff97b11"
|
||||
@@ -734,6 +808,11 @@ is-number@^7.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
is-plain-object@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
|
||||
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
|
||||
|
||||
is-what@^4.1.8:
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.8.tgz#0e2a8807fda30980ddb2571c79db3d209b14cbe4"
|
||||
@@ -802,6 +881,11 @@ nanoid@^3.3.4:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
||||
|
||||
nanoid@^3.3.6:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
||||
|
||||
node-releases@^2.0.6:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
|
||||
@@ -812,6 +896,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
parse-srcset@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
|
||||
|
||||
path-parse@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
@@ -827,6 +916,15 @@ picomatch@^2.0.4, picomatch@^2.2.1:
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
postcss@^8.3.11:
|
||||
version "8.4.23"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"
|
||||
integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8.4.20:
|
||||
version "8.4.20"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.20.tgz#64c52f509644cecad8567e949f4081d98349dc56"
|
||||
@@ -869,6 +967,18 @@ rollup@^3.7.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
sanitize-html@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.10.0.tgz#74d28848dfcf72c39693139131895c78900ab452"
|
||||
integrity sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==
|
||||
dependencies:
|
||||
deepmerge "^4.2.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
htmlparser2 "^8.0.0"
|
||||
is-plain-object "^5.0.0"
|
||||
parse-srcset "^1.0.2"
|
||||
postcss "^8.3.11"
|
||||
|
||||
sass@^1.57.1:
|
||||
version "1.57.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "db_client"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
types = { package = "monitor_types", path = "../types" }
|
||||
mungos = "0.3.8"
|
||||
mungos = "0.3.19"
|
||||
anyhow = "1.0"
|
||||
@@ -1,6 +1,9 @@
|
||||
use anyhow::Context;
|
||||
use mungos::{Collection, Mungos};
|
||||
use types::{Action, Build, Deployment, Group, Procedure, Server, SystemStatsRecord, Update, User};
|
||||
use types::{
|
||||
Action, Build, Deployment, Group, PeripheryCommand, Procedure, Server, SystemStatsRecord,
|
||||
Update, User,
|
||||
};
|
||||
|
||||
pub async fn users_collection(mungos: &Mungos, db_name: &str) -> anyhow::Result<Collection<User>> {
|
||||
let coll = mungos.collection(db_name, "users");
|
||||
@@ -133,3 +136,17 @@ pub async fn server_stats_collection(
|
||||
.context("failed at creating ts index")?;
|
||||
Ok(coll)
|
||||
}
|
||||
|
||||
pub async fn commands_collection(
|
||||
mungos: &Mungos,
|
||||
db_name: &str,
|
||||
) -> anyhow::Result<Collection<PeripheryCommand>> {
|
||||
let coll = mungos.collection(db_name, "commands");
|
||||
coll.create_unique_index("name")
|
||||
.await
|
||||
.context("failed at creating name index")?;
|
||||
coll.create_index("server_id")
|
||||
.await
|
||||
.context("failed at creating server_id index")?;
|
||||
Ok(coll)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use collections::{
|
||||
actions_collection, builds_collection, deployments_collection, groups_collection,
|
||||
procedures_collection, server_stats_collection, servers_collection, updates_collection,
|
||||
users_collection,
|
||||
actions_collection, builds_collection, commands_collection, deployments_collection,
|
||||
groups_collection, procedures_collection, server_stats_collection, servers_collection,
|
||||
updates_collection, users_collection,
|
||||
};
|
||||
use mungos::{Collection, Mungos};
|
||||
use types::{
|
||||
Action, Build, Deployment, Group, MongoConfig, PermissionLevel, Procedure, Server,
|
||||
SystemStatsRecord, Update, User,
|
||||
Action, Build, Deployment, Group, MongoConfig, PeripheryCommand, PermissionLevel, Procedure,
|
||||
Server, SystemStatsRecord, Update, User,
|
||||
};
|
||||
|
||||
mod collections;
|
||||
@@ -20,6 +20,7 @@ pub struct DbClient {
|
||||
pub procedures: Collection<Procedure>,
|
||||
pub actions: Collection<Action>,
|
||||
pub groups: Collection<Group>,
|
||||
pub commands: Collection<PeripheryCommand>,
|
||||
pub updates: Collection<Update>,
|
||||
pub stats: Collection<SystemStatsRecord>,
|
||||
}
|
||||
@@ -61,6 +62,9 @@ impl DbClient {
|
||||
stats: server_stats_collection(&mungos, db_name)
|
||||
.await
|
||||
.expect("failed to make stats collection"),
|
||||
commands: commands_collection(&mungos, db_name)
|
||||
.await
|
||||
.expect("failed to make commands collection"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,4 +201,28 @@ impl DbClient {
|
||||
.unwrap_or_default();
|
||||
Ok(permissions)
|
||||
}
|
||||
|
||||
pub async fn get_command(&self, command_id: &str) -> anyhow::Result<PeripheryCommand> {
|
||||
let command = self
|
||||
.commands
|
||||
.find_one_by_id(command_id)
|
||||
.await
|
||||
.context(format!("failed at mongo query for command {command_id}"))?
|
||||
.ok_or(anyhow!("command at {command_id} doesn't exist"))?;
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
pub async fn get_user_permission_on_command(
|
||||
&self,
|
||||
user_id: &str,
|
||||
command_id: &str,
|
||||
) -> anyhow::Result<PermissionLevel> {
|
||||
let permissions = *self
|
||||
.get_command(command_id)
|
||||
.await?
|
||||
.permissions
|
||||
.get(user_id)
|
||||
.unwrap_or_default();
|
||||
Ok(permissions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_helpers"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "helpers used as dependency for mogh tech monitor"
|
||||
@@ -12,7 +12,4 @@ license = "GPL-3.0-or-later"
|
||||
types = { package = "monitor_types", path = "../types" }
|
||||
anyhow = "1.0"
|
||||
axum = "0.6"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
toml = "0.7"
|
||||
rand = "0.8"
|
||||
|
||||
+1
-18
@@ -1,26 +1,9 @@
|
||||
use std::{borrow::Borrow, net::SocketAddr, str::FromStr};
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::http::StatusCode;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use types::Log;
|
||||
|
||||
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 get_socket_addr(port: u16) -> SocketAddr {
|
||||
SocketAddr::from_str(&format!("0.0.0.0:{}", port)).expect("failed to parse socket addr")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_client"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "a client to interact with the monitor system"
|
||||
@@ -9,9 +9,10 @@ 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.3.0"
|
||||
monitor_types = "0.3.4"
|
||||
# monitor_types = { path = "../types" }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
tokio-tungstenite = { version = "0.19", features=["native-tls"] }
|
||||
tokio = { version = "1.25", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
anyhow = "1.0"
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# monitor client
|
||||
|
||||
## *interact with the monitor system programatically*
|
||||
|
||||
with this crate you can leverage all the functionality of monitor through rust code. for example, you can...
|
||||
|
||||
- create and manage complex deployments
|
||||
- execute builds on specific schedules
|
||||
- monitor server stats programatically
|
||||
- program response actions to monitor updates sent over websocket
|
||||
|
||||
## initialize the client
|
||||
|
||||
you can initialize the client by directly passing args to the various initializers:
|
||||
|
||||
```rust
|
||||
use monitor_client::MonitorClient;
|
||||
|
||||
let MONITOR_URL: &str = "https://monitor.mogh.tech";
|
||||
|
||||
let monitor = MonitorClient::new_with_token(MONITOR_URL, jwt_token).await?; // pass a valid jwt
|
||||
let monitor = MonitorClient::new_with_password(MONITOR_URL, username, password).await?; // pass local user credentials
|
||||
let monitor = MonitorClient::new_with_secret(MONITOR_URL, username, secret).await?; // pass api secret
|
||||
```
|
||||
|
||||
or from the application environment / dotenv:
|
||||
|
||||
```sh
|
||||
MONITOR_URL=https://monitor.mogh.tech # required
|
||||
MONITOR_TOKEN=<jwt> # optional. pass the jwt directly.
|
||||
MONITOR_USERNAME=<username> # required for password / secret login
|
||||
MONITOR_PASSWORD=<password> # the users password
|
||||
MONITOR_SECRET=<secret> # the api secret
|
||||
```
|
||||
|
||||
to log in, you must pass either
|
||||
1. MONITOR_TOKEN
|
||||
2. MONITOR_USERNAME and MONITOR_PASSWORD
|
||||
3. MONITOR_USERNAME and MONITOR_SECRET
|
||||
|
||||
you can then initialize the client using this method:
|
||||
```rust
|
||||
let monitor = MonitorClient::new_from_env().await?;
|
||||
```
|
||||
|
||||
## use the client
|
||||
|
||||
the following will select a server, build monitor core on it, and deploy it.
|
||||
|
||||
```rust
|
||||
let server = monitor
|
||||
.list_servers(None)
|
||||
.await?
|
||||
.pop()
|
||||
.ok_or(anyhow!("no servers"))?;
|
||||
|
||||
let build = BuildBuilder::default()
|
||||
.name("monitor_core".into())
|
||||
.server_id(server.server.id.clone().into())
|
||||
.repo("mbecker20/monitor".to_string().into())
|
||||
.branch("main".to_string().into())
|
||||
.docker_build_args(
|
||||
DockerBuildArgs {
|
||||
build_path: ".".into(),
|
||||
dockerfile_path: "core/Dockerfile".to_string().into(),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.pre_build(
|
||||
Command {
|
||||
path: "frontend".into(),
|
||||
command: "yarn && yarn build".into(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let build = monitor.create_full_build(&build).await?;
|
||||
|
||||
println!("{build:#?}");
|
||||
|
||||
let build_update = monitor.build(&build.id).await?;
|
||||
|
||||
println!("{build_update:#?}");
|
||||
|
||||
let deployment = DeploymentBuilder::default()
|
||||
.name("monitor_core_1".into())
|
||||
.server_id(server.server.id.clone())
|
||||
.build_id(build.id.clone().into())
|
||||
.docker_run_args(
|
||||
DockerRunArgsBuilder::default()
|
||||
.volumes(vec![Conversion {
|
||||
local: "/home/max/.monitor/core.config.toml".into(),
|
||||
container: "/config/config.toml".into(),
|
||||
}])
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let deployment = monitor.create_full_deployment(&deployment).await?;
|
||||
|
||||
println!("{deployment:#?}");
|
||||
|
||||
let deploy_update = monitor.deploy_container(&deployment.id).await?;
|
||||
|
||||
println!("{deploy_update:#?}");
|
||||
```
|
||||
|
||||
note. this crate re-exports the [monitor types](https://crates.io/crates/monitor_types) crate under the module "types"
|
||||
@@ -1,12 +0,0 @@
|
||||
# monitor client
|
||||
|
||||
## *interact with the monitor system programatically*
|
||||
|
||||
with this crate you can leverage all the functionality of monitor through rust code. for example, you can...
|
||||
|
||||
- create and manage complex deployment cycles
|
||||
- execute builds on specific schedules
|
||||
- monitor server stats programatically
|
||||
- program response actions to monitor updates sent over websocket
|
||||
|
||||
this crate re-exports the [monitor types](https://crates.io/crates/monitor_types) crate under the module "types"
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "periphery_client"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
types = { package = "monitor_types", path = "../types" }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
tokio-tungstenite = { version = "0.19", features=["native-tls"] }
|
||||
tokio = "1.25"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = "1.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::PeripheryClient;
|
||||
impl PeripheryClient {
|
||||
pub async fn build(&self, server: &Server, build: &Build) -> anyhow::Result<Option<Vec<Log>>> {
|
||||
let res = self
|
||||
.post_json::<_, Vec<Log>>(server, "/build", build)
|
||||
.post_json(server, "/build", build, ())
|
||||
.await
|
||||
.context("failed to build image on periphery");
|
||||
match res {
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::PeripheryClient;
|
||||
|
||||
impl PeripheryClient {
|
||||
pub async fn run_command(&self, server: &Server, command: &Command) -> anyhow::Result<Log> {
|
||||
self.post_json(server, &format!("/command"), command)
|
||||
self.post_json(server, &format!("/command"), command, ())
|
||||
.await
|
||||
.context("failed to run command on periphery")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Context;
|
||||
use serde_json::json;
|
||||
use types::{BasicContainerInfo, Deployment, DockerContainerStats, Log, Server};
|
||||
use types::{BasicContainerInfo, Deployment, DockerContainerStats, Log, Server, TerminationSignal};
|
||||
|
||||
use crate::PeripheryClient;
|
||||
|
||||
@@ -37,6 +37,7 @@ impl PeripheryClient {
|
||||
server,
|
||||
"/container/start",
|
||||
&json!({ "name": container_name }),
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.context("failed to start container on periphery")
|
||||
@@ -46,11 +47,14 @@ impl PeripheryClient {
|
||||
&self,
|
||||
server: &Server,
|
||||
container_name: &str,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Log> {
|
||||
self.post_json(
|
||||
server,
|
||||
"/container/stop",
|
||||
&json!({ "name": container_name }),
|
||||
(("stop_signal", stop_signal), ("stop_time", stop_time)),
|
||||
)
|
||||
.await
|
||||
.context("failed to stop container on periphery")
|
||||
@@ -60,11 +64,14 @@ impl PeripheryClient {
|
||||
&self,
|
||||
server: &Server,
|
||||
container_name: &str,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Log> {
|
||||
self.post_json(
|
||||
server,
|
||||
"/container/remove",
|
||||
&json!({ "name": container_name }),
|
||||
(("stop_signal", stop_signal), ("stop_time", stop_time)),
|
||||
)
|
||||
.await
|
||||
.context("failed to remove container on periphery")
|
||||
@@ -80,19 +87,31 @@ impl PeripheryClient {
|
||||
server,
|
||||
"/container/rename",
|
||||
&json!({ "curr_name": curr_name, "new_name": new_name }),
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.context("failed to rename container on periphery")
|
||||
}
|
||||
|
||||
pub async fn deploy(&self, server: &Server, deployment: &Deployment) -> anyhow::Result<Log> {
|
||||
self.post_json(server, "/container/deploy", deployment)
|
||||
.await
|
||||
.context("failed to deploy container on periphery")
|
||||
pub async fn deploy(
|
||||
&self,
|
||||
server: &Server,
|
||||
deployment: &Deployment,
|
||||
stop_signal: Option<TerminationSignal>,
|
||||
stop_time: Option<i32>,
|
||||
) -> anyhow::Result<Log> {
|
||||
self.post_json(
|
||||
server,
|
||||
"/container/deploy",
|
||||
deployment,
|
||||
(("stop_signal", stop_signal), ("stop_time", stop_time)),
|
||||
)
|
||||
.await
|
||||
.context("failed to deploy container on periphery")
|
||||
}
|
||||
|
||||
pub async fn container_prune(&self, server: &Server) -> anyhow::Result<Log> {
|
||||
self.post_json(server, "/container/prune", &json!({}))
|
||||
self.post_json(server, "/container/prune", &json!({}), ())
|
||||
.await
|
||||
.context("failed to prune containers on periphery")
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ impl PeripheryClient {
|
||||
clone_args: impl Into<CloneArgs>,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let clone_args: CloneArgs = clone_args.into();
|
||||
self.post_json(server, "/git/clone", &clone_args)
|
||||
self.post_json(server, "/git/clone", &clone_args, ())
|
||||
.await
|
||||
.context("failed to clone repo on periphery")
|
||||
}
|
||||
@@ -27,13 +27,14 @@ impl PeripheryClient {
|
||||
server,
|
||||
"/git/pull",
|
||||
&json!({ "name": name, "branch": branch, "on_pull": on_pull }),
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.context("failed to pull repo on periphery")
|
||||
}
|
||||
|
||||
pub async fn delete_repo(&self, server: &Server, build_name: &str) -> anyhow::Result<Log> {
|
||||
self.post_json(server, "/git/delete", &json!({ "name": build_name }))
|
||||
self.post_json(server, "/git/delete", &json!({ "name": build_name }), ())
|
||||
.await
|
||||
.context("failed to delete repo on periphery")
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ impl PeripheryClient {
|
||||
}
|
||||
|
||||
pub async fn image_prune(&self, server: &Server) -> anyhow::Result<Log> {
|
||||
self.post_json(server, &format!("/image/prune"), &())
|
||||
self.post_json(server, &format!("/image/prune"), &(), ())
|
||||
.await
|
||||
.context("failed to prune images on periphery")
|
||||
}
|
||||
|
||||
@@ -170,17 +170,19 @@ impl PeripheryClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_json<B: Serialize, R: DeserializeOwned>(
|
||||
async fn post_json<B: Serialize, R: DeserializeOwned, Q: Serialize>(
|
||||
&self,
|
||||
server: &Server,
|
||||
endpoint: &str,
|
||||
body: &B,
|
||||
query: impl Into<Option<Q>>,
|
||||
) -> anyhow::Result<R> {
|
||||
self.health_check(server).await?;
|
||||
let res = self
|
||||
.http_client
|
||||
.post(format!("{}{endpoint}", server.address))
|
||||
.header("authorization", &self.passkey)
|
||||
.query(&query.into())
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -24,19 +24,20 @@ impl PeripheryClient {
|
||||
"name": name,
|
||||
"driver": driver
|
||||
}),
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.context("failed to create network on periphery")
|
||||
}
|
||||
|
||||
pub async fn network_delete(&self, server: &Server, name: &str) -> anyhow::Result<Log> {
|
||||
self.post_json(server, "/network/delete", &json!({ "name": name }))
|
||||
self.post_json(server, "/network/delete", &json!({ "name": name }), ())
|
||||
.await
|
||||
.context("failed to delete network on periphery")
|
||||
}
|
||||
|
||||
pub async fn network_prune(&self, server: &Server) -> anyhow::Result<Log> {
|
||||
self.post_json(server, "/network/prune", &json!({}))
|
||||
self.post_json(server, "/network/prune", &json!({}), ())
|
||||
.await
|
||||
.context("failed to prune networks on periphery")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_types"
|
||||
version = "0.3.0"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "types for the mogh tech monitor"
|
||||
@@ -10,13 +10,12 @@ license = "GPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
bson = "2.4"
|
||||
bson = "2.6"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
diff-struct = "0.5"
|
||||
bollard = "0.14.0"
|
||||
derive_builder = "0.12"
|
||||
typeshare = "1.0.0"
|
||||
typeshare = "1.0.1"
|
||||
chrono = "0.4"
|
||||
anyhow = "1.0"
|
||||
@@ -46,10 +46,30 @@ pub struct Deployment {
|
||||
#[diff(attr(#[serde(skip_serializing_if = "docker_run_args_diff_no_change")]))]
|
||||
pub docker_run_args: DockerRunArgs,
|
||||
|
||||
#[serde(default = "default_term_signal_labels")]
|
||||
#[builder(default = "vec![TerminationSignalLabel::default()]")]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "vec_diff_no_change")]))]
|
||||
pub term_signal_labels: Vec<TerminationSignalLabel>,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "termination_signal_diff_no_change")]))]
|
||||
pub termination_signal: TerminationSignal,
|
||||
|
||||
#[serde(default = "default_termination_timeout")]
|
||||
#[builder(default = "10")]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "i32_diff_no_change")]))]
|
||||
pub termination_timeout: i32,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
pub build_id: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub redeploy_on_build: bool,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
pub build_version: Option<Version>,
|
||||
@@ -89,6 +109,14 @@ pub struct Deployment {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
fn default_termination_timeout() -> i32 {
|
||||
10
|
||||
}
|
||||
|
||||
fn default_term_signal_labels() -> Vec<TerminationSignalLabel> {
|
||||
vec![TerminationSignalLabel::default()]
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct DeploymentWithContainerState {
|
||||
@@ -110,6 +138,18 @@ pub struct DeploymentActionState {
|
||||
pub renaming: bool,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, Diff, Builder)]
|
||||
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
|
||||
pub struct TerminationSignalLabel {
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "termination_signal_diff_no_change")]))]
|
||||
pub signal: TerminationSignal,
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Diff, Builder)]
|
||||
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
|
||||
@@ -243,10 +283,22 @@ impl Default for DockerContainerState {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy, Diff,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Display,
|
||||
EnumString,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Diff,
|
||||
Default,
|
||||
)]
|
||||
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
|
||||
pub enum RestartMode {
|
||||
#[default]
|
||||
#[serde(rename = "no")]
|
||||
#[strum(serialize = "no")]
|
||||
NoRestart,
|
||||
@@ -261,8 +313,32 @@ pub enum RestartMode {
|
||||
UnlessStopped,
|
||||
}
|
||||
|
||||
impl Default for RestartMode {
|
||||
fn default() -> RestartMode {
|
||||
RestartMode::NoRestart
|
||||
}
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Display,
|
||||
EnumString,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Diff,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
|
||||
pub enum TerminationSignal {
|
||||
#[serde(alias = "1")]
|
||||
SigHup,
|
||||
#[serde(alias = "2")]
|
||||
SigInt,
|
||||
#[serde(alias = "3")]
|
||||
SigQuit,
|
||||
#[default]
|
||||
#[serde(alias = "15")]
|
||||
SigTerm,
|
||||
}
|
||||
|
||||
+15
-3
@@ -1,8 +1,8 @@
|
||||
use diff::{Diff, OptionDiff, VecDiff};
|
||||
|
||||
use crate::{
|
||||
deployment::{DockerRunArgsDiff, RestartModeDiff},
|
||||
TimelengthDiff,
|
||||
deployment::{DockerRunArgsDiff, RestartModeDiff, TerminationSignalDiff},
|
||||
CommandDiff, TimelengthDiff,
|
||||
};
|
||||
|
||||
pub fn f64_diff_no_change(f64_diff: &f64) -> bool {
|
||||
@@ -13,6 +13,10 @@ pub fn f32_diff_no_change(f32_diff: &f32) -> bool {
|
||||
*f32_diff == 0.0
|
||||
}
|
||||
|
||||
pub fn i32_diff_no_change(i32_diff: &i32) -> bool {
|
||||
*i32_diff == 0
|
||||
}
|
||||
|
||||
pub fn option_diff_no_change<T: Diff>(option_diff: &OptionDiff<T>) -> bool
|
||||
where
|
||||
<T as Diff>::Repr: PartialEq,
|
||||
@@ -24,7 +28,7 @@ pub fn vec_diff_no_change<T: Diff>(vec_diff: &VecDiff<T>) -> bool {
|
||||
vec_diff.0.is_empty()
|
||||
}
|
||||
|
||||
// pub fn hashmap_diff_no_change<T: Diff>(hashmap_diff: &HashMapDiff<String, T>) -> bool {
|
||||
// pub fn hashmap_diff_no_change<K: Hash + Eq, T: Diff>(hashmap_diff: &HashMapDiff<K, T>) -> bool {
|
||||
// hashmap_diff.altered.is_empty() && hashmap_diff.removed.is_empty()
|
||||
// }
|
||||
|
||||
@@ -48,3 +52,11 @@ pub fn restart_mode_diff_no_change(restart_mode: &RestartModeDiff) -> bool {
|
||||
pub fn timelength_diff_no_change(timelength: &TimelengthDiff) -> bool {
|
||||
timelength == &TimelengthDiff::NoChange
|
||||
}
|
||||
|
||||
pub fn termination_signal_diff_no_change(term_signal: &TerminationSignalDiff) -> bool {
|
||||
term_signal == &TerminationSignalDiff::NoChange
|
||||
}
|
||||
|
||||
pub fn command_diff_no_change(command: &CommandDiff) -> bool {
|
||||
command.command.is_none() && command.path.is_none()
|
||||
}
|
||||
|
||||
+32
-10
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ::diff::Diff;
|
||||
use anyhow::Context;
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use anyhow::{anyhow, Context};
|
||||
use chrono::{DateTime, LocalResult, SecondsFormat, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::{Display, EnumString};
|
||||
use typeshare::typeshare;
|
||||
@@ -17,6 +17,7 @@ mod config;
|
||||
mod deployment;
|
||||
mod diff;
|
||||
mod group;
|
||||
mod periphery_command;
|
||||
mod procedure;
|
||||
mod server;
|
||||
mod update;
|
||||
@@ -27,6 +28,7 @@ pub use build::*;
|
||||
pub use config::*;
|
||||
pub use deployment::*;
|
||||
pub use group::*;
|
||||
pub use periphery_command::*;
|
||||
pub use procedure::*;
|
||||
pub use server::*;
|
||||
pub use update::*;
|
||||
@@ -49,7 +51,7 @@ pub struct CloneArgs {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Diff)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Diff)]
|
||||
#[diff(attr(#[derive(Debug, PartialEq, Serialize)]))]
|
||||
pub struct Command {
|
||||
#[serde(default)]
|
||||
@@ -125,6 +127,12 @@ pub enum Operation {
|
||||
UpdateProcedure,
|
||||
DeleteProcedure,
|
||||
|
||||
// command
|
||||
CreateCommand,
|
||||
UpdateCommand,
|
||||
DeleteCommand,
|
||||
RunCommand,
|
||||
|
||||
// group
|
||||
CreateGroup,
|
||||
UpdateGroup,
|
||||
@@ -199,7 +207,18 @@ pub enum PermissionsTarget {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy, Diff,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Display,
|
||||
EnumString,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Diff,
|
||||
Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
@@ -220,6 +239,7 @@ pub enum Timelength {
|
||||
#[serde(rename = "30-sec")]
|
||||
#[strum(serialize = "30-sec")]
|
||||
ThirtySeconds,
|
||||
#[default]
|
||||
#[serde(rename = "1-min")]
|
||||
#[strum(serialize = "1-min")]
|
||||
OneMinute,
|
||||
@@ -270,12 +290,6 @@ pub enum Timelength {
|
||||
ThirtyDays,
|
||||
}
|
||||
|
||||
impl Default for Timelength {
|
||||
fn default() -> Timelength {
|
||||
Timelength::OneMinute
|
||||
}
|
||||
}
|
||||
|
||||
pub fn monitor_timestamp() -> String {
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, false)
|
||||
}
|
||||
@@ -285,3 +299,11 @@ pub fn unix_from_monitor_ts(ts: &str) -> anyhow::Result<i64> {
|
||||
.context("failed to parse rfc3339 timestamp")?
|
||||
.timestamp_millis())
|
||||
}
|
||||
|
||||
pub fn monitor_ts_from_unix(ts: i64) -> anyhow::Result<String> {
|
||||
match Utc.timestamp_millis_opt(ts) {
|
||||
LocalResult::Single(dt) => Ok(dt.to_rfc3339_opts(SecondsFormat::Millis, false)),
|
||||
LocalResult::None => Err(anyhow!("out of bounds timestamp passed")),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
use bson::serde_helpers::hex_string_as_object_id;
|
||||
use derive_builder::Builder;
|
||||
use diff::Diff;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::{diff::command_diff_no_change, Command, PermissionsMap};
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, Diff, Builder)]
|
||||
#[diff(attr(#[derive(Debug, Serialize)]))]
|
||||
pub struct PeripheryCommand {
|
||||
#[serde(
|
||||
default,
|
||||
rename = "_id",
|
||||
skip_serializing_if = "String::is_empty",
|
||||
with = "hex_string_as_object_id"
|
||||
)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
#[builder(setter(skip))]
|
||||
pub id: String,
|
||||
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub name: String, // must be formatted to be compat with docker
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub description: String,
|
||||
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub server_id: String,
|
||||
|
||||
#[serde(default)]
|
||||
#[diff(attr(#[serde(skip_serializing)]))]
|
||||
#[builder(setter(skip))]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "command_diff_no_change")]))]
|
||||
pub command: Command,
|
||||
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
#[diff(attr(#[serde(skip)]))]
|
||||
#[builder(setter(skip))]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
#[diff(attr(#[serde(skip)]))]
|
||||
#[builder(setter(skip))]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct CommandActionState {
|
||||
pub running: bool,
|
||||
}
|
||||
+14
-2
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Build, BuildActionState, CloneArgs, Deployment, DeploymentActionState, Group, PermissionLevel,
|
||||
PermissionsMap, Procedure, Server, ServerActionState,
|
||||
Build, BuildActionState, CloneArgs, CommandActionState, Deployment, DeploymentActionState,
|
||||
Group, PeripheryCommand, PermissionLevel, PermissionsMap, Procedure, Server, ServerActionState,
|
||||
};
|
||||
|
||||
pub trait Permissioned {
|
||||
@@ -41,6 +41,12 @@ impl Permissioned for Group {
|
||||
}
|
||||
}
|
||||
|
||||
impl Permissioned for PeripheryCommand {
|
||||
fn permissions_map(&self) -> &PermissionsMap {
|
||||
&self.permissions
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Busy {
|
||||
fn busy(&self) -> bool;
|
||||
}
|
||||
@@ -70,6 +76,12 @@ impl Busy for BuildActionState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Busy for CommandActionState {
|
||||
fn busy(&self) -> bool {
|
||||
self.running
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Deployment> for CloneArgs {
|
||||
fn from(d: &Deployment) -> Self {
|
||||
CloneArgs {
|
||||
|
||||
@@ -75,6 +75,7 @@ pub enum UpdateTarget {
|
||||
Server(String),
|
||||
Procedure(String),
|
||||
Group(String),
|
||||
Command(String),
|
||||
}
|
||||
|
||||
impl Default for UpdateTarget {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user