use crate::{
client::Client,
error::Result,
query_pack::{QueryPack, WorkspaceScope},
query_job::{QueryJobBuilder, QueryJobResult},
workspace::Workspace,
};
use crate::cli::args::OutputFormat;
use std::path::Path;
pub async fn execute(
pack_path: String,
workspaces_override: Option<String>,
format: OutputFormat,
json_output: bool,
validate_only: bool,
) -> Result<()> {
let pack = load_pack(&pack_path)?;
pack.validate()?;
if validate_only {
eprintln!("✓ Query pack is valid");
eprintln!(" Name: {}", pack.name);
eprintln!(" Queries: {}", pack.get_queries().len());
return Ok(());
}
let client = Client::new()?;
eprintln!("Authenticating with Azure...");
client.force_validate_auth().await?;
eprintln!("Loading workspaces...");
let all_workspaces = client.list_workspaces().await?;
let selected_workspaces = select_workspaces(
&all_workspaces,
workspaces_override,
pack.workspaces.as_ref(),
)?;
if selected_workspaces.is_empty() {
return Err(crate::error::KqlPanopticonError::QueryPackValidation(
"No workspaces selected for execution".into()
));
}
eprintln!(
"Executing {} quer{} across {} workspace{}...",
pack.get_queries().len(),
if pack.get_queries().len() == 1 { "y" } else { "ies" },
selected_workspaces.len(),
if selected_workspaces.len() == 1 { "" } else { "s" }
);
let base_settings = pack.settings.clone().unwrap_or_default();
let mut all_results = Vec::new();
for pack_query in pack.get_queries() {
eprintln!("\nExecuting: {}", pack_query.name);
let mut settings = base_settings.clone();
settings.job_name = sanitize_name(&pack_query.name);
let results = QueryJobBuilder::new()
.workspaces(selected_workspaces.clone())
.queries(vec![pack_query.query.clone()])
.settings(settings)
.execute(&client)
.await?;
all_results.extend(results);
}
let session_name = format!(
"{}-{}",
sanitize_name(&pack.name),
chrono::Utc::now().format("%Y-%m-%d_%H%M%S")
);
let effective_format = if json_output {
OutputFormat::Stdout
} else {
format
};
match effective_format {
OutputFormat::Files => {
output_to_files(&all_results, &pack)?;
print_summary(&all_results);
eprintln!("\nSession: {}", session_name);
}
OutputFormat::Stdout => {
output_to_stdout(&all_results)?;
}
}
Ok(())
}
fn load_pack(path_str: &str) -> Result<QueryPack> {
let path = Path::new(path_str);
if path.is_absolute() {
return QueryPack::load_from_file(path);
}
if path.exists() {
return QueryPack::load_from_file(path);
}
let library_path = QueryPack::get_library_path(path_str)?;
if library_path.exists() {
return QueryPack::load_from_file(&library_path);
}
Err(crate::error::KqlPanopticonError::QueryPackNotFound(
path_str.to_string()
))
}
fn select_workspaces(
all_workspaces: &[Workspace],
cli_override: Option<String>,
pack_scope: Option<&WorkspaceScope>,
) -> Result<Vec<Workspace>> {
if let Some(override_spec) = cli_override {
return parse_workspace_spec(&override_spec, all_workspaces);
}
if let Some(scope) = pack_scope {
return match scope {
WorkspaceScope::All => Ok(all_workspaces.to_vec()),
WorkspaceScope::Selected { ids } => {
Ok(all_workspaces
.iter()
.filter(|ws| ids.contains(&ws.workspace_id) || ids.contains(&ws.resource_id))
.cloned()
.collect())
}
WorkspaceScope::Pattern { pattern } => {
filter_workspaces_by_pattern(all_workspaces, pattern)
}
};
}
Ok(all_workspaces.to_vec())
}
fn parse_workspace_spec(spec: &str, all_workspaces: &[Workspace]) -> Result<Vec<Workspace>> {
if spec == "all" {
return Ok(all_workspaces.to_vec());
}
let ids: Vec<&str> = spec.split(',').map(|s| s.trim()).collect();
Ok(all_workspaces
.iter()
.filter(|ws| ids.iter().any(|id| ws.workspace_id.contains(id) || ws.name.contains(id)))
.cloned()
.collect())
}
fn filter_workspaces_by_pattern(workspaces: &[Workspace], pattern: &str) -> Result<Vec<Workspace>> {
let pattern = pattern.replace('*', ".*");
let regex = regex::Regex::new(&pattern)
.map_err(|e| crate::error::KqlPanopticonError::QueryPackValidation(
format!("Invalid workspace pattern: {}", e)
))?;
Ok(workspaces
.iter()
.filter(|ws| regex.is_match(&ws.name))
.cloned()
.collect())
}
fn sanitize_name(name: &str) -> String {
name.chars()
.map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '-' })
.collect::<String>()
.to_lowercase()
}
fn output_to_files(results: &[QueryJobResult], _pack: &QueryPack) -> Result<()> {
let success = results.iter().filter(|r| r.result.is_ok()).count();
if success > 0 {
eprintln!("\n✓ Results written to output directory");
}
Ok(())
}
fn output_to_stdout(results: &[QueryJobResult]) -> Result<()> {
let output: Vec<_> = results
.iter()
.map(|result| {
serde_json::json!({
"workspace": result.workspace_name,
"workspace_id": result.workspace_id,
"success": result.result.is_ok(),
"elapsed_ms": result.elapsed.as_millis(),
"data": result.result.as_ref().ok(),
"error": result.result.as_ref().err().map(|e| e.to_string()),
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
fn print_summary(results: &[QueryJobResult]) {
let total = results.len();
let success = results.iter().filter(|r| r.result.is_ok()).count();
let failed = total - success;
eprintln!("\n--- Summary ---");
eprintln!("Total executions: {}", total);
eprintln!("Succeeded: {}", success);
eprintln!("Failed: {}", failed);
if failed > 0 {
eprintln!("\nFailed executions:");
for result in results {
if let Err(e) = &result.result {
eprintln!(" - {}: {}", result.workspace_name, e);
}
}
}
}