use anyhow::Result;
use client_core::constants::{docker, timeout};
use ducker::docker::container::DockerContainer;
use ducker::docker::util::new_local_docker_connection;
use rust_i18n::t;
use serde_yaml::Value;
use std::fs;
use std::path::Path;
use std::time::Duration;
use tokio::time::sleep;
use tracing::{error, info, warn};
#[derive(Debug, Clone)]
pub enum ServiceFilter {
NameContains(Vec<String>),
All,
}
impl ServiceFilter {
pub fn matches(&self, container: &DockerContainer) -> bool {
match self {
ServiceFilter::NameContains(keywords) => {
if keywords.is_empty() {
return true;
}
let container_name_lower = container.names.to_lowercase();
keywords.iter().any(|keyword| {
let keyword_lower = keyword.to_lowercase();
let separators = vec!["_", "-"];
for separator in separators {
let pattern1 = format!("{separator}{keyword_lower}{separator}");
if container_name_lower.contains(&pattern1) {
return true;
}
let pattern2 = format!("{separator}{keyword_lower}");
if container_name_lower.ends_with(&pattern2) {
return true;
}
let pattern3 = format!("{keyword_lower}{separator}");
if container_name_lower.starts_with(&pattern3) {
return true;
}
}
container_name_lower == keyword_lower
})
}
ServiceFilter::All => true,
}
}
}
pub async fn check_services_running(filter: &ServiceFilter) -> Result<bool> {
match new_local_docker_connection(docker::DOCKER_SOCKET_PATH, None).await {
Ok(docker) => {
match DockerContainer::list(&docker).await {
Ok(containers) => {
let filtered_containers: Vec<_> =
containers.iter().filter(|c| filter.matches(c)).collect();
let running_count = filtered_containers
.iter()
.filter(|container| container.running)
.count();
let total_filtered = filtered_containers.len();
match filter {
ServiceFilter::All => {
info!(
"Found {running} running containers (total {total})",
running = running_count,
total = total_filtered
);
}
ServiceFilter::NameContains(keywords) => {
info!(
"Matched {keywords} containers: {running} running (total {total})",
keywords = format!("{:?}", keywords),
running = running_count,
total = total_filtered
);
}
}
Ok(running_count > 0)
}
Err(e) => {
error!(
"Failed to get container list: {error}",
error = e.to_string()
);
Err(anyhow::anyhow!(
t!("docker_utils.get_containers_failed", error = e.to_string()).to_string()
))
}
}
}
Err(e) => {
error!("Cannot connect to Docker: {error}", error = e.to_string());
Err(anyhow::anyhow!(
t!("docker_utils.docker_connect_failed", error = e.to_string()).to_string()
))
}
}
}
#[allow(dead_code)]
pub async fn wait_for_services_stopped(filter: &ServiceFilter, timeout_secs: u64) -> Result<bool> {
let start_time = tokio::time::Instant::now();
let timeout = Duration::from_secs(timeout_secs);
info!(
"Waiting for services to stop, filter: {filter}, timeout: {timeout}s",
filter = format!("{:?}", filter),
timeout = timeout_secs
);
while start_time.elapsed() < timeout {
match check_services_running(filter).await {
Ok(false) => {
info!("Specified Docker services stopped");
return Ok(true);
}
Ok(true) => {
info!("Waiting for Docker services to stop...");
sleep(Duration::from_secs(timeout::SERVICE_CHECK_INTERVAL)).await;
}
Err(e) => {
warn!(
"Error checking service status: {error}",
error = e.to_string()
);
sleep(Duration::from_secs(timeout::SERVICE_CHECK_INTERVAL)).await;
}
}
}
warn!(
"Wait for service stop timeout ({timeout}s)",
timeout = timeout_secs
);
Ok(false)
}
pub async fn wait_for_services_started(filter: &ServiceFilter, timeout_secs: u64) -> Result<bool> {
let start_time = tokio::time::Instant::now();
let timeout = Duration::from_secs(timeout_secs);
info!(
"Waiting for services to start, filter: {filter}, timeout: {timeout}s",
filter = format!("{:?}", filter),
timeout = timeout_secs
);
while start_time.elapsed() < timeout {
match check_services_running(filter).await {
Ok(true) => {
info!("Specified Docker services started");
return Ok(true);
}
Ok(false) => {
info!("Waiting for Docker services to start...");
sleep(Duration::from_secs(timeout::SERVICE_CHECK_INTERVAL)).await;
}
Err(e) => {
warn!(
"Error checking service status: {error}",
error = e.to_string()
);
sleep(Duration::from_secs(timeout::SERVICE_CHECK_INTERVAL)).await;
}
}
}
warn!(
"Wait for service start timeout ({timeout}s)",
timeout = timeout_secs
);
Ok(false)
}
pub async fn parse_service_names_from_compose(compose_file_path: &Path) -> Result<Vec<String>> {
if !compose_file_path.exists() {
warn!(
"docker-compose.yml file not found: {path}",
path = compose_file_path.display()
);
return Ok(vec![]);
}
match fs::read_to_string(compose_file_path) {
Ok(content) => match serde_yaml::from_str::<Value>(&content) {
Ok(yaml) => {
let mut service_names = Vec::new();
if let Some(services) = yaml.get("services")
&& let Some(services_map) = services.as_mapping()
{
for (key, _value) in services_map {
if let Some(service_name) = key.as_str() {
service_names.push(service_name.to_string());
}
}
}
info!(
"Parsed {count} services from {path}:",
path = compose_file_path.display(),
count = service_names.len()
);
for name in &service_names {
info!(" - {}", name);
}
Ok(service_names)
}
Err(e) => {
error!(
"Failed to parse docker-compose.yml: {error}",
error = e.to_string()
);
Err(anyhow::anyhow!(
t!("docker_utils.parse_compose_failed", error = e.to_string()).to_string()
))
}
},
Err(e) => {
error!(
"Failed to read docker-compose.yml: {error}",
error = e.to_string()
);
Err(anyhow::anyhow!(
t!("docker_utils.read_compose_failed", error = e.to_string()).to_string()
))
}
}
}
pub async fn create_compose_filter(compose_file_path: &Path) -> Result<ServiceFilter> {
let service_names = parse_service_names_from_compose(compose_file_path).await?;
if service_names.is_empty() {
warn!("No services found, will check all containers");
Ok(ServiceFilter::All)
} else {
Ok(ServiceFilter::NameContains(service_names))
}
}
#[allow(dead_code)]
pub async fn wait_for_compose_services_stopped(
compose_file_path: &Path,
timeout_secs: u64,
) -> Result<bool> {
let filter = create_compose_filter(compose_file_path).await?;
wait_for_services_stopped(&filter, timeout_secs).await
}
pub async fn wait_for_compose_services_started(
compose_file_path: &Path,
timeout_secs: u64,
) -> Result<bool> {
let filter = create_compose_filter(compose_file_path).await?;
wait_for_services_started(&filter, timeout_secs).await
}
pub async fn wait_for_mysql_ready(_compose_path: &Path, timeout_secs: u64) -> Result<bool> {
let filter = ServiceFilter::NameContains(vec!["mysql".to_string()]);
wait_for_services_started(&filter, timeout_secs).await
}