use crate::{ adapters::process::{CommandRunner, CommandSpec, KnownProgram}, domain::{ ids::{AzureResourceId, OperationId, SubscriptionId, TenantId}, inventory::{DatabaseRef, FlexibleServer, Subscription, Tenant}, }, error::{AppError, SafeError}, }; use serde::Deserialize; use std::{ffi::OsString, sync::Arc, time::Duration}; use tokio_util::sync::CancellationToken; pub struct AzureCliAdapter { runner: Arc, } impl AzureCliAdapter { pub fn new(runner: Arc) -> Self { Self { runner } } async fn call( &self, args: &[&str], op: OperationId, cancellation: CancellationToken, ) -> Result, AppError> { let mut all: Vec = args.iter().map(OsString::from).collect(); all.extend( ["--only-show-errors", "--output", "json"] .iter() .map(OsString::from), ); Ok(self .runner .run( CommandSpec { program: KnownProgram::AzureCli, args: all, timeout: Duration::from_secs(30), stdout_limit_bytes: 2 * 1024 * 1024, stderr_limit_bytes: 64 * 1024, operation_id: op, }, cancellation, ) .await? .stdout) } pub async fn current_tenant( &self, op: OperationId, cancellation: CancellationToken, ) -> Result { let raw = self.call(&["account", "show"], op, cancellation).await?; let dto: AccountDto = serde_json::from_slice(&raw).map_err(|_| AppError(SafeError::process()))?; Ok(Tenant { id: TenantId::new(dto.tenant_id).map_err(|_| AppError(SafeError::process()))?, display_name: dto.name.unwrap_or_else(|| "Current tenant".to_owned()), }) } pub async fn subscriptions( &self, tenant: &TenantId, op: OperationId, cancellation: CancellationToken, ) -> Result, AppError> { let raw = self .call(&["account", "list", "--all"], op, cancellation) .await?; let dtos: Vec = serde_json::from_slice(&raw).map_err(|_| AppError(SafeError::process()))?; Ok(dtos .into_iter() .filter(|d| d.tenant_id == tenant.as_str() && d.state.as_deref() == Some("Enabled")) .filter_map(|d| { Some(Subscription { id: SubscriptionId::new(d.id?).ok()?, display_name: d.name.unwrap_or_else(|| "Unnamed subscription".to_owned()), tenant_id: tenant.clone(), }) }) .collect()) } pub async fn servers( &self, subscription: &Subscription, op: OperationId, cancellation: CancellationToken, ) -> Result, AppError> { let raw = self .call( &[ "postgres", "flexible-server", "list", "--subscription", subscription.id.as_str(), ], op, cancellation, ) .await?; let dtos: Vec = serde_json::from_slice(&raw).map_err(|_| AppError(SafeError::process()))?; dtos.into_iter() .map(|d| { let id = AzureResourceId::new(d.id).map_err(|_| AppError(SafeError::process()))?; let group = resource_group(id.as_str()).ok_or_else(|| AppError(SafeError::process()))?; Ok(FlexibleServer { resource_id: id, subscription_id: subscription.id.clone(), resource_group: group, name: d.name, host: d.fully_qualified_domain_name, location: d.location.unwrap_or_default(), version: d.version, }) }) .collect() } pub async fn databases( &self, server: &FlexibleServer, op: OperationId, cancellation: CancellationToken, ) -> Result, AppError> { let raw = self .call( &[ "postgres", "flexible-server", "db", "list", "--resource-group", &server.resource_group, "--server-name", &server.name, "--subscription", server.subscription_id.as_str(), ], op, cancellation, ) .await?; let dtos: Vec = serde_json::from_slice(&raw).map_err(|_| AppError(SafeError::process()))?; dtos.into_iter() .map(|d| { let name = d.name; Ok(DatabaseRef { id: crate::domain::ids::DatabaseId::new(name.clone()) .map_err(|_| AppError(SafeError::process()))?, server_id: crate::domain::ids::ServerId::new(server.resource_id.as_str()) .map_err(|_| AppError(SafeError::process()))?, system: name == "postgres" || name.starts_with("azure_"), name, }) }) .collect() } } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct AccountDto { id: Option, tenant_id: String, name: Option, state: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ServerDto { id: String, name: String, fully_qualified_domain_name: Option, location: Option, version: Option, } #[derive(Deserialize)] struct DatabaseDto { name: String, } fn resource_group(id: &str) -> Option { let parts: Vec<&str> = id.split('/').collect(); parts .windows(2) .find(|w| w[0].eq_ignore_ascii_case("resourceGroups")) .map(|w| w[1].to_owned()) }