use crate::config::{AiProvider, Config, DeploymentRole, DeploymentTarget};
use super::discover;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Status {
Found,
Missing,
Unknown,
}
#[derive(Debug, Clone)]
pub struct Check {
pub label: String,
pub status: Status,
pub hint: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Report {
pub checks: Vec<Check>,
}
impl Report {
pub fn has_missing(&self) -> bool {
self.checks.iter().any(|c| c.status == Status::Missing)
}
pub fn print(&self) {
println!("\nPrerequisite check:");
for c in &self.checks {
let glyph = match c.status {
Status::Found => " ✓",
Status::Missing => " ✗",
Status::Unknown => " ?",
};
println!("{glyph} {}", c.label);
if c.status == Status::Missing
&& let Some(hint) = &c.hint
{
for line in hint.lines() {
println!(" {line}");
}
}
}
let missing = self
.checks
.iter()
.filter(|c| c.status == Status::Missing)
.count();
let unknown = self
.checks
.iter()
.filter(|c| c.status == Status::Unknown)
.count();
if missing > 0 || unknown > 0 {
println!(
"\n{missing} prerequisite(s) missing, {unknown} could not be checked. \
See https://github.com/mklab-se/quelch/blob/main/docs/getting-started.md \
for the full prerequisites list."
);
} else {
println!("\nAll prerequisites present.");
}
}
}
pub async fn check_all(config: &Config) -> Report {
let mut checks = Vec::new();
let sub = &config.azure.subscription_id;
checks.push(
check_cosmos(
sub,
config.cosmos_resource_group(),
config.cosmos.account.as_deref(),
)
.await,
);
checks.push(
check_search(
sub,
config.search_resource_group(),
config.search.service.as_deref(),
)
.await,
);
checks.push(check_ai_provider(sub, config.ai_resource_group(), config).await);
let has_azure_deployment = config
.deployments
.iter()
.any(|d| matches!(d.target, DeploymentTarget::Azure));
if has_azure_deployment {
checks
.push(check_container_apps_env(sub, config.container_apps_env_resource_group()).await);
checks.push(
check_application_insights(sub, config.application_insights_resource_group()).await,
);
checks.push(check_key_vault(sub, config.key_vault_resource_group()).await);
}
if config.deployments.iter().any(|d| {
matches!(d.role, DeploymentRole::Mcp) && matches!(d.target, DeploymentTarget::Azure)
}) {
checks.push(Check {
label: "Container App for MCP — will be created by `quelch azure deploy`".to_string(),
status: Status::Found,
hint: None,
});
}
Report { checks }
}
async fn check_cosmos(sub: &str, rg: &str, expected_name: Option<&str>) -> Check {
let label = match expected_name {
Some(n) => format!("Cosmos DB account '{n}' in '{rg}'"),
None => format!("Cosmos DB account in '{rg}'"),
};
let hint = Some(format!(
"az cosmosdb create -n <name> -g {rg} \
--kind GlobalDocumentDB --capabilities EnableServerless"
));
let Ok(list) = discover::list_cosmos_accounts(sub, rg).await else {
return Check {
label,
status: Status::Unknown,
hint,
};
};
let found = match expected_name {
Some(n) => list.iter().any(|a| a.name == n),
None => !list.is_empty(),
};
Check {
label,
status: if found {
Status::Found
} else {
Status::Missing
},
hint,
}
}
async fn check_search(sub: &str, rg: &str, expected_name: Option<&str>) -> Check {
let label = match expected_name {
Some(n) => format!("Azure AI Search service '{n}' in '{rg}'"),
None => format!("Azure AI Search service in '{rg}'"),
};
let hint = Some(format!(
"az search service create -n <name> -g {rg} --sku basic"
));
let Ok(list) = discover::list_search_services(sub, rg).await else {
return Check {
label,
status: Status::Unknown,
hint,
};
};
let found = match expected_name {
Some(n) => list.iter().any(|s| s.name == n),
None => !list.is_empty(),
};
Check {
label,
status: if found {
Status::Found
} else {
Status::Missing
},
hint,
}
}
async fn check_ai_provider(sub: &str, rg: &str, config: &Config) -> Check {
let endpoint = &config.ai.endpoint;
match config.ai.provider {
AiProvider::AzureOpenai => {
let label = format!("Azure OpenAI account at {endpoint}");
let hint = Some(format!(
"Create one in the portal or via:\n\
az cognitiveservices account create -n <name> -g {rg} \\\n \
--kind OpenAI --sku S0 -l <region>\n\
Then deploy the embedding and chat models you need."
));
let Ok(list) = discover::list_openai_accounts(sub, rg).await else {
return Check {
label,
status: Status::Unknown,
hint,
};
};
let found = list.iter().any(|a| a.endpoint == *endpoint);
Check {
label,
status: if found {
Status::Found
} else {
Status::Missing
},
hint,
}
}
AiProvider::Foundry => {
let label = format!("Microsoft Foundry project at {endpoint}");
let hint = Some(format!(
"Create a Foundry project in the portal (https://ai.azure.com)\n\
inside resource group '{rg}', then deploy the embedding and\n\
chat models you need."
));
let Ok(list) = discover::list_foundry_projects(sub, rg).await else {
return Check {
label,
status: Status::Unknown,
hint,
};
};
let found = list.iter().any(|p| p.endpoint == *endpoint);
Check {
label,
status: if found {
Status::Found
} else {
Status::Missing
},
hint,
}
}
}
}
async fn check_container_apps_env(sub: &str, rg: &str) -> Check {
let label = format!("Container Apps environment in '{rg}'");
let hint = Some(format!(
"az containerapp env create -n <name> -g {rg} -l <region>"
));
let Ok(list) = discover::list_container_apps_environments(sub, rg).await else {
return Check {
label,
status: Status::Unknown,
hint,
};
};
Check {
label,
status: if list.is_empty() {
Status::Missing
} else {
Status::Found
},
hint,
}
}
async fn check_application_insights(sub: &str, rg: &str) -> Check {
let label = format!("Application Insights component in '{rg}'");
let hint = Some(format!(
"az monitor app-insights component create --app <name> -g {rg} -l <region>"
));
let Ok(list) = discover::list_application_insights(sub, rg).await else {
return Check {
label,
status: Status::Unknown,
hint,
};
};
Check {
label,
status: if list.is_empty() {
Status::Missing
} else {
Status::Found
},
hint,
}
}
async fn check_key_vault(sub: &str, rg: &str) -> Check {
let label = format!("Key Vault in '{rg}'");
let hint = Some(format!(
"az keyvault create -n <globally-unique-name> -g {rg} -l <region>"
));
let Ok(list) = discover::list_key_vaults(sub, rg).await else {
return Check {
label,
status: Status::Unknown,
hint,
};
};
Check {
label,
status: if list.is_empty() {
Status::Missing
} else {
Status::Found
},
hint,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_has_missing_returns_true_when_any_check_missing() {
let r = Report {
checks: vec![
Check {
label: "a".into(),
status: Status::Found,
hint: None,
},
Check {
label: "b".into(),
status: Status::Missing,
hint: None,
},
],
};
assert!(r.has_missing());
}
#[test]
fn report_has_missing_returns_false_when_all_found_or_unknown() {
let r = Report {
checks: vec![
Check {
label: "a".into(),
status: Status::Found,
hint: None,
},
Check {
label: "b".into(),
status: Status::Unknown,
hint: None,
},
],
};
assert!(!r.has_missing());
}
}