use crate::client::KibanaClient;
use crate::etl::Loader;
use crate::kibana::workflows::{WORKFLOW_CREATE_PATH, workflow_resource_path};
use crate::{Error, Result};
use serde_json::Value;
use tokio::task::JoinSet;
pub struct WorkflowsLoader {
client: KibanaClient,
}
impl WorkflowsLoader {
pub fn new(client: KibanaClient) -> Self {
Self { client }
}
fn sanitize_workflow(workflow: &Value) -> Value {
let mut sanitized = workflow.clone();
if let Value::Object(ref mut map) = sanitized {
map.remove("createdAt");
map.remove("lastUpdatedAt");
map.remove("createdBy");
map.remove("lastUpdatedBy");
map.remove("valid");
map.remove("validationErrors");
map.remove("history");
}
sanitized
}
}
impl Loader for WorkflowsLoader {
type Item = Value;
async fn load(&self, items: Vec<Self::Item>) -> Result<usize> {
let mut count = 0;
let mut set = JoinSet::new();
for workflow in items {
let client = self.client.clone();
set.spawn(async move {
let workflow_id = workflow.get("id").and_then(|v| v.as_str()).ok_or(
Error::MissingResourceId {
resource: "workflow",
},
)?;
let path = workflow_resource_path(workflow_id);
let exists = match client.head_internal(&path).await?.status().as_u16() {
200 => true,
404 => false,
status => {
return Err(Error::message(format!(
"Failed to check workflow existence ({workflow_id}): {status}"
)));
}
};
let workflow_name = workflow
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
if exists {
let sanitized = WorkflowsLoader::sanitize_workflow(&workflow);
let response = client.put_json_value_internal(&path, &sanitized).await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::api_response(status, body));
}
tracing::info!("Updated workflow: {} (id: {})", workflow_name, workflow_id);
} else {
let sanitized = WorkflowsLoader::sanitize_workflow(&workflow);
let response = client
.post_json_value_internal(WORKFLOW_CREATE_PATH, &sanitized)
.await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::api_response(status, body));
}
tracing::info!("Created workflow: {} (id: {})", workflow_name, workflow_id);
}
Ok::<(), Error>(())
});
}
while let Some(res) = set.join_next().await {
match res {
Ok(Ok(())) => count += 1,
Ok(Err(e)) => tracing::error!("Failed to load workflow: {}", e),
Err(e) => tracing::error!("Task panicked: {}", e),
}
}
Ok(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{Auth, KibanaClient};
use crate::test_support::{MockResponse, TestServer};
use serde_json::json;
use url::Url;
#[test]
fn test_loader_creation() {
let url = Url::parse("http://localhost:5601").unwrap();
let client = KibanaClient::new(url, Auth::None).unwrap();
let space_client = client.space("default").unwrap();
let _loader = WorkflowsLoader::new(space_client);
}
#[tokio::test]
async fn test_missing_id_fails() {
let url = Url::parse("http://localhost:5601").unwrap();
let client = KibanaClient::new(url, Auth::None).unwrap();
let space_client = client.space("default").unwrap();
let loader = WorkflowsLoader::new(space_client);
let workflow = json!({"name": "No ID"});
let result = loader.load(vec![workflow]).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_sanitize_workflow() {
let workflow = json!({
"id": "workflow-123",
"name": "test-workflow",
"createdAt": "2023-01-01T00:00:00Z",
"lastUpdatedAt": "2023-01-02T00:00:00Z",
"createdBy": "user",
"lastUpdatedBy": "user",
"valid": true,
"validationErrors": [],
"history": [],
"definition": {"some": "data"},
"yaml": "name: test"
});
let sanitized = WorkflowsLoader::sanitize_workflow(&workflow);
let sanitized_obj = sanitized.as_object().unwrap();
assert!(sanitized_obj.contains_key("id"));
assert!(sanitized_obj.contains_key("name"));
assert!(sanitized_obj.contains_key("yaml"));
assert!(sanitized_obj.contains_key("definition"));
assert!(!sanitized_obj.contains_key("createdAt"));
assert!(!sanitized_obj.contains_key("lastUpdatedAt"));
assert!(!sanitized_obj.contains_key("createdBy"));
assert!(!sanitized_obj.contains_key("lastUpdatedBy"));
assert!(!sanitized_obj.contains_key("valid"));
assert!(!sanitized_obj.contains_key("validationErrors"));
assert!(!sanitized_obj.contains_key("history"));
}
#[tokio::test]
async fn creates_workflow_with_documented_endpoint() {
let server = TestServer::new(vec![
MockResponse {
method: "HEAD",
path: "/api/workflows/workflow/workflow-123",
status: 404,
body: json!({}),
},
MockResponse {
method: "POST",
path: "/api/workflows/workflow",
status: 200,
body: json!({"id": "workflow-123"}),
},
]);
let loader = WorkflowsLoader::new(server.client().unwrap());
let workflow = json!({
"id": "workflow-123",
"name": "test-workflow",
"createdAt": "2023-01-01T00:00:00Z",
"yaml": "name: test"
});
let count = loader.load(vec![workflow]).await.unwrap();
assert_eq!(count, 1);
let requests = server.requests();
assert_eq!(requests[0].method, "HEAD");
assert_eq!(requests[0].path, "/api/workflows/workflow/workflow-123");
assert_eq!(requests[1].method, "POST");
assert_eq!(requests[1].path, "/api/workflows/workflow");
assert!(!requests[1].body.contains("createdAt"));
}
#[tokio::test]
async fn updates_workflow_with_documented_endpoint() {
let server = TestServer::new(vec![
MockResponse {
method: "HEAD",
path: "/api/workflows/workflow/workflow-123",
status: 200,
body: json!({}),
},
MockResponse {
method: "PUT",
path: "/api/workflows/workflow/workflow-123",
status: 200,
body: json!({"id": "workflow-123"}),
},
]);
let loader = WorkflowsLoader::new(server.client().unwrap());
let workflow = json!({
"id": "workflow-123",
"name": "test-workflow",
"yaml": "name: test"
});
let count = loader.load(vec![workflow]).await.unwrap();
assert_eq!(count, 1);
let paths = server
.requests()
.into_iter()
.map(|request| (request.method, request.path))
.collect::<Vec<_>>();
assert_eq!(
paths,
vec![
(
"HEAD".to_string(),
"/api/workflows/workflow/workflow-123".to_string()
),
(
"PUT".to_string(),
"/api/workflows/workflow/workflow-123".to_string()
)
]
);
}
}