Skip to main content

cfgd_csi/
errors.rs

1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum CsiError {
5    #[error("OCI pull failed: {0}")]
6    PullFailed(Box<cfgd_core::errors::OciError>),
7
8    #[error("invalid volume attribute: {key}")]
9    InvalidAttribute { key: String },
10
11    #[error("IO error: {0}")]
12    Io(#[from] std::io::Error),
13
14    /// Metrics HTTP server setup or serve failure. Emitted by
15    /// `metrics::serve_metrics` on bind / serve errors. In current production
16    /// flow this surfaces only through tests because `serve_metrics` is
17    /// invoked via fire-and-forget `tokio::spawn` from `app::run`, with errors
18    /// logged via `tracing`. The typed variant preserves a clean error
19    /// contract should propagation tighten later.
20    #[error("metrics server error: {0}")]
21    Metrics(String),
22}
23
24impl From<cfgd_core::errors::OciError> for CsiError {
25    fn from(e: cfgd_core::errors::OciError) -> Self {
26        CsiError::PullFailed(Box::new(e))
27    }
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use cfgd_core::errors::OciError;
34
35    #[test]
36    fn pullfailed_display_includes_inner_oci_error_message() {
37        let oci_err = OciError::ManifestNotFound {
38            reference: "ghcr.io/example/mod:v1".into(),
39        };
40        let e = CsiError::PullFailed(Box::new(oci_err));
41        assert_eq!(
42            format!("{e}"),
43            "OCI pull failed: manifest not found: ghcr.io/example/mod:v1"
44        );
45    }
46
47    #[test]
48    fn from_oci_error_wraps_in_pullfailed_preserving_inner() {
49        let oci_err = OciError::ManifestNotFound {
50            reference: "ghcr.io/example/mod:v1".into(),
51        };
52        let csi: CsiError = oci_err.into();
53        match csi {
54            CsiError::PullFailed(boxed) => {
55                assert_eq!(
56                    format!("{boxed}"),
57                    "manifest not found: ghcr.io/example/mod:v1"
58                );
59            }
60            other => panic!("expected PullFailed, got {other:?}"),
61        }
62    }
63
64    #[test]
65    fn invalid_attribute_display_uses_exact_key() {
66        let e = CsiError::InvalidAttribute {
67            key: "csi.cfgd.io/oci-uri".into(),
68        };
69        assert_eq!(
70            format!("{e}"),
71            "invalid volume attribute: csi.cfgd.io/oci-uri"
72        );
73    }
74
75    #[test]
76    fn from_io_error_wraps_in_io_variant_preserving_kind_and_message() {
77        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such device");
78        let csi: CsiError = io_err.into();
79        match csi {
80            CsiError::Io(io) => {
81                assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
82                assert_eq!(format!("{io}"), "no such device");
83            }
84            other => panic!("expected Io, got {other:?}"),
85        }
86    }
87
88    #[test]
89    fn io_display_uses_io_error_prefix() {
90        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such device");
91        let csi: CsiError = io_err.into();
92        assert_eq!(format!("{csi}"), "IO error: no such device");
93    }
94}