Files
azure-database-tui/src/adapters/azure_cli.rs
T
Louis Frei 8dda5dcf23
validate / macos-arm64 (push) Canceled after 0s
validate / windows-x86_64 (push) Canceled after 0s
feat: current state
2026-08-13 20:18:30 +02:00

191 lines
6.2 KiB
Rust

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<dyn CommandRunner>,
}
impl AzureCliAdapter {
pub fn new(runner: Arc<dyn CommandRunner>) -> Self {
Self { runner }
}
async fn call(
&self,
args: &[&str],
op: OperationId,
cancellation: CancellationToken,
) -> Result<Vec<u8>, AppError> {
let mut all: Vec<OsString> = 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<Tenant, AppError> {
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<Vec<Subscription>, AppError> {
let raw = self
.call(&["account", "list", "--all"], op, cancellation)
.await?;
let dtos: Vec<AccountDto> =
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<Vec<FlexibleServer>, AppError> {
let raw = self
.call(
&[
"postgres",
"flexible-server",
"list",
"--subscription",
subscription.id.as_str(),
],
op,
cancellation,
)
.await?;
let dtos: Vec<ServerDto> =
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<Vec<DatabaseRef>, 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<DatabaseDto> =
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<String>,
tenant_id: String,
name: Option<String>,
state: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServerDto {
id: String,
name: String,
fully_qualified_domain_name: Option<String>,
location: Option<String>,
version: Option<String>,
}
#[derive(Deserialize)]
struct DatabaseDto {
name: String,
}
fn resource_group(id: &str) -> Option<String> {
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())
}