use std::sync::Arc;
use super::super::crd::{
AmberCluster, AmberWorkload, ClusterPhase, Condition, ConditionStatus, ConditionType,
WorkloadPhase,
};
use k8s_openapi::api::apps::v1::{Deployment, StatefulSet};
use kube::{Api, Client, ResourceExt};
use tracing::{debug, error, info, warn};
pub struct Reconciler {
client: Client,
}
impl Reconciler {
pub fn new(client: Client) -> Self {
Self { client }
}
pub async fn reconcile_cluster(
&self,
cluster: Arc<AmberCluster>,
) -> Result<ReconcileResult, super::controller::Error> {
let start_time: _ = Instant::now();
let name: _ = cluster.name_any();
let namespace: _ = cluster.namespace().unwrap_or_default();
info!(
"Starting reconciliation for AmberCluster: {} in {}",
name, namespace
);
let current_state: _ = self.get_current_state(&cluster).await?;
let desired_state: _ = self.calculate_desired_state(&cluster)?;
let diff: _ = self.calculate_diff(¤t_state, &desired_state)?;
debug!("Reconciliation diff for {}: {:?}", name, diff);
if !diff.is_empty() {
self.apply_changes(&cluster, &diff).await?;
}
self.update_status(&cluster, ¤t_state).await?;
let elapsed: _ = start_time.elapsed();
debug!("Completed reconciliation for {} in {:?}", name, elapsed);
Ok(ReconcileResult {
requeue_after: Some(Duration::from_secs(30)),
message: "Reconciliation completed successfully".to_string(),
})
}
pub async fn reconcile_workload(
&self,
workload: Arc<AmberWorkload>,
) -> Result<ReconcileResult, super::controller::Error> {
let start_time: _ = Instant::now();
let name: _ = workload.name_any();
let namespace: _ = workload.namespace().unwrap_or_default();
info!(
"Starting reconciliation for AmberWorkload: {} in {}",
name, namespace
);
let current_state: _ = self.get_workload_current_state(&workload).await?;
let desired_state: _ = self.calculate_workload_desired_state(&workload)?;
let diff: _ = self.calculate_workload_diff(¤t_state, &desired_state)?;
debug!("Reconciliation diff for {}: {:?}", name, diff);
if !diff.is_empty() {
self.apply_workload_changes(&workload, &diff).await?;
}
self.update_workload_status(&workload, ¤t_state)
.await?;
let elapsed: _ = start_time.elapsed();
debug!("Completed reconciliation for {} in {:?}", name, elapsed);
Ok(ReconcileResult {
requeue_after: Some(Duration::from_secs(30)),
message: "Reconciliation completed successfully".to_string(),
})
}
async fn get_current_state(
&self,
cluster: &AmberCluster,
) -> Result<ClusterState, super::controller::Error> {
let namespace: _ = cluster.namespace().unwrap_or_default();
let statefulsets: _ = Api::<StatefulSet>::namespaced(self.client.clone(), &namespace);
let statefulset: _ = statefulsets.get(&cluster.name_any()).await?;
let ready_replicas: _ = statefulset
.status
.as_ref()
.and_then(|s| s.ready_replicas)
.unwrap_or(0) as u32;
let replicas: _ = statefulset
.spec
.as_ref()
.and_then(|s| s.replicas)
.unwrap_or(0) as u32;
let services: _ =
Api::<k8s_openapi::api::core::v1::Service>::namespaced(self.client.clone(), &namespace);
let _service: _ = services.get(&cluster.name_any()).await?;
let phase: _ = if ready_replicas == 0 {
ClusterPhase::Pending
} else if ready_replicas < replicas {
ClusterPhase::Creating
} else {
ClusterPhase::Running
};
Ok(ClusterState {
phase,
ready_replicas,
total_replicas: replicas,
conditions: vec![Condition {
condition_type: ConditionType::Ready,
status: if ready_replicas == replicas {
ConditionStatus::True
} else {
ConditionStatus::False
},
last_probe_time: Some(chrono::Utc::now().to_rfc3339()),
last_transition_time: Some(chrono::Utc::now().to_rfc3339()),
reason: Some("Reconciling".to_string()),
message: Some(format!("{}/{} replicas ready", ready_replicas, replicas)),
}],
})
}
fn calculate_desired_state(
&self,
cluster: &AmberCluster,
) -> Result<ClusterState, super::controller::Error> {
Ok(ClusterState {
phase: ClusterPhase::Running,
ready_replicas: cluster.spec.nodes,
total_replicas: cluster.spec.nodes,
conditions: vec![Condition {
condition_type: ConditionType::Ready,
status: ConditionStatus::True,
last_probe_time: Some(chrono::Utc::now().to_rfc3339()),
last_transition_time: Some(chrono::Utc::now().to_rfc3339()),
reason: Some("AllReplicasReady".to_string()),
message: Some(format!("All {} replicas are ready", cluster.spec.nodes)),
}],
})
}
fn calculate_diff(
&self,
current: &ClusterState,
desired: &ClusterState,
) -> Result<ClusterDiff, super::controller::Error> {
Ok(ClusterDiff {
needs_update: current.phase != desired.phase
|| current.ready_replicas != desired.ready_replicas,
needs_scale: current.total_replicas != desired.total_replicas,
})
}
async fn apply_changes(
&self,
cluster: &AmberCluster,
diff: &ClusterDiff,
) -> Result<(), super::controller::Error> {
if diff.needs_scale {
info!(
"Scaling cluster {} from {} to {} replicas",
cluster.name_any(),
cluster.spec.nodes,
cluster.spec.nodes
);
}
Ok(())
}
async fn update_status(
&self,
cluster: &AmberCluster,
state: &ClusterState,
) -> Result<(), super::controller::Error> {
Ok(())
}
async fn get_workload_current_state(
&self,
workload: &AmberWorkload,
) -> Result<WorkloadState, super::controller::Error> {
let namespace: _ = workload.namespace().unwrap_or_default();
let deployments: _ = Api::<Deployment>::namespaced(self.client.clone(), &namespace);
let deployment: _ = deployments.get(&workload.name_any()).await?;
let ready_replicas: _ = deployment
.status
.as_ref()
.and_then(|s| s.ready_replicas)
.unwrap_or(0) as u32;
let replicas: _ = deployment
.spec
.as_ref()
.and_then(|s| s.replicas)
.unwrap_or(0) as u32;
let phase: _ = if ready_replicas == 0 {
WorkloadPhase::Pending
} else if ready_replicas < replicas {
WorkloadPhase::Creating
} else {
WorkloadPhase::Running
};
Ok(WorkloadState {
phase,
ready_replicas,
total_replicas: replicas,
conditions: vec![],
})
}
fn calculate_workload_desired_state(
&self,
workload: &AmberWorkload,
) -> Result<WorkloadState, super::controller::Error> {
Ok(WorkloadState {
phase: WorkloadPhase::Running,
ready_replicas: workload.spec.replicas,
total_replicas: workload.spec.replicas,
conditions: vec![],
})
}
fn calculate_workload_diff(
&self,
current: &WorkloadState,
desired: &WorkloadState,
) -> Result<WorkloadDiff, super::controller::Error> {
Ok(WorkloadDiff {
needs_update: current.phase != desired.phase
|| current.ready_replicas != desired.ready_replicas,
needs_scale: current.total_replicas != desired.total_replicas,
})
}
async fn apply_workload_changes(
&self,
workload: &AmberWorkload,
diff: &WorkloadDiff,
) -> Result<(), super::controller::Error> {
if diff.needs_scale {
info!(
"Scaling workload {} from {} to {} replicas",
workload.name_any(),
workload.spec.replicas,
workload.spec.replicas
);
}
Ok(())
}
async fn update_workload_status(
&self,
workload: &AmberWorkload,
state: &WorkloadState,
) -> Result<(), super::controller::Error> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ClusterState {
pub phase: ClusterPhase,
pub ready_replicas: u32,
pub total_replicas: u32,
pub conditions: Vec<Condition>,
}
#[derive(Debug, Clone)]
pub struct ClusterDiff {
pub needs_update: bool,
pub needs_scale: bool,
}
impl ClusterDiff {
pub fn is_empty(&self) -> bool {
!self.needs_update && !self.needs_scale
}
}
#[derive(Debug, Clone)]
pub struct WorkloadState {
pub phase: WorkloadPhase,
pub ready_replicas: u32,
pub total_replicas: u32,
pub conditions: Vec<Condition>,
}
#[derive(Debug, Clone)]
pub struct WorkloadDiff {
pub needs_update: bool,
pub needs_scale: bool,
}
impl WorkloadDiff {
pub fn is_empty(&self) -> bool {
!self.needs_update && !self.needs_scale
}
}
pub struct ReconcileResult {
pub requeue_after: Option<Duration>,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeMap, HashMap};
use std::time::{Duration, Instant};
#[test]
fn test_cluster_diff_empty() {
let diff: _ = ClusterDiff {
needs_update: false,
needs_scale: false,
};
assert!(diff.is_empty());
}
#[test]
fn test_cluster_diff_not_empty() {
let diff: _ = ClusterDiff {
needs_update: true,
needs_scale: false,
};
assert!(!diff.is_empty());
}
#[test]
fn test_workload_diff_empty() {
let diff: _ = WorkloadDiff {
needs_update: false,
needs_scale: false,
};
assert!(diff.is_empty());
}
}