Skip to main content

lava_operator/
finalizer.rs

1//! Finalizer — runs the destroy path before the CR is removed from
2//! etcd. Solid abstraction: the controller calls
3//! [`run_finalizer`] with a typed `DestroyBackend`; this module
4//! never imports kube-rs (lives in lib defaults too).
5//!
6//! Backends:
7//!   - `EmbeddedMagmaBackend` — wraps magma::apply::run_plan with the
8//!     Destroy plan; lives in lava-operator's magma-bridge module.
9//!   - `MockDestroy` — caller-supplied outcome for tests.
10
11use crate::{Condition, Phase, ReconcileOutcome};
12
13/// Pluggable destroy seam. Production: magma in-process destroy
14/// (added behind the `magma-bridge` feature at L3.1). Tests: mock.
15pub trait DestroyBackend {
16    /// Drive the destroy. `Ok(diagnostics)` on success;
17    /// `Err(message)` on failure.
18    ///
19    /// # Errors
20    /// Backend-specific failures surface as a string the controller
21    /// folds into a typed `Condition::fail`.
22    fn destroy(&self, source: &crate::Source, bindings: &indexmap::IndexMap<String, String>)
23        -> Result<Vec<String>, String>;
24}
25
26/// Run the finalizer for one LavaArchitecture. Returns a
27/// [`ReconcileOutcome`] the controller patches back into status.
28#[must_use]
29pub fn run_finalizer<B: DestroyBackend>(
30    backend: &B,
31    source: &crate::Source,
32    bindings: &indexmap::IndexMap<String, String>,
33) -> ReconcileOutcome {
34    match backend.destroy(source, bindings) {
35        Ok(diagnostics) => ReconcileOutcome {
36            phase: Phase::Finalizing,
37            conditions: {
38                let mut c = vec![Condition::ok("Destroyed", "BackendOk")];
39                for d in diagnostics {
40                    c.push(Condition::ok("Diagnostic", d));
41                }
42                c
43            },
44            terraform_json: None,
45        },
46        Err(e) => ReconcileOutcome {
47            phase: Phase::Failed,
48            conditions: vec![Condition::fail("Destroyed", e)],
49            terraform_json: None,
50        },
51    }
52}
53
54/// Test backend — returns the supplied outcome unconditionally.
55pub struct MockDestroy {
56    pub outcome: Result<Vec<String>, String>,
57}
58
59impl MockDestroy {
60    #[must_use]
61    pub fn ok() -> Self {
62        Self {
63            outcome: Ok(vec![]),
64        }
65    }
66    #[must_use]
67    pub fn with_diagnostics(msgs: Vec<String>) -> Self {
68        Self { outcome: Ok(msgs) }
69    }
70    #[must_use]
71    pub fn err(msg: impl Into<String>) -> Self {
72        Self {
73            outcome: Err(msg.into()),
74        }
75    }
76}
77
78impl DestroyBackend for MockDestroy {
79    fn destroy(
80        &self,
81        _src: &crate::Source,
82        _bindings: &indexmap::IndexMap<String, String>,
83    ) -> Result<Vec<String>, String> {
84        self.outcome.clone()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use indexmap::IndexMap;
92
93    #[test]
94    fn successful_destroy_drives_to_finalizing_phase() {
95        let outcome = run_finalizer(
96            &MockDestroy::ok(),
97            &crate::Source::Inline { inline: "src".into() },
98            &IndexMap::new(),
99        );
100        assert_eq!(outcome.phase, Phase::Finalizing);
101        assert_eq!(outcome.conditions[0].status, "True");
102    }
103
104    #[test]
105    fn destroy_diagnostics_surface_as_conditions() {
106        let outcome = run_finalizer(
107            &MockDestroy::with_diagnostics(vec!["removed 3 resources".into()]),
108            &crate::Source::Inline { inline: "src".into() },
109            &IndexMap::new(),
110        );
111        assert_eq!(outcome.conditions.len(), 2);
112        assert_eq!(outcome.conditions[1].kind, "Diagnostic");
113    }
114
115    #[test]
116    fn failed_destroy_drives_to_failed_phase_with_message() {
117        let outcome = run_finalizer(
118            &MockDestroy::err("provider unreachable"),
119            &crate::Source::Inline { inline: "src".into() },
120            &IndexMap::new(),
121        );
122        assert_eq!(outcome.phase, Phase::Failed);
123        assert_eq!(outcome.conditions[0].status, "False");
124        assert_eq!(
125            outcome.conditions[0].message.as_deref(),
126            Some("provider unreachable"),
127        );
128    }
129}