kibana_sync/kibana/workflows/
extractor.rs1use 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
13pub struct WorkflowsExtractor {
40 client: KibanaClient,
41 manifest: Option<super::WorkflowsManifest>,
42}
43
44impl WorkflowsExtractor {
45 pub fn new(client: KibanaClient, manifest: Option<super::WorkflowsManifest>) -> Self {
51 Self { client, manifest }
52 }
53
54 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 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 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 self.fetch_manifest_workflows(manifest).await?
174 } else {
175 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}