use serde::{Deserialize, Serialize};
use std::path::Path;
use which::which;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterCapabilityStatus {
pub available: bool,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterCapabilities {
pub pg_dump: ClusterCapabilityStatus,
pub pg_restore: ClusterCapabilityStatus,
#[serde(default = "unknown_capability")]
pub s3cmd: ClusterCapabilityStatus,
pub s3_access: ClusterCapabilityStatus,
pub backup_create: ClusterCapabilityStatus,
pub backup_restore: ClusterCapabilityStatus,
pub openapi_download: ClusterCapabilityStatus,
pub management_api: ClusterCapabilityStatus,
pub gateway_fetch: ClusterCapabilityStatus,
#[serde(default)]
pub deadpool_experimental: Option<ClusterCapabilityStatus>,
}
fn unknown_capability() -> ClusterCapabilityStatus {
ClusterCapabilityStatus {
available: false,
detail: "Capability not reported by this node".to_string(),
}
}
fn env_non_empty(key: &str) -> bool {
std::env::var(key)
.ok()
.map(|value| !value.trim().is_empty())
.unwrap_or(false)
}
fn detect_tool_path(
env_key: &str,
fallback_names: &[&str],
fallback_windows: &[&str],
) -> Option<String> {
if let Ok(path) = std::env::var(env_key) {
if !path.trim().is_empty() && Path::new(path.trim()).is_file() {
return Some(path);
}
}
for name in fallback_names {
if let Ok(path) = which(name) {
return Some(path.display().to_string());
}
}
#[cfg(target_os = "windows")]
{
for candidate in fallback_windows {
if Path::new(candidate).is_file() {
return Some((*candidate).to_string());
}
}
}
None
}
pub(super) fn local_capabilities() -> ClusterCapabilities {
let dump_path: Option<String> = detect_tool_path(
"ATHENA_PG_DUMP_PATH",
&["pg_dump"],
&[r"C:\Program Files\PostgreSQL\18\bin\pg_dump.exe"],
);
let restore_path: Option<String> = detect_tool_path(
"ATHENA_PG_RESTORE_PATH",
&["pg_restore"],
&[r"C:\Program Files\PostgreSQL\18\bin\pg_restore.exe"],
);
let has_dump: bool = dump_path.is_some();
let has_restore: bool = restore_path.is_some();
let s3cmd_path: Option<String> = detect_tool_path("ATHENA_S3CMD_PATH", &["s3cmd"], &[]);
let has_s3cmd: bool = s3cmd_path.is_some();
let has_bucket: bool = env_non_empty("ATHENA_BACKUP_S3_BUCKET");
let has_access_key: bool = env_non_empty("ATHENA_BACKUP_S3_ACCESS_KEY");
let has_secret_key: bool = env_non_empty("ATHENA_BACKUP_S3_SECRET_KEY");
let dump_detail: String = dump_path
.as_ref()
.map(|path| format!("resolved at {}", path))
.unwrap_or_else(|| "pg_dump not found (ATHENA_PG_DUMP_PATH/PATH)".to_string());
let restore_detail: String = restore_path
.as_ref()
.map(|path| format!("resolved at {}", path))
.unwrap_or_else(|| "pg_restore not found (ATHENA_PG_RESTORE_PATH/PATH)".to_string());
let s3cmd_detail: String = s3cmd_path
.as_ref()
.map(|path| format!("resolved at {}", path))
.unwrap_or_else(|| "s3cmd not found (ATHENA_S3CMD_PATH/PATH)".to_string());
let s3_ready: bool =
has_bucket && ((has_access_key && has_secret_key) || (!has_access_key && !has_secret_key));
let s3_detail: String = if !has_bucket {
"ATHENA_BACKUP_S3_BUCKET is not set".to_string()
} else if has_access_key ^ has_secret_key {
"Set both ATHENA_BACKUP_S3_ACCESS_KEY and ATHENA_BACKUP_S3_SECRET_KEY, or neither"
.to_string()
} else {
"S3 backup environment variables are configured".to_string()
};
ClusterCapabilities {
pg_dump: ClusterCapabilityStatus {
available: has_dump,
detail: dump_detail,
},
pg_restore: ClusterCapabilityStatus {
available: has_restore,
detail: restore_detail,
},
s3cmd: ClusterCapabilityStatus {
available: has_s3cmd,
detail: s3cmd_detail,
},
s3_access: ClusterCapabilityStatus {
available: s3_ready,
detail: s3_detail,
},
backup_create: ClusterCapabilityStatus {
available: has_dump && s3_ready,
detail: if has_dump && s3_ready {
"Backup creation dependencies are available".to_string()
} else {
"Requires pg_dump and S3 access".to_string()
},
},
backup_restore: ClusterCapabilityStatus {
available: has_restore && s3_ready,
detail: if has_restore && s3_ready {
"Backup restore dependencies are available".to_string()
} else {
"Requires pg_restore and S3 access".to_string()
},
},
openapi_download: ClusterCapabilityStatus {
available: true,
detail: "OpenAPI route is exposed at /openapi.yaml".to_string(),
},
management_api: ClusterCapabilityStatus {
available: true,
detail: "Management routes are exposed under /management/*".to_string(),
},
gateway_fetch: ClusterCapabilityStatus {
available: true,
detail: "Gateway fetch route is exposed at /gateway/fetch".to_string(),
},
deadpool_experimental: Some(ClusterCapabilityStatus {
available: cfg!(feature = "deadpool_experimental"),
detail: if cfg!(feature = "deadpool_experimental") {
"Experimental deadpool backend compiled in".to_string()
} else {
"Experimental deadpool backend not compiled in".to_string()
},
}),
}
}
pub(super) fn unavailable_capabilities(reason: &str) -> ClusterCapabilities {
ClusterCapabilities {
pg_dump: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
pg_restore: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
s3cmd: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
s3_access: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
backup_create: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
backup_restore: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
openapi_download: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
management_api: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
gateway_fetch: ClusterCapabilityStatus {
available: false,
detail: reason.to_string(),
},
deadpool_experimental: None,
}
}