Skip to main content

kibana_sync/kibana/workflows/
extractor.rs

1//! Workflows API extractor
2//!
3//! Extracts workflow definitions from Kibana via GET /api/workflows/workflow/<id>
4
5use crate::client::KibanaClient;
6use crate::etl::Extractor;
7use crate::kibana::workflows::workflow_resource_path;
8
9use crate::{Error, Result, ResultContext};
10use serde_json::Value;
11use tokio::task::JoinSet;
12
13/// Extractor for Kibana workflows
14///
15/// Fetches workflows by ID from the manifest. If no manifest is provided,
16/// you should use the search API to discover workflows first.
17///
18/// # Example
19/// ```no_run
20/// use kibana_sync::kibana::workflows::{WorkflowsExtractor, WorkflowsManifest, WorkflowEntry};
21/// use kibana_sync::client::{Auth, KibanaClient};
22/// use kibana_sync::etl::Extractor;
23/// use url::Url;
24///
25/// # async fn example() -> kibana_sync::Result<()> {
26/// let url = Url::parse("http://localhost:5601")?;
27/// let client = KibanaClient::new(url, Auth::None)?;
28/// let space_client = client.space("default")?;
29/// let manifest = WorkflowsManifest::with_workflows(vec![
30///     WorkflowEntry::new("workflow-123", "my-workflow"),
31///     WorkflowEntry::new("workflow-456", "alert-workflow")
32/// ]);
33///
34/// let extractor = WorkflowsExtractor::new(space_client, Some(manifest));
35/// let workflows = extractor.extract().await?;
36/// # Ok(())
37/// # }
38/// ```
39pub struct WorkflowsExtractor {
40    client: KibanaClient,
41    manifest: Option<super::WorkflowsManifest>,
42}
43
44impl WorkflowsExtractor {
45    /// Create a new workflows extractor
46    ///
47    /// # Arguments
48    /// * `client` - Space-scoped Kibana client
49    /// * `manifest` - Manifest containing workflow IDs to extract
50    pub fn new(client: KibanaClient, manifest: Option<super::WorkflowsManifest>) -> Self {
51        Self { client, manifest }
52    }
53
54    /// Search for workflows via the Workflows API
55    ///
56    /// Uses POST /api/workflows/search with optional query parameter.
57    /// This is useful for discovering workflows before adding them to the manifest.
58    ///
59    /// # Arguments
60    /// * `query` - Optional search query string to filter workflows
61    /// * `size` - Maximum number of results to return (default: 100)
62    ///
63    /// # Returns
64    /// Vector of workflow JSON objects from the search results
65    pub async fn search_workflows(
66        &self,
67        query: Option<&str>,
68        size: Option<usize>,
69    ) -> Result<Vec<Value>> {
70        let search_body = serde_json::json!({
71            "size": size.unwrap_or(100),
72            "query": query.unwrap_or("")
73        });
74
75        tracing::debug!(
76            "Searching workflows with query: {:?} in space '{}'",
77            query,
78            self.client.space_id()
79        );
80
81        let response = self
82            .client
83            .post_json_value_internal("api/workflows/search", &search_body)
84            .await
85            .context("Failed to search workflows")?;
86
87        if !response.status().is_success() {
88            let status = response.status();
89            let body = response.text().await.unwrap_or_default();
90            return Err(Error::api_response(status, body));
91        }
92
93        let search_result: Value = response
94            .json()
95            .await
96            .context("Failed to parse workflow search response")?;
97
98        // Extract workflows from results array
99        let workflows: Vec<Value> = search_result
100            .get("results")
101            .and_then(|v| v.as_array())
102            .map(|arr| arr.to_vec())
103            .unwrap_or_default();
104
105        tracing::info!("Found {} workflow(s) via search", workflows.len());
106
107        Ok(workflows)
108    }
109
110    /// Fetch specific workflows by ID from manifest
111    async fn fetch_manifest_workflows(
112        &self,
113        manifest: &super::WorkflowsManifest,
114    ) -> Result<Vec<Value>> {
115        let mut workflows = Vec::new();
116        let mut set = JoinSet::new();
117
118        for entry in &manifest.workflows {
119            let client = self.client.clone();
120            let workflow_id = entry.id.clone();
121            let workflow_name = entry.name.clone();
122
123            set.spawn(async move {
124                let path = workflow_resource_path(&workflow_id);
125                tracing::debug!(
126                    "Fetching workflow '{}' from space '{}'",
127                    workflow_id,
128                    client.space_id()
129                );
130
131                let response = client.get_internal(&path).await.with_context(|| {
132                    format!(
133                        "Failed to fetch workflow '{}' ({})",
134                        workflow_name, workflow_id
135                    )
136                })?;
137
138                if !response.status().is_success() {
139                    let status = response.status();
140                    let body = response.text().await.unwrap_or_default();
141                    return Err(Error::api_response(status, body));
142                }
143
144                let workflow: Value = response.json().await.with_context(|| {
145                    format!("Failed to parse workflow '{}' response", workflow_id)
146                })?;
147
148                tracing::debug!("Fetched workflow: {}", workflow_id);
149                Ok::<Value, Error>(workflow)
150            });
151        }
152
153        while let Some(res) = set.join_next().await {
154            match res {
155                Ok(Ok(workflow)) => workflows.push(workflow),
156                Ok(Err(e)) => tracing::warn!("{}", e),
157                Err(e) => tracing::error!("Task panicked: {}", e),
158            }
159        }
160
161        tracing::info!("Fetched {} workflow(s) from manifest", workflows.len());
162
163        Ok(workflows)
164    }
165}
166
167impl Extractor for WorkflowsExtractor {
168    type Item = Value;
169
170    async fn extract(&self) -> Result<Vec<Self::Item>> {
171        let workflows = if let Some(manifest) = &self.manifest {
172            // Fetch only workflows from manifest by ID
173            self.fetch_manifest_workflows(manifest).await?
174        } else {
175            // No manifest provided - return empty list
176            // Use search API separately to discover workflows
177            tracing::warn!("No manifest provided - use search API to discover workflows");
178            Vec::new()
179        };
180
181        tracing::info!(
182            "Extracted {} workflow(s){}",
183            workflows.len(),
184            if self.manifest.is_some() {
185                " (from manifest)"
186            } else {
187                ""
188            }
189        );
190
191        Ok(workflows)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::client::{Auth, KibanaClient};
199    use crate::test_support::{MockResponse, TestServer};
200    use serde_json::json;
201    use url::Url;
202
203    #[test]
204    fn test_extractor_creation() {
205        let url = Url::parse("http://localhost:5601").unwrap();
206        let client = KibanaClient::new(url, Auth::None).unwrap();
207        let space_client = client.space("default").unwrap();
208        let _extractor = WorkflowsExtractor::new(space_client, None);
209    }
210
211    #[tokio::test]
212    async fn fetches_manifest_workflow_with_documented_endpoint() {
213        let server = TestServer::new(vec![MockResponse {
214            method: "GET",
215            path: "/api/workflows/workflow/workflow-123",
216            status: 200,
217            body: json!({
218                "id": "workflow-123",
219                "name": "test-workflow",
220                "yaml": "name: test"
221            }),
222        }]);
223        let manifest = super::super::WorkflowsManifest::with_workflows(vec![
224            super::super::WorkflowEntry::new("workflow-123", "test-workflow"),
225        ]);
226        let extractor = WorkflowsExtractor::new(server.client().unwrap(), Some(manifest));
227
228        let workflows = extractor.extract().await.unwrap();
229
230        assert_eq!(workflows.len(), 1);
231        assert_eq!(workflows[0]["id"], "workflow-123");
232        let requests = server.requests();
233        assert_eq!(requests[0].method, "GET");
234        assert_eq!(requests[0].path, "/api/workflows/workflow/workflow-123");
235    }
236}