added healthcheck command - fixes #1432, fixes #1453

This commit is contained in:
Eugene 2025-08-23 19:37:30 +02:00
parent 45c84e9593
commit bf98c3c595
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
15 changed files with 151 additions and 58 deletions

86
Cargo.lock generated
View file

@ -197,13 +197,29 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "asn1-rs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
dependencies = [
"asn1-rs-derive 0.5.1",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "asn1-rs"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
dependencies = [
"asn1-rs-derive",
"asn1-rs-derive 0.6.0",
"asn1-rs-impl",
"displaydoc",
"nom",
@ -213,6 +229,18 @@ dependencies = [
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"synstructure",
]
[[package]]
name = "asn1-rs-derive"
version = "0.6.0"
@ -1059,13 +1087,27 @@ dependencies = [
"zeroize",
]
[[package]]
name = "der-parser"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
dependencies = [
"asn1-rs 0.6.2",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "der-parser"
version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
dependencies = [
"asn1-rs",
"asn1-rs 0.7.1",
"displaydoc",
"nom",
"num-bigint",
@ -2676,13 +2718,22 @@ dependencies = [
"memchr",
]
[[package]]
name = "oid-registry"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
dependencies = [
"asn1-rs 0.6.2",
]
[[package]]
name = "oid-registry"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
dependencies = [
"asn1-rs",
"asn1-rs 0.7.1",
]
[[package]]
@ -3516,6 +3567,7 @@ dependencies = [
"pem",
"rustls-pki-types",
"time",
"x509-parser 0.16.0",
"yasna",
"zeroize",
]
@ -5492,6 +5544,7 @@ dependencies = [
"futures",
"notify",
"rcgen",
"reqwest",
"rustls",
"schemars",
"sd-notify",
@ -5583,7 +5636,7 @@ dependencies = [
"uuid",
"warpgate-sso",
"webpki",
"x509-parser",
"x509-parser 0.17.0",
]
[[package]]
@ -5691,6 +5744,7 @@ dependencies = [
"poem-openapi",
"regex",
"reqwest",
"rustls-pemfile",
"sea-orm",
"serde",
"serde_json",
@ -6391,18 +6445,36 @@ dependencies = [
"tap",
]
[[package]]
name = "x509-parser"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
dependencies = [
"asn1-rs 0.6.2",
"data-encoding",
"der-parser 9.0.0",
"lazy_static",
"nom",
"oid-registry 0.7.1",
"ring",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "x509-parser"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460"
dependencies = [
"asn1-rs",
"asn1-rs 0.7.1",
"data-encoding",
"der-parser",
"der-parser 10.0.0",
"lazy_static",
"nom",
"oid-registry",
"oid-registry 0.8.1",
"rusticata-macros",
"thiserror 2.0.12",
"time",

View file

@ -63,6 +63,15 @@ rand_core = { version = "0.6", features = ["std"], default-features = false }
dialoguer = { version = "0.11", default-features = false, features = ["editor", "password"] }
tokio = { version = "1.20", features = ["tracing", "signal", "macros", "rt-multi-thread", "io-util"], default-features = false }
governor = { version = "0.10.0", default-features = false, features = ["std", "quanta", "jitter"] }
rcgen = { version = "0.13", features = ["zeroize", "crypto", "aws_lc_rs", "pem", "x509-parser"], default-features = false }
x509-parser = { version = "0.17.0", default-features = false }
uuid = { version = "1.3", features = ["v4", "serde"], default-features = false }
reqwest = { version = "0.12", features = [
"http2", # required for connecting to targets behind AWS ELB
"rustls-tls-native-roots-no-provider",
"stream",
"gzip",
], default-features = false }
[profile.release]
lto = true

View file

@ -47,11 +47,10 @@ COPY --from=build /opt/warpgate/target/release/warpgate /usr/local/bin/warpgate
VOLUME /data
HEALTHCHECK CMD wget --no-verbose --tries=1 --no-check-certificate --spider http://localhost:8888/@warpgate/api/info || exit 1
HEALTHCHECK CMD warpgate healthcheck
ENV DOCKER=1
USER warpgate
ENTRYPOINT ["warpgate", "--config", "/data/warpgate.yaml"]
CMD ["run"]

View file

@ -226,9 +226,6 @@ pub struct SniCertificateConfig {
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct HttpConfig {
#[serde(default = "_default_false")]
pub enable: bool,
#[serde(default = "_default_http_listen")]
pub listen: ListenEndpoint,
@ -259,7 +256,6 @@ pub struct HttpConfig {
impl Default for HttpConfig {
fn default() -> Self {
HttpConfig {
enable: false,
listen: _default_http_listen(),
external_port: None,
certificate: "".to_owned(),

View file

@ -16,6 +16,10 @@ use crate::WarpgateError;
pub struct ListenEndpoint(SocketAddr);
impl ListenEndpoint {
pub fn address(&self) -> SocketAddr {
self.0
}
pub fn addresses_to_listen_on(&self) -> Result<Vec<SocketAddr>, WarpgateError> {
// For [::], explicitly return both addresses so that we are not affected
// by the state of the ipv6only sysctl.

View file

@ -16,12 +16,8 @@ http = { version = "1.0", default-features = false }
once_cell = { version = "1.17", default-features = false }
poem.workspace = true
poem-openapi.workspace = true
reqwest = { version = "0.12", features = [
"http2", # required for connecting to targets behind AWS ELB
"rustls-tls-native-roots-no-provider",
"stream",
"gzip",
], default-features = false }
reqwest.workspace = true
rustls-pemfile.workspace = true
sea-orm.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -124,11 +124,7 @@ impl Api {
} else {
None
},
http: if config.store.http.enable {
Some(config.store.http.external_port())
} else {
None
},
http: Some(config.store.http.external_port()),
mysql: if config.store.mysql.enable {
Some(config.store.mysql.external_port())
} else {

View file

@ -18,7 +18,8 @@ dialoguer.workspace = true
enum_dispatch.workspace = true
futures.workspace = true
notify = { version = "8.0", default-features = false, features = ["fsevent-sys"] }
rcgen = { version = "0.13", features = ["zeroize", "crypto", "aws_lc_rs", "pem"], default-features = false }
rcgen.workspace = true
reqwest.workspace = true
rustls.workspace = true
serde_json.workspace = true
serde_yaml = { version = "0.9", default-features = false }

View file

@ -6,18 +6,16 @@ use crate::config::load_config;
pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
let config = load_config(&cli.config, true)?;
if config.store.http.enable {
TlsCertificateBundle::from_file(
config
.paths_relative_to
.join(&config.store.http.certificate),
)
TlsCertificateBundle::from_file(
config
.paths_relative_to
.join(&config.store.http.certificate),
)
.await
.with_context(|| "Checking HTTPS certificate".to_string())?;
TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.http.key))
.await
.with_context(|| "Checking HTTPS certificate".to_string())?;
TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.http.key))
.await
.with_context(|| "Checking HTTPS key".to_string())?;
}
.with_context(|| "Checking HTTPS key".to_string())?;
if config.store.mysql.enable {
TlsCertificateBundle::from_file(
config

View file

@ -0,0 +1,27 @@
use anyhow::{Context, Result};
use tokio::time::timeout;
use crate::config::load_config;
pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
let config = load_config(&cli.config, true)?;
let url = format!(
"https://{}/@warpgate/api/info",
config.store.http.listen.address()
);
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.use_rustls_tls()
.build()?;
let response = timeout(std::time::Duration::from_secs(5), client.get(&url).send())
.await
.context("Timeout")?
.context("Failed to send request")?;
response.error_for_status()?;
Ok(())
}

View file

@ -1,6 +1,7 @@
pub mod check;
pub mod client_keys;
mod common;
pub mod healthcheck;
pub mod recover_access;
pub mod run;
pub mod setup;

View file

@ -60,6 +60,14 @@ pub(crate) async fn command(cli: &crate::Cli, enable_admin_token: bool) -> Resul
let mut protocol_futures = futures::stream::FuturesUnordered::new();
protocol_futures.push(
run_protocol_server(
HTTPProtocolServer::new(&services).await?,
config.store.http.listen.clone(),
)
.boxed(),
);
if config.store.ssh.enable {
protocol_futures.push(
run_protocol_server(
@ -70,16 +78,6 @@ pub(crate) async fn command(cli: &crate::Cli, enable_admin_token: bool) -> Resul
);
}
if config.store.http.enable {
protocol_futures.push(
run_protocol_server(
HTTPProtocolServer::new(&services).await?,
config.store.http.listen.clone(),
)
.boxed(),
);
}
if config.store.mysql.enable {
protocol_futures.push(
run_protocol_server(

View file

@ -74,13 +74,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
);
let theme = ColorfulTheme::default();
let mut store = WarpgateConfigStore {
http: HttpConfig {
enable: true,
..Default::default()
},
..Default::default()
};
let mut store = WarpgateConfigStore::default();
// ---
@ -138,7 +132,6 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
}
});
store.http.enable = true;
if let Commands::UnattendedSetup { http_port, .. } = &cli.command {
store.http.listen =
ListenEndpoint::from(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), *http_port));

View file

@ -28,10 +28,10 @@ pub async fn init_logging(config: Option<&WarpgateConfig>, cli: &Cli) {
let registry = tracing_subscriber::registry();
#[cfg(all(debug_assertions, feature = "tokio-console"))]
let console_layer = console_subscriber::spawn();
#[cfg(all(debug_assertions, feature = "tokio-console"))]
let registry = registry.with(console_layer);
// #[cfg(all(debug_assertions, feature = "tokio-console"))]
// let console_layer = console_subscriber::spawn();
// #[cfg(all(debug_assertions, feature = "tokio-console"))]
// let registry = registry.with(console_layer);
let socket_layer = match config {
Some(config) => Some(make_socket_logger_layer(config).await),

View file

@ -93,6 +93,8 @@ pub(crate) enum Commands {
},
/// Show version information
Version,
/// Automatic healthcheck for running Warpgate in a container
Healthcheck,
}
async fn _main() -> Result<()> {
@ -124,6 +126,7 @@ async fn _main() -> Result<()> {
Commands::RecoverAccess { username } => {
crate::commands::recover_access::command(&cli, username).await
}
Commands::Healthcheck => crate::commands::healthcheck::command(&cli).await,
}
}