feat(cli): add connection command with DSN and human formats

This commit is contained in:
hdbg
2025-12-06 19:56:38 +01:00
parent c45e9305e5
commit 676c53fabb
7 changed files with 173 additions and 30 deletions

67
Cargo.lock generated
View File

@@ -249,6 +249,20 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "cliclack"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2381872509dfa50d8b92b92a5da8367ba68458ab9494be4134b57ad6ca26295f"
dependencies = [
"console 0.15.11",
"indicatif",
"once_cell",
"strsim",
"textwrap",
"zeroize",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.4" version = "1.0.4"
@@ -275,6 +289,19 @@ dependencies = [
"unicode-width 0.2.2", "unicode-width 0.2.2",
] ]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.2",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "console" name = "console"
version = "0.16.1" version = "0.16.1"
@@ -382,6 +409,15 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "dsn"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68ec86c8ab056c40c4d3f6a543ad0ad6a251d7d01dac251feed242aa44e754a"
dependencies = [
"percent-encoding",
]
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.20" version = "1.0.20"
@@ -841,7 +877,7 @@ version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
dependencies = [ dependencies = [
"console", "console 0.16.1",
"portable-atomic", "portable-atomic",
"unicode-segmentation", "unicode-segmentation",
"unicode-width 0.2.2", "unicode-width 0.2.2",
@@ -1064,8 +1100,10 @@ version = "0.0.1"
dependencies = [ dependencies = [
"bollard", "bollard",
"clap", "clap",
"cliclack",
"colored", "colored",
"comfy-table", "comfy-table",
"dsn",
"futures", "futures",
"indicatif", "indicatif",
"miette", "miette",
@@ -1408,6 +1446,12 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.1" version = "0.6.1"
@@ -1489,6 +1533,7 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [ dependencies = [
"smawk",
"unicode-linebreak", "unicode-linebreak",
"unicode-width 0.2.2", "unicode-width 0.2.2",
] ]
@@ -2176,6 +2221,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.3" version = "0.2.3"

View File

@@ -9,8 +9,10 @@ license = "MIT"
[dependencies] [dependencies]
bollard = "0.19.4" bollard = "0.19.4"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
cliclack = "0.3.7"
colored = "3.0.0" colored = "3.0.0"
comfy-table = "7.2.1" comfy-table = "7.2.1"
dsn = "1.2.1"
futures = "0.3.31" futures = "0.3.31"
indicatif = { version = "0.18.3", features = ["improved_unicode"] } indicatif = { version = "0.18.3", features = ["improved_unicode"] }
miette = { version = "7.6.0", features = ["fancy"] } miette = { version = "7.6.0", features = ["fancy"] }

View File

@@ -36,13 +36,19 @@ pub enum ControlCommands {
/// Restart postgres instance /// Restart postgres instance
Restart, Restart,
/// (WARNING!) Destroy postgres instance /// (WARNING!) Destroy postgres instance
Destroy, Destroy { accept: bool },
/// (WARNING!) Destruct database
Wipe { accept: bool },
/// Status of instance /// Status of instance
Status, Status,
/// View logs produced by postgres /// View logs produced by postgres
Logs { follow: bool }, Logs { follow: bool },
/// (Sensitive) get connection details /// (Sensitive) get connection details
Connection { format: ConnectionFormat }, Connection {
#[arg(short, long, default_value = "dsn")]
format: ConnectionFormat,
},
} }
#[derive(Subcommand)] #[derive(Subcommand)]

View File

@@ -1,14 +1,14 @@
use std::time::Duration; use dsn::DSN;
use miette::miette;
use miette::{Diagnostic, bail, miette};
use colored::Colorize; use colored::Colorize;
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL}; use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use miette::Result; use miette::Result;
use thiserror::Error;
use crate::{ use crate::{
config::{PGDConfig, PostgresVersion, Project}, cli::ConnectionFormat,
config::{PGDConfig, Project},
consts::{DATABASE, USERNAME},
controller::{docker::DockerController, reconciler::Reconciler}, controller::{docker::DockerController, reconciler::Reconciler},
state::{InstanceState, StateManager}, state::{InstanceState, StateManager},
}; };
@@ -26,6 +26,16 @@ pub struct Context {
} }
impl Context { impl Context {
pub fn require_instance(&self) -> Result<&InstanceState> {
self.instance.as_ref().ok_or(miette!("This command requires instance. Either initiliaze a project, or pass -I with instance name"))
}
pub fn require_project(&self) -> Result<&Project> {
self.project.as_ref().ok_or(miette!(
"This command requires project. Please, initiliaze a project."
))
}
pub async fn new(instance_override: Option<String>) -> Result<Self> { pub async fn new(instance_override: Option<String>) -> Result<Self> {
let project = Project::load()?; let project = Project::load()?;
let state = StateManager::new()?; let state = StateManager::new()?;
@@ -55,6 +65,59 @@ impl Controller {
Self { ctx } Self { ctx }
} }
pub async fn show_connection(&self, format: ConnectionFormat) -> Result<()> {
let project = self.ctx.require_project()?;
let reconciler = Reconciler { ctx: &self.ctx };
reconciler.reconcile(project).await?;
match format {
ConnectionFormat::DSN => {
let dsn = DSN::builder()
.driver("postgres")
.username(USERNAME)
.password(project.config.password.clone())
.host("127.0.0.1")
.port(project.config.port)
.database(DATABASE)
.build();
println!("{}", dsn.to_string());
}
ConnectionFormat::Human => {
let mut table = create_ui_table("Instance");
table.add_row(vec![
Cell::new("Project").fg(Color::White),
Cell::new(&project.name).add_attribute(Attribute::Bold),
]);
table.add_row(vec![
Cell::new("PostgreSQL Version").fg(Color::White),
Cell::new(project.config.version.to_string()).add_attribute(Attribute::Bold),
]);
table.add_row(vec![
Cell::new("Host").fg(Color::White),
Cell::new("127.0.0.1").add_attribute(Attribute::Bold),
]);
table.add_row(vec![
Cell::new("Port").fg(Color::White),
Cell::new(project.config.port.to_string()).add_attribute(Attribute::Bold),
]);
table.add_row(vec![
Cell::new("Username").fg(Color::White),
Cell::new(USERNAME).add_attribute(Attribute::Bold),
]);
table.add_row(vec![
Cell::new("Password").fg(Color::White),
Cell::new(project.config.password.clone()).fg(Color::DarkGrey),
]);
println!("{}", table);
}
}
Ok(())
}
pub async fn init_project(&self) -> Result<()> { pub async fn init_project(&self) -> Result<()> {
let reconciler = Reconciler { ctx: &self.ctx }; let reconciler = Reconciler { ctx: &self.ctx };
@@ -83,20 +146,7 @@ impl Controller {
project.path.display().to_string().bright_white().bold() project.path.display().to_string().bright_white().bold()
); );
let mut table = Table::new(); let mut table = create_ui_table("Project Configuration");
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_style(comfy_table::TableComponent::MiddleIntersections, ' ')
.set_header(vec![
Cell::new("Instance Configuration").add_attribute(Attribute::Bold),
]);
use comfy_table::TableComponent::*;
table.set_style(TopLeftCorner, '╭');
table.set_style(TopRightCorner, '╮');
table.set_style(BottomLeftCorner, '╰');
table.set_style(BottomRightCorner, '╯');
table.add_row(vec![ table.add_row(vec![
Cell::new("Project").fg(Color::White), Cell::new("Project").fg(Color::White),
Cell::new(&project.name).add_attribute(Attribute::Bold), Cell::new(&project.name).add_attribute(Attribute::Bold),
@@ -123,3 +173,19 @@ impl Controller {
Ok(()) Ok(())
} }
} }
fn create_ui_table(header: &'static str) -> Table {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_style(comfy_table::TableComponent::MiddleIntersections, ' ')
.set_header(vec![Cell::new(header).add_attribute(Attribute::Bold)]);
use comfy_table::TableComponent::*;
table.set_style(TopLeftCorner, '╭');
table.set_style(TopRightCorner, '╮');
table.set_style(BottomLeftCorner, '╰');
table.set_style(BottomRightCorner, '╯');
table
}

View File

@@ -1,19 +1,18 @@
use std::time::Duration; use std::time::Duration;
use miette::{Diagnostic, bail, miette}; use miette::{Diagnostic, bail};
use colored::Colorize; use colored::Colorize;
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use miette::Result; use miette::Result;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
config::{PGDConfig, PostgresVersion, Project}, config::{PostgresVersion, Project},
controller::{ controller::{
Context, Context,
docker::{self, DockerController}, docker::{self},
}, },
state::{InstanceState, StateManager}, state::InstanceState,
}; };
const MAX_RETRIES: usize = 10; const MAX_RETRIES: usize = 10;

View File

@@ -34,10 +34,15 @@ async fn main() -> Result<()> {
ControlCommands::Start => {} ControlCommands::Start => {}
ControlCommands::Stop => {} ControlCommands::Stop => {}
ControlCommands::Restart => {} ControlCommands::Restart => {}
ControlCommands::Destroy => {} ControlCommands::Destroy { accept } => {}
ControlCommands::Logs { follow } => todo!(), ControlCommands::Logs { follow } => todo!(),
ControlCommands::Status => {} ControlCommands::Status => {}
ControlCommands::Connection { format: _ } => {} // can't override an instance for this command, because password is in config
ControlCommands::Connection { format } => {
let ctx = Context::new(None).await?;
Controller::new(ctx).show_connection(format).await?;
}
ControlCommands::Wipe { accept } => {}
}, },
} }

View File

@@ -1,6 +1,6 @@
use miette::{Context, IntoDiagnostic, Result}; use miette::{Context, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cell::{Ref, RefCell}; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;