feat: initial sunbeam-proxy implementation
Custom Pingora-based edge proxy for the Sunbeam infrastructure stack. - HTTPS termination: mkcert file-based (local dev) or rustls-acme ACME (production) - Host-prefix routing with path-based sub-routing (auth virtual host) - HTTP→HTTPS redirect, WebSocket passthrough - cert-manager HTTP-01 challenge routing via Kubernetes Ingress watcher - TLS cert auto-reload via K8s Secret watcher - JSON structured audit logging (tracing-subscriber) - OpenTelemetry OTLP stub (disabled by default) - Multi-stage Dockerfile: musl static binary on chainguard/static distroless image Signed-off-by: Sienna Meridian Satterwhite <sienna@sunbeam.pt>
This commit is contained in:
127
src/main.rs
Normal file
127
src/main.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
mod acme;
|
||||
mod cert;
|
||||
mod config;
|
||||
mod proxy;
|
||||
mod telemetry;
|
||||
mod watcher;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use kube::Client;
|
||||
use pingora::server::{configuration::Opt, Server};
|
||||
use pingora_proxy::http_proxy_service;
|
||||
use proxy::SunbeamProxy;
|
||||
use std::sync::RwLock;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Install the aws-lc-rs crypto provider for rustls before any TLS init.
|
||||
// Required because rustls 0.23 no longer auto-selects a provider at compile time.
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
.expect("crypto provider already installed");
|
||||
|
||||
let config_path = std::env::var("SUNBEAM_CONFIG")
|
||||
.unwrap_or_else(|_| "/etc/pingora/config.toml".to_string());
|
||||
let cfg = config::Config::load(&config_path)?;
|
||||
|
||||
// 1. Init telemetry (JSON logs + optional OTEL traces).
|
||||
telemetry::init(&cfg.telemetry.otlp_endpoint);
|
||||
|
||||
// 2. Detect --upgrade flag. When present, Pingora inherits listening socket
|
||||
// FDs from the upgrade Unix socket instead of binding fresh ports, enabling
|
||||
// zero-downtime cert/config reloads triggered by the K8s watcher below.
|
||||
let upgrade = std::env::args().any(|a| a == "--upgrade");
|
||||
|
||||
// 3. Fetch the TLS cert from K8s before Pingora binds the TLS port.
|
||||
// The Client is created and dropped within this temp runtime — we do NOT
|
||||
// carry it across runtime boundaries, which would kill its tower workers.
|
||||
// The watcher thread creates its own fresh Client on its own runtime.
|
||||
let k8s_available = {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
rt.block_on(async {
|
||||
match Client::try_default().await {
|
||||
Ok(c) => {
|
||||
if !upgrade {
|
||||
if let Err(e) =
|
||||
cert::fetch_and_write(&c, &cfg.tls.cert_path, &cfg.tls.key_path).await
|
||||
{
|
||||
// Non-fatal: Secret may not exist yet on first deploy (cert-manager
|
||||
// is still issuing), or the Secret name may differ in dev.
|
||||
tracing::warn!(error = %e, "cert fetch from K8s failed; using existing files");
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "no K8s client; cert auto-reload and ACME routing disabled");
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let opt = Opt {
|
||||
upgrade,
|
||||
daemon: false,
|
||||
nocapture: false,
|
||||
test: false,
|
||||
conf: None,
|
||||
};
|
||||
|
||||
// 4. Create Pingora server and bootstrap (binds ports or inherits FDs).
|
||||
let mut server = Server::new(Some(opt))?;
|
||||
server.bootstrap();
|
||||
|
||||
// 5. Shared ACME challenge route table. Populated by the Ingress watcher;
|
||||
// consulted by the proxy for every /.well-known/acme-challenge/ request.
|
||||
// Uses std::sync::RwLock so reads are sync and lock-guard-safe across
|
||||
// Pingora's async proxy calls without cross-runtime waker concerns.
|
||||
let acme_routes: acme::AcmeRoutes = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
let proxy = SunbeamProxy {
|
||||
routes: cfg.routes.clone(),
|
||||
acme_routes: acme_routes.clone(),
|
||||
};
|
||||
let mut svc = http_proxy_service(&server.configuration, proxy);
|
||||
|
||||
// Port 80: plain HTTP — 301 → HTTPS, except for ACME HTTP-01 challenges.
|
||||
// Port 443: TLS-terminated HTTPS. Cert written to /etc/tls/ by cert::* above.
|
||||
svc.add_tcp(&cfg.listen.http);
|
||||
svc.add_tls(&cfg.listen.https, &cfg.tls.cert_path, &cfg.tls.key_path)?;
|
||||
|
||||
server.add_service(svc);
|
||||
|
||||
// 6. Background K8s watchers on their own OS thread + tokio runtime so they
|
||||
// don't interfere with Pingora's internal runtime. A fresh Client is
|
||||
// created here so its tower workers live on this runtime (not the
|
||||
// now-dropped temp runtime from step 3).
|
||||
if k8s_available {
|
||||
let cert_path = cfg.tls.cert_path.clone();
|
||||
let key_path = cfg.tls.key_path.clone();
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("watcher runtime");
|
||||
rt.block_on(async move {
|
||||
let client = match Client::try_default().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "watcher: failed to create K8s client; watchers disabled");
|
||||
return;
|
||||
}
|
||||
};
|
||||
tokio::join!(
|
||||
acme::watch_ingresses(client.clone(), acme_routes),
|
||||
watcher::run_watcher(client, cert_path, key_path),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tracing::info!(upgrade, "sunbeam-proxy starting");
|
||||
server.run_forever();
|
||||
}
|
||||
Reference in New Issue
Block a user