use crate::docker_service::architecture::{Architecture, detect_architecture};
use crate::docker_service::directory_permissions::DirectoryPermissionManager;
use crate::docker_service::error::{DockerServiceError, DockerServiceResult};
use crate::docker_service::health_check::{HealthChecker, HealthReport};
use crate::docker_service::image_loader::{ImageLoader, LoadResult, TagResult};
use crate::docker_service::port_manager::PortManager;
use crate::docker_service::script_permissions::ScriptPermissionManager;
use client_core::config::AppConfig;
use client_core::constants::timeout;
use client_core::container::DockerManager;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, warn};
pub struct DockerServiceManager {
#[allow(dead_code)]
config: Arc<AppConfig>,
docker_manager: Arc<DockerManager>,
work_dir: PathBuf,
architecture: Architecture,
image_loader: ImageLoader,
health_checker: HealthChecker,
port_manager: PortManager,
script_permission_manager: ScriptPermissionManager,
directory_permission_manager: DirectoryPermissionManager,
}
impl DockerServiceManager {
pub fn new(
config: Arc<AppConfig>,
docker_manager: Arc<DockerManager>,
work_dir: PathBuf,
) -> Self {
let architecture = detect_architecture();
let image_loader = ImageLoader::new(docker_manager.clone(), work_dir.clone())
.expect("Failed to create image loader");
let health_checker = HealthChecker::new(docker_manager.clone());
Self {
config,
docker_manager,
work_dir: work_dir.clone(),
architecture,
image_loader,
health_checker,
port_manager: PortManager::new(),
script_permission_manager: ScriptPermissionManager::new(work_dir.clone()),
directory_permission_manager: DirectoryPermissionManager::new(work_dir.clone()),
}
}
pub fn get_architecture(&self) -> Architecture {
self.architecture
}
pub fn get_work_dir(&self) -> &PathBuf {
&self.work_dir
}
pub async fn deploy_services(&mut self) -> DockerServiceResult<()> {
info!("Starting Docker service deployment...");
self.check_environment().await?;
self.docker_manager
.ensure_host_volumes_exist()
.await
.map_err(|err| DockerServiceError::DirectorySetup(err.to_string()))?;
self.directory_permission_manager
.ensure_mysql_config_safe()?;
self.script_permission_manager
.check_and_fix_script_permissions()
.await?;
let load_result = self.load_images().await?;
self.setup_image_tags_with_ducker_validation(&load_result.image_mappings)
.await?;
self.start_services().await?;
info!("Docker service deployment completed");
Ok(())
}
pub async fn check_environment(&self) -> DockerServiceResult<()> {
info!("Checking Docker environment...");
if !self.work_dir.exists() {
return Err(DockerServiceError::EnvironmentCheck(format!(
"{}",
t!(
"docker_service_manager.work_dir_not_exists",
path = self.work_dir.display()
)
)));
}
let images_dir = self
.work_dir
.join(client_core::constants::docker::IMAGES_DIR_NAME);
if !images_dir.exists() {
return Err(DockerServiceError::EnvironmentCheck(format!(
"{}",
t!(
"docker_service_manager.images_dir_not_exists",
path = images_dir.display()
)
)));
}
let compose_file = self
.work_dir
.join(client_core::constants::docker::COMPOSE_FILE_NAME);
if !compose_file.exists() {
return Err(DockerServiceError::EnvironmentCheck(format!(
"{}",
t!(
"docker_service_manager.compose_file_not_exists",
path = compose_file.display()
)
)));
}
let runtime_env = self.docker_manager.get_runtime_environment();
if runtime_env.needs_special_handling() {
info!(
" Environment: {env} - special handling required",
env = runtime_env.summary()
);
} else {
info!(" Environment: {env}", env = runtime_env.summary());
}
info!("Environment check passed");
Ok(())
}
pub async fn ensure_compose_mount_directories(&self) -> DockerServiceResult<()> {
info!("🔍 Checking and creating mount directories from docker-compose.yml...");
let runtime_env = self.docker_manager.get_runtime_environment();
if runtime_env.needs_special_handling() {
info!("⚠️ Windows Podman Desktop environment detected");
info!(" Podman Desktop does not auto-create mount directories, creating proactively");
}
self.docker_manager
.ensure_host_volumes_exist()
.await
.map_err(|err| DockerServiceError::DirectorySetup(err.to_string()))?;
info!("✅ Mount directory check completed");
Ok(())
}
pub async fn load_images(&self) -> DockerServiceResult<LoadResult> {
info!("Starting Docker image loading...");
let result = self.image_loader.load_all_images().await?;
if !result.is_all_successful() {
warn!(
"Some image loading failed: success {success}, failed {failed}",
success = result.success_count(),
failed = result.failure_count()
);
}
Ok(result)
}
pub async fn setup_image_tags_with_mappings(
&self,
image_mappings: &[(String, String)],
) -> DockerServiceResult<TagResult> {
info!("Starting image tag setup...");
let result = self
.image_loader
.setup_image_tags_with_mappings(image_mappings)
.await?;
if !result.is_all_successful() {
warn!(
"Some tag setup failed: success {success}, failed {failed}",
success = result.success_count(),
failed = result.failure_count()
);
}
Ok(result)
}
pub async fn setup_image_tags_with_ducker_validation(
&self,
image_mappings: &[(String, String)],
) -> DockerServiceResult<TagResult> {
info!("Starting validated image tag setup...");
let result = self
.image_loader
.setup_image_tags_with_validation(image_mappings)
.await?;
if !result.is_all_successful() {
warn!(
"Some tag setup failed: success {success}, failed {failed}",
success = result.success_count(),
failed = result.failure_count()
);
}
Ok(result)
}
pub async fn list_docker_images_with_ducker(&self) -> DockerServiceResult<Vec<String>> {
info!("Using ducker to list images...");
self.image_loader.list_images_with_ducker().await
}
pub async fn start_services(&mut self) -> DockerServiceResult<()> {
info!("Starting Docker Compose services...");
self.script_permission_manager
.check_and_fix_script_permissions()
.await?;
self.docker_manager
.ensure_host_volumes_exist()
.await
.map_err(|err| DockerServiceError::DirectorySetup(err.to_string()))?;
self.directory_permission_manager
.ensure_mysql_config_safe()?;
self.check_port_conflicts().await?;
let result = self.docker_manager.start_services().await;
match result {
Ok(_) => {
info!("Waiting for services to become ready...");
let check_interval = Duration::from_secs(timeout::HEALTH_CHECK_INTERVAL);
match self
.health_checker
.wait_for_services_ready(check_interval)
.await
{
Ok(report) => {
info!("All services started successfully!");
self.print_service_status(&report).await;
}
Err(e) => {
warn!(
"Wait for services failed or timed out: {error}",
error = e.to_string()
);
if let Ok(report) = self.health_checker.health_check().await {
self.print_service_status_with_failures(&report).await;
}
}
}
Ok(())
}
Err(e) => {
error!("Docker Compose start command failed, checking container status...");
error!("Error detail: {error}", error = format!("{e:?}"));
match self.health_checker.health_check().await {
Ok(report) => {
if report.get_running_count() > 0 {
info!(
"🔍 {running}/{total} containers are running, entering health-check phase",
running = report.get_running_count(),
total = report.get_total_count()
);
let check_interval =
Duration::from_secs(timeout::HEALTH_CHECK_INTERVAL);
match self
.health_checker
.wait_for_services_ready(check_interval)
.await
{
Ok(final_report) => {
info!("🎉 Some services eventually started successfully!");
self.print_service_status(&final_report).await;
return Ok(()); }
Err(_health_error) => {
warn!(
"⏰ Health check timed out, but some services are still running"
);
self.print_service_status_with_failures(&report).await;
info!(
"You can inspect logs: nuwax-cli docker-service logs [service]"
);
return Ok(()); }
}
} else {
error!("No running containers found");
self.print_detailed_error_analysis(&report, &e.to_string())
.await;
}
}
Err(e) => {
error!("❌ Failed to get container status details");
error!("Error detail: {error}", error = format!("{e:?}"));
}
}
Err(DockerServiceError::ServiceManagement(e.to_string()))
}
}
}
pub async fn stop_services(&self) -> DockerServiceResult<()> {
info!("Stopping Docker Compose services...");
let result = self.docker_manager.stop_services().await;
match result {
Ok(_) => {
info!("Services stopped successfully");
Ok(())
}
Err(e) => {
error!("Failed to stop services: {error}", error = e.to_string());
Err(DockerServiceError::ServiceManagement(e.to_string()))
}
}
}
pub async fn restart_services(&mut self) -> DockerServiceResult<()> {
info!("Restarting Docker Compose services...");
let result = self.docker_manager.restart_services().await;
match result {
Ok(_) => {
info!("Waiting for services to become ready after restart...");
let check_interval = Duration::from_secs(timeout::HEALTH_CHECK_INTERVAL);
match self
.health_checker
.wait_for_services_ready(check_interval)
.await
{
Ok(report) => {
info!("All services restarted successfully!");
self.print_service_status(&report).await;
}
Err(e) => {
warn!(
"Wait for services after restart failed or timed out: {error}",
error = e.to_string()
);
if let Ok(report) = self.health_checker.health_check().await {
self.print_service_status_with_failures(&report).await;
}
}
}
Ok(())
}
Err(e) => {
error!("Failed to restart services: {error}", error = e.to_string());
Err(DockerServiceError::ServiceManagement(e.to_string()))
}
}
}
pub async fn restart_container(&self, container_name: &str) -> DockerServiceResult<()> {
info!("Restarting container: {name}", name = container_name);
let result = self.docker_manager.restart_service(container_name).await;
match result {
Ok(_) => {
info!(
"Container {name} restarted successfully",
name = container_name
);
Ok(())
}
Err(e) => {
error!(
"Container {name} restart failed: {error}",
name = container_name,
error = e.to_string()
);
Err(DockerServiceError::ServiceManagement(e.to_string()))
}
}
}
pub async fn health_check(&self) -> DockerServiceResult<HealthReport> {
self.health_checker.health_check().await
}
pub async fn get_status_summary(&self) -> DockerServiceResult<String> {
self.health_checker.get_status_summary().await
}
async fn print_service_status(&self, report: &HealthReport) {
info!("=== Service Status Overview ===");
info!(
"Overall status: {status}",
status = report.finalize().display_name()
);
info!(
"Running containers: {running}/{total}",
running = report.get_running_count(),
total = report.get_total_count()
);
if !report.containers.is_empty() {
info!("Container details:");
for container in &report.containers {
info!(
" • {name} - {status} ({image})",
name = container.name,
status = container.status.display_name(),
image = container.image
);
}
}
if !report.errors.is_empty() {
warn!("Errors:");
for error in &report.errors {
warn!(" • {error}", error = error);
}
}
if report.finalize().is_healthy() {
info!("=== Service Access Info ===");
use client_core::constants::docker::ports;
info!(
"• Frontend: http://localhost:{port}",
port = ports::DEFAULT_FRONTEND_PORT
);
info!(
"• Backend API: http://localhost:{port}",
port = ports::DEFAULT_BACKEND_PORT
);
info!("• Service management complete. Ready to use.");
}
}
async fn print_service_status_with_failures(&self, report: &HealthReport) {
info!("=== Service Status Details ===");
info!(
"Overall status: {status}",
status = report.finalize().display_name()
);
info!(
"Health summary: {running}/{total} containers healthy",
running = report.get_running_count(),
total = report.get_total_count()
);
let running_containers: Vec<_> = report
.containers
.iter()
.filter(|c| c.status.is_healthy())
.collect();
let failed_containers: Vec<_> = report
.containers
.iter()
.filter(|c| !c.status.is_healthy() && !c.status.is_transitioning())
.collect();
let starting_containers: Vec<_> = report
.containers
.iter()
.filter(|c| c.status.is_transitioning())
.collect();
if !running_containers.is_empty() {
info!("✅ Running containers:");
for container in running_containers {
info!(
" • {name} ({image})",
name = container.name,
image = container.image
);
}
}
if !starting_containers.is_empty() {
warn!("🔄 Starting containers:");
for container in starting_containers {
warn!(
" • {name} - {status}",
name = container.name,
status = container.status.display_name()
);
}
}
if !failed_containers.is_empty() {
error!("❌ Failed containers:");
for container in failed_containers {
error!(
" • {name} - {status} ({image})",
name = container.name,
status = container.status.display_name(),
image = container.image
);
self.print_container_troubleshooting(&container.name, &container.image)
.await;
}
}
if report.get_running_count() > 0 {
info!("=== Available Service Access Info ===");
use client_core::constants::docker::ports;
let has_frontend = report
.containers
.iter()
.any(|c| c.status.is_healthy() && c.name.contains("frontend"));
let has_backend = report
.containers
.iter()
.any(|c| c.status.is_healthy() && c.name.contains("backend"));
if has_frontend {
info!(
"• Frontend: http://localhost:{port}",
port = ports::DEFAULT_FRONTEND_PORT
);
}
if has_backend {
info!(
"• Backend API: http://localhost:{port}",
port = ports::DEFAULT_BACKEND_PORT
);
}
let failed_count = report
.containers
.iter()
.filter(|c| !c.status.is_healthy() && !c.status.is_transitioning())
.count();
if failed_count == 0 {
info!("• All services are running normally!");
} else {
warn!("• Some services failed, but available services remain usable");
}
}
}
async fn print_detailed_error_analysis(&self, report: &HealthReport, original_error: &str) {
error!("=== Startup Failure Analysis ===");
let failed_containers: Vec<_> = report
.containers
.iter()
.filter(|c| !c.status.is_healthy())
.collect();
if failed_containers.is_empty() {
error!("❌ Failed to get container status details");
error!("❌ Original error: {error}", error = original_error);
return;
}
error!(
"❌ Failed containers: {failed}/{total}",
failed = failed_containers.len(),
total = report.get_total_count()
);
for container in failed_containers {
error!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
error!("Container: {name}", name = container.name);
error!("Image: {image}", image = container.image);
error!(
"Current status: {status}",
status = container.status.display_name()
);
self.print_container_troubleshooting(&container.name, &container.image)
.await;
}
self.analyze_docker_error(original_error).await;
}
async fn print_container_troubleshooting(&self, container_name: &str, image_name: &str) {
if container_name.contains("video-analysis-worker") {
warn!("💡 Analysis:");
warn!(
" - This container requires NVIDIA GPU support, which may be unavailable on this system"
);
warn!(" - Architecture mismatch detected (amd64 vs arm64)");
warn!("💡 Suggested fix:");
warn!(" - On Mac ARM64, disable this service or use an ARM64 image");
warn!(" - Comment out this service in docker-compose.yml");
warn!(" - Or update image version in .env to an ARM64 variant");
} else if image_name.contains("amd64") {
warn!("💡 Analysis:");
warn!(" - Architecture mismatch: image is amd64 but system is arm64");
warn!("💡 Suggested fix:");
warn!(" - Use an arm64 image");
warn!(" - Or add --platform linux/amd64 when running container");
} else if container_name.contains("mysql") || container_name.contains("redis") {
warn!("💡 Analysis:");
warn!(
" - Database startup failed, likely due to port conflict or data directory permissions"
);
warn!("💡 Suggested fix:");
warn!(" - Check whether port 3306(MySQL) or 6379(Redis) is occupied");
warn!(" - Check directory permissions: ./data/mysql or ./data/redis");
} else if container_name.contains("backend") || container_name.contains("entrypoint") {
warn!("💡 Analysis:");
warn!(" - Container startup script may be missing execute permission");
warn!("💡 Suggested fix:");
warn!(" - Check permissions for scripts like docker-entrypoint.sh");
warn!(" - Run: chmod +x config/docker-entrypoint.sh");
warn!(
" - View logs: docker-compose logs {name}",
name = container_name
);
} else {
warn!("💡 Suggestion:");
warn!(
" - View logs: docker-compose logs {name}",
name = container_name
);
warn!(" - Verify images were pulled successfully");
warn!(" - Verify environment variables");
}
}
async fn analyze_docker_error(&self, error_message: &str) {
error!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
error!("🔍 Error analysis:");
let mut has_issues = false;
if error_message.contains("nvidia") {
error!(" ❌ NVIDIA GPU driver issue");
error!(" 💡 NVIDIA GPU may be unsupported or driver not installed");
error!(" 💡 Consider disabling services that require GPU");
has_issues = true;
}
if error_message.contains("platform")
&& error_message.contains("amd64")
&& error_message.contains("arm64")
{
error!(" ❌ Container architecture mismatch");
error!(" 💡 amd64 image cannot run natively on arm64 system");
error!(" 💡 Use image that matches your architecture");
has_issues = true;
}
if error_message.contains("Permission denied") && error_message.contains("entrypoint") {
error!(" ❌ Script permission issue");
error!(" 💡 Startup script lacks execute permission");
error!(" 💡 Add execute permission with chmod +x");
has_issues = true;
}
if error_message.contains("port") || error_message.contains("bind") {
error!(" ❌ Port bind failed");
error!(" 💡 There may be a port conflict");
error!(" 💡 Check current port occupancy");
has_issues = true;
}
if !has_issues {
error!(" ❓ Unrecognized error type, key lines:");
let key_lines: Vec<&str> = error_message
.lines()
.filter(|line| {
line.contains("Error")
|| line.contains("failed")
|| line.contains("denied")
|| line.contains("not found")
|| line.contains("connection")
|| line.trim().starts_with("Container")
})
.take(5)
.collect();
if !key_lines.is_empty() {
for line in key_lines {
error!(" {line}", line = line.trim());
}
} else {
for line in error_message.lines().take(3) {
if !line.trim().is_empty() {
error!(" {line}", line = line.trim());
}
}
}
}
error!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}
async fn check_port_conflicts(&mut self) -> DockerServiceResult<()> {
let compose_file = self.docker_manager.get_compose_file();
let env_file = self.docker_manager.get_env_file();
if !compose_file.exists() {
warn!("docker-compose.yml not found, skipping port conflict check");
return Ok(());
}
info!("🔍 Starting smart port-conflict check...");
match self
.port_manager
.smart_check_compose_port_conflicts(compose_file, env_file)
.await
{
Ok(report) => {
if report.has_conflicts {
warn!("⚠️ Port conflict detected, proceeding with smart handling");
self.port_manager.print_smart_conflict_report(&report);
warn!("💡 Note: Docker may handle port binding automatically");
warn!(
" - If occupied by related service, container may reuse existing binding"
);
warn!(" - If occupied by unrelated service, startup may fail");
warn!(" - Check startup result and resolve conflicts manually if needed");
} else {
info!("✅ Port check passed, no conflict found");
if report.total_checked > 0 {
info!(
"Checked {total} port mappings in total",
total = report.total_checked
);
}
}
}
Err(e) => {
warn!(
"Port check failed: {error}, continuing startup",
error = e.to_string()
);
}
}
Ok(())
}
}