diff --git a/src/cli/ahab.rs b/src/cli/ahab.rs index 61a9a4a..3cb10f4 100644 --- a/src/cli/ahab.rs +++ b/src/cli/ahab.rs @@ -1,4 +1,4 @@ -use super::{Django, Link, Postgres}; +use super::{Django, Link, Postgres, Store}; use clap::builder::styling::{AnsiColor, Effects, Styles}; use clap::{Parser, Subcommand}; @@ -44,6 +44,12 @@ pub enum Commands { command: Link, }, + /// Show what ahab makes of this project + Status { + #[command(flatten)] + store: Store, + }, + /// Print a shell completion script on stdout Completions { /// Shell to generate the script for diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 72ef580..663207e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,5 +7,5 @@ mod postgres; pub use ahab::{Ahab, Commands}; pub use django::Django; -pub use link::Link; +pub use link::{Link, Store}; pub use postgres::{Format, Postgres}; diff --git a/src/commands/link.rs b/src/commands/link.rs index 534840d..5ae669f 100644 --- a/src/commands/link.rs +++ b/src/commands/link.rs @@ -182,6 +182,19 @@ fn stored_paths(repo: &Repo, dir: &Path) -> Result> { Ok(found) } +// what the store holds, without the git questions check asks +pub fn stored_summary(ctx: &Ctx, store: Option<&Path>) -> Result<(PathBuf, usize, usize)> { + let repo = Repo::discover(ctx, store)?; + let stored = stored_paths(&repo, &repo.store)?; + + let linked = stored + .iter() + .filter(|(_, state)| *state == Stored::Linked) + .count(); + + Ok((repo.store, linked, stored.len() - linked)) +} + // list what the store holds for this repository, the inverse of check pub fn list(ctx: &Ctx, store: Option<&Path>) -> Result<()> { let repo = Repo::discover(ctx, store)?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e33f8ca..80b0fbd 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,3 +2,4 @@ pub mod completions; pub mod django; pub mod link; pub mod postgres; +pub mod status; diff --git a/src/commands/status.rs b/src/commands/status.rs new file mode 100644 index 0000000..4c6bdc5 --- /dev/null +++ b/src/commands/status.rs @@ -0,0 +1,85 @@ +use std::path::Path; + +use anyhow::Result; + +use crate::cmd::{Cmd, Ps}; +use crate::commands::link; +use crate::ctx::Ctx; +use crate::project::Project; + +// which services ahab picked, what it would talk to, what the store holds +pub fn status(ctx: &Ctx, store: Option<&Path>) -> Result<()> { + // a role it cannot pick out is worth reporting; a project it cannot read is not + let project = match Project::resolve(ctx) { + Ok(project) => project, + Err(e) => { + stored(ctx, store); + return Err(e); + } + }; + + println!("services: {}", project.names().join(", ")); + + role(ctx, "django", project.django(), &project); + role(ctx, "postgres", project.postgres(), &project); + stored(ctx, store); + + Ok(()) +} + +// what the store holds, and where to look for what it does not +fn stored(ctx: &Ctx, store: Option<&Path>) { + match link::stored_summary(ctx, store) { + Err(e) => println!("store: {e:#}"), + Ok((store, linked, other)) => { + println!("store: {}", store.display()); + println!("\tlinked: {linked}"); + + if other > 0 { + println!("\tnot linked: {other} (see `ahab link list`)"); + } + + println!("\t`ahab link check` lists what a sandbox can still read"); + } + } +} + +// one detected service: the name, the image behind it, and whether it is up +fn role(ctx: &Ctx, role: &str, detected: Result, project: &Project) { + let service = match detected { + Ok(service) => service, + Err(e) => { + println!("{role}: {e:#}"); + return; + } + }; + + let image = project + .image(&service) + .unwrap_or("built from the project") + .to_string(); + + println!("{role}: {service} ({image})"); + + match Ps::id_of(&service).capture(ctx) { + Ok(id) if id.trim().is_empty() => println!("\tcontainer: not running"), + Ok(id) => println!("\tcontainer: {}", short(id.trim())), + Err(e) => println!("\tcontainer: {e:#}"), + } + + if role == "postgres" { + let (user, database) = project.postgres_credentials(&service); + let source = |key: &str| match project.env(&service, key) { + Some(_) => "", + None => " (default, nothing in the environment)", + }; + + println!("\tuser: {user}{}", source("POSTGRES_USER")); + println!("\tdatabase: {database}{}", source("POSTGRES_DB")); + } +} + +// a container id is 64 characters and only the first few are ever typed +fn short(id: &str) -> String { + id.chars().take(12).collect() +} diff --git a/src/main.rs b/src/main.rs index a8422e3..39a6e05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,6 +97,11 @@ fn run(ctx: &Ctx, command: cli::Commands) -> Result { store.root.as_deref(), ), }, + cli::Commands::Status { store } => { + commands::status::status(ctx, store.root.as_deref())?; + + Ok(done) + } cli::Commands::Completions { shell } => { commands::completions::completions(shell)?; diff --git a/src/project.rs b/src/project.rs index 296b3eb..239df00 100644 --- a/src/project.rs +++ b/src/project.rs @@ -91,7 +91,7 @@ impl Project { (user, database) } - fn names(&self) -> Vec<&str> { + pub fn names(&self) -> Vec<&str> { let mut names: Vec<&str> = self .services .as_object() @@ -102,6 +102,16 @@ impl Project { names } + // the image a service runs, which a service that builds its own does not have + pub fn image(&self, service: &str) -> Option<&str> { + self.services[service].get("image")?.as_str() + } + + // whether a value was set in the project or is ahab's own fallback + pub fn env(&self, service: &str, key: &str) -> Option { + env_var(&self.services[service], key) + } + fn publishes_ports(&self, service: &str) -> bool { self.services[service] .get("ports")