v1.13.1 improve stack edge cases, and UI action responsiveness (#26)

* get stack state from project

* move custom image name / tag below image setting for build config

* services also trigger stack action state

* add status to stack page

* 1.13.1 patch
This commit is contained in:
Maxwell Becker
2024-08-11 17:01:09 -07:00
committed by GitHub
parent b0f80cafc3
commit 31a23dfe2d
12 changed files with 149 additions and 149 deletions
Generated
+13 -13
View File
@@ -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",
+2 -2
View File
@@ -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" }
+5 -28
View File
@@ -200,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,
(),
)
@@ -224,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,
(),
@@ -248,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,
(),
)
@@ -272,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,
(),
)
@@ -300,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,
)
+2
View File
@@ -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,
+41 -60
View File
@@ -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")]
+1 -1
View File
@@ -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,
+4 -3
View File
@@ -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
+22 -18
View File
@@ -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,
+4
View File
@@ -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.
+4
View File
@@ -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
@@ -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,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>
// );
// },