use std::time::Duration;
use anyhow::Result;
use owo_colors::OwoColorize;
use crate::api::client::{ApiClient, NewServer};
use crate::api::models::{EnvironmentMove, Server};
use crate::exit;
use crate::output::Format;
use crate::render::sanitise;
use crate::render::table::{Cell, Table};
use crate::render::theme;
use crate::render::time;
const MOVE_POLL: Duration = Duration::from_secs(2);
pub fn status_word(server: &Server) -> &'static str {
match server.status.as_str() {
"ready" if server.draining => "drenándose",
"ready" if !server.schedulable => "acordonado",
"ready" => "listo",
"unreachable" => "no responde",
_ => "pendiente",
}
}
fn status_style(server: &Server) -> owo_colors::Style {
match server.status.as_str() {
"ready" if server.draining || !server.schedulable => theme::warning(),
"ready" => theme::success(),
"unreachable" => theme::danger(),
_ => theme::muted(),
}
}
pub fn memory_line(server: &Server) -> String {
server.capacity.as_ref().map_or_else(
|| "—".to_owned(),
|capacity| {
format!(
"{} % de {:.1} GB",
capacity.memory_used_percent,
capacity.memory_total_mb as f64 / 1024.0
)
},
)
}
pub async fn list(client: &ApiClient, format: Format) -> Result<()> {
let answer = client.servers().await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let mut table = Table::new(&[
"servidor",
"estado",
"dirección",
"memoria",
"cpus",
"entornos",
"contenedores",
]);
for server in &answer.body.servers {
let name = if server.primary {
format!("{} (principal)", sanitise::plain(&server.name))
} else {
sanitise::plain(&server.name)
};
table.push(vec![
Cell::styled(name, theme::strong()),
Cell::styled(status_word(server), status_style(server)),
Cell::styled(sanitise::plain(&server.address), theme::muted()),
Cell::plain(memory_line(server)),
Cell::plain(
server
.capacity
.as_ref()
.map_or_else(|| "—".to_owned(), |capacity| capacity.cpus.to_string()),
),
Cell::plain(server.environments.unwrap_or_default().to_string()),
Cell::plain(server.containers.unwrap_or_default().to_string()),
]);
}
table.print();
let running: Vec<&EnvironmentMove> = answer
.body
.moves
.iter()
.filter(|movement| movement.status == "running")
.collect();
if !running.is_empty() {
println!();
for movement in running {
println!(" {}", describe_move(movement));
}
}
Ok(())
}
pub async fn show(client: &ApiClient, server: &str, format: Format) -> Result<()> {
let answer = client.server(server).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let detail = &answer.body;
let server = &detail.server;
println!(
" {} {} {}",
theme::state_dot(if server.status == "ready" {
"active"
} else {
"crashed"
}),
sanitise::plain(&server.name).style(theme::strong()),
status_word(server).style(status_style(server))
);
if let Some(capacity) = &server.capacity {
println!(
" {}",
format!(
"{} · podman {} · {} CPUs · memoria {} · {} contenedores",
sanitise::plain(&capacity.hostname),
sanitise::plain(&capacity.podman_version),
capacity.cpus,
memory_line(server),
capacity.containers_running
)
.style(theme::muted())
);
}
if let Some(error) = &server.last_error {
println!(" {}", sanitise::plain(error).style(theme::danger()));
}
println!();
let mut table = Table::new(&["proyecto", "entorno", "servicios"]);
for placed in &detail.environments {
table.push(vec![
Cell::styled(sanitise::plain(&placed.project), theme::strong()),
Cell::plain(sanitise::plain(&placed.environment)),
Cell::plain(placed.services.to_string()),
]);
}
table.print_or("No tiene entornos.");
Ok(())
}
pub struct ServerRequest<'a> {
pub name: &'a str,
pub connection: &'a str,
pub address: &'a str,
pub identity: Option<&'a str>,
pub region: Option<&'a str>,
}
pub async fn add(client: &ApiClient, request: ServerRequest<'_>, format: Format) -> Result<()> {
let answer = client
.add_server(&NewServer {
name: request.name,
connection: request.connection,
address: request.address,
identity: request.identity,
region: request.region,
})
.await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let server = &answer.body.server;
println!(
" {} {} se ha unido: {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&server.name).style(theme::strong()),
memory_line(server)
);
println!(
" {}",
"Los entornos nuevos irán al servidor con más memoria libre.".style(theme::muted())
);
Ok(())
}
pub async fn cordon(
client: &ApiClient,
server: &str,
cordoned: bool,
format: Format,
) -> Result<()> {
let answer = client.cordon_server(server, cordoned).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let server = &answer.body.server;
println!(
" {} {} {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&server.name).style(theme::strong()),
if cordoned {
"ya no acepta entornos nuevos"
} else {
"vuelve a aceptar entornos nuevos"
}
);
Ok(())
}
pub async fn failover(client: &ApiClient, server: &str, on: bool, format: Format) -> Result<()> {
let answer = client.set_server_failover(server, on).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let server = &answer.body.server;
println!(
" {} {} {}",
theme::CHECK.style(theme::success()),
sanitise::plain(&server.name).style(theme::strong()),
if on {
"mudará sus entornos sin datos a otro servidor si pasa cinco minutos sin responder"
} else {
"esperará a volver si deja de responder; nada se muda solo"
}
);
Ok(())
}
pub async fn drain(client: &ApiClient, server: &str, dry_run: bool, format: Format) -> Result<()> {
let answer = if dry_run {
client.plan_drain(server).await?
} else {
client.drain_server(server).await?
};
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let plan = &answer.body;
let mut table = Table::new(&["proyecto", "entorno", "destino"]);
for movement in &plan.moves {
table.push(vec![
Cell::styled(sanitise::plain(&movement.project_name), theme::strong()),
Cell::plain(sanitise::plain(&movement.environment_name)),
Cell::styled(
sanitise::plain(movement.to.as_deref().unwrap_or("—")),
theme::accent(),
),
]);
}
for staying in &plan.staying {
table.push(vec![
Cell::styled(sanitise::plain(&staying.project_name), theme::strong()),
Cell::plain(sanitise::plain(&staying.environment_name)),
Cell::styled(
format!(
"se queda: {}",
sanitise::plain(staying.reason.as_deref().unwrap_or(""))
),
theme::warning(),
),
]);
}
table.print_or(format_args!(
"{} no tiene entornos que mover.",
sanitise::plain(&plan.server.name)
));
if !dry_run && !plan.moves.is_empty() {
println!();
println!(
" {}",
format!(
"Se mueven de uno en uno; sigue el progreso con fundaia servers list. {} ya no acepta entornos nuevos.",
sanitise::plain(&plan.server.name)
)
.style(theme::muted())
);
}
Ok(())
}
pub async fn remove(
client: &ApiClient,
server: &str,
confirmation: Option<&str>,
format: Format,
) -> Result<()> {
let detail = client.server(server).await?.body;
let name = detail.server.name.clone();
if confirmation != Some(name.as_str()) {
return Err(exit::Refused::invalid(format!(
"quitar el servidor `{name}` lo olvida para siempre: repite su nombre con --confirm {name}"
))
.into());
}
let answer = client.remove_server(&detail.server.id, &name).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
println!(
" {} {} ya no forma parte de la plataforma",
theme::CHECK.style(theme::success()),
sanitise::plain(&name).style(theme::strong())
);
Ok(())
}
pub fn describe_move(movement: &EnvironmentMove) -> String {
let route = format!(
"{} {} {}",
sanitise::plain(&movement.from.name),
theme::ARROW,
sanitise::plain(&movement.to.name)
);
match movement.status.as_str() {
"succeeded" => format!(
"{} {route} · terminada {}",
theme::CHECK.style(theme::success()),
movement
.finished_at
.as_deref()
.map(time::when)
.unwrap_or_default()
),
"failed" => format!(
"{} {route} · {}",
theme::CROSS.style(theme::danger()),
sanitise::plain(movement.error.as_deref().unwrap_or("falló"))
),
_ => format!(
"{} {route} · {}",
theme::PENDING.style(theme::accent()),
sanitise::plain(&movement.step)
),
}
}
pub async fn environment_server(
client: &ApiClient,
project_reference: &str,
environment: &str,
format: Format,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let answer = client.environment_server(&project.id, environment).await?;
if format.answered_in_json(&answer.raw) {
return Ok(());
}
let placement = &answer.body;
println!(
" {} vive en {}{}",
sanitise::plain(environment).style(theme::strong()),
sanitise::plain(&placement.server.name).style(theme::accent()),
if placement.server.primary {
" (principal)"
} else {
""
}
);
if let Some(movement) = &placement.movement {
println!(" {}", describe_move(movement));
}
for reason in &placement.stays_on_primary {
println!(" {}", sanitise::plain(reason).style(theme::muted()));
}
Ok(())
}
pub async fn move_environment(
client: &ApiClient,
project_reference: &str,
environment: &str,
server: &str,
wait: bool,
format: Format,
) -> Result<()> {
let project = client.find_project(project_reference).await?;
let mut answer = client
.move_environment(&project.id, environment, server)
.await?;
if wait {
while answer.body.movement.status == "running" {
tokio::time::sleep(MOVE_POLL).await;
answer = client.environment_move(&answer.body.movement.id).await?;
}
}
if format.answered_in_json(&answer.raw) {
return failed_move(&answer.body.movement);
}
println!(" {}", describe_move(&answer.body.movement));
if !wait && answer.body.movement.status == "running" {
println!(
" {}",
format!(
"Sigue en marcha; fundaia env server {} {} dice cómo va.",
project.slug, environment
)
.style(theme::muted())
);
}
failed_move(&answer.body.movement)
}
fn failed_move(movement: &EnvironmentMove) -> Result<()> {
if movement.status == "failed" {
return Err(exit::Refused::upstream(
movement
.error
.clone()
.unwrap_or_else(|| "la mudanza falló".to_owned()),
)
.into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn server(status: &str, schedulable: bool, draining: bool) -> Server {
Server {
id: "node_1".to_owned(),
name: "madrid".to_owned(),
primary: false,
address: "10.0.0.5".to_owned(),
region: None,
status: status.to_owned(),
schedulable,
draining,
connection: None,
capacity: None,
last_seen_at: None,
last_error: None,
failover: true,
environments: None,
containers: None,
}
}
#[test]
fn it_should_call_a_cordoned_server_cordoned() {
assert_eq!(status_word(&server("ready", false, false)), "acordonado");
}
#[test]
fn it_should_call_a_draining_server_draining() {
assert_eq!(status_word(&server("ready", false, true)), "drenándose");
}
#[test]
fn it_should_say_an_unreachable_server_does_not_answer() {
assert_eq!(
status_word(&server("unreachable", true, false)),
"no responde"
);
}
#[test]
fn it_should_show_a_dash_for_a_server_never_measured() {
assert_eq!(memory_line(&server("pending", true, false)), "—");
}
}