vpg/
main.rs

1//! Update Postgres credentials from Vault, rewrite pgpass & nvim-dbee, optionally launch pgcli.
2//!
3//! # Arguments
4//! - `alias` Optional database alias (interactive selector if missing).
5//!
6//! # Usage
7//! ```bash
8//! vpg # pick alias interactively -> update credentials -> optional connect
9//! vpg reporting # directly update 'reporting' alias then prompt to connect
10//! ```
11//!
12//! # Flow
13//! 1. Read ~/.pgpass & parse entries.
14//! 2. Resolve alias (arg or picker).
15//! 3. Vault login (if needed) + read secret.
16//! 4. Update pgpass & nvim dbee conns file.
17//! 5. Prompt to start pgcli.
18//!
19//! # Errors
20//! - External command (`pgcli`, `vault`) fails or exits non-zero.
21//! - File read / write operations fail.
22//! - JSON serialization or deserialization fails.
23//! - Required environment variable missing or invalid Unicode.
24//! - User selection or prompt interaction fails.
25#![feature(exit_status_error)]
26
27use std::process::Command;
28use std::process::Stdio;
29
30use color_eyre::eyre::Context;
31use color_eyre::owo_colors::OwoColorize as _;
32use ytil_sys::cli::Args;
33
34use crate::pgpass::PgpassEntry;
35use crate::pgpass::PgpassFile;
36use crate::vault::VaultReadOutput;
37
38mod nvim_dbee;
39mod pgpass;
40mod vault;
41
42/// Executes the `vault` CLI to read a secret as JSON and deserialize it.
43///
44/// Runs `vault read <vault_path> --format=json` using [`std::process::Command`] and
45/// deserializes the JSON standard output into a [`VaultReadOutput`].
46///
47/// # Errors
48/// - Launching or running the [`vault`] process fails (I/O error from [`Command`]).
49/// - The command standard output cannot be deserialized into [`VaultReadOutput`] via [`serde_json`].
50/// - The standard output is not valid UTF-8 when constructing the contextual error message.
51fn exec_vault_read_cmd(vault_path: &str) -> color_eyre::Result<VaultReadOutput> {
52    let mut cmd = Command::new("vault");
53    cmd.args(["read", vault_path, "--format=json"]);
54
55    let cmd_stdout = &cmd.output()?.stdout;
56
57    serde_json::from_slice(cmd_stdout).with_context(|| {
58        str::from_utf8(cmd_stdout).map_or_else(
59            |error| format!("cmd stdout invalid utf-8 | cmd={cmd:#?} error={error:?}"),
60            |str_stdout| format!("cannot build VaultReadOutput from vault cmd {cmd:#?} stdout {str_stdout:?}"),
61        )
62    })
63}
64
65/// Update Postgres credentials from Vault, rewrite pgpass & nvim-dbee, optionally launch pgcli.
66fn main() -> color_eyre::Result<()> {
67    color_eyre::install()?;
68
69    let args = ytil_sys::cli::get();
70    if args.has_help() {
71        println!("{}", include_str!("../help.txt"));
72        return Ok(());
73    }
74
75    let pgpass_path = ytil_sys::dir::build_home_path(&[".pgpass"])?;
76    let pgpass_content = std::fs::read_to_string(&pgpass_path)?;
77    let pgpass_file = PgpassFile::parse(pgpass_content.as_str())?;
78
79    let args = ytil_sys::cli::get();
80    let Some(mut pgpass_entry) = ytil_tui::get_item_from_cli_args_or_select(
81        &args,
82        |(idx, _)| *idx == 0,
83        pgpass_file.entries,
84        |alias: &str| Box::new(move |entry: &PgpassEntry| entry.metadata.alias == alias),
85    )?
86    else {
87        return Ok(());
88    };
89
90    println!(
91        "\nLogging into Vault @ {}\n{}\n",
92        std::env::var("VAULT_ADDR")?.bold(),
93        "(be sure to have the VPN on!)".bold()
94    );
95    vault::log_into_vault_if_required()?;
96    let vault_read_output = exec_vault_read_cmd(&pgpass_entry.metadata.vault_path)?;
97
98    pgpass_entry.connection_params.update(&vault_read_output.data);
99    pgpass::save_new_pgpass_file(pgpass_file.idx_lines, &pgpass_entry.connection_params, &pgpass_path)?;
100
101    let nvim_dbee_conns_path = ytil_sys::dir::build_home_path(&[".local", "state", "nvim", "dbee", "conns.json"])?;
102    nvim_dbee::save_new_nvim_dbee_conns_file(&pgpass_entry, &nvim_dbee_conns_path)?;
103
104    println!(
105        "{} credentials updated in {}",
106        pgpass_entry.metadata.alias.green().bold(),
107        pgpass_path.display()
108    );
109    println!(
110        "{} credentials updated in {}",
111        pgpass_entry.metadata.alias.green().bold(),
112        nvim_dbee_conns_path.display()
113    );
114
115    println!(); // Cosmetic spacing.
116
117    if Some(true) == ytil_tui::yes_no_select(&format!("Connect to {}? ", pgpass_entry.metadata.alias))? {
118        let db_url = pgpass_entry.connection_params.db_url();
119        println!(
120            "\nConnecting to {} @\n\n{}\n",
121            pgpass_entry.metadata.alias.bold(),
122            db_url.bold()
123        );
124
125        if let Some(psql_exit_code) = Command::new("pgcli")
126            .arg(&db_url)
127            .stdin(Stdio::inherit())
128            .stdout(Stdio::inherit())
129            .stderr(Stdio::inherit())
130            .spawn()?
131            .wait()?
132            .code()
133        {
134            std::process::exit(psql_exit_code);
135        }
136
137        eprintln!("{}", format!("pgcli {db_url} terminated by signal.").red().bold());
138        std::process::exit(1);
139    }
140
141    Ok(())
142}