Coverage Report

Created: 2026-05-19 10:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
crates/flotilla-controllers/src/reconcilers/environment.rs
Line
Count
Source
1
use std::sync::Arc;
2
3
use async_trait::async_trait;
4
use flotilla_resources::{
5
    controller::{ReconcileOutcome, Reconciler},
6
    DockerEnvironmentSpec, Environment, EnvironmentPhase, EnvironmentStatusPatch, ResourceError, ResourceObject,
7
};
8
9
#[async_trait]
10
pub trait DockerEnvironmentRuntime: Send + Sync {
11
    async fn provision(&self, name: &str, spec: &DockerEnvironmentSpec) -> Result<String, String>;
12
    async fn destroy(&self, container_id: &str) -> Result<(), String>;
13
}
14
15
pub struct EnvironmentReconciler<R> {
16
    docker: Arc<R>,
17
}
18
19
impl<R> EnvironmentReconciler<R> {
20
7
    pub fn new(docker: Arc<R>) -> Self {
21
7
        Self { docker }
22
7
    }
23
}
24
25
pub enum EnvironmentDeps {
26
    None,
27
    Ready { docker_container_id: Option<String> },
28
    Failed(String),
29
}
30
31
impl<R> Reconciler for EnvironmentReconciler<R>
32
where
33
    R: DockerEnvironmentRuntime + 'static,
34
{
35
    type Resource = Environment;
36
    type Dependencies = EnvironmentDeps;
37
38
14
    async fn fetch_dependencies(&self, obj: &ResourceObject<Self::Resource>) -> Result<Self::Dependencies, ResourceError> {
39
14
        match obj.status.as_ref().map(|status| status.phase).unwrap_or(EnvironmentPhase::Pending) {
40
            EnvironmentPhase::Pending => {
41
5
                if let Some(
spec1
) = &obj.spec.docker {
42
1
                    match self.docker.provision(&obj.metadata.name, spec).await {
43
1
                        Ok(container_id) => Ok(EnvironmentDeps::Ready { docker_container_id: Some(container_id) }),
44
0
                        Err(err) => Ok(EnvironmentDeps::Failed(err)),
45
                    }
46
                } else {
47
4
                    Ok(EnvironmentDeps::None)
48
                }
49
            }
50
9
            _ => Ok(EnvironmentDeps::None),
51
        }
52
14
    }
53
54
14
    fn reconcile(
55
14
        &self,
56
14
        obj: &ResourceObject<Self::Resource>,
57
14
        deps: &Self::Dependencies,
58
14
        _now: chrono::DateTime<chrono::Utc>,
59
14
    ) -> ReconcileOutcome<Self::Resource> {
60
14
        let patch = match obj.status.as_ref().map(|status| status.phase).unwrap_or(EnvironmentPhase::Pending) {
61
5
            EnvironmentPhase::Pending if obj.spec.host_direct.is_some(
)4
=> {
62
4
                Some(EnvironmentStatusPatch::MarkReady { docker_container_id: None })
63
            }
64
1
            EnvironmentPhase::Pending => match deps {
65
1
                EnvironmentDeps::Ready { docker_container_id } => {
66
1
                    Some(EnvironmentStatusPatch::MarkReady { docker_container_id: docker_container_id.clone() })
67
                }
68
0
                EnvironmentDeps::Failed(message) => Some(EnvironmentStatusPatch::MarkFailed { message: message.clone() }),
69
0
                EnvironmentDeps::None => None,
70
            },
71
9
            _ => None,
72
        };
73
74
14
        ReconcileOutcome::new(patch)
75
14
    }
76
77
0
    async fn run_finalizer(&self, obj: &ResourceObject<Self::Resource>) -> Result<(), ResourceError> {
78
0
        if let Some(container_id) = obj.status.as_ref().and_then(|status| status.docker_container_id.as_deref()) {
79
0
            self.docker.destroy(container_id).await.map_err(ResourceError::other)?;
80
0
        }
81
0
        Ok(())
82
0
    }
83
84
20
    fn finalizer_name(&self) -> Option<&'static str> {
85
20
        Some("flotilla.work/environment-teardown")
86
20
    }
87
}