use std::collections::HashMap;
use tracing::{debug, info};
use crate::error::Result;
use super::client::RunPodClient;
use super::types::{Pod, PodStatus};
pub const TAG_PROJECT: &str = "halldyll_project";
pub const TAG_ENV: &str = "halldyll_env";
pub const TAG_POD: &str = "halldyll_pod";
pub const TAG_SPEC_HASH: &str = "halldyll_spec_hash";
#[derive(Debug)]
pub struct PodObserver {
client: RunPodClient,
}
#[derive(Debug, Clone)]
pub struct ObservedPod {
pub id: String,
pub name: String,
pub project: Option<String>,
pub environment: Option<String>,
pub pod_name: Option<String>,
pub spec_hash: Option<String>,
pub status: PodStatus,
pub gpu_type: Option<String>,
pub gpu_count: u32,
pub image: String,
pub endpoints: HashMap<u16, String>,
pub tags: HashMap<String, String>,
}
impl PodObserver {
#[must_use]
pub const fn new(client: RunPodClient) -> Self {
Self { client }
}
pub async fn list_all_pods(&self) -> Result<Vec<ObservedPod>> {
info!("Listing all pods");
let pods = self.client.list_pods().await?;
let observed: Vec<ObservedPod> = pods.iter().map(Self::to_observed).collect();
debug!("Found {} pods", observed.len());
Ok(observed)
}
pub async fn list_project_pods(
&self,
project: &str,
environment: &str,
) -> Result<Vec<ObservedPod>> {
info!("Listing pods for project: {project}/{environment}");
let all_pods = self.list_all_pods().await?;
let filtered: Vec<ObservedPod> = all_pods
.into_iter()
.filter(|p| {
p.project.as_deref() == Some(project)
&& p.environment.as_deref() == Some(environment)
})
.collect();
debug!(
"Found {} pods for {}/{}",
filtered.len(),
project,
environment
);
Ok(filtered)
}
pub async fn get_pod(&self, pod_id: &str) -> Result<ObservedPod> {
debug!("Getting pod: {pod_id}");
let pod = self.client.get_pod(pod_id).await?;
Ok(Self::to_observed(&pod))
}
pub async fn find_pod_by_name(
&self,
project: &str,
environment: &str,
pod_name: &str,
) -> Result<Option<ObservedPod>> {
let pods = self.list_project_pods(project, environment).await?;
Ok(pods
.into_iter()
.find(|p| p.pod_name.as_deref() == Some(pod_name)))
}
fn to_observed(pod: &Pod) -> ObservedPod {
let tags = pod.custom_tags.clone().unwrap_or_default();
let endpoints = pod
.endpoints()
.into_iter()
.map(|e| (e.port, e.url))
.collect();
ObservedPod {
id: pod.id.clone(),
name: pod.name.clone(),
project: tags.get(TAG_PROJECT).cloned(),
environment: tags.get(TAG_ENV).cloned(),
pod_name: tags.get(TAG_POD).cloned(),
spec_hash: tags.get(TAG_SPEC_HASH).cloned(),
status: pod.desired_status,
gpu_type: pod.gpu_type_name().map(String::from),
gpu_count: pod.gpu_count,
image: pod.image_name.clone(),
endpoints,
tags,
}
}
pub async fn find_pod_by_spec_hash(
&self,
project: &str,
environment: &str,
spec_hash: &str,
) -> Result<Option<ObservedPod>> {
let pods = self.list_project_pods(project, environment).await?;
Ok(pods
.into_iter()
.find(|p| p.spec_hash.as_deref() == Some(spec_hash)))
}
pub async fn get_project_status(
&self,
project: &str,
environment: &str,
) -> Result<ProjectStatus> {
let pods = self.list_project_pods(project, environment).await?;
let mut running = 0;
let mut stopped = 0;
let mut error = 0;
let mut other = 0;
for pod in &pods {
match pod.status {
PodStatus::Running => running += 1,
PodStatus::Stopped | PodStatus::Exited => stopped += 1,
PodStatus::Unknown => error += 1,
_ => other += 1,
}
}
Ok(ProjectStatus {
project: project.to_string(),
environment: environment.to_string(),
total_pods: pods.len(),
running,
stopped,
error,
other,
pods,
})
}
#[must_use]
pub const fn client(&self) -> &RunPodClient {
&self.client
}
}
#[derive(Debug)]
pub struct ProjectStatus {
pub project: String,
pub environment: String,
pub total_pods: usize,
pub running: usize,
pub stopped: usize,
pub error: usize,
pub other: usize,
pub pods: Vec<ObservedPod>,
}
impl ProjectStatus {
#[must_use]
pub const fn is_healthy(&self) -> bool {
self.total_pods > 0 && self.running == self.total_pods
}
#[must_use]
pub const fn has_errors(&self) -> bool {
self.error > 0
}
}
impl ObservedPod {
#[must_use]
pub const fn is_running(&self) -> bool {
matches!(self.status, PodStatus::Running)
}
#[must_use]
pub const fn is_managed(&self) -> bool {
self.project.is_some() && self.environment.is_some()
}
#[must_use]
pub fn full_name(&self) -> String {
match (&self.project, &self.environment, &self.pod_name) {
(Some(proj), Some(env), Some(name)) => format!("{proj}-{env}-{name}"),
_ => self.name.clone(),
}
}
}