#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
use std::env;
use std::process::ExitCode;
use reallyme_foundationdb_kit::FoundationDbTenantName;
use reallyme_foundationdb_kit::{FdbConfig, FdbContext, fdb::tenant::admin};
use thiserror::Error;
const USAGE: &str = "usage: fdb-tenant-admin <ensure|repair|delete|exists|id> <tenant> | recover-delete <tenant> <expected-tenant-id>";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
enum AdminToolError {
#[error("{USAGE}")]
Usage,
#[error("tenant name must use lowercase ASCII letters, digits, and interior hyphens")]
InvalidTenant,
#[error("expected tenant ID must be a nonnegative integer")]
InvalidTenantId,
#[error("invalid FoundationDB configuration")]
InvalidConfig,
#[error("failed to connect to FoundationDB")]
ConnectFailed,
#[error("tenant ensure operation failed")]
EnsureFailed,
#[error("tenant metadata repair operation failed")]
RepairFailed,
#[error("tenant interrupted-delete recovery operation failed")]
RecoverDeleteFailed,
#[error("tenant delete operation failed")]
DeleteFailed,
#[error("tenant exists operation failed")]
ExistsFailed,
#[error("tenant ID lookup failed")]
IdLookupFailed,
#[error("tenant does not exist")]
TenantMissing,
}
async fn run() -> Result<(), AdminToolError> {
let mut args = env::args().skip(1);
let command = args.next().ok_or(AdminToolError::Usage)?;
let tenant = args.next().ok_or(AdminToolError::Usage)?;
let expected_tenant_id = if command == "recover-delete" {
Some(parse_expected_tenant_id(
args.next().ok_or(AdminToolError::Usage)?,
)?)
} else {
None
};
if args.next().is_some() {
return Err(AdminToolError::Usage);
}
let tenant = parse_tenant(tenant)?;
let config = FdbConfig::from_env().map_err(|_| AdminToolError::InvalidConfig)?;
#[allow(unsafe_code)]
let context =
unsafe { FdbContext::connect(&config) }.map_err(|_| AdminToolError::ConnectFailed)?;
match command.as_str() {
"ensure" => admin::ensure_tenant(&context, tenant)
.await
.map_err(|_| AdminToolError::EnsureFailed),
"repair" => admin::repair_tenant_metadata(&context, tenant)
.await
.map_err(|_| AdminToolError::RepairFailed),
"recover-delete" => admin::recover_interrupted_delete(
&context,
tenant,
expected_tenant_id.ok_or(AdminToolError::Usage)?,
)
.await
.map_err(|_| AdminToolError::RecoverDeleteFailed),
"delete" => admin::delete_tenant(&context, tenant)
.await
.map_err(|_| AdminToolError::DeleteFailed),
"exists" => {
let exists = admin::tenant_exists(&context, tenant)
.await
.map_err(|_| AdminToolError::ExistsFailed)?;
if exists {
println!("true");
} else {
println!("false");
}
Ok(())
}
"id" => {
let id = admin::tenant_id(&context, tenant)
.await
.map_err(|_| AdminToolError::IdLookupFailed)?
.ok_or(AdminToolError::TenantMissing)?;
println!("{id}");
Ok(())
}
_ => Err(AdminToolError::Usage),
}
}
fn parse_tenant(value: String) -> Result<FoundationDbTenantName, AdminToolError> {
FoundationDbTenantName::new(value.as_str()).map_err(|_| AdminToolError::InvalidTenant)
}
fn parse_expected_tenant_id(value: String) -> Result<i64, AdminToolError> {
value
.parse::<i64>()
.ok()
.filter(|id| *id >= 0)
.ok_or(AdminToolError::InvalidTenantId)
}
#[tokio::main]
async fn main() -> ExitCode {
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("{message}");
ExitCode::FAILURE
}
}
}
#[cfg(test)]
#[path = "fdb-tenant-admin_tests.rs"]
mod tests;