From a959a50e072c416efc977d34c4fa81e038efe2c0 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher <656602+sdwilsh@users.noreply.github.com> Date: Sat, 11 Oct 2025 07:56:49 -0700 Subject: [PATCH] server: allow specifying the healthcheck addresses This change adds two new optional configuration options: - `ldap_healthcheck_host` to pair with `ldap_host` - `http_healthcheck_host` to pair with `http_host` These both default to `localhost` to preserve the existing behavior. Fixes #700 --- lldap_config.docker_template.toml | 13 ++++++++++++ server/src/cli.rs | 15 ++++++++++++++ server/src/configuration.rs | 33 +++++++++++++++++++++++++++++-- server/src/healthcheck.rs | 18 ++++++++--------- server/src/main.rs | 15 +++++++++++--- 5 files changed, 79 insertions(+), 15 deletions(-) diff --git a/lldap_config.docker_template.toml b/lldap_config.docker_template.toml index 05e63ef..fb78d9d 100644 --- a/lldap_config.docker_template.toml +++ b/lldap_config.docker_template.toml @@ -159,3 +159,16 @@ key_seed = "RanD0m STR1ng" #cert_file="/data/cert.pem" ## Certificate key file. #key_file="/data/key.pem" + +## Options to configure the healthcheck command. +## To set these options from environment variables, use the following format +## (example with http_host): LLDAP_HEALTHCHECK_OPTIONS__HTTP_HOST +[healthcheck_options] +## The host address that the healthcheck should verify for the HTTP server. +## If "http_host" is set to a specific IP address, this must be set to match if the built-in +## healthcheck command is used. Note: if this is an IPv6 address, it must be wrapped in []. +#http_host = "localhost" +## The host address that the healthcheck should verify for the LDAP server. +## If "ldap_host" is set to a specific IP address, this must be set to match if the built-in +## healthcheck command is used. +#ldap_host = "localhost" diff --git a/server/src/cli.rs b/server/src/cli.rs index 7ae7ab8..c0a86d8 100644 --- a/server/src/cli.rs +++ b/server/src/cli.rs @@ -174,6 +174,9 @@ pub struct RunOpts { #[clap(flatten)] pub ldaps_opts: LdapsOpts, + + #[clap(flatten)] + pub healthcheck_opts: HealthcheckOpts, } #[derive(Debug, Parser, Clone)] @@ -264,6 +267,18 @@ pub struct ExportGraphQLSchemaOpts { pub output_file: Option, } +#[derive(Debug, Parser, Clone)] +#[clap(next_help_heading = Some("HEALTHCHECK"))] +pub struct HealthcheckOpts { + /// Change the HTTP Host to test the health of. Default: "localhost" + #[clap(long, env = "LLDAP_HEALTHCHECK_OPTIONS__HTTP_HOST")] + pub healthcheck_http_host: Option, + + /// Change the LDAP Host to test the health of. Default: "localhost" + #[clap(long, env = "LLDAP_HEALTHCHECK_OPTIONS__LDAP_HOST")] + pub healthcheck_ldap_host: Option, +} + pub fn init() -> CLIOpts { CLIOpts::parse() } diff --git a/server/src/configuration.rs b/server/src/configuration.rs index bd15d06..d26e7ef 100644 --- a/server/src/configuration.rs +++ b/server/src/configuration.rs @@ -1,7 +1,7 @@ use crate::{ cli::{ - GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts, - TrueFalseAlways, + GeneralConfigOpts, HealthcheckOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, + TestEmailOpts, TrueFalseAlways, }, database_string::DatabaseUrl, }; @@ -83,6 +83,21 @@ impl std::default::Default for LdapsOptions { } } +#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)] +#[builder(pattern = "owned")] +pub struct HealthcheckOptions { + #[builder(default = r#"String::from("localhost")"#)] + pub http_host: String, + #[builder(default = r#"String::from("localhost")"#)] + pub ldap_host: String, +} + +impl std::default::Default for HealthcheckOptions { + fn default() -> Self { + HealthcheckOptionsBuilder::default().build().unwrap() + } +} + #[derive(Clone, Deserialize, Serialize, derive_more::Debug)] #[debug(r#""{_0}""#)] pub struct HttpUrl(pub Url); @@ -138,6 +153,8 @@ pub struct Configuration { #[serde(skip)] #[builder(field(private), default = "None")] server_setup: Option, + #[builder(default)] + pub healthcheck_options: HealthcheckOptions, } impl std::default::Default for Configuration { @@ -523,6 +540,18 @@ impl ConfigOverrider for SmtpOpts { } } +impl ConfigOverrider for HealthcheckOpts { + fn override_config(&self, config: &mut Configuration) { + self.healthcheck_http_host + .as_ref() + .inspect(|host| config.healthcheck_options.http_host.clone_from(host)); + + self.healthcheck_ldap_host + .as_ref() + .inspect(|host| config.healthcheck_options.ldap_host.clone_from(host)); + } +} + fn extract_keys(dict: &figment::value::Dict) -> HashSet { use figment::value::{Dict, Value}; fn process_value(value: &Dict, keys: &mut HashSet, path: &mut Vec) { diff --git a/server/src/healthcheck.rs b/server/src/healthcheck.rs index 0f25889..b7daa1f 100644 --- a/server/src/healthcheck.rs +++ b/server/src/healthcheck.rs @@ -70,8 +70,8 @@ where } #[instrument(level = "info", err)] -pub async fn check_ldap(port: u16) -> Result<()> { - check_ldap_endpoint(TcpStream::connect(format!("localhost:{port}")).await?).await +pub async fn check_ldap(host: &str, port: u16) -> Result<()> { + check_ldap_endpoint(TcpStream::connect((host, port)).await?).await } fn get_root_certificates() -> rustls::RootCertStore { @@ -126,21 +126,19 @@ fn get_tls_connector(ldaps_options: &LdapsOptions) -> Result Ok(std::sync::Arc::new(client_config).into()) } -#[instrument(skip_all, level = "info", err, fields(port = %ldaps_options.port))] -pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> { +#[instrument(skip_all, level = "info", err, fields(host = %host, port = %ldaps_options.port))] +pub async fn check_ldaps(host: &str, ldaps_options: &LdapsOptions) -> Result<()> { if !ldaps_options.enabled { info!("LDAPS not enabled"); return Ok(()); }; let tls_connector = get_tls_connector(ldaps_options).context("while preparing the tls connection")?; - let url = format!("localhost:{}", ldaps_options.port); check_ldap_endpoint( tls_connector .connect( - rustls::ServerName::try_from("localhost") - .context("while parsing the server name")?, - TcpStream::connect(&url) + rustls::ServerName::try_from(host).context("while parsing the server name")?, + TcpStream::connect((host, ldaps_options.port)) .await .context("while connecting TCP")?, ) @@ -151,8 +149,8 @@ pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> { } #[instrument(level = "info", err)] -pub async fn check_api(port: u16) -> Result<()> { - reqwest::get(format!("http://localhost:{port}/health")) +pub async fn check_api(host: &str, port: u16) -> Result<()> { + reqwest::get(format!("http://{host}:{port}/health")) .await? .error_for_status()?; info!("Success"); diff --git a/server/src/main.rs b/server/src/main.rs index cf58072..8af06df 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -255,9 +255,18 @@ async fn run_healthcheck(opts: RunOpts) -> Result<()> { use tokio::time::timeout; let delay = Duration::from_millis(3000); let (ldap, ldaps, api) = tokio::join!( - timeout(delay, healthcheck::check_ldap(config.ldap_port)), - timeout(delay, healthcheck::check_ldaps(&config.ldaps_options)), - timeout(delay, healthcheck::check_api(config.http_port)), + timeout( + delay, + healthcheck::check_ldap(&config.healthcheck_options.ldap_host, config.ldap_port) + ), + timeout( + delay, + healthcheck::check_ldaps(&config.healthcheck_options.ldap_host, &config.ldaps_options) + ), + timeout( + delay, + healthcheck::check_api(&config.healthcheck_options.http_host, config.http_port) + ), ); let failure = [ldap, ldaps, api]