mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-21 22:00:36 -05:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
392e691f92 | ||
|
|
495e208ccd | ||
|
|
14474adb90 | ||
|
|
896784e2e3 | ||
|
|
2e690bce24 | ||
|
|
7172d24512 | ||
|
|
b754c89118 | ||
|
|
31a23dfe2d | ||
|
|
b0f80cafc3 | ||
|
|
85a16f6c6f | ||
|
|
29a7e4c27b | ||
|
|
a73b572725 | ||
|
|
aa44bf04e8 | ||
|
|
93348621c5 | ||
|
|
4b2139ede2 | ||
|
|
3251216be7 | ||
|
|
1f980a45e8 | ||
|
|
94da1dce99 | ||
|
|
d4fc015494 | ||
|
|
5800fc91d2 | ||
|
|
91785e1e8f | ||
|
|
41fccdb16e | ||
|
|
78cf93da8a | ||
|
|
ea36549dbe | ||
|
|
a319095869 |
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -41,7 +41,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alerter"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.5",
|
||||
@@ -967,7 +967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"monitor_client",
|
||||
"run_command",
|
||||
@@ -1351,7 +1351,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"serror",
|
||||
]
|
||||
@@ -1482,7 +1482,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -2085,7 +2085,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logger"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -2154,7 +2154,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "migrator"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2289,7 +2289,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_cli"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2309,7 +2309,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_client"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2341,7 +2341,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_core"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2399,7 +2399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_periphery"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -2835,7 +2835,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_client",
|
||||
@@ -3987,7 +3987,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tests"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenvy",
|
||||
@@ -4594,7 +4594,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "update_logger"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"logger",
|
||||
|
||||
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
edition = "2021"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -15,7 +15,7 @@ monitor_client = { path = "client/core/rs" }
|
||||
|
||||
[workspace.dependencies]
|
||||
# LOCAL
|
||||
monitor_client = "1.13.0"
|
||||
monitor_client = "1.13.1"
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
command = { path = "lib/command" }
|
||||
|
||||
@@ -8,6 +8,8 @@ Monitor CLI is a tool to sync monitor resources and execute operations.
|
||||
cargo install monitor_cli
|
||||
```
|
||||
|
||||
Note: On Ubuntu, also requires `apt install build-essential pkg-config libssl-dev`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Credentials
|
||||
|
||||
@@ -32,8 +32,8 @@ pub struct CliArgs {
|
||||
}
|
||||
|
||||
fn default_creds() -> String {
|
||||
let home = std::env::var("HOME")
|
||||
.expect("no HOME env var. cannot get default config path.");
|
||||
let home =
|
||||
std::env::var("HOME").unwrap_or_else(|_| String::from("/root"));
|
||||
format!("{home}/.config/monitor/creds.toml")
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,19 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
return Err(anyhow!("Must attach builder to RunBuild"));
|
||||
}
|
||||
|
||||
// get the action state for the build (or insert default).
|
||||
let action_state =
|
||||
action_states().build.get_or_insert_default(&build.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure build not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.building = true)?;
|
||||
|
||||
build.config.version.increment();
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let git_token = git_token(
|
||||
&build.config.git_provider,
|
||||
&build.config.git_account,
|
||||
@@ -80,22 +93,9 @@ impl Resolve<RunBuild, (User, Update)> for State {
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", build.config.git_provider, build.config.git_account),
|
||||
)?;
|
||||
|
||||
// get the action state for the build (or insert default).
|
||||
let action_state =
|
||||
action_states().build.get_or_insert_default(&build.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure build not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.building = true)?;
|
||||
|
||||
let (registry_token, aws_ecr) =
|
||||
validate_account_extract_registry_token_aws_ecr(&build).await?;
|
||||
|
||||
build.config.version.increment();
|
||||
update.version = build.config.version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
let mut cancel_recv =
|
||||
|
||||
@@ -6,8 +6,7 @@ use monitor_client::{
|
||||
build::{Build, ImageRegistry},
|
||||
config::core::AwsEcrConfig,
|
||||
deployment::{
|
||||
extract_registry_domain, Deployment, DeploymentActionState,
|
||||
DeploymentImage,
|
||||
extract_registry_domain, Deployment, DeploymentImage,
|
||||
},
|
||||
get_image_name,
|
||||
permission::PermissionLevel,
|
||||
@@ -36,7 +35,6 @@ use crate::{
|
||||
async fn setup_deployment_execution(
|
||||
deployment: &str,
|
||||
user: &User,
|
||||
set_in_progress: impl Fn(&mut DeploymentActionState),
|
||||
) -> anyhow::Result<(Deployment, Server)> {
|
||||
let deployment = resource::get_check_permissions::<Deployment>(
|
||||
deployment,
|
||||
@@ -49,16 +47,6 @@ async fn setup_deployment_execution(
|
||||
return Err(anyhow!("deployment has no server configured"));
|
||||
}
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard = action_state.update(set_in_progress)?;
|
||||
|
||||
let (server, status) =
|
||||
get_server_with_status(&deployment.config.server_id).await?;
|
||||
if status != ServerState::Ok {
|
||||
@@ -82,10 +70,21 @@ impl Resolve<Deploy, (User, Update)> for State {
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let (mut deployment, server) =
|
||||
setup_deployment_execution(&deployment, &user, |state| {
|
||||
state.deploying = true
|
||||
})
|
||||
.await?;
|
||||
setup_deployment_execution(&deployment, &user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.deploying = true)?;
|
||||
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
@@ -234,10 +233,21 @@ impl Resolve<StartContainer, (User, Update)> for State {
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&deployment, &user, |state| {
|
||||
state.starting = true
|
||||
})
|
||||
.await?;
|
||||
setup_deployment_execution(&deployment, &user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.starting = true)?;
|
||||
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
@@ -271,10 +281,21 @@ impl Resolve<RestartContainer, (User, Update)> for State {
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&deployment, &user, |state| {
|
||||
state.restarting = true
|
||||
})
|
||||
.await?;
|
||||
setup_deployment_execution(&deployment, &user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.restarting = true)?;
|
||||
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
@@ -310,10 +331,21 @@ impl Resolve<PauseContainer, (User, Update)> for State {
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&deployment, &user, |state| {
|
||||
state.pausing = true
|
||||
})
|
||||
.await?;
|
||||
setup_deployment_execution(&deployment, &user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pausing = true)?;
|
||||
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
@@ -347,10 +379,21 @@ impl Resolve<UnpauseContainer, (User, Update)> for State {
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&deployment, &user, |state| {
|
||||
state.unpausing = true
|
||||
})
|
||||
.await?;
|
||||
setup_deployment_execution(&deployment, &user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.unpausing = true)?;
|
||||
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
@@ -390,10 +433,21 @@ impl Resolve<StopContainer, (User, Update)> for State {
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&deployment, &user, |state| {
|
||||
state.stopping = true
|
||||
})
|
||||
.await?;
|
||||
setup_deployment_execution(&deployment, &user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.stopping = true)?;
|
||||
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
@@ -437,10 +491,21 @@ impl Resolve<RemoveContainer, (User, Update)> for State {
|
||||
(user, mut update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let (deployment, server) =
|
||||
setup_deployment_execution(&deployment, &user, |state| {
|
||||
state.removing = true
|
||||
})
|
||||
.await?;
|
||||
setup_deployment_execution(&deployment, &user).await?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
.get_or_insert_default(&deployment.id)
|
||||
.await;
|
||||
|
||||
// Will check to ensure deployment not already busy before updating, and return Err if so.
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.removing = true)?;
|
||||
|
||||
// Send update after setting action state, this way frontend gets correct state.
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ fn resolve_inner(
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.running = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let update = Mutex::new(update);
|
||||
|
||||
let res = execute_procedure(&procedure, &update).await;
|
||||
|
||||
@@ -54,6 +54,17 @@ impl Resolve<CloneRepo, (User, Update)> for State {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// get the action state for the repo (or insert default).
|
||||
let action_state =
|
||||
action_states().repo.get_or_insert_default(&repo.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure repo not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.cloning = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let git_token = git_token(
|
||||
&repo.config.git_provider,
|
||||
&repo.config.git_account,
|
||||
@@ -64,15 +75,6 @@ impl Resolve<CloneRepo, (User, Update)> for State {
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", repo.config.git_provider, repo.config.git_account),
|
||||
)?;
|
||||
|
||||
// get the action state for the repo (or insert default).
|
||||
let action_state =
|
||||
action_states().repo.get_or_insert_default(&repo.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure repo not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.cloning = true)?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached"));
|
||||
}
|
||||
@@ -138,6 +140,8 @@ impl Resolve<PullRepo, (User, Update)> for State {
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pulling = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
if repo.config.server_id.is_empty() {
|
||||
return Err(anyhow!("repo has no server attached"));
|
||||
}
|
||||
@@ -241,6 +245,17 @@ impl Resolve<BuildRepo, (User, Update)> for State {
|
||||
return Err(anyhow!("Must attach builder to BuildRepo"));
|
||||
}
|
||||
|
||||
// get the action state for the repo (or insert default).
|
||||
let action_state =
|
||||
action_states().repo.get_or_insert_default(&repo.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure repo not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.building = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let git_token = git_token(
|
||||
&repo.config.git_provider,
|
||||
&repo.config.git_account,
|
||||
@@ -251,15 +266,6 @@ impl Resolve<BuildRepo, (User, Update)> for State {
|
||||
|| format!("Failed to get git token in call to db. This is a database error, not a token exisitence error. Stopping run. | {} | {}", repo.config.git_provider, repo.config.git_account),
|
||||
)?;
|
||||
|
||||
// get the action state for the repo (or insert default).
|
||||
let action_state =
|
||||
action_states().repo.get_or_insert_default(&repo.id).await;
|
||||
|
||||
// This will set action state back to default when dropped.
|
||||
// Will also check to ensure repo not already busy before updating.
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.building = true)?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let cancel_clone = cancel.clone();
|
||||
let mut cancel_recv =
|
||||
|
||||
@@ -47,6 +47,8 @@ impl Resolve<StopAllContainers, (User, Update)> for State {
|
||||
let _action_guard = action_state
|
||||
.update(|state| state.stopping_containers = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let logs = periphery_client(&server)?
|
||||
.request(api::container::StopAllContainers {})
|
||||
.await
|
||||
@@ -90,6 +92,8 @@ impl Resolve<PruneContainers, (User, Update)> for State {
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pruning_containers = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let log = match periphery
|
||||
@@ -144,6 +148,8 @@ impl Resolve<PruneNetworks, (User, Update)> for State {
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pruning_networks = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let log = match periphery
|
||||
@@ -196,6 +202,8 @@ impl Resolve<PruneImages, (User, Update)> for State {
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.pruning_images = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
let log =
|
||||
|
||||
@@ -48,6 +48,8 @@ impl Resolve<DeployStack, (User, Update)> for State {
|
||||
let _action_guard =
|
||||
action_state.update(|state| state.deploying = true)?;
|
||||
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let git_token = crate::helpers::git_token(
|
||||
&stack.config.git_provider,
|
||||
&stack.config.git_account,
|
||||
@@ -198,16 +200,11 @@ impl Resolve<StartStack, (User, Update)> for State {
|
||||
StartStack { stack, service }: StartStack,
|
||||
(user, update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let no_service = service.is_none();
|
||||
execute_compose::<StartStack>(
|
||||
&stack,
|
||||
service,
|
||||
&user,
|
||||
|state| {
|
||||
if no_service {
|
||||
state.starting = true
|
||||
}
|
||||
},
|
||||
|state| state.starting = true,
|
||||
update,
|
||||
(),
|
||||
)
|
||||
@@ -222,15 +219,12 @@ impl Resolve<RestartStack, (User, Update)> for State {
|
||||
RestartStack { stack, service }: RestartStack,
|
||||
(user, update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let no_service = service.is_none();
|
||||
execute_compose::<RestartStack>(
|
||||
&stack,
|
||||
service,
|
||||
&user,
|
||||
|state| {
|
||||
if no_service {
|
||||
state.restarting = true;
|
||||
}
|
||||
state.restarting = true;
|
||||
},
|
||||
update,
|
||||
(),
|
||||
@@ -246,16 +240,11 @@ impl Resolve<PauseStack, (User, Update)> for State {
|
||||
PauseStack { stack, service }: PauseStack,
|
||||
(user, update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let no_service = service.is_none();
|
||||
execute_compose::<PauseStack>(
|
||||
&stack,
|
||||
service,
|
||||
&user,
|
||||
|state| {
|
||||
if no_service {
|
||||
state.pausing = true
|
||||
}
|
||||
},
|
||||
|state| state.pausing = true,
|
||||
update,
|
||||
(),
|
||||
)
|
||||
@@ -270,16 +259,11 @@ impl Resolve<UnpauseStack, (User, Update)> for State {
|
||||
UnpauseStack { stack, service }: UnpauseStack,
|
||||
(user, update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let no_service = service.is_none();
|
||||
execute_compose::<UnpauseStack>(
|
||||
&stack,
|
||||
service,
|
||||
&user,
|
||||
|state| {
|
||||
if no_service {
|
||||
state.unpausing = true
|
||||
}
|
||||
},
|
||||
|state| state.unpausing = true,
|
||||
update,
|
||||
(),
|
||||
)
|
||||
@@ -298,16 +282,11 @@ impl Resolve<StopStack, (User, Update)> for State {
|
||||
}: StopStack,
|
||||
(user, update): (User, Update),
|
||||
) -> anyhow::Result<Update> {
|
||||
let no_service = service.is_none();
|
||||
execute_compose::<StopStack>(
|
||||
&stack,
|
||||
service,
|
||||
&user,
|
||||
|state| {
|
||||
if no_service {
|
||||
state.stopping = true
|
||||
}
|
||||
},
|
||||
|state| state.stopping = true,
|
||||
update,
|
||||
stop_time,
|
||||
)
|
||||
|
||||
@@ -58,6 +58,9 @@ impl Resolve<RunSync, (User, Update)> for State {
|
||||
return Err(anyhow!("resource sync repo not configured"));
|
||||
}
|
||||
|
||||
// Send update here for FE to recheck action state
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let (res, logs, hash, message) =
|
||||
crate::helpers::sync::remote::get_remote_resources(&sync)
|
||||
.await
|
||||
|
||||
@@ -107,11 +107,16 @@ impl Resolve<GetReposSummary, User> for State {
|
||||
(_, action_states) if action_states.pulling => {
|
||||
res.pulling += 1;
|
||||
}
|
||||
(_, action_states) if action_states.building => {
|
||||
res.building += 1;
|
||||
}
|
||||
(RepoState::Ok, _) => res.ok += 1,
|
||||
(RepoState::Failed, _) => res.failed += 1,
|
||||
(RepoState::Unknown, _) => res.unknown += 1,
|
||||
// will never come off the cache in the building state, since that comes from action states
|
||||
(RepoState::Cloning, _) | (RepoState::Pulling, _) => {
|
||||
(RepoState::Cloning, _)
|
||||
| (RepoState::Pulling, _)
|
||||
| (RepoState::Building, _) => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -132,6 +137,7 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
|
||||
managed: false,
|
||||
clone_enabled: false,
|
||||
pull_enabled: false,
|
||||
build_enabled: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -149,6 +155,7 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
|
||||
managed: false,
|
||||
clone_enabled: false,
|
||||
pull_enabled: false,
|
||||
build_enabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,6 +167,7 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
|
||||
managed: false,
|
||||
clone_enabled: false,
|
||||
pull_enabled: false,
|
||||
build_enabled: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -185,23 +193,33 @@ impl Resolve<GetRepoWebhooksEnabled, User> for State {
|
||||
format!("{host}/listener/github/repo/{}/clone", repo.id);
|
||||
let pull_url =
|
||||
format!("{host}/listener/github/repo/{}/pull", repo.id);
|
||||
let build_url =
|
||||
format!("{host}/listener/github/repo/{}/build", repo.id);
|
||||
|
||||
let mut clone_enabled = false;
|
||||
let mut pull_enabled = false;
|
||||
let mut build_enabled = false;
|
||||
|
||||
for webhook in webhooks {
|
||||
if webhook.active && webhook.config.url == clone_url {
|
||||
if !webhook.active {
|
||||
continue;
|
||||
}
|
||||
if webhook.config.url == clone_url {
|
||||
clone_enabled = true
|
||||
}
|
||||
if webhook.active && webhook.config.url == pull_url {
|
||||
if webhook.config.url == pull_url {
|
||||
pull_enabled = true
|
||||
}
|
||||
if webhook.config.url == build_url {
|
||||
build_enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GetRepoWebhooksEnabledResponse {
|
||||
managed: true,
|
||||
clone_enabled,
|
||||
pull_enabled,
|
||||
build_enabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,8 @@ impl Resolve<GetStacksSummary, User> for State {
|
||||
StackState::Paused => res.paused += 1,
|
||||
StackState::Stopped => res.stopped += 1,
|
||||
StackState::Restarting => res.restarting += 1,
|
||||
StackState::Created => res.created += 1,
|
||||
StackState::Removing => res.removing += 1,
|
||||
StackState::Dead => res.dead += 1,
|
||||
StackState::Unhealthy => res.unhealthy += 1,
|
||||
StackState::Down => res.down += 1,
|
||||
|
||||
@@ -309,6 +309,13 @@ impl Resolve<ExportResourcesToToml, User> for State {
|
||||
.get(&repo.config.server_id)
|
||||
.unwrap_or(&String::new()),
|
||||
);
|
||||
// replace repo builder with name
|
||||
repo.config.builder_id.clone_from(
|
||||
names
|
||||
.builders
|
||||
.get(&repo.config.builder_id)
|
||||
.unwrap_or(&String::new()),
|
||||
);
|
||||
res.repos.push(convert_resource::<Repo>(repo, &names.tags))
|
||||
}
|
||||
ResourceTarget::Stack(id) => {
|
||||
|
||||
@@ -225,6 +225,9 @@ impl Resolve<CreateRepoWebhook, User> for State {
|
||||
RepoWebhookAction::Pull => {
|
||||
format!("{host}/listener/github/repo/{}/pull", repo.id)
|
||||
}
|
||||
RepoWebhookAction::Build => {
|
||||
format!("{host}/listener/github/repo/{}/build", repo.id)
|
||||
}
|
||||
};
|
||||
|
||||
for webhook in webhooks {
|
||||
@@ -339,6 +342,9 @@ impl Resolve<DeleteRepoWebhook, User> for State {
|
||||
RepoWebhookAction::Pull => {
|
||||
format!("{host}/listener/github/repo/{}/pull", repo.id)
|
||||
}
|
||||
RepoWebhookAction::Build => {
|
||||
format!("{host}/listener/github/repo/{}/build", repo.id)
|
||||
}
|
||||
};
|
||||
|
||||
for webhook in webhooks {
|
||||
|
||||
@@ -5,13 +5,13 @@ use monitor_client::entities::{
|
||||
alerter::Alerter,
|
||||
build::Build,
|
||||
builder::Builder,
|
||||
deployment::{ContainerSummary, Deployment, DeploymentState},
|
||||
deployment::{Deployment, DeploymentState},
|
||||
permission::PermissionLevel,
|
||||
procedure::Procedure,
|
||||
repo::Repo,
|
||||
server::{Server, ServerState},
|
||||
server_template::ServerTemplate,
|
||||
stack::{Stack, StackServiceNames, StackState},
|
||||
stack::{ComposeProject, Stack, StackState},
|
||||
sync::ResourceSync,
|
||||
tag::Tag,
|
||||
update::{ResourceTarget, ResourceTargetVariant, Update},
|
||||
@@ -33,11 +33,6 @@ use crate::{
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::stack::{
|
||||
compose_container_match_regex,
|
||||
services::extract_services_from_stack,
|
||||
};
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
// user: Id or username
|
||||
pub async fn get_user(user: &str) -> anyhow::Result<User> {
|
||||
@@ -98,63 +93,51 @@ pub async fn get_deployment_state(
|
||||
}
|
||||
|
||||
/// Can pass all the containers from the same server
|
||||
pub fn get_stack_state_from_containers(
|
||||
services: &[StackServiceNames],
|
||||
containers: &[ContainerSummary],
|
||||
pub fn get_stack_state_from_projects(
|
||||
stack: &Stack,
|
||||
projects: &[ComposeProject],
|
||||
) -> StackState {
|
||||
// first filter the containers to only ones which match the service
|
||||
let containers = containers.iter().filter(|container| {
|
||||
services.iter().any(|StackServiceNames { service_name, container_name }| {
|
||||
match compose_container_match_regex(container_name)
|
||||
.with_context(|| format!("failed to construct container name matching regex for service {service_name}"))
|
||||
{
|
||||
Ok(regex) => regex,
|
||||
Err(e) => {
|
||||
warn!("{e:#}");
|
||||
return false
|
||||
}
|
||||
}.is_match(&container.name)
|
||||
let project_name = stack.project_name(false);
|
||||
let Some(status) = projects
|
||||
.iter()
|
||||
.find(|project| project.name == project_name)
|
||||
.and_then(|project| project.status.as_deref())
|
||||
else {
|
||||
return StackState::Down;
|
||||
};
|
||||
let Ok(states) = status
|
||||
.split(", ")
|
||||
.filter_map(|state| state.split('(').next())
|
||||
.map(|state| {
|
||||
state.parse::<DeploymentState>().with_context(|| {
|
||||
format!("failed to parse stack state entry: {state}")
|
||||
})
|
||||
})
|
||||
}).collect::<Vec<_>>();
|
||||
if containers.is_empty() {
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.inspect_err(|e| warn!("{e:#}"))
|
||||
else {
|
||||
return StackState::Unknown;
|
||||
};
|
||||
if states.is_empty() {
|
||||
return StackState::Down;
|
||||
}
|
||||
if services.len() != containers.len() {
|
||||
if states.len() > 1 {
|
||||
return StackState::Unhealthy;
|
||||
}
|
||||
let running = containers
|
||||
.iter()
|
||||
.all(|container| container.state == DeploymentState::Running);
|
||||
if running {
|
||||
return StackState::Running;
|
||||
match states[0] {
|
||||
DeploymentState::Unknown => StackState::Unknown,
|
||||
DeploymentState::NotDeployed => StackState::Down,
|
||||
DeploymentState::Created => StackState::Created,
|
||||
DeploymentState::Restarting => StackState::Restarting,
|
||||
DeploymentState::Running => StackState::Running,
|
||||
DeploymentState::Removing => StackState::Removing,
|
||||
DeploymentState::Paused => StackState::Paused,
|
||||
DeploymentState::Exited => StackState::Stopped,
|
||||
DeploymentState::Dead => StackState::Dead,
|
||||
}
|
||||
let paused = containers
|
||||
.iter()
|
||||
.all(|container| container.state == DeploymentState::Paused);
|
||||
if paused {
|
||||
return StackState::Paused;
|
||||
}
|
||||
let stopped = containers
|
||||
.iter()
|
||||
.all(|container| container.state == DeploymentState::Exited);
|
||||
if stopped {
|
||||
return StackState::Stopped;
|
||||
}
|
||||
let restarting = containers
|
||||
.iter()
|
||||
.all(|container| container.state == DeploymentState::Restarting);
|
||||
if restarting {
|
||||
return StackState::Restarting;
|
||||
}
|
||||
let dead = containers
|
||||
.iter()
|
||||
.all(|container| container.state == DeploymentState::Dead);
|
||||
if dead {
|
||||
return StackState::Dead;
|
||||
}
|
||||
StackState::Unhealthy
|
||||
}
|
||||
|
||||
/// Gets stack state fresh from periphery
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn get_stack_state(
|
||||
stack: &Stack,
|
||||
@@ -167,13 +150,11 @@ pub async fn get_stack_state(
|
||||
if status != ServerState::Ok {
|
||||
return Ok(StackState::Unknown);
|
||||
}
|
||||
let containers = super::periphery_client(&server)?
|
||||
.request(periphery_client::api::container::GetContainerList {})
|
||||
let projects = super::periphery_client(&server)?
|
||||
.request(periphery_client::api::compose::ListComposeProjects {})
|
||||
.await?;
|
||||
|
||||
let services = extract_services_from_stack(stack, false).await?;
|
||||
|
||||
Ok(get_stack_state_from_containers(&services, &containers))
|
||||
Ok(get_stack_state_from_projects(stack, &projects))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
|
||||
@@ -48,6 +48,9 @@ pub async fn execute_compose<T: ExecuteCompose>(
|
||||
// The returned guard will set the action state back to default when dropped.
|
||||
let _action_guard = action_state.update(set_in_progress)?;
|
||||
|
||||
// Send update here for frontend to recheck action state
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let periphery = periphery_client(&server)?;
|
||||
|
||||
if let Some(service) = &service {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{fs, path::Path};
|
||||
use std::{fs, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::format_serror;
|
||||
@@ -38,6 +38,8 @@ pub async fn get_remote_compose_contents(
|
||||
.context("failed to clone stack repo")?;
|
||||
|
||||
let run_directory = repo_path.join(&stack.config.run_directory);
|
||||
// This will remove any intermediate '/./' which can be a problem for some OS.
|
||||
let run_directory = run_directory.components().collect::<PathBuf>();
|
||||
|
||||
let mut oks = Vec::new();
|
||||
let mut errs = Vec::new();
|
||||
|
||||
@@ -148,6 +148,13 @@ impl ResourceSync for Repo {
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Need to replace builder id with name
|
||||
original.builder_id = resources
|
||||
.builders
|
||||
.get(&original.builder_id)
|
||||
.map(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(original.partial_diff(update))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,23 @@ pub async fn add_update(
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn add_update_without_send(
|
||||
update: &Update,
|
||||
) -> anyhow::Result<String> {
|
||||
let id = db_client()
|
||||
.await
|
||||
.updates
|
||||
.insert_one(update)
|
||||
.await
|
||||
.context("failed to insert update into db")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not object id")?
|
||||
.to_string();
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn update_update(update: Update) -> anyhow::Result<()> {
|
||||
update_one_by_id(&db_client().await.updates, &update.id, mungos::update::Update::Set(to_document(&update)?), None)
|
||||
@@ -312,6 +329,7 @@ pub async fn init_execution_update(
|
||||
};
|
||||
let mut update = make_update(target, operation, user);
|
||||
update.in_progress();
|
||||
update.id = add_update(update.clone()).await?;
|
||||
// Don't actually send it here, let the handlers send it after they can set action state.
|
||||
update.id = add_update_without_send(&update).await?;
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,24 @@ pub fn router() -> Router {
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/repo/:id/build",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, body: String| async move {
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("repo_build_webhook", id);
|
||||
async {
|
||||
let res = repo::handle_repo_build_webhook(id.clone(), headers, body).await;
|
||||
if let Err(e) = res {
|
||||
warn!("failed to run repo build webook for repo {id} | {e:#}");
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
},
|
||||
)
|
||||
)
|
||||
.route(
|
||||
"/stack/:id/refresh",
|
||||
post(
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::OnceLock;
|
||||
use anyhow::anyhow;
|
||||
use axum::http::HeaderMap;
|
||||
use monitor_client::{
|
||||
api::execute::{CloneRepo, PullRepo},
|
||||
api::execute::{BuildRepo, CloneRepo, PullRepo},
|
||||
entities::{repo::Repo, user::git_webhook_user},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
@@ -84,3 +84,35 @@ pub async fn handle_repo_pull_webhook(
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_repo_build_webhook(
|
||||
repo_id: String,
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Acquire and hold lock to make a task queue for
|
||||
// subsequent listener calls on same resource.
|
||||
// It would fail if we let it go through from action state busy.
|
||||
let lock = repo_locks().get_or_insert_default(&repo_id).await;
|
||||
let _lock = lock.lock().await;
|
||||
|
||||
verify_gh_signature(headers, &body).await?;
|
||||
let request_branch = extract_branch(&body)?;
|
||||
let repo = resource::get::<Repo>(&repo_id).await?;
|
||||
if !repo.config.webhook_enabled {
|
||||
return Err(anyhow!("repo does not have webhook enabled"));
|
||||
}
|
||||
if request_branch != repo.config.branch {
|
||||
return Err(anyhow!("request branch does not match expected"));
|
||||
}
|
||||
let user = git_webhook_user().to_owned();
|
||||
let req = crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
|
||||
repo: repo_id,
|
||||
});
|
||||
let update = init_execution_update(&req, &user).await?;
|
||||
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req else {
|
||||
unreachable!()
|
||||
};
|
||||
State.resolve(req, (user, update)).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -214,7 +214,7 @@ pub async fn update_cache_for_server(server: &Server) {
|
||||
Ok((containers, networks, images, projects)) => {
|
||||
tokio::join!(
|
||||
resources::update_deployment_cache(deployments, &containers),
|
||||
resources::update_stack_cache(stacks, &containers),
|
||||
resources::update_stack_cache(stacks, &containers, &projects),
|
||||
);
|
||||
insert_server_status(
|
||||
server,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use anyhow::Context;
|
||||
use monitor_client::entities::{
|
||||
deployment::{ContainerSummary, Deployment, DeploymentState},
|
||||
stack::{Stack, StackService, StackServiceNames},
|
||||
stack::{ComposeProject, Stack, StackService, StackServiceNames},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
query::get_stack_state_from_containers,
|
||||
query::get_stack_state_from_projects,
|
||||
stack::{
|
||||
compose_container_match_regex,
|
||||
services::extract_services_from_stack,
|
||||
@@ -55,6 +55,7 @@ pub async fn update_deployment_cache(
|
||||
pub async fn update_stack_cache(
|
||||
stacks: Vec<Stack>,
|
||||
containers: &[ContainerSummary],
|
||||
projects: &[ComposeProject],
|
||||
) {
|
||||
let stack_status_cache = stack_status_cache();
|
||||
for stack in stacks {
|
||||
@@ -92,7 +93,7 @@ pub async fn update_stack_cache(
|
||||
.map(|s| s.curr.state);
|
||||
let status = CachedStackStatus {
|
||||
id: stack.id.clone(),
|
||||
state: get_stack_state_from_containers(&services, containers),
|
||||
state: get_stack_state_from_projects(&stack, projects),
|
||||
services: services_with_containers,
|
||||
};
|
||||
stack_status_cache
|
||||
|
||||
@@ -174,10 +174,12 @@ async fn validate_config(
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(builder_id) = &config.builder_id {
|
||||
let builder = super::get_check_permissions::<Builder>(builder_id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create build using this builder. user must have at least read permissions on the builder.")?;
|
||||
config.builder_id = Some(builder.id)
|
||||
if !builder_id.is_empty() {
|
||||
let builder = super::get_check_permissions::<Builder>(builder_id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("cannot create build using this builder. user must have at least read permissions on the builder.")?;
|
||||
config.builder_id = Some(builder.id)
|
||||
}
|
||||
}
|
||||
if let Some(build_args) = &mut config.build_args {
|
||||
build_args.retain(|v| {
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::time::Duration;
|
||||
use anyhow::Context;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::entities::{
|
||||
builder::Builder,
|
||||
permission::PermissionLevel,
|
||||
repo::{
|
||||
PartialRepoConfig, Repo, RepoConfig, RepoConfigDiff, RepoInfo,
|
||||
@@ -203,18 +204,25 @@ async fn validate_config(
|
||||
config: &mut PartialRepoConfig,
|
||||
user: &User,
|
||||
) -> anyhow::Result<()> {
|
||||
match &config.server_id {
|
||||
Some(server_id) if !server_id.is_empty() => {
|
||||
if let Some(server_id) = &config.server_id {
|
||||
if !server_id.is_empty() {
|
||||
let server = get_check_permissions::<Server>(
|
||||
server_id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
.context("cannot create repo on this server. user must have update permissions on the server.")?;
|
||||
server_id,
|
||||
user,
|
||||
PermissionLevel::Write,
|
||||
)
|
||||
.await
|
||||
.context("Cannot attach repo to this server. User must have write permissions on the server.")?;
|
||||
config.server_id = Some(server.id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(builder_id) = &config.builder_id {
|
||||
if !builder_id.is_empty() {
|
||||
let builder = super::get_check_permissions::<Builder>(builder_id, user, PermissionLevel::Read)
|
||||
.await
|
||||
.context("Cannot attach repo to this builder. User must have at least read permissions on the builder.")?;
|
||||
config.builder_id = Some(builder.id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -231,6 +239,8 @@ async fn get_repo_state(id: &String) -> RepoState {
|
||||
Some(RepoState::Cloning)
|
||||
} else if s.pulling {
|
||||
Some(RepoState::Pulling)
|
||||
} else if s.building {
|
||||
Some(RepoState::Building)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -67,28 +67,32 @@ impl super::MonitorResource for Stack {
|
||||
.map(|service| service.service_name)
|
||||
.collect();
|
||||
// This is only true if it is KNOWN to be true. so other cases are false.
|
||||
let (project_missing, status) = if stack.config.server_id.is_empty()
|
||||
|| matches!(state, StackState::Down | StackState::Unknown)
|
||||
{
|
||||
(false, None)
|
||||
} else if let Some(status) = server_status_cache()
|
||||
.get(&stack.config.server_id)
|
||||
.await
|
||||
.as_ref()
|
||||
{
|
||||
if let Some(projects) = &status.projects {
|
||||
if let Some(project) = projects.iter().find(|project| project.name == project_name) {
|
||||
(false, project.status.clone())
|
||||
let (project_missing, status) =
|
||||
if stack.config.server_id.is_empty()
|
||||
|| matches!(state, StackState::Down | StackState::Unknown)
|
||||
{
|
||||
(false, None)
|
||||
} else if let Some(status) = server_status_cache()
|
||||
.get(&stack.config.server_id)
|
||||
.await
|
||||
.as_ref()
|
||||
{
|
||||
if let Some(projects) = &status.projects {
|
||||
if let Some(project) = projects
|
||||
.iter()
|
||||
.find(|project| project.name == project_name)
|
||||
{
|
||||
(false, project.status.clone())
|
||||
} else {
|
||||
// The project doesn't exist
|
||||
(true, None)
|
||||
}
|
||||
} else {
|
||||
// The project doesn't exist
|
||||
(true, None)
|
||||
(false, None)
|
||||
}
|
||||
} else {
|
||||
(false, None)
|
||||
}
|
||||
} else {
|
||||
(false, None)
|
||||
};
|
||||
};
|
||||
StackListItem {
|
||||
id: stack.id,
|
||||
name: stack.name,
|
||||
|
||||
@@ -22,7 +22,7 @@ impl Resolve<ListComposeProjects, ()> for State {
|
||||
let docker_compose = docker_compose();
|
||||
let res = run_monitor_command(
|
||||
"list projects",
|
||||
format!("{docker_compose} ls --format json"),
|
||||
format!("{docker_compose} ls --all --format json"),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ pub async fn compose_up(
|
||||
res.logs.push(log);
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(&root).await.with_context(|| {
|
||||
format!("failed to clean up files after deploy | path: {root:?}")
|
||||
format!("failed to clean up files after deploy | path: {root:?} | ensure all volumes are mounted outside the repo directory (preferably use absolute path for mounts)")
|
||||
}) {
|
||||
res
|
||||
.logs
|
||||
@@ -194,6 +194,9 @@ async fn write_stack(
|
||||
.stack_dir
|
||||
.join(to_monitor_name(&stack.name));
|
||||
let run_directory = root.join(&stack.config.run_directory);
|
||||
// This will remove any intermediate '/./' in the path, which is a problem for some OS.
|
||||
// Cannot use canonicalize yet as directory may not exist.
|
||||
let run_directory = run_directory.components().collect::<PathBuf>();
|
||||
|
||||
if stack.config.file_contents.is_empty() {
|
||||
// Clone the repo
|
||||
@@ -283,7 +286,9 @@ async fn write_stack(
|
||||
} else {
|
||||
// Ensure run directory exists
|
||||
fs::create_dir_all(&run_directory).await.with_context(|| {
|
||||
format!("failed to create stack run directory at {root:?}")
|
||||
format!(
|
||||
"failed to create stack run directory at {run_directory:?}"
|
||||
)
|
||||
})?;
|
||||
let env_file_path = match write_environment_file(
|
||||
&stack.config.environment,
|
||||
|
||||
@@ -105,6 +105,8 @@ pub struct GetReposSummaryResponse {
|
||||
pub cloning: u32,
|
||||
/// The number of repos currently pulling.
|
||||
pub pulling: u32,
|
||||
/// The number of repos currently building.
|
||||
pub building: u32,
|
||||
/// The number of repos with failed state.
|
||||
pub failed: u32,
|
||||
/// The number of repos with unknown state.
|
||||
@@ -137,4 +139,6 @@ pub struct GetRepoWebhooksEnabledResponse {
|
||||
pub clone_enabled: bool,
|
||||
/// Whether pushes to branch trigger pull. Will always be false if managed is false.
|
||||
pub pull_enabled: bool,
|
||||
/// Whether pushes to branch trigger build. Will always be false if managed is false.
|
||||
pub build_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -213,6 +213,10 @@ pub struct GetStacksSummaryResponse {
|
||||
pub restarting: u32,
|
||||
/// The number of stacks with Dead state.
|
||||
pub dead: u32,
|
||||
/// The number of stacks with Created state.
|
||||
pub created: u32,
|
||||
/// The number of stacks with Removing state.
|
||||
pub removing: u32,
|
||||
/// The number of stacks with Unhealthy state.
|
||||
pub unhealthy: u32,
|
||||
/// The number of stacks with Down state.
|
||||
|
||||
@@ -105,6 +105,7 @@ pub struct RefreshRepoCache {
|
||||
pub enum RepoWebhookAction {
|
||||
Clone,
|
||||
Pull,
|
||||
Build,
|
||||
}
|
||||
|
||||
/// Create a webhook on the github repo attached to the (monitor) repo
|
||||
|
||||
@@ -48,7 +48,7 @@ impl Busy for BuildActionState {
|
||||
|
||||
impl Busy for RepoActionState {
|
||||
fn busy(&self) -> bool {
|
||||
self.cloning || self.pulling
|
||||
self.cloning || self.pulling || self.building
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,10 @@ pub enum RepoState {
|
||||
Failed,
|
||||
/// Currently cloning
|
||||
Cloning,
|
||||
/// Currently pullling
|
||||
/// Currently pulling
|
||||
Pulling,
|
||||
/// Currently building
|
||||
Building,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -107,10 +107,14 @@ pub enum StackState {
|
||||
Paused,
|
||||
/// All contianers are stopped
|
||||
Stopped,
|
||||
/// All containers are created
|
||||
Created,
|
||||
/// All containers are restarting
|
||||
Restarting,
|
||||
/// All containers are dead
|
||||
Dead,
|
||||
/// All containers are removing
|
||||
Removing,
|
||||
/// The containers are in a mix of states
|
||||
Unhealthy,
|
||||
/// The stack is not deployed
|
||||
|
||||
@@ -1060,8 +1060,10 @@ export enum RepoState {
|
||||
Failed = "Failed",
|
||||
/** Currently cloning */
|
||||
Cloning = "Cloning",
|
||||
/** Currently pullling */
|
||||
/** Currently pulling */
|
||||
Pulling = "Pulling",
|
||||
/** Currently building */
|
||||
Building = "Building",
|
||||
}
|
||||
|
||||
export interface RepoListItemInfo {
|
||||
@@ -1617,10 +1619,14 @@ export enum StackState {
|
||||
Paused = "paused",
|
||||
/** All contianers are stopped */
|
||||
Stopped = "stopped",
|
||||
/** All containers are created */
|
||||
Created = "created",
|
||||
/** All containers are restarting */
|
||||
Restarting = "restarting",
|
||||
/** All containers are dead */
|
||||
Dead = "dead",
|
||||
/** All containers are removing */
|
||||
Removing = "removing",
|
||||
/** The containers are in a mix of states */
|
||||
Unhealthy = "unhealthy",
|
||||
/** The stack is not deployed */
|
||||
@@ -3231,6 +3237,8 @@ export interface GetReposSummaryResponse {
|
||||
cloning: number;
|
||||
/** The number of repos currently pulling. */
|
||||
pulling: number;
|
||||
/** The number of repos currently building. */
|
||||
building: number;
|
||||
/** The number of repos with failed state. */
|
||||
failed: number;
|
||||
/** The number of repos with unknown state. */
|
||||
@@ -3254,6 +3262,8 @@ export interface GetRepoWebhooksEnabledResponse {
|
||||
clone_enabled: boolean;
|
||||
/** Whether pushes to branch trigger pull. Will always be false if managed is false. */
|
||||
pull_enabled: boolean;
|
||||
/** Whether pushes to branch trigger build. Will always be false if managed is false. */
|
||||
build_enabled: boolean;
|
||||
}
|
||||
|
||||
/** Find resources matching a common query. Response: [FindResourcesResponse]. */
|
||||
@@ -3586,6 +3596,10 @@ export interface GetStacksSummaryResponse {
|
||||
restarting: number;
|
||||
/** The number of stacks with Dead state. */
|
||||
dead: number;
|
||||
/** The number of stacks with Created state. */
|
||||
created: number;
|
||||
/** The number of stacks with Removing state. */
|
||||
removing: number;
|
||||
/** The number of stacks with Unhealthy state. */
|
||||
unhealthy: number;
|
||||
/** The number of stacks with Down state. */
|
||||
@@ -4377,6 +4391,7 @@ export interface RefreshRepoCache {
|
||||
export enum RepoWebhookAction {
|
||||
Clone = "Clone",
|
||||
Pull = "Pull",
|
||||
Build = "Build",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
environment: # https://github.com/mbecker20/monitor/blob/main/config_example/core.config.example.toml
|
||||
MONITOR_HOST: https://demo.monitor.dev
|
||||
## MONGO
|
||||
MONITOR_MONGO_ADDRESS: demo-mongo:27017
|
||||
MONITOR_MONGO_ADDRESS: monitor-mongo:27017
|
||||
MONITOR_MONGO_USERNAME: admin # match ones below
|
||||
MONITOR_MONGO_PASSWORD: admin
|
||||
## KEYS
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Layout = () => {
|
||||
<div className="h-screen overflow-y-auto">
|
||||
<div className="container">
|
||||
<Sidebar />
|
||||
<div className="md:ml-64 md:pl-8 py-24">
|
||||
<div className="lg:ml-64 lg:pl-8 py-24">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +53,6 @@ interface PageProps {
|
||||
subtitle?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
superHeader?: ReactNode;
|
||||
wrapSize?: "md" | "lg" | "xl" | "2xl";
|
||||
}
|
||||
|
||||
export const Page = ({
|
||||
@@ -72,7 +71,7 @@ export const Page = ({
|
||||
{superHeader}
|
||||
{(title || icon || subtitle || actions) && (
|
||||
<div
|
||||
className={`flex flex-col gap-6 lg:flex-row lg:gap-0 lg:justify-between`}
|
||||
className={`flex flex-col gap-6 md:flex-row md:gap-0 md:justify-between`}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
@@ -89,7 +88,7 @@ export const Page = ({
|
||||
) : (
|
||||
(title || icon || subtitle || actions) && (
|
||||
<div
|
||||
className={`flex flex-col gap-6 lg:flex-row lg:gap-0 lg:justify-between`}
|
||||
className={`flex flex-col gap-6 md:flex-row md:gap-0 md:justify-between`}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
|
||||
@@ -113,20 +113,6 @@ export const BuildConfig = ({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Custom Name / Tag",
|
||||
components: {
|
||||
image_name: {
|
||||
description: "Optional. Push the image under a different name",
|
||||
placeholder: "Custom image name",
|
||||
},
|
||||
image_tag: {
|
||||
description:
|
||||
"Optional. Postfix the image version with a custom tag.",
|
||||
placeholder: "Custom image tag",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Git",
|
||||
components: {
|
||||
@@ -194,6 +180,20 @@ export const BuildConfig = ({
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Custom Name / Tag",
|
||||
components: {
|
||||
image_name: {
|
||||
description: "Optional. Push the image under a different name",
|
||||
placeholder: "Custom image name",
|
||||
},
|
||||
image_tag: {
|
||||
description:
|
||||
"Optional. Postfix the image version with a custom tag.",
|
||||
placeholder: "Custom image tag",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Extra Args",
|
||||
description: (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { FolderGit, Hammer, Loader2, RefreshCcw } from "lucide-react";
|
||||
import { Factory, FolderGit, Hammer, Loader2, RefreshCcw } from "lucide-react";
|
||||
import { BuildConfig } from "./config";
|
||||
import { BuildTable } from "./table";
|
||||
import { DeleteResource, NewResource } from "../common";
|
||||
import { DeleteResource, NewResource, ResourceLink } from "../common";
|
||||
import { DeploymentTable } from "../deployment/table";
|
||||
import { RunBuild } from "./actions";
|
||||
import {
|
||||
@@ -23,6 +23,7 @@ import { Card } from "@ui/card";
|
||||
import { Badge } from "@ui/badge";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { Button } from "@ui/button";
|
||||
import { useBuilder } from "../builder";
|
||||
|
||||
export const useBuild = (id?: string) =>
|
||||
useRead("ListBuilds", {}, { refetchInterval: 5000 }).data?.find(
|
||||
@@ -209,6 +210,18 @@ export const BuildComponents: RequiredResourceComponents = {
|
||||
},
|
||||
|
||||
Info: {
|
||||
Builder: ({ id }) => {
|
||||
const info = useBuild(id)?.info;
|
||||
const builder = useBuilder(info?.builder_id);
|
||||
return builder?.id ? (
|
||||
<ResourceLink type="Builder" id={builder?.id} />
|
||||
) : (
|
||||
<div className="flex gap-2 items-center text-sm">
|
||||
<Factory className="w-4 h-4" />
|
||||
<div>Unknown Builder</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
Repo: ({ id }) => {
|
||||
const repo = useBuild(id)?.info.repo;
|
||||
return (
|
||||
|
||||
@@ -491,14 +491,28 @@ const default_enabled_execution: () => Types.EnabledExecution = () => ({
|
||||
const EXECUTION_TYPES: Types.Execution["type"][] = [
|
||||
"RunBuild",
|
||||
"Deploy",
|
||||
"RestartContainer",
|
||||
"StartContainer",
|
||||
"PauseContainer",
|
||||
"UnpauseContainer",
|
||||
"StopContainer",
|
||||
"StopAllContainers",
|
||||
"RemoveContainer",
|
||||
"DeployStack",
|
||||
"StartStack",
|
||||
"RestartStack",
|
||||
"PauseStack",
|
||||
"UnpauseStack",
|
||||
"StopStack",
|
||||
"DestroyStack",
|
||||
"CloneRepo",
|
||||
"PullRepo",
|
||||
"BuildRepo",
|
||||
"RunProcedure",
|
||||
"RunSync",
|
||||
"PruneContainers",
|
||||
"PruneImages",
|
||||
"PruneNetworks",
|
||||
"Sleep",
|
||||
];
|
||||
|
||||
|
||||
@@ -12,14 +12,16 @@ import { Types } from "@monitor/client";
|
||||
import { useBuilder } from "../builder";
|
||||
|
||||
export const CloneRepo = ({ id }: { id: string }) => {
|
||||
const hash = useRepo(id)?.info.latest_hash;
|
||||
const isCloned = (hash?.length || 0) > 0;
|
||||
const { mutate, isPending } = useExecute("CloneRepo");
|
||||
const cloning = useRead(
|
||||
"GetRepoActionState",
|
||||
{ repo: id },
|
||||
{ refetchInterval: 5000 }
|
||||
).data?.cloning;
|
||||
const info = useRepo(id)?.info;
|
||||
if (!info?.server_id) return null;
|
||||
const hash = info?.latest_hash;
|
||||
const isCloned = (hash?.length || 0) > 0;
|
||||
const pending = isPending || cloning;
|
||||
return (
|
||||
<ConfirmButton
|
||||
@@ -45,7 +47,9 @@ export const PullRepo = ({ id }: { id: string }) => {
|
||||
{ repo: id },
|
||||
{ refetchInterval: 5000 }
|
||||
).data?.pulling;
|
||||
const hash = useRepo(id)?.info.latest_hash;
|
||||
const info = useRepo(id)?.info;
|
||||
if (!info?.server_id) return null;
|
||||
const hash = info?.latest_hash;
|
||||
const isCloned = (hash?.length || 0) > 0;
|
||||
if (!isCloned) return null;
|
||||
const pending = isPending || pulling;
|
||||
@@ -89,6 +93,9 @@ export const BuildRepo = ({ id }: { id: string }) => {
|
||||
const builder = useBuilder(repo?.info.builder_id);
|
||||
const canCancel = builder?.info.builder_type !== "Server";
|
||||
|
||||
// Don't show if builder not attached
|
||||
if (!builder) return null;
|
||||
|
||||
// make sure hidden without perms.
|
||||
// not usually necessary, but this button also used in deployment actions.
|
||||
if (
|
||||
|
||||
@@ -164,6 +164,11 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
<CopyGithubWebhook path={`/repo/${id}/clone`} />
|
||||
</ConfigItem>
|
||||
),
|
||||
["build" as any]: () => (
|
||||
<ConfigItem label="Build">
|
||||
<CopyGithubWebhook path={`/repo/${id}/build`} />
|
||||
</ConfigItem>
|
||||
),
|
||||
webhook_enabled: webhooks !== undefined && !webhooks.managed,
|
||||
["managed" as any]: () => {
|
||||
const inv = useInvalidate();
|
||||
@@ -247,44 +252,95 @@ export const RepoConfig = ({ id }: { id: string }) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!webhooks.clone_enabled && !webhooks.pull_enabled && (
|
||||
{webhooks.build_enabled && (
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
Incoming webhook is{" "}
|
||||
<div
|
||||
className={text_color_class_by_intention(
|
||||
"Critical"
|
||||
)}
|
||||
className={text_color_class_by_intention("Good")}
|
||||
>
|
||||
DISABLED
|
||||
ENABLED
|
||||
</div>
|
||||
and will trigger
|
||||
<div
|
||||
className={text_color_class_by_intention("Neutral")}
|
||||
>
|
||||
BUILD
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmButton
|
||||
title="Enable Clone"
|
||||
icon={<CirclePlus className="w-4 h-4" />}
|
||||
title="Disable"
|
||||
icon={<Ban className="w-4 h-4" />}
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
createWebhook({
|
||||
deleteWebhook({
|
||||
repo: id,
|
||||
action: Types.RepoWebhookAction.Clone,
|
||||
action: Types.RepoWebhookAction.Build,
|
||||
})
|
||||
}
|
||||
loading={createPending}
|
||||
disabled={disabled || createPending}
|
||||
/>
|
||||
<ConfirmButton
|
||||
title="Enable Pull"
|
||||
icon={<CirclePlus className="w-4 h-4" />}
|
||||
onClick={() =>
|
||||
createWebhook({
|
||||
repo: id,
|
||||
action: Types.RepoWebhookAction.Pull,
|
||||
})
|
||||
}
|
||||
loading={createPending}
|
||||
disabled={disabled || createPending}
|
||||
loading={deletePending}
|
||||
disabled={disabled || deletePending}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!webhooks.clone_enabled &&
|
||||
!webhooks.pull_enabled &&
|
||||
!webhooks.build_enabled && (
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
Incoming webhook is{" "}
|
||||
<div
|
||||
className={text_color_class_by_intention(
|
||||
"Critical"
|
||||
)}
|
||||
>
|
||||
DISABLED
|
||||
</div>
|
||||
</div>
|
||||
{(update.server_id ?? config.server_id) && (
|
||||
<ConfirmButton
|
||||
title="Enable Clone"
|
||||
icon={<CirclePlus className="w-4 h-4" />}
|
||||
onClick={() =>
|
||||
createWebhook({
|
||||
repo: id,
|
||||
action: Types.RepoWebhookAction.Clone,
|
||||
})
|
||||
}
|
||||
loading={createPending}
|
||||
disabled={disabled || createPending}
|
||||
/>
|
||||
)}
|
||||
{(update.server_id ?? config.server_id) && (
|
||||
<ConfirmButton
|
||||
title="Enable Pull"
|
||||
icon={<CirclePlus className="w-4 h-4" />}
|
||||
onClick={() =>
|
||||
createWebhook({
|
||||
repo: id,
|
||||
action: Types.RepoWebhookAction.Pull,
|
||||
})
|
||||
}
|
||||
loading={createPending}
|
||||
disabled={disabled || createPending}
|
||||
/>
|
||||
)}
|
||||
{(update.builder_id ?? config.builder_id) && (
|
||||
<ConfirmButton
|
||||
title="Enable Build"
|
||||
icon={<CirclePlus className="w-4 h-4" />}
|
||||
onClick={() =>
|
||||
createWebhook({
|
||||
repo: id,
|
||||
action: Types.RepoWebhookAction.Build,
|
||||
})
|
||||
}
|
||||
loading={createPending}
|
||||
disabled={disabled || createPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ConfigItem>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Card } from "@ui/card";
|
||||
import {
|
||||
Factory,
|
||||
FolderGit,
|
||||
GitBranch,
|
||||
Loader2,
|
||||
@@ -25,6 +26,7 @@ import { StatusBadge } from "@components/util";
|
||||
import { Badge } from "@ui/badge";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { Button } from "@ui/button";
|
||||
import { useBuilder } from "../builder";
|
||||
|
||||
export const useRepo = (id?: string) =>
|
||||
useRead("ListRepos", {}, { refetchInterval: 5000 }).data?.find(
|
||||
@@ -195,6 +197,30 @@ export const RepoComponents: RequiredResourceComponents = {
|
||||
},
|
||||
|
||||
Info: {
|
||||
Server: ({ id }) => {
|
||||
const info = useRepo(id)?.info;
|
||||
const server = useServer(info?.server_id);
|
||||
return server?.id ? (
|
||||
<ResourceLink type="Server" id={server?.id} />
|
||||
) : (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Server className="w-4 h-4" />
|
||||
<div>No Server</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
Builder: ({ id }) => {
|
||||
const info = useRepo(id)?.info;
|
||||
const builder = useBuilder(info?.builder_id);
|
||||
return builder?.id ? (
|
||||
<ResourceLink type="Builder" id={builder?.id} />
|
||||
) : (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Factory className="w-4 h-4" />
|
||||
<div>No Builder</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
Repo: ({ id }) => {
|
||||
const repo = useRepo(id)?.info.repo;
|
||||
return (
|
||||
@@ -204,24 +230,12 @@ export const RepoComponents: RequiredResourceComponents = {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
// Branch: ({ id }) => {
|
||||
// const branch = useRepo(id)?.info.branch;
|
||||
// return (
|
||||
// <div className="flex items-center gap-2">
|
||||
// <FolderGit className="w-4 h-4" />
|
||||
// {branch}
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
Server: ({ id }) => {
|
||||
const info = useRepo(id)?.info;
|
||||
const server = useServer(info?.server_id);
|
||||
return server?.id ? (
|
||||
<ResourceLink type="Server" id={server?.id} />
|
||||
) : (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Server className="w-4 h-4" />
|
||||
<div>Unknown Server</div>
|
||||
Branch: ({ id }) => {
|
||||
const branch = useRepo(id)?.info.branch;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="w-4 h-4" />
|
||||
{branch}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { RepoComponents } from ".";
|
||||
import { Types } from "@monitor/client";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const serverName = useCallback(
|
||||
(id: string) => servers?.find((server) => server.id === id)?.name,
|
||||
[servers]
|
||||
);
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="repos"
|
||||
@@ -33,24 +26,9 @@ export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => {
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.server_id",
|
||||
sortingFn: (a, b) => {
|
||||
const sa = serverName(a.original.info.server_id);
|
||||
const sb = serverName(b.original.info.server_id);
|
||||
|
||||
if (!sa && !sb) return 0;
|
||||
if (!sa) return -1;
|
||||
if (!sb) return 1;
|
||||
|
||||
if (sa > sb) return 1;
|
||||
else if (sa < sb) return -1;
|
||||
else return 0;
|
||||
},
|
||||
accessorKey: "info.branch",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Server" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ResourceLink type="Server" id={row.original.info.server_id} />
|
||||
<SortableHeader column={column} title="Branch" />
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ServerConfig = ({
|
||||
label: "General",
|
||||
components: {
|
||||
address: {
|
||||
placeholder: "http://localhost:8120",
|
||||
placeholder: "http://12.34.56.78:8120",
|
||||
description:
|
||||
"The http/s address of periphery in your network, eg. http://12.34.56.78:8120",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { ActionWithDialog, ConfirmButton } from "@components/util";
|
||||
import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { Pause, Pen, Play, RefreshCcw, Rocket, Square, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Pause,
|
||||
Pen,
|
||||
Play,
|
||||
RefreshCcw,
|
||||
Rocket,
|
||||
Square,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useStack } from ".";
|
||||
import { Types } from "@monitor/client";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
@@ -20,8 +28,6 @@ export const DeployStack = ({ id }: { id: string }) => {
|
||||
if (!stack || state === Types.StackState.Unknown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pending = isPending || deploying;
|
||||
const deployed =
|
||||
state !== undefined &&
|
||||
[
|
||||
@@ -39,8 +45,8 @@ export const DeployStack = ({ id }: { id: string }) => {
|
||||
title="Redeploy"
|
||||
icon={<Rocket className="h-4 w-4" />}
|
||||
onClick={() => deploy({ stack: id })}
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
disabled={isPending}
|
||||
loading={isPending || deploying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -50,8 +56,8 @@ export const DeployStack = ({ id }: { id: string }) => {
|
||||
title="Deploy"
|
||||
icon={<Rocket className="w-4 h-4" />}
|
||||
onClick={() => deploy({ stack: id })}
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
disabled={isPending}
|
||||
loading={isPending || deploying}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -74,8 +80,6 @@ export const DestroyStack = ({ id }: { id: string }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pending = isPending || destroying;
|
||||
|
||||
if (!stack) {
|
||||
return null;
|
||||
}
|
||||
@@ -86,8 +90,8 @@ export const DestroyStack = ({ id }: { id: string }) => {
|
||||
title="Destroy"
|
||||
icon={<Trash2 className="h-4 w-4" />}
|
||||
onClick={() => destroy({ stack: id })}
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
disabled={isPending}
|
||||
loading={isPending || destroying}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Card } from "@ui/card";
|
||||
import { FolderGit, Layers, Loader2, RefreshCcw, Server } from "lucide-react";
|
||||
import {
|
||||
FolderGit,
|
||||
Layers,
|
||||
Loader2,
|
||||
NotepadText,
|
||||
RefreshCcw,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
import { StackConfig } from "./config";
|
||||
import { DeleteResource, NewResource, ResourceLink } from "../common";
|
||||
import { StackTable } from "./table";
|
||||
@@ -132,6 +139,15 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
}
|
||||
return <StatusBadge text={state} intent={stack_state_intention(state)} />;
|
||||
},
|
||||
Status: ({ id }) => {
|
||||
const info = useStack(id)?.info;
|
||||
if (info?.state !== Types.StackState.Unhealthy) return null;
|
||||
return (
|
||||
info?.status && (
|
||||
<p className="text-sm text-muted-foreground">{info.status}</p>
|
||||
)
|
||||
);
|
||||
},
|
||||
NoConfig: ({ id }) => {
|
||||
const config = useFullStack(id)?.config;
|
||||
if (config?.file_contents || config?.repo) {
|
||||
@@ -148,9 +164,9 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start">
|
||||
<div className="grid gap-2">
|
||||
No configuration provided for stack. Cannot get stack state. Either
|
||||
paste the compose file contents into the UI, or configure a git repo
|
||||
containing your files.
|
||||
No configuration provided for stack. Cannot get stack state.
|
||||
Either paste the compose file contents into the UI, or configure a
|
||||
git repo containing your files.
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
@@ -286,21 +302,32 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
},
|
||||
|
||||
Info: {
|
||||
Repo: ({ id }) => {
|
||||
const repo = useStack(id)?.info.repo;
|
||||
Contents: ({ id }) => {
|
||||
const config = useFullStack(id)?.config;
|
||||
const file_contents = config?.file_contents;
|
||||
if (file_contents) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<NotepadText className="w-4 h-4" />
|
||||
Local
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderGit className="w-4 h-4" />
|
||||
{repo}
|
||||
{config?.repo}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
// Branch: ({ id }) => {
|
||||
// const branch = useStack(id)?.info.branch;
|
||||
// const config = useFullStack(id)?.config;
|
||||
// const file_contents = config?.file_contents;
|
||||
// if (file_contents || !config?.branch) return null
|
||||
// return (
|
||||
// <div className="flex items-center gap-2">
|
||||
// <FolderGit className="w-4 h-4" />
|
||||
// {branch}
|
||||
// <GitBranch className="w-4 h-4" />
|
||||
// {config.branch}
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
|
||||
@@ -22,8 +22,8 @@ export const StackInfo = ({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
deployed contents:{" "}
|
||||
{stack?.info?.deployed_contents?.map((content) => (
|
||||
<pre className="flex flex-col gap-2">
|
||||
{stack?.info?.deployed_contents?.map((content, i) => (
|
||||
<pre key={i} className="flex flex-col gap-2">
|
||||
path: {content.path}
|
||||
<pre>{content.contents}</pre>
|
||||
</pre>
|
||||
@@ -48,8 +48,8 @@ export const StackInfo = ({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
latest contents:{" "}
|
||||
{stack?.info?.remote_contents?.map((content) => (
|
||||
<pre className="flex flex-col gap-2">
|
||||
{stack?.info?.remote_contents?.map((content, i) => (
|
||||
<pre key={i} className="flex flex-col gap-2">
|
||||
path: {content.path}
|
||||
<pre>{content.contents}</pre>
|
||||
</pre>
|
||||
@@ -62,8 +62,8 @@ export const StackInfo = ({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
remote errors:{" "}
|
||||
{stack?.info?.remote_errors?.map((content) => (
|
||||
<pre className="flex flex-col gap-2">
|
||||
{stack?.info?.remote_errors?.map((content, i) => (
|
||||
<pre key={i} className="flex flex-col gap-2">
|
||||
path: {content.path}
|
||||
<pre>{content.contents}</pre>
|
||||
</pre>
|
||||
|
||||
@@ -50,7 +50,7 @@ export const StackServices = ({
|
||||
);
|
||||
return (
|
||||
<Link
|
||||
to={`/stacks/${id}/${row.original.service}`}
|
||||
to={`/stacks/${id}/service/${row.original.service}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -17,7 +17,7 @@ import { homeViewAtom } from "@main";
|
||||
export const Sidebar = () => {
|
||||
const [view, setView] = useAtom(homeViewAtom);
|
||||
return (
|
||||
<div className="fixed top-24 w-64 border-r hidden md:block pr-8 pb-4 h-[85vh] overflow-y-auto">
|
||||
<div className="fixed top-24 w-64 border-r hidden lg:block pr-8 pb-4 h-[85vh] overflow-y-auto">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="pl-4 pb-1 text-xs text-muted-foreground">Overviews</p>
|
||||
<SidebarLink
|
||||
|
||||
@@ -47,14 +47,14 @@ export const Topbar = () => {
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 w-full bg-background z-50 border-b shadow-sm">
|
||||
<div className="container h-16 flex items-center justify-between md:grid md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="container h-16 flex items-center justify-between md:grid md:grid-cols-[auto_1fr] lg:grid-cols-3">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
to="/"
|
||||
className="flex gap-3 items-center text-2xl tracking-widest md:mx-2"
|
||||
>
|
||||
<img src="/monitor-circle.png" className="w-[28px] dark:invert" />
|
||||
<div className="hidden md:block">MONITOR</div>
|
||||
<div className="hidden lg:block">MONITOR</div>
|
||||
</Link>
|
||||
|
||||
{/* Searchbar */}
|
||||
@@ -131,7 +131,7 @@ const MobileDropdown = () => {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="md:hidden justify-self-end">
|
||||
<DropdownMenuTrigger asChild className="lg:hidden justify-self-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex justify-start items-center gap-2 w-36 px-3"
|
||||
|
||||
@@ -156,6 +156,8 @@ export const repo_state_intention = (state?: Types.RepoState) => {
|
||||
return "Warning";
|
||||
case Types.RepoState.Pulling:
|
||||
return "Warning";
|
||||
case Types.RepoState.Building:
|
||||
return "Warning";
|
||||
case Types.RepoState.Failed:
|
||||
return "Critical";
|
||||
default:
|
||||
|
||||
@@ -266,7 +266,7 @@ export const WsStatusIndicator = () => {
|
||||
variant="ghost"
|
||||
onClick={onclick}
|
||||
size="icon"
|
||||
className="inline-flex"
|
||||
className="hidden lg:inline-flex"
|
||||
>
|
||||
<Circle
|
||||
className={cn(
|
||||
|
||||
@@ -84,14 +84,18 @@ const ResourceRow = ({ type }: { type: UsableResource }) => {
|
||||
<History className="w-4" />
|
||||
Recently Viewed
|
||||
</p>
|
||||
<div className="h-52 grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="h-52 grid sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
|
||||
{ids.map((id, i) => (
|
||||
<RecentCard
|
||||
key={type + id}
|
||||
type={type}
|
||||
id={id}
|
||||
className={
|
||||
i > 3 ? "hidden lg:flex" : i > 1 ? "hidden md:flex" : undefined
|
||||
i > 4
|
||||
? "hidden 2xl:flex"
|
||||
: i > 1
|
||||
? "hidden sm:flex lg:hidden xl:flex"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
@@ -126,7 +130,7 @@ const RecentCard = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-sm text-nowrap">
|
||||
<Components.Icon id={id} />
|
||||
<ResourceName type={type} id={id} />
|
||||
</div>
|
||||
|
||||
@@ -139,7 +139,7 @@ const ResourceHeader = ({ type, id }: { type: UsableResource; id: string }) => {
|
||||
{infoEntries.map(([key, Info]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="pr-4 border-r last:pr-0 last:border-none"
|
||||
className="pr-4 text-sm border-r last:pr-0 last:border-none"
|
||||
>
|
||||
<Info id={id} />
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,6 @@ export const Resources = () => {
|
||||
|
||||
return (
|
||||
<Page
|
||||
wrapSize="lg"
|
||||
title={`${name}s`}
|
||||
subtitle={
|
||||
<div className="text-muted-foreground">
|
||||
|
||||
@@ -34,10 +34,24 @@ const Actions: { [action: string]: IdServiceComponent } = {
|
||||
};
|
||||
|
||||
export const StackServicePage = () => {
|
||||
const { id: stack_id, service } = useParams() as {
|
||||
const { type, id, service } = useParams() as {
|
||||
type: string;
|
||||
id: string;
|
||||
service: string;
|
||||
};
|
||||
if (type !== "stacks") {
|
||||
return <div>This resource type does not have any services.</div>;
|
||||
}
|
||||
return <StackServicePageInner stack_id={id} service={service} />;
|
||||
};
|
||||
|
||||
const StackServicePageInner = ({
|
||||
stack_id,
|
||||
service,
|
||||
}: {
|
||||
stack_id: string;
|
||||
service: string;
|
||||
}) => {
|
||||
const stack = useStack(stack_id);
|
||||
useSetTitle(`${stack?.name} | ${service}`);
|
||||
const nav = useNavigate();
|
||||
|
||||
@@ -38,10 +38,6 @@ const ROUTER = createBrowserRouter([
|
||||
{ path: ":id", element: <UserPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "stacks/:id/:service",
|
||||
element: <StackServicePage />,
|
||||
},
|
||||
{
|
||||
path: ":type",
|
||||
children: [
|
||||
@@ -50,6 +46,10 @@ const ROUTER = createBrowserRouter([
|
||||
{ path: ":id/stats", element: <ResourceStats /> },
|
||||
{ path: ":id/updates", element: <Updates /> },
|
||||
{ path: ":id/alerts", element: <Alerts /> },
|
||||
{
|
||||
path: ":id/service/:service",
|
||||
element: <StackServicePage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Monitor 🦎
|
||||
|
||||
A tool to build and deploy software across many servers. [See the docs](https://docs.monitor.dev)
|
||||
A tool to build and deploy software across many servers. [See the docs](https://docs.monitor.dev). [Try the Demo](https://demo.monitor.dev).
|
||||
|
||||
Docs for periphery setup script can be found in [scripts/readme.md](https://github.com/mbecker20/monitor/blob/main/scripts/readme.md).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Warning. This is open source software (GPL-V3), and while the Monitor team makes a best effort to ensure main is stable,
|
||||
Warning. This is open source software (GPL-V3), and while the Monitor team (mainly me) makes a best effort to ensure releases are stable and bug-free,
|
||||
there are no warranties. Use at your own risk.
|
||||
|
||||
## Links
|
||||
|
||||
Reference in New Issue
Block a user