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
This commit is contained in:
Shawn Wilsher
2025-10-11 07:56:49 -07:00
committed by nitnelave
parent ab4389fc5f
commit a959a50e07
5 changed files with 79 additions and 15 deletions
+13
View File
@@ -159,3 +159,16 @@ key_seed = "RanD0m STR1ng"
#cert_file="/data/cert.pem" #cert_file="/data/cert.pem"
## Certificate key file. ## Certificate key file.
#key_file="/data/key.pem" #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"
+15
View File
@@ -174,6 +174,9 @@ pub struct RunOpts {
#[clap(flatten)] #[clap(flatten)]
pub ldaps_opts: LdapsOpts, pub ldaps_opts: LdapsOpts,
#[clap(flatten)]
pub healthcheck_opts: HealthcheckOpts,
} }
#[derive(Debug, Parser, Clone)] #[derive(Debug, Parser, Clone)]
@@ -264,6 +267,18 @@ pub struct ExportGraphQLSchemaOpts {
pub output_file: Option<String>, pub output_file: Option<String>,
} }
#[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<String>,
/// Change the LDAP Host to test the health of. Default: "localhost"
#[clap(long, env = "LLDAP_HEALTHCHECK_OPTIONS__LDAP_HOST")]
pub healthcheck_ldap_host: Option<String>,
}
pub fn init() -> CLIOpts { pub fn init() -> CLIOpts {
CLIOpts::parse() CLIOpts::parse()
} }
+31 -2
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
cli::{ cli::{
GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts, GeneralConfigOpts, HealthcheckOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts,
TrueFalseAlways, TestEmailOpts, TrueFalseAlways,
}, },
database_string::DatabaseUrl, 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)] #[derive(Clone, Deserialize, Serialize, derive_more::Debug)]
#[debug(r#""{_0}""#)] #[debug(r#""{_0}""#)]
pub struct HttpUrl(pub Url); pub struct HttpUrl(pub Url);
@@ -138,6 +153,8 @@ pub struct Configuration {
#[serde(skip)] #[serde(skip)]
#[builder(field(private), default = "None")] #[builder(field(private), default = "None")]
server_setup: Option<ServerSetupConfig>, server_setup: Option<ServerSetupConfig>,
#[builder(default)]
pub healthcheck_options: HealthcheckOptions,
} }
impl std::default::Default for Configuration { 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<String> { fn extract_keys(dict: &figment::value::Dict) -> HashSet<String> {
use figment::value::{Dict, Value}; use figment::value::{Dict, Value};
fn process_value(value: &Dict, keys: &mut HashSet<String>, path: &mut Vec<String>) { fn process_value(value: &Dict, keys: &mut HashSet<String>, path: &mut Vec<String>) {
+8 -10
View File
@@ -70,8 +70,8 @@ where
} }
#[instrument(level = "info", err)] #[instrument(level = "info", err)]
pub async fn check_ldap(port: u16) -> Result<()> { pub async fn check_ldap(host: &str, port: u16) -> Result<()> {
check_ldap_endpoint(TcpStream::connect(format!("localhost:{port}")).await?).await check_ldap_endpoint(TcpStream::connect((host, port)).await?).await
} }
fn get_root_certificates() -> rustls::RootCertStore { fn get_root_certificates() -> rustls::RootCertStore {
@@ -126,21 +126,19 @@ fn get_tls_connector(ldaps_options: &LdapsOptions) -> Result<RustlsTlsConnector>
Ok(std::sync::Arc::new(client_config).into()) Ok(std::sync::Arc::new(client_config).into())
} }
#[instrument(skip_all, level = "info", err, fields(port = %ldaps_options.port))] #[instrument(skip_all, level = "info", err, fields(host = %host, port = %ldaps_options.port))]
pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> { pub async fn check_ldaps(host: &str, ldaps_options: &LdapsOptions) -> Result<()> {
if !ldaps_options.enabled { if !ldaps_options.enabled {
info!("LDAPS not enabled"); info!("LDAPS not enabled");
return Ok(()); return Ok(());
}; };
let tls_connector = let tls_connector =
get_tls_connector(ldaps_options).context("while preparing the tls connection")?; get_tls_connector(ldaps_options).context("while preparing the tls connection")?;
let url = format!("localhost:{}", ldaps_options.port);
check_ldap_endpoint( check_ldap_endpoint(
tls_connector tls_connector
.connect( .connect(
rustls::ServerName::try_from("localhost") rustls::ServerName::try_from(host).context("while parsing the server name")?,
.context("while parsing the server name")?, TcpStream::connect((host, ldaps_options.port))
TcpStream::connect(&url)
.await .await
.context("while connecting TCP")?, .context("while connecting TCP")?,
) )
@@ -151,8 +149,8 @@ pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
} }
#[instrument(level = "info", err)] #[instrument(level = "info", err)]
pub async fn check_api(port: u16) -> Result<()> { pub async fn check_api(host: &str, port: u16) -> Result<()> {
reqwest::get(format!("http://localhost:{port}/health")) reqwest::get(format!("http://{host}:{port}/health"))
.await? .await?
.error_for_status()?; .error_for_status()?;
info!("Success"); info!("Success");
+12 -3
View File
@@ -255,9 +255,18 @@ async fn run_healthcheck(opts: RunOpts) -> Result<()> {
use tokio::time::timeout; use tokio::time::timeout;
let delay = Duration::from_millis(3000); let delay = Duration::from_millis(3000);
let (ldap, ldaps, api) = tokio::join!( let (ldap, ldaps, api) = tokio::join!(
timeout(delay, healthcheck::check_ldap(config.ldap_port)), timeout(
timeout(delay, healthcheck::check_ldaps(&config.ldaps_options)), delay,
timeout(delay, healthcheck::check_api(config.http_port)), 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] let failure = [ldap, ldaps, api]