use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Subscription {
pub id: String,
pub name: String,
#[serde(rename = "isDefault", default)]
pub is_default: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ResourceGroup {
pub name: String,
pub location: String,
}
#[derive(Debug, Clone)]
pub struct CosmosAccount {
pub name: String,
pub endpoint: String,
}
#[derive(Debug, Clone)]
pub struct SearchService {
pub name: String,
}
#[derive(Debug, Clone)]
pub struct OpenAiAccount {
pub name: String,
pub endpoint: String,
}
#[derive(Debug, Clone)]
pub struct FoundryProject {
pub name: String,
pub endpoint: String,
}
#[derive(Debug, Clone)]
pub struct ModelDeployment {
pub name: String,
pub model_name: String,
pub kind: String,
}
pub async fn list_subscriptions() -> anyhow::Result<Vec<Subscription>> {
run_az_json(&["account", "list", "--output", "json"])
}
pub async fn list_resource_groups(subscription_id: &str) -> anyhow::Result<Vec<ResourceGroup>> {
run_az_json(&[
"group",
"list",
"--subscription",
subscription_id,
"--output",
"json",
])
}
pub async fn list_cosmos_accounts(
subscription_id: &str,
resource_group: &str,
) -> anyhow::Result<Vec<CosmosAccount>> {
#[derive(Deserialize)]
struct RawCosmos {
name: String,
#[serde(rename = "documentEndpoint")]
document_endpoint: Option<String>,
}
let list: Vec<RawCosmos> = run_az_json(&[
"cosmosdb",
"list",
"--subscription",
subscription_id,
"--resource-group",
resource_group,
"--output",
"json",
])
.unwrap_or_default();
Ok(list
.into_iter()
.map(|a| CosmosAccount {
endpoint: a
.document_endpoint
.unwrap_or_else(|| format!("https://{}.documents.azure.com:443/", a.name)),
name: a.name,
})
.collect())
}
pub async fn find_cosmos_account(
subscription_id: &str,
resource_group: &str,
) -> anyhow::Result<Option<CosmosAccount>> {
Ok(list_cosmos_accounts(subscription_id, resource_group)
.await?
.into_iter()
.next())
}
pub async fn list_search_services(
subscription_id: &str,
resource_group: &str,
) -> anyhow::Result<Vec<SearchService>> {
#[derive(Deserialize)]
struct RawSearch {
name: String,
}
let list: Vec<RawSearch> = run_az_json(&[
"search",
"service",
"list",
"--subscription",
subscription_id,
"--resource-group",
resource_group,
"--output",
"json",
])
.unwrap_or_default();
Ok(list
.into_iter()
.map(|s| SearchService { name: s.name })
.collect())
}
pub async fn find_search_service(
subscription_id: &str,
resource_group: &str,
) -> anyhow::Result<Option<SearchService>> {
Ok(list_search_services(subscription_id, resource_group)
.await?
.into_iter()
.next())
}
pub async fn list_openai_accounts(
subscription_id: &str,
resource_group: &str,
) -> anyhow::Result<Vec<OpenAiAccount>> {
#[derive(Deserialize)]
struct RawOai {
name: String,
kind: Option<String>,
properties: Option<OaiProperties>,
}
#[derive(Deserialize)]
struct OaiProperties {
endpoint: Option<String>,
}
let list: Vec<RawOai> = run_az_json(&[
"cognitiveservices",
"account",
"list",
"--subscription",
subscription_id,
"--resource-group",
resource_group,
"--output",
"json",
])
.unwrap_or_default();
Ok(list
.into_iter()
.filter(|a| a.kind.as_deref() == Some("OpenAI"))
.map(|a| {
let endpoint = a
.properties
.and_then(|p| p.endpoint)
.unwrap_or_else(|| format!("https://{}.openai.azure.com", a.name));
OpenAiAccount {
name: a.name,
endpoint,
}
})
.collect())
}
pub async fn find_openai_account(
subscription_id: &str,
resource_group: &str,
) -> anyhow::Result<Option<OpenAiAccount>> {
Ok(list_openai_accounts(subscription_id, resource_group)
.await?
.into_iter()
.next())
}
pub async fn list_foundry_projects(
subscription_id: &str,
resource_group: &str,
) -> anyhow::Result<Vec<FoundryProject>> {
#[derive(Deserialize)]
struct RawCog {
name: String,
kind: Option<String>,
properties: Option<CogProperties>,
}
#[derive(Deserialize)]
struct CogProperties {
endpoint: Option<String>,
}
let list: Vec<RawCog> = run_az_json(&[
"cognitiveservices",
"account",
"list",
"--subscription",
subscription_id,
"--resource-group",
resource_group,
"--output",
"json",
])
.unwrap_or_default();
Ok(list
.into_iter()
.filter(|a| a.kind.as_deref() == Some("AIServices"))
.map(|a| {
let endpoint = a
.properties
.and_then(|p| p.endpoint)
.unwrap_or_else(|| format!("https://{}.cognitiveservices.azure.com", a.name));
FoundryProject {
name: a.name,
endpoint,
}
})
.collect())
}
pub async fn list_model_deployments(
subscription_id: &str,
resource_group: &str,
account_name: &str,
) -> anyhow::Result<Vec<ModelDeployment>> {
#[derive(Deserialize)]
struct RawDep {
name: String,
properties: Option<DepProperties>,
}
#[derive(Deserialize)]
struct DepProperties {
model: Option<DepModel>,
}
#[derive(Deserialize)]
struct DepModel {
name: Option<String>,
format: Option<String>,
}
let list: Vec<RawDep> = run_az_json(&[
"cognitiveservices",
"account",
"deployment",
"list",
"--subscription",
subscription_id,
"--resource-group",
resource_group,
"--name",
account_name,
"--output",
"json",
])
.unwrap_or_default();
Ok(list
.into_iter()
.map(|d| {
let (model_name, kind) = d
.properties
.and_then(|p| p.model)
.map(|m| {
(
m.name.unwrap_or_default(),
m.format.unwrap_or_else(|| "OpenAI".to_string()),
)
})
.unwrap_or_else(|| (String::new(), "OpenAI".to_string()));
ModelDeployment {
name: d.name,
model_name,
kind,
}
})
.collect())
}
fn run_az_json<T: serde::de::DeserializeOwned>(args: &[&str]) -> anyhow::Result<T> {
let output = std::process::Command::new("az").args(args).output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("az command failed: {stderr}");
}
let parsed: T = serde_json::from_slice(&output.stdout)?;
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "requires az CLI on PATH with a logged-in account"]
async fn list_subscriptions_returns_at_least_one() {
let subs = list_subscriptions().await.unwrap();
assert!(!subs.is_empty(), "expected at least one subscription");
for s in &subs {
println!(" {} — {}", s.name, s.id);
}
}
}