diff --git a/bin/core/src/helpers/query.rs b/bin/core/src/helpers/query.rs index e9c95bf7e..68e949436 100644 --- a/bin/core/src/helpers/query.rs +++ b/bin/core/src/helpers/query.rs @@ -5,13 +5,13 @@ use monitor_client::entities::{ alerter::Alerter, build::Build, builder::Builder, - deployment::{Deployment, DeploymentState}, + deployment::{ContainerSummary, Deployment, DeploymentState}, permission::PermissionLevel, procedure::Procedure, repo::Repo, server::{Server, ServerState}, server_template::ServerTemplate, - stack::{ComposeProject, Stack, StackState}, + stack::{Stack, StackServiceNames, StackState}, sync::ResourceSync, tag::Tag, update::{ResourceTarget, ResourceTargetVariant, Update}, @@ -33,6 +33,11 @@ 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 { @@ -93,51 +98,76 @@ pub async fn get_deployment_state( } /// Can pass all the containers from the same server -pub fn get_stack_state_from_projects( - stack: &Stack, - projects: &[ComposeProject], +pub fn get_stack_state_from_containers( + ignore_services: &[String], + services: &[StackServiceNames], + containers: &[ContainerSummary], ) -> StackState { - let project_name = stack.project_name(false); - let Some(status) = projects + // first filter the containers to only ones which match the service + let services = services .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::().with_context(|| { - format!("failed to parse stack state entry: {state}") - }) + .filter(|service| { + !ignore_services.contains(&service.service_name) }) - .collect::>>() - .inspect_err(|e| warn!("{e:#}")) - else { - return StackState::Unknown; - }; - if states.is_empty() { + .collect::>(); + 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) + }) + }).collect::>(); + if containers.is_empty() { return StackState::Down; } - if states.len() > 1 { + if services.len() != containers.len() { return StackState::Unhealthy; } - 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 running = containers + .iter() + .all(|container| container.state == DeploymentState::Running); + if running { + return StackState::Running; } + 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; + } + let removing = containers + .iter() + .all(|container| container.state == DeploymentState::Removing); + if removing { + return StackState::Removing; + } + StackState::Unhealthy } -/// Gets stack state fresh from periphery #[instrument(level = "debug")] pub async fn get_stack_state( stack: &Stack, @@ -150,11 +180,17 @@ pub async fn get_stack_state( if status != ServerState::Ok { return Ok(StackState::Unknown); } - let projects = super::periphery_client(&server)? - .request(periphery_client::api::compose::ListComposeProjects {}) + let containers = super::periphery_client(&server)? + .request(periphery_client::api::container::GetContainerList {}) .await?; - Ok(get_stack_state_from_projects(stack, &projects)) + let services = extract_services_from_stack(stack, false).await?; + + Ok(get_stack_state_from_containers( + &stack.config.ignore_services, + &services, + &containers, + )) } #[instrument(level = "debug")] diff --git a/bin/core/src/monitor/mod.rs b/bin/core/src/monitor/mod.rs index 415c8fe22..6fcc9bb3c 100644 --- a/bin/core/src/monitor/mod.rs +++ b/bin/core/src/monitor/mod.rs @@ -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, &projects), + resources::update_stack_cache(stacks, &containers), ); insert_server_status( server, diff --git a/bin/core/src/monitor/resources.rs b/bin/core/src/monitor/resources.rs index 13b33245c..13c103ec2 100644 --- a/bin/core/src/monitor/resources.rs +++ b/bin/core/src/monitor/resources.rs @@ -1,12 +1,12 @@ use anyhow::Context; use monitor_client::entities::{ deployment::{ContainerSummary, Deployment, DeploymentState}, - stack::{ComposeProject, Stack, StackService, StackServiceNames}, + stack::{Stack, StackService, StackServiceNames}, }; use crate::{ helpers::{ - query::get_stack_state_from_projects, + query::get_stack_state_from_containers, stack::{ compose_container_match_regex, services::extract_services_from_stack, @@ -55,7 +55,6 @@ pub async fn update_deployment_cache( pub async fn update_stack_cache( stacks: Vec, containers: &[ContainerSummary], - projects: &[ComposeProject], ) { let stack_status_cache = stack_status_cache(); for stack in stacks { @@ -93,7 +92,11 @@ pub async fn update_stack_cache( .map(|s| s.curr.state); let status = CachedStackStatus { id: stack.id.clone(), - state: get_stack_state_from_projects(&stack, projects), + state: get_stack_state_from_containers( + &stack.config.ignore_services, + &services, + containers, + ), services: services_with_containers, }; stack_status_cache diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 87958e0b8..c8bffa69e 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -306,6 +306,13 @@ pub struct StackConfig { #[builder(default)] pub file_contents: String, + /// Ignore certain services declared in the compose file when checking + /// the stack status. For example, an init service might be exited, but the + /// stack should be healthy. This init service should be in `ignore_services` + #[serde(default)] + #[builder(default)] + pub ignore_services: Vec, + /// The git provider domain. Default: github.com #[serde(default = "default_git_provider")] #[builder(default = "default_git_provider()")] @@ -402,6 +409,7 @@ impl Default for StackConfig { registry_provider: Default::default(), registry_account: Default::default(), file_contents: Default::default(), + ignore_services: Default::default(), extra_args: Default::default(), environment: Default::default(), env_file_path: default_env_file_path(), diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 1b78505ea..808627f16 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -1492,6 +1492,12 @@ export interface StackConfig { * repo based compose file. */ file_contents?: string; + /** + * Ignore certain services declared in the compose file when checking + * the stack status. For example, an init service might be exited, but the + * stack should be healthy. This init service should be in `ignore_services` + */ + ignore_services?: string[]; /** The git provider domain. Default: github.com */ git_provider: string; /** diff --git a/frontend/src/components/resources/stack/config.tsx b/frontend/src/components/resources/stack/config.tsx index 9f0ddd02f..9e16bc4f6 100644 --- a/frontend/src/components/resources/stack/config.tsx +++ b/frontend/src/components/resources/stack/config.tsx @@ -212,6 +212,42 @@ export const StackConfig = ({ }, }, }, + { + label: "Ignore Services", + description: + "If your compose file has init containers that exit early, ignore them here so your stack will report the correct health.", + actions: !disabled && ( + + ), + components: { + ignore_services: (values, set) => ( + + ), + }, + }, ], "Git Repo": [ {