Skip to main content

xianyun_deploy/reloader/
mod.rs

1//! Interfaces for running local development processes with hot-reload
2//! capabilities.
3//!
4//! Ported from `internal/reloader/{interface,errors}.go`.
5
6use thiserror::Error;
7use xianyun_core::config::latest::{CaproniConfig, ReloaderConfig};
8
9pub mod mirror;
10pub mod registry;
11pub mod skaffold;
12
13/// The current state of a reloader.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum State {
16    /// The target deployment doesn't exist.
17    NotDeployed,
18    /// The reloader cannot run (e.g. target resource missing).
19    NotReady,
20    /// The reloader is ready to run.
21    Ready,
22    /// The reloader is actively running with `caproni run`.
23    Editing,
24    /// The reloader failed.
25    Failed,
26}
27
28impl State {
29    /// The string form used in Go (`"not-deployed"`, etc.), for display
30    /// and any serialization boundary.
31    #[must_use]
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            State::NotDeployed => "not-deployed",
35            State::NotReady => "not-ready",
36            State::Ready => "ready",
37            State::Editing => "editing",
38            State::Failed => "failed",
39        }
40    }
41
42    /// A human-readable label for this state. Ported from
43    /// `State.PrettyString`; the Go original also colors the string via
44    /// `github.com/gookit/color`, which has no equivalent dependency in
45    /// this crate yet, so this port returns the plain label. Adding
46    /// color is Phase 4 UI polish, not behavior this crate's tests
47    /// depend on.
48    #[must_use]
49    pub fn pretty_string(&self) -> &'static str {
50        match self {
51            State::NotDeployed => "Not deployed",
52            State::NotReady => "Not ready",
53            State::Ready => "Ready",
54            State::Editing => "Editing",
55            State::Failed => "Failed",
56        }
57    }
58}
59
60/// The current status of a reloader.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Status {
63    /// The current state of the reloader.
64    pub state: State,
65    /// The reloader type (e.g. `"mirror"`).
66    pub reloader_type: String,
67}
68
69/// Errors common across reloader managers.
70#[derive(Debug, Error)]
71pub enum ReloaderError {
72    /// Trying to create a reloader with an unknown type.
73    #[error("unknown reloader type")]
74    UnknownType,
75    /// A reloader run operation failed.
76    #[error("reloader run failed")]
77    RunFailed,
78    /// Unable to determine reloader status.
79    #[error("failed to get reloader status")]
80    StatusFailed,
81    /// The target resource doesn't exist.
82    #[error("target resource not found")]
83    TargetNotFound,
84}
85
86/// The interface for managing local development processes.
87/// Implementations are responsible for running processes with
88/// hot-reload capabilities using specific strategies (e.g. mirrord,
89/// skaffold).
90pub trait Manager {
91    /// The reloader type that this manager handles (e.g. `"mirror"`,
92    /// `"skaffold"`).
93    fn reloader_type(&self) -> String;
94
95    /// Verifies that the environment is ready for this reloader to
96    /// run. For example, mirror reloaders check that no stale
97    /// `*-caproni` deployments exist. Returns `Ok(())` if all
98    /// preconditions are met.
99    fn check_preconditions(
100        &self,
101        reloader_config: &ReloaderConfig,
102        caproni_config: &CaproniConfig,
103        get_tool_version: &dyn Fn(&str) -> Option<String>,
104    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
105
106    /// Runs the reloader processes, blocking until cancelled.
107    ///
108    /// `kubeconfig_path` is the path to a temporary kubeconfig for the
109    /// caproni-selected cluster (created and owned by the caller).
110    /// Implementations that launch external tooling should pin it to
111    /// this kubeconfig rather than relying on the user's global
112    /// current-context. It may be empty.
113    fn run(
114        &self,
115        reloader_config: &ReloaderConfig,
116        caproni_config: &CaproniConfig,
117        kubeconfig_path: &str,
118    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
119
120    /// Returns the current status of the reloader. Returns an error if
121    /// unable to determine reloader state.
122    fn status(
123        &self,
124        reloader_config: &ReloaderConfig,
125        caproni_config: &CaproniConfig,
126    ) -> Result<Status, Box<dyn std::error::Error + Send + Sync>>;
127
128    /// Validates the reloader configuration and referenced files.
129    /// Returns `Ok(())` if valid, or an error describing validation
130    /// failures. Files in uncloned repository directories should not
131    /// cause validation to fail.
132    fn validate(
133        &self,
134        reloader_config: &ReloaderConfig,
135        caproni_config: &CaproniConfig,
136    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    /// `State` has no Go counterpart to port a test from: Go's `State` is a
144    /// typed `string` constant, so there's no conversion function to call.
145    /// `as_str()` only exists because Rust's `State` is a real enum; this is
146    /// new coverage for it.
147    #[test]
148    fn state_as_str_matches_go_string_values() {
149        assert_eq!(State::NotDeployed.as_str(), "not-deployed");
150        assert_eq!(State::NotReady.as_str(), "not-ready");
151        assert_eq!(State::Ready.as_str(), "ready");
152        assert_eq!(State::Editing.as_str(), "editing");
153        assert_eq!(State::Failed.as_str(), "failed");
154    }
155}