add ignore_services to stack

This commit is contained in:
mbecker20
2024-08-11 20:00:20 -07:00
parent c456b67018
commit 6ba0184551
6 changed files with 134 additions and 45 deletions

View File

@@ -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<User> {
@@ -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::<DeploymentState>().with_context(|| {
format!("failed to parse stack state entry: {state}")
})
.filter(|service| {
!ignore_services.contains(&service.service_name)
})
.collect::<anyhow::Result<Vec<_>>>()
.inspect_err(|e| warn!("{e:#}"))
else {
return StackState::Unknown;
};
if states.is_empty() {
.collect::<Vec<_>>();
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::<Vec<_>>();
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")]

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, &projects),
resources::update_stack_cache(stacks, &containers),
);
insert_server_status(
server,

View File

@@ -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<Stack>,
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

View File

@@ -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<String>,
/// 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(),

View File

@@ -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;
/**

View File

@@ -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 && (
<Button
variant="secondary"
onClick={() =>
set((update) => ({
...update,
ignore_services: [
...(update.ignore_services ??
config.ignore_services ??
[]),
"",
],
}))
}
className="flex items-center gap-2 w-[200px]"
>
<PlusCircle className="w-4 h-4" />
Add Service
</Button>
),
components: {
ignore_services: (values, set) => (
<InputList
field="ignore_services"
values={values ?? []}
set={set}
disabled={disabled}
placeholder="Input service name"
/>
),
},
},
],
"Git Repo": [
{