diff --git a/pgdog/src/admin/error.rs b/pgdog/src/admin/error.rs index c9ca3f29..3213a0ea 100644 --- a/pgdog/src/admin/error.rs +++ b/pgdog/src/admin/error.rs @@ -36,6 +36,9 @@ pub enum Error { #[error("address is not valid")] InvalidAddress, + + #[error("{0}")] + Toml(#[from] toml::ser::Error), } impl From for Error { diff --git a/pgdog/src/admin/mod.rs b/pgdog/src/admin/mod.rs index 521119c8..a34eb0d0 100644 --- a/pgdog/src/admin/mod.rs +++ b/pgdog/src/admin/mod.rs @@ -22,6 +22,7 @@ pub mod setup_schema; pub mod show_client_memory; pub mod show_clients; pub mod show_config; +pub mod show_config_file; pub mod show_instance_id; pub mod show_lists; pub mod show_mirrors; diff --git a/pgdog/src/admin/parser.rs b/pgdog/src/admin/parser.rs index 5d94ab44..e4bf7ac8 100644 --- a/pgdog/src/admin/parser.rs +++ b/pgdog/src/admin/parser.rs @@ -5,12 +5,12 @@ use super::{ prelude::Message, probe::Probe, reconnect::Reconnect, reload::Reload, reset_query_cache::ResetQueryCache, set::Set, setup_schema::SetupSchema, show_client_memory::ShowClientMemory, show_clients::ShowClients, show_config::ShowConfig, - show_instance_id::ShowInstanceId, show_lists::ShowLists, show_mirrors::ShowMirrors, - show_peers::ShowPeers, show_pools::ShowPools, show_prepared_statements::ShowPreparedStatements, - show_query_cache::ShowQueryCache, show_replication::ShowReplication, - show_server_memory::ShowServerMemory, show_servers::ShowServers, show_stats::ShowStats, - show_transactions::ShowTransactions, show_version::ShowVersion, shutdown::Shutdown, Command, - Error, + show_config_file::ShowConfigFile, show_instance_id::ShowInstanceId, show_lists::ShowLists, + show_mirrors::ShowMirrors, show_peers::ShowPeers, show_pools::ShowPools, + show_prepared_statements::ShowPreparedStatements, show_query_cache::ShowQueryCache, + show_replication::ShowReplication, show_server_memory::ShowServerMemory, + show_servers::ShowServers, show_stats::ShowStats, show_transactions::ShowTransactions, + show_version::ShowVersion, shutdown::Shutdown, Command, Error, }; use tracing::debug; @@ -23,6 +23,7 @@ pub enum ParseResult { Reload(Reload), ShowPools(ShowPools), ShowConfig(ShowConfig), + ShowConfigFile(ShowConfigFile), ShowServers(ShowServers), ShowPeers(ShowPeers), ShowQueryCache(ShowQueryCache), @@ -58,6 +59,7 @@ impl ParseResult { Reload(reload) => reload.execute().await, ShowPools(show_pools) => show_pools.execute().await, ShowConfig(show_config) => show_config.execute().await, + ShowConfigFile(show_config_file) => show_config_file.execute().await, ShowServers(show_servers) => show_servers.execute().await, ShowPeers(show_peers) => show_peers.execute().await, ShowQueryCache(show_query_cache) => show_query_cache.execute().await, @@ -93,6 +95,7 @@ impl ParseResult { Reload(reload) => reload.name(), ShowPools(show_pools) => show_pools.name(), ShowConfig(show_config) => show_config.name(), + ShowConfigFile(show_config_file) => show_config_file.name(), ShowServers(show_servers) => show_servers.name(), ShowPeers(show_peers) => show_peers.name(), ShowQueryCache(show_query_cache) => show_query_cache.name(), @@ -137,7 +140,22 @@ impl Parser { "show" => match iter.next().ok_or(Error::Syntax)?.trim() { "clients" => ParseResult::ShowClients(ShowClients::parse(&sql)?), "pools" => ParseResult::ShowPools(ShowPools::parse(&sql)?), - "config" => ParseResult::ShowConfig(ShowConfig::parse(&sql)?), + "config" => { + let next_keyword = iter.clone().find_map(|token| { + let trimmed = token.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } + }); + + if next_keyword == Some("file") { + ParseResult::ShowConfigFile(ShowConfigFile::parse(&sql)?) + } else { + ParseResult::ShowConfig(ShowConfig::parse(&sql)?) + } + } "servers" => ParseResult::ShowServers(ShowServers::parse(&sql)?), "server" => match iter.next().ok_or(Error::Syntax)?.trim() { "memory" => ParseResult::ShowServerMemory(ShowServerMemory::parse(&sql)?), @@ -229,4 +247,10 @@ mod tests { let result = Parser::parse("SHOW CLIENT MEMORY;"); assert!(matches!(result, Ok(ParseResult::ShowClientMemory(_)))); } + + #[test] + fn parses_show_config_file_command() { + let result = Parser::parse("SHOW CONFIG FILE;"); + assert!(matches!(result, Ok(ParseResult::ShowConfigFile(_)))); + } } diff --git a/pgdog/src/admin/show_config_file.rs b/pgdog/src/admin/show_config_file.rs new file mode 100644 index 00000000..0123de7f --- /dev/null +++ b/pgdog/src/admin/show_config_file.rs @@ -0,0 +1,55 @@ +//! SHOW CONFIG FILE command. + +use crate::config::config; + +use super::prelude::*; + +pub struct ShowConfigFile; + +#[async_trait] +impl Command for ShowConfigFile { + fn name(&self) -> String { + "SHOW".into() + } + + fn parse(_sql: &str) -> Result { + Ok(Self {}) + } + + async fn execute(&self) -> Result, Error> { + let config = config(); + let snapshot = sanitize_for_toml(config.config.clone()); + let toml = toml::to_string_pretty(&snapshot)?; + + let mut messages = vec![RowDescription::new(&[Field::text("pgdog.toml")]).message()?]; + + let mut row = DataRow::new(); + row.add(&toml); + messages.push(row.message()?); + + Ok(messages) + } +} + +fn sanitize_for_toml(mut config: crate::config::Config) -> crate::config::Config { + let max_i64 = i64::MAX as u64; + let max_i64_usize = i64::MAX as usize; + + if config.general.client_idle_timeout > max_i64 { + config.general.client_idle_timeout = max_i64; + } + + if config.general.query_timeout > max_i64 { + config.general.query_timeout = max_i64; + } + + if config.general.lsn_check_delay > max_i64 { + config.general.lsn_check_delay = max_i64; + } + + if config.general.prepared_statements_limit > max_i64_usize { + config.general.prepared_statements_limit = max_i64_usize; + } + + config +} diff --git a/pgdog/src/admin/tests/mod.rs b/pgdog/src/admin/tests/mod.rs index a5fda216..e2d92a03 100644 --- a/pgdog/src/admin/tests/mod.rs +++ b/pgdog/src/admin/tests/mod.rs @@ -6,6 +6,7 @@ use crate::net::messages::{DataRow, DataType, FromBytes, Protocol, RowDescriptio use super::show_client_memory::ShowClientMemory; use super::show_config::ShowConfig; +use super::show_config_file::ShowConfigFile; use super::show_lists::ShowLists; use super::show_mirrors::ShowMirrors; use super::show_pools::ShowPools; @@ -197,6 +198,91 @@ async fn show_config_pretty_prints_general_settings() { assert_eq!(connect_timeout, "2s"); } +#[tokio::test(flavor = "current_thread")] +async fn show_config_file_dumps_live_state_as_toml() { + let context = TestAdminContext::new(); + + let mut config = ConfigAndUsers::default(); + config.config.general.default_pool_size = 11; + config.config.general.connect_timeout = 7_000; + config.config.general.log_connections = true; + config.config.general.tls_client_required = true; + config.config.databases.push(Database { + name: "app".into(), + host: "127.0.0.1".into(), + database_name: Some("postgres".into()), + user: Some("postgres".into()), + password: Some("hunter2".into()), + shard: 0, + ..Default::default() + }); + + context.set_config(config); + + let command = ShowConfigFile; + let messages = command + .execute() + .await + .expect("show config file execution failed"); + + assert_eq!(messages.len(), 2, "expected row description plus data row"); + + let row_description = RowDescription::from_bytes(messages[0].payload()) + .expect("row description message should parse"); + let columns: Vec<&str> = row_description + .fields + .iter() + .map(|field| field.name.as_str()) + .collect(); + assert_eq!(columns, vec!["pgdog.toml"]); + assert_eq!(row_description.fields[0].data_type(), DataType::Text); + + let row = DataRow::from_bytes(messages[1].payload()).expect("data row should parse"); + let payload = row.get_text(0).expect("toml payload should be text"); + + let actual_value: toml::Value = payload + .parse() + .expect("payload should parse back into toml value"); + + let general = actual_value + .get("general") + .and_then(|value| value.as_table()) + .expect("general section should exist and be a table"); + assert_eq!( + general + .get("default_pool_size") + .and_then(|v| v.as_integer()), + Some(11) + ); + assert_eq!( + general.get("connect_timeout").and_then(|v| v.as_integer()), + Some(7_000) + ); + assert_eq!( + general.get("log_connections").and_then(|v| v.as_bool()), + Some(true) + ); + assert_eq!( + general.get("tls_client_required").and_then(|v| v.as_bool()), + Some(true) + ); + + let databases = actual_value + .get("databases") + .and_then(|value| value.as_array()) + .expect("databases array should be present"); + assert_eq!(databases.len(), 1); + + let database = databases[0] + .as_table() + .expect("database entry should be a table"); + assert_eq!(database.get("name").and_then(|v| v.as_str()), Some("app")); + assert_eq!( + database.get("password").and_then(|v| v.as_str()), + Some("hunter2") + ); +} + #[tokio::test(flavor = "current_thread")] async fn show_mirrors_reports_counts() { let context = TestAdminContext::new();