Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
791 changes: 286 additions & 505 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ hostname = "0.4.0"
http = "1.3.1"
http-body-util = "0.1.3"
humantime = "2.2.0"
ic-bn-lib = { version = "0.2.1", features = [
ic-bn-lib = { version = "0.4", features = [
"acme-alpn",
"cert-providers",
"clients-hyper",
"lb",
"vector",
] }
ic-bn-lib-common = "0.2.1"
ic-custom-domains-backend = "0.2"
ic-custom-domains-base = "0.2"
itertools = "0.15.0"
prometheus = "0.14.0"
serde = "1.0"
Expand Down
8 changes: 4 additions & 4 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,9 @@ pub fn setup_api_axum_router(
if cli.sev_snp.sev_snp_enable {
router = router.route(
"/sev-snp/report",
post(ic_bn_lib::utils::sev_snp::handler)
post(ic_bn_lib::sev_snp::handler)
.with_state(
ic_bn_lib::utils::sev_snp::SevSnpState::new(
ic_bn_lib::sev_snp::SevSnpState::new(
cli.sev_snp.sev_snp_cache_ttl,
cli.sev_snp.sev_snp_cache_size,
)
Expand Down Expand Up @@ -237,7 +237,7 @@ mod test {

#[tokio::test]
async fn test_api_auth() {
let _ = ic_bn_lib::rustls::crypto::ring::default_provider().install_default();
let _ = ic_bn_lib::rustls::crypto::aws_lc_rs::default_provider().install_default();

let args: Vec<&str> = vec!["", "--config-path", "foo", "--api-token", "deadbeef"];
let cli = Cli::parse_from(args);
Expand Down Expand Up @@ -301,7 +301,7 @@ mod test {

#[tokio::test]
async fn test_config() {
let _ = ic_bn_lib::rustls::crypto::ring::default_provider().install_default();
let _ = ic_bn_lib::rustls::crypto::aws_lc_rs::default_provider().install_default();

let args: Vec<&str> = vec!["", "--config-path", "foo", "--api-token", "deadbeef"];
let cli = Cli::parse_from(args);
Expand Down
178 changes: 168 additions & 10 deletions src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,14 @@ use axum::{body::Body, extract::Request, response::Response};
use derive_new::new;
use http::{Uri, Version, uri::PathAndQuery};
use ic_bn_lib::{
http::headers::strip_connection_headers,
utils::{
http::{ClientHttp, Error as HttpError, headers::strip_connection_headers},
lb::{
ChecksTarget, ExecutesRequest, TargetState,
backend_router::BackendRouter,
distributor::{self, Strategy},
health_check::{self},
},
};
use ic_bn_lib_common::{
traits::{
Run,
http::ClientHttp,
utils::{ChecksTarget, ExecutesRequest},
},
types::{http::Error as HttpError, utils::TargetState},
tasks::Run,
};
use itertools::Itertools;
use prometheus::Registry;
Expand Down Expand Up @@ -483,3 +477,167 @@ impl ExecutesRequest<Arc<Backend>> for RequestExecutor {
self.client.execute(req).await
}
}

#[cfg(test)]
mod test {
use std::{net::SocketAddr, path::PathBuf};

use axum::Router;
use http::StatusCode;
use ic_bn_lib::http::{HyperClient, Server, server::ServerOptions};
use tokio::net::TcpListener;

use super::*;

fn backend_conf(name: &str, url: &str) -> BackendConf {
BackendConf {
name: name.into(),
url: Url::parse(url).unwrap(),
enabled: true,
weight: 1,
}
}

fn new_manager() -> BackendManager {
let client = Arc::new(HyperClient::default());
BackendManager::new(
client,
PathBuf::new(),
Duration::from_millis(50),
Duration::from_millis(500),
&Registry::new(),
)
}

/// Spawns a minimal HTTP server that responds to any request (including `/health`) with 200.
async fn spawn_healthy_backend() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();

let server = Server::new(
ic_bn_lib::network::Addr::Tcp(addr),
Router::new().fallback(async || StatusCode::OK),
ServerOptions::default(),
ic_bn_lib::http::server::metrics::Metrics::new(&Registry::new()),
None,
);

tokio::spawn(async move {
server
.serve_with_listener(listener.into(), CancellationToken::new())
.await
.unwrap();
});

addr
}

#[test]
fn test_config_default() {
let cfg = Config::default();
assert_eq!(cfg.strategy, Strategy::LeastOutstandingRequests);
assert!(cfg.backends.is_empty());
assert!(cfg.fallback.is_none());
}

#[test]
fn test_backend_from_conf_builds_health_uri() {
let conf = backend_conf("foo", "http://example.com:1234/base/path");
let backend: Backend = conf.into();

assert_eq!(backend.name, "foo");
assert_eq!(
backend.uri_health.to_string(),
"http://example.com:1234/health"
);
}

#[tokio::test]
async fn test_set_config_rejects_duplicate_names() {
let bm = new_manager();

let config = Config {
strategy: Strategy::LeastOutstandingRequests,
backends: vec![backend_conf("dup", "http://127.0.0.1:1")],
fallback: Some(vec![backend_conf("dup", "http://127.0.0.1:2")]),
};

let err = bm.set_config(config).await.unwrap_err();
assert!(err.to_string().contains("Non-unique"));
}

#[tokio::test]
async fn test_set_config_empty_backends_ok() {
let bm = new_manager();

let config = Config {
strategy: Strategy::LeastOutstandingRequests,
backends: vec![],
fallback: None,
};

bm.set_config(config.clone()).await.unwrap();
assert_eq!(bm.get_config().await, config);
assert!(bm.get_backend_router().is_none());
assert!(bm.get_healthy_nodes().is_empty());
}

#[tokio::test]
async fn test_set_backend_state_not_found() {
let bm = new_manager();
let err = bm
.set_backend_state("nonexistent".into(), true)
.await
.unwrap_err();
assert!(err.to_string().contains("Backend not found"));
}

#[tokio::test]
async fn test_set_backend_state_toggle() {
let bm = new_manager();

// Backend is disabled initially - no health check happens
let config = Config {
strategy: Strategy::LeastOutstandingRequests,
backends: vec![BackendConf {
enabled: false,
..backend_conf("foo", "http://127.0.0.1:1")
}],
fallback: None,
};
bm.set_config(config).await.unwrap();
assert!(bm.get_backend_router().is_none());

// Enabling it triggers a health check against a closed port -> stays unhealthy
bm.set_backend_state("foo".into(), true).await.unwrap();
assert!(bm.get_healthy_nodes().is_empty());
assert!(bm.get_backend_router().is_none());

// Disabling it again removes the router entirely
bm.set_backend_state("foo".into(), false).await.unwrap();
assert!(bm.get_backend_router().is_none());
}

#[tokio::test]
async fn test_get_backend_router_falls_back_when_main_unhealthy() {
let bm = new_manager();
let healthy_addr = spawn_healthy_backend().await;

let config = Config {
strategy: Strategy::LeastOutstandingRequests,
backends: vec![backend_conf("main", "http://127.0.0.1:1")],
fallback: Some(vec![backend_conf("fb", &format!("http://{healthy_addr}"))]),
};

bm.set_config(config).await.unwrap();

// Main backend is unreachable, so it has no healthy nodes
assert!(bm.get_healthy_nodes().is_empty());

// But the router falls back to the healthy fallback backend
let router = bm.get_backend_router().expect("should fall back");
let healthy = router.get_healthy();
assert_eq!(healthy.len(), 1);
assert_eq!(healthy[0].name, "fb");
}
}
17 changes: 8 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration};
use clap::{Args, Parser};
use fqdn::FQDN;
use humantime::parse_duration;
use ic_bn_lib_common::{
use ic_bn_lib::{
custom_domains::base::cli::CustomDomainsCli,
dns::cli::DnsCli,
http::{client::cli::HttpClientCli, middleware::waf::WafCli, server::cli::HttpServerCli},
parse_size_usize,
types::{
acme::AcmeUrl,
dns::DnsCli,
http::{HttpClientCli, HttpServerCli, WafCli},
vector::VectorCli,
},
tls::acme::AcmeUrl,
vector::cli::VectorCli,
};

use crate::core::{AUTHOR_NAME, SERVICE_NAME};
Expand Down Expand Up @@ -52,7 +51,7 @@ pub struct Cli {
pub dns: DnsCli,

#[command(flatten, next_help_heading = "Custom Domains")]
pub custom_domains: Option<ic_custom_domains_base::cli::CustomDomainsCli>,
pub custom_domains: Option<CustomDomainsCli>,

#[command(flatten, next_help_heading = "Certificates")]
pub cert: Cert,
Expand All @@ -65,7 +64,7 @@ pub struct Cli {

#[cfg(all(target_os = "linux", feature = "sev-snp"))]
#[command(flatten, next_help_heading = "SEV-SNP")]
pub sev_snp: ic_bn_lib_common::types::utils::SevSnpCli,
pub sev_snp: ic_bn_lib::sev_snp::SevSnpCli,

#[command(flatten, next_help_heading = "Misc")]
pub misc: Misc,
Expand Down
17 changes: 6 additions & 11 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,17 @@ use std::sync::{Arc, OnceLock};
use anyhow::{Context, Error};
use axum::{Router, body::Body};
use ic_bn_lib::{
dns::{Options as DnsOptions, resolvers::Resolver},
http::{
self as bnhttp, HyperClient, HyperClientLeastLoaded, ReqwestClient, ServerBuilder, dns,
middleware::waf::WafLayer, redirect_to_https,
self as bnhttp, ClientHttp, HyperClient, HyperClientLeastLoaded, ReqwestClient,
ServerBuilder, client::ClientOptions, middleware::waf::WafLayer, redirect_to_https,
server::metrics::Metrics,
},
rustls,
tasks::TaskManager,
tls::{prepare_client_config, verify::NoopServerCertVerifier},
vector::{VectorOptions, client::Vector},
};
use ic_bn_lib_common::{
traits::http::ClientHttp,
types::{
dns::Options as DnsOptions,
http::{ClientOptions, Metrics},
},
};
use prometheus::Registry;
use tokio::{
select,
Expand Down Expand Up @@ -48,7 +43,7 @@ pub async fn main(
cli: &Cli,
log_handle: Handle<LevelFilter, tracing_subscriber::Registry>,
) -> Result<(), Error> {
let _ = ic_bn_lib::rustls::crypto::ring::default_provider().install_default();
let _ = ic_bn_lib::rustls::crypto::aws_lc_rs::default_provider().install_default();

ENV.set(cli.misc.env.clone()).unwrap();
HOSTNAME.set(cli.misc.hostname.clone()).unwrap();
Expand All @@ -74,7 +69,7 @@ pub async fn main(
http_client_opts.tls_config = Some(http_client_tls_config);

let dns_opts: DnsOptions = (&cli.dns).into();
let resolver = dns::Resolver::new(dns_opts).context("unable to create DNS resolver")?;
let resolver = Resolver::new(dns_opts).context("unable to create DNS resolver")?;

let http_client_reqwest = Arc::new(
ReqwestClient::new(http_client_opts.clone(), Some(resolver.clone()))
Expand Down
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ mod test {
use clap::Parser;
use http::StatusCode;
use ic_bn_lib::{
http::Server,
http::{
Server,
server::{ServerOptions, metrics::Metrics},
},
network::Addr,
reqwest,
tests::{TEST_CERT_1, TEST_KEY_1},
};
use ic_bn_lib_common::types::http::{Addr, Metrics, ServerOptions};
use prometheus::Registry;
use serde_json::json;
use tempfile::tempdir;
Expand Down
27 changes: 25 additions & 2 deletions src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ use async_trait::async_trait;
use axum::{Router, extract::State, response::IntoResponse, routing::get};
use bytes::{BufMut, Bytes, BytesMut};
use http::header::CONTENT_TYPE;
use ic_bn_lib::tasks::TaskManager;
use ic_bn_lib_common::traits::Run;
use ic_bn_lib::tasks::{Run, TaskManager};
use prometheus::{Encoder, IntGauge, Registry, TextEncoder, register_int_gauge_with_registry};
use tikv_jemalloc_ctl::{epoch, stats};
use tokio_util::sync::CancellationToken;
Expand Down Expand Up @@ -137,3 +136,27 @@ pub fn setup(registry: &Registry, tasks: &mut TaskManager) -> Router {
)
.with_state(cache)
}

#[cfg(test)]
mod test {
use http::StatusCode;

use super::*;

#[tokio::test]
async fn test_metrics_handler_serves_cached_snapshot() {
let registry = Registry::new();
let cache = Arc::new(MetricsCache::new());
let runner = MetricsRunner::new(cache.clone(), &registry);

runner.update().unwrap();

let response = handler(State(cache)).await.into_response();

assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get(CONTENT_TYPE).unwrap(),
PROMETHEUS_CONTENT_TYPE
);
}
}
6 changes: 4 additions & 2 deletions src/middleware/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ use derive_new::new;
use http::{HeaderValue, StatusCode};
use ic_bn_lib::{
dyn_event,
http::{extract_authority, headers::X_REAL_IP, http_method, http_version},
http::{
extract_authority, headers::X_REAL_IP, http_method, http_version, server::conn::ConnInfo,
},
network::TlsInfo,
vector::client::Vector,
};
use ic_bn_lib_common::types::http::{ConnInfo, TlsInfo};
use prometheus::{
HistogramVec, IntCounterVec, Registry, register_histogram_vec_with_registry,
register_int_counter_vec_with_registry,
Expand Down
Loading
Loading