191 lines
6.2 KiB
Rust
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())
|
|
}
|