use std::fmt;
use rust_i18n::t;
use tabled::Tabled;
use timeweb_rs::{
apis::{configuration::Configuration, databases_api},
models as db_models
};
use crate::{error::TwcError, output::OutputFormat};
fn fmt_id<T: std::fmt::Display>(v: T) -> String {
v.to_string()
}
fn opt_display(v: Option<&str>, default: &str) -> String {
v.map_or_else(|| default.to_string(), ToString::to_string)
}
#[derive(Tabled)]
struct DbRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Status")]
status: String,
#[tabled(rename = "Engine")]
engine: String,
#[tabled(rename = "Location")]
location: String
}
impl fmt::Display for DbRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {}",
self.id, self.name, self.status, self.engine, self.location
)
}
}
#[derive(Tabled)]
struct BackupRow {
#[tabled(rename = "ID")]
id: i32,
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Status")]
status: String,
#[tabled(rename = "Size (MB)")]
size_mb: i32,
#[tabled(rename = "Type")]
backup_type: String,
#[tabled(rename = "Created")]
created_at: String
}
impl fmt::Display for BackupRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {} {}",
self.id, self.name, self.status, self.size_mb, self.backup_type, self.created_at
)
}
}
#[derive(Tabled)]
struct UserRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Login")]
login: String,
#[tabled(rename = "Description")]
desc: String,
#[tabled(rename = "Created")]
created: String,
#[tabled(rename = "Host")]
host: String
}
impl fmt::Display for UserRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {}",
self.id, self.login, self.desc, self.created, self.host
)
}
}
#[derive(Tabled)]
struct PresetRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Type")]
engine: String,
#[tabled(rename = "CPU")]
cpu: String,
#[tabled(rename = "RAM (MB)")]
ram: String,
#[tabled(rename = "Disk (GB)")]
disk: String,
#[tabled(rename = "Price")]
price: String,
#[tabled(rename = "Location")]
location: String,
#[tabled(rename = "Description")]
description: String
}
impl fmt::Display for PresetRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {} {} {} {}",
self.id,
self.engine,
self.cpu,
self.ram,
self.disk,
self.price,
self.location,
self.description
)
}
}
#[derive(Tabled)]
struct TypeRow {
#[tabled(rename = "Type")]
engine: String,
#[tabled(rename = "Version")]
version: String,
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Replication")]
replication: String,
#[tabled(rename = "Deprecated")]
deprecated: String
}
impl fmt::Display for TypeRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {} {}",
self.engine, self.version, self.name, self.replication, self.deprecated
)
}
}
#[derive(Tabled)]
struct InstanceRow {
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Description")]
description: String,
#[tabled(rename = "Created")]
created_at: String
}
impl fmt::Display for InstanceRow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {} {}",
self.id, self.name, self.description, self.created_at
)
}
}
pub async fn list(
config: &Configuration,
limit: Option<i32>,
offset: Option<i32>,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = databases_api::get_database_clusters(config, limit, offset).await?;
let rows: Vec<DbRow> = resp
.dbs
.iter()
.map(|d| DbRow {
id: fmt_id(d.id),
name: d.name.clone(),
status: format!("{:?}", d.status),
engine: d.r#type.clone(),
location: d.location.clone().unwrap_or_else(|| "-".to_string())
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_databases_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.dbs)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for d in &resp.dbs {
println!("{}\t{}", fmt_id(d.id), d.name);
}
}
}
Ok(())
}
pub async fn info(config: &Configuration, id: i32, format: OutputFormat) -> Result<(), TwcError> {
let resp = databases_api::get_database_cluster(config, id).await?;
let db = &resp.db;
match format {
OutputFormat::Table => {
let disk_size = db
.disk
.as_ref()
.and_then(|o| o.as_ref())
.map_or(0.0, |disk| disk.size);
println!("ID: {}", fmt_id(db.id));
println!("Name: {}", db.name);
println!("Status: {:?}", db.status);
println!("Engine: {}", String::new());
println!(
"Port: {}",
db.port.map_or_else(|| "-".to_string(), |p| p.to_string())
);
println!("Location: {:?}", db.location);
println!("Preset ID: {}", db.preset_id);
println!("Created at: {}", db.created_at);
println!("Disk (GB): {disk_size}");
println!(
"Public network: {}",
if db.is_enabled_public_network {
"yes"
} else {
"no"
}
);
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.db)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}\t{:?}", fmt_id(db.id), db.name, db.status);
}
}
Ok(())
}
pub async fn delete(config: &Configuration, id: i32) -> Result<(), TwcError> {
databases_api::delete_database_cluster(config, id, None, None).await?;
println!("{}", t!("cli.database_deleted", id => id));
Ok(())
}
pub async fn update(
config: &Configuration,
id: i32,
name: Option<&str>,
format: OutputFormat
) -> Result<(), TwcError> {
let mut update = db_models::UpdateCluster::default();
if let Some(n) = name {
update.name = Some(n.to_string());
}
let resp = databases_api::update_database_cluster(config, id, update).await?;
let db = &resp.db;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.database_updated", name => db.name, id => fmt_id(db.id))
);
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.db)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}", fmt_id(db.id), db.name);
}
}
Ok(())
}
pub async fn backup_list(
config: &Configuration,
id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = databases_api::get_database_backups(config, id, None, None).await?;
let rows: Vec<BackupRow> = resp
.backups
.iter()
.map(|b| BackupRow {
id: b.id,
name: b.name.clone(),
status: format!("{:?}", b.status),
size_mb: b.size,
backup_type: format!("{:?}", b.r#type),
created_at: b.created_at.to_string()
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_backups_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.backups)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for b in &resp.backups {
println!("{}\t{}", b.id, b.name);
}
}
}
Ok(())
}
pub async fn backup_create(config: &Configuration, id: i32) -> Result<(), TwcError> {
let _resp = databases_api::create_database_backup(config, id, None).await?;
println!("{}", t!("cli.backup_created", id => id));
Ok(())
}
pub async fn user_list(
config: &Configuration,
id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = databases_api::get_database_users(config, id).await?;
let rows: Vec<UserRow> = resp
.admins
.iter()
.map(|u| UserRow {
id: fmt_id(u.id),
login: u.login.clone(),
desc: u.description.clone(),
created: u.created_at.clone(),
host: opt_display(u.host.as_deref(), "-")
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_users_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.admins)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for u in &resp.admins {
println!("{}\t{}", fmt_id(u.id), u.login);
}
}
}
Ok(())
}
pub async fn user_create(
config: &Configuration,
db_id: i32,
login: &str,
password: &str,
format: OutputFormat
) -> Result<(), TwcError> {
let req = db_models::CreateAdmin::new(
login.to_string(),
password.to_string(),
vec![db_models::create_admin::Privileges::Select]
);
let resp = databases_api::create_database_user(config, db_id, req).await?;
let admin = &resp.admin;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.db_user_created", login => admin.login, db_id => db_id, id => fmt_id(admin.id))
);
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.admin)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}", fmt_id(admin.id), admin.login);
}
}
Ok(())
}
pub async fn user_delete(
config: &Configuration,
db_id: i32,
user_name: &str
) -> Result<(), TwcError> {
let users = databases_api::get_database_users(config, db_id).await?;
let target = users.admins.iter().find(|u| u.login == user_name);
let Some(admin) = target else {
return Err(TwcError::Api(format!(
"user '{user_name}' not found in database {db_id}"
)));
};
#[allow(clippy::cast_possible_truncation)]
let admin_id = admin.id as i32;
databases_api::delete_database_user(config, db_id, admin_id).await?;
println!(
"{}",
t!("cli.db_user_deleted", login => user_name, db_id => db_id)
);
Ok(())
}
pub async fn preset_list(config: &Configuration, format: OutputFormat) -> Result<(), TwcError> {
let resp = databases_api::get_databases_presets(config, None).await?;
let rows: Vec<PresetRow> = resp
.databases_presets
.iter()
.map(|p| PresetRow {
id: p.id.map_or_else(|| "-".to_string(), fmt_id),
engine: p.r#type.clone().unwrap_or_else(|| "-".to_string()),
cpu: p.cpu.map_or_else(|| "-".to_string(), |c| format!("{c}")),
ram: p.ram.map_or_else(|| "-".to_string(), |r| format!("{r}")),
disk: p.disk.map_or_else(|| "-".to_string(), |d| format!("{d}")),
price: p
.price
.map_or_else(|| "-".to_string(), |pr| format!("{pr}")),
location: p.location.clone().unwrap_or_else(|| "-".to_string()),
description: p
.description_short
.as_deref()
.map_or_else(|| "-".to_string(), ToString::to_string)
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_presets_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.databases_presets)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for p in &resp.databases_presets {
println!(
"{}\t{}\t{}",
p.id.map_or_else(|| "-".to_string(), fmt_id),
p.r#type.clone().unwrap_or_else(|| "-".to_string()),
p.description_short
.as_deref()
.map_or_else(|| "-".to_string(), ToString::to_string)
);
}
}
}
Ok(())
}
pub async fn create(
config: &Configuration,
name: &str,
db_type: &str,
preset_id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let password = format!("twc-{}", chrono::Utc::now().timestamp_micros());
let type_val = parse_db_type(db_type)?;
let mut req = db_models::CreateCluster::new(name.to_string(), type_val);
req.preset_id = Some(preset_id);
let resp = databases_api::create_database_cluster(config, req).await?;
let db = &resp.db;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.database_created", name => db.name, id => fmt_id(db.id))
);
println!("{}", t!("cli.password", password => password));
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.db)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}", fmt_id(db.id), db.name);
}
}
Ok(())
}
fn parse_db_type(s: &str) -> Result<String, TwcError> {
let canonical = match s.to_lowercase().as_str() {
"mysql" | "mysql5" => "mysql",
"mysql8" | "mysql84" => "mysql8_4",
"postgres" | "pg" | "postgres14" => "postgres14",
"postgres15" => "postgres15",
"postgres16" => "postgres16",
"postgres17" => "postgres17",
"redis" | "redis7" => "redis7",
"redis8" | "redis81" => "redis8_1",
"mongo" | "mongodb" | "mongodb7" => "mongodb7",
"mongodb8" | "mongodb80" => "mongodb8_0",
"opensearch" | "opensearch2" | "opensearch219" => "opensearch",
"clickhouse" | "clickhouse24" | "clickhouse25" => "clickhouse",
"kafka" => "kafka",
"rabbitmq" | "rabbitmq4" | "rabbitmq40" => "rabbitmq4_0",
_ => {
return Err(TwcError::Api(format!(
"unknown database type: {s} (expected mysql, postgres, redis, \
mongodb, opensearch, clickhouse, kafka, rabbitmq)"
)));
}
};
Ok(canonical.to_string())
}
pub async fn list_types(config: &Configuration, format: OutputFormat) -> Result<(), TwcError> {
let resp = databases_api::get_database_cluster_types(config).await?;
let rows: Vec<TypeRow> = resp
.types
.iter()
.map(|t| TypeRow {
engine: t.r#type.clone(),
version: t.version.clone(),
name: t.name.clone(),
replication: if t.is_available_replication {
"yes".to_string()
} else {
"no".to_string()
},
deprecated: if t.is_deprecated {
"yes".to_string()
} else {
"no".to_string()
}
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_database_types_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
if let Some(out) = crate::output::serialized(format, &resp.types) {
println!("{}", out?);
}
}
OutputFormat::Quiet => {
for t in &resp.types {
println!("{}\t{}", t.r#type, t.version);
}
}
}
Ok(())
}
pub async fn list_instances(
config: &Configuration,
id: i32,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = databases_api::get_database_instances(config, id).await?;
let rows: Vec<InstanceRow> = resp
.instances
.iter()
.map(|i| InstanceRow {
id: fmt_id(i.id),
name: i.name.clone(),
description: i.description.clone(),
created_at: i.created_at.clone()
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_database_instances_found"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
if let Some(out) = crate::output::serialized(format, &resp.instances) {
println!("{}", out?);
}
}
OutputFormat::Quiet => {
for i in &resp.instances {
println!("{}\t{}", fmt_id(i.id), i.name);
}
}
}
Ok(())
}