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

View File

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

View File

@@ -1,14 +1,14 @@
use std::time::Duration;
use miette::{Diagnostic, bail, miette};
use dsn::DSN;
use miette::miette;
use colored::Colorize;
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use miette::Result;
use thiserror::Error;
use crate::{
config::{PGDConfig, PostgresVersion, Project},
cli::ConnectionFormat,
config::{PGDConfig, Project},
consts::{DATABASE, USERNAME},
controller::{docker::DockerController, reconciler::Reconciler},
state::{InstanceState, StateManager},
};
@@ -26,6 +26,16 @@ pub struct 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> {
let project = Project::load()?;
let state = StateManager::new()?;
@@ -55,6 +65,59 @@ impl Controller {
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<()> {
let reconciler = Reconciler { ctx: &self.ctx };
@@ -83,20 +146,7 @@ impl Controller {
project.path.display().to_string().bright_white().bold()
);
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("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, '╯');
let mut table = create_ui_table("Project Configuration");
table.add_row(vec![
Cell::new("Project").fg(Color::White),
Cell::new(&project.name).add_attribute(Attribute::Bold),
@@ -123,3 +173,19 @@ impl Controller {
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 miette::{Diagnostic, bail, miette};
use miette::{Diagnostic, bail};
use colored::Colorize;
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use miette::Result;
use thiserror::Error;
use crate::{
config::{PGDConfig, PostgresVersion, Project},
config::{PostgresVersion, Project},
controller::{
Context,
docker::{self, DockerController},
docker::{self},
},
state::{InstanceState, StateManager},
state::InstanceState,
};
const MAX_RETRIES: usize = 10;

View File

@@ -34,10 +34,15 @@ async fn main() -> Result<()> {
ControlCommands::Start => {}
ControlCommands::Stop => {}
ControlCommands::Restart => {}
ControlCommands::Destroy => {}
ControlCommands::Destroy { accept } => {}
ControlCommands::Logs { follow } => todo!(),
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 serde::{Deserialize, Serialize};
use std::cell::{Ref, RefCell};
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;