Skip to main content

kibana_sync/kibana/saved_objects/
extractor.rs

1//! Saved Objects API extractor
2//!
3//! Extracts saved objects from Kibana via POST /api/saved_objects/_export
4
5use crate::client::KibanaClient;
6use crate::etl::Extractor;
7
8use crate::{Error, Result, ResultContext};
9use serde_json::Value;
10
11/// Extractor for Kibana saved objects
12///
13/// Exports saved objects from Kibana using the manifest as the export specification.
14/// The manifest defines which objects to export and is sent as the POST body.
15///
16/// # Example
17/// ```no_run
18/// use kibana_sync::kibana::saved_objects::{SavedObjectsExtractor, SavedObjectsManifest, SavedObject};
19/// use kibana_sync::client::{Auth, KibanaClient};
20/// use kibana_sync::etl::Extractor;
21/// use url::Url;
22///
23/// # async fn example() -> kibana_sync::Result<()> {
24/// let url = Url::parse("http://localhost:5601")?;
25/// let client = KibanaClient::new(url, Auth::None)?;
26/// let space_client = client.space("default")?;
27/// let mut manifest = SavedObjectsManifest::new();
28/// manifest.add_object(SavedObject::new("dashboard", "my-dashboard-id"));
29///
30/// let extractor = SavedObjectsExtractor::new(space_client, manifest);
31/// let objects = extractor.extract().await?;
32/// # Ok(())
33/// # }
34/// ```
35pub struct SavedObjectsExtractor {
36    client: KibanaClient,
37    manifest: super::SavedObjectsManifest,
38}
39
40impl SavedObjectsExtractor {
41    /// Create a new saved objects extractor
42    ///
43    /// # Arguments
44    /// * `client` - Space-scoped Kibana client
45    /// * `manifest` - Manifest defining which objects to export
46    pub fn new(client: KibanaClient, manifest: super::SavedObjectsManifest) -> Self {
47        Self { client, manifest }
48    }
49
50    /// Export saved objects from Kibana
51    ///
52    /// POSTs the manifest to /api/saved_objects/_export and receives
53    /// NDJSON response containing the exported objects.
54    async fn export_objects(&self) -> Result<Vec<Value>> {
55        let path = "api/saved_objects/_export";
56
57        tracing::debug!(
58            "Exporting {} object(s) from space '{}'",
59            self.manifest.count(),
60            self.client.space_id()
61        );
62
63        let response = self
64            .client
65            .post_json_value(path, &serde_json::to_value(&self.manifest)?)
66            .await
67            .with_context(|| "Failed to export saved objects from Kibana")?;
68
69        if !response.status().is_success() {
70            let status = response.status();
71            let body = response.text().await.unwrap_or_default();
72            return Err(Error::api_response(status, body));
73        }
74
75        // Kibana returns NDJSON format
76        let body = response
77            .text()
78            .await
79            .with_context(|| "Failed to read export response")?;
80
81        let objects: Vec<Value> = body
82            .lines()
83            .filter(|line| !line.trim().is_empty())
84            .map(serde_json::from_str)
85            .collect::<std::result::Result<Vec<_>, serde_json::Error>>()
86            .with_context(|| "Failed to parse NDJSON response")?;
87
88        tracing::info!("Exported {} object(s) from Kibana", objects.len());
89
90        Ok(objects)
91    }
92}
93
94impl Extractor for SavedObjectsExtractor {
95    type Item = Value;
96
97    async fn extract(&self) -> Result<Vec<Self::Item>> {
98        let objects = self.export_objects().await?;
99
100        tracing::info!("Extracted {} saved object(s)", objects.len());
101
102        Ok(objects)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::client::{Auth, KibanaClient};
110    use crate::kibana::saved_objects::SavedObjectsManifest;
111    use crate::kibana::saved_objects::manifest::SavedObject;
112    use url::Url;
113
114    #[test]
115    fn test_extractor_creation() {
116        let url = Url::parse("http://localhost:5601").unwrap();
117        let client = KibanaClient::new(url, Auth::None).unwrap();
118        let space_client = client.space("default").unwrap();
119        let manifest = SavedObjectsManifest::new();
120        let extractor = SavedObjectsExtractor::new(space_client, manifest);
121        assert_eq!(extractor.client.space_id(), "default");
122    }
123
124    #[test]
125    fn test_extractor_with_manifest() {
126        let url = Url::parse("http://localhost:5601").unwrap();
127        let client = KibanaClient::builder(url)
128            .spaces([
129                ("default".to_string(), "Default".to_string()),
130                ("marketing".to_string(), "Marketing".to_string()),
131            ])
132            .build()
133            .unwrap();
134        let space_client = client.space("marketing").unwrap();
135
136        let mut so_manifest = SavedObjectsManifest::new();
137        so_manifest.add_object(SavedObject::new("dashboard", "test-dashboard"));
138        so_manifest.add_object(SavedObject::new("visualization", "test-viz"));
139
140        let extractor = SavedObjectsExtractor::new(space_client, so_manifest.clone());
141        assert_eq!(extractor.client.space_id(), "marketing");
142        assert_eq!(extractor.manifest.count(), 2);
143    }
144}