use std::sync::Arc;
use futures::future::BoxFuture;
use serde::Serialize;
use tokio_util::sync::CancellationToken;
use crate::error::Result;
pub mod names {
pub const CERT_OBTAINING: &str = "cert_obtaining";
pub const CERT_OBTAINED: &str = "cert_obtained";
pub const CERT_RENEWED: &str = "cert_renewed";
pub const CERT_REVOKED: &str = "cert_revoked";
pub const CERT_FAILED: &str = "cert_failed";
pub const TLS_GET_CERTIFICATE: &str = "tls_get_certificate";
pub const CERT_OCSP_REVOKED: &str = "cert_ocsp_revoked";
pub const CACHED_MANAGED_CERT: &str = "cached_managed_cert";
}
#[derive(Debug, Clone)]
pub struct EventContext {
pub ct: CancellationToken,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum EventKind {
CertObtaining(CertObtainingData),
CertObtained(CertObtainedData),
CertRenewed(CertRenewedData),
CertRevoked(CertRevokedData),
CertFailed(CertFailedData),
TlsGetCertificate(TlsGetCertificateData),
CertOcspRevoked(CertOcspRevokedData),
CachedManagedCert(CachedManagedCertData),
}
impl EventKind {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::CertObtaining(_) => names::CERT_OBTAINING,
Self::CertObtained(_) => names::CERT_OBTAINED,
Self::CertRenewed(_) => names::CERT_RENEWED,
Self::CertRevoked(_) => names::CERT_REVOKED,
Self::CertFailed(_) => names::CERT_FAILED,
Self::TlsGetCertificate(_) => names::TLS_GET_CERTIFICATE,
Self::CertOcspRevoked(_) => names::CERT_OCSP_REVOKED,
Self::CachedManagedCert(_) => names::CACHED_MANAGED_CERT,
}
}
#[must_use]
pub fn is_abortable(&self) -> bool {
matches!(self, Self::CertObtaining(_) | Self::TlsGetCertificate(_))
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CertObtainingData {
pub identifier: String,
pub renewal: bool,
pub forced: bool,
pub remaining: Option<std::time::Duration>,
pub issuer: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CertObtainedData {
pub identifier: String,
pub renewal: bool,
pub storage_path: Option<String>,
pub private_key_path: Option<String>,
pub metadata_path: Option<String>,
pub csr_pem: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CertRenewedData {
pub identifier: String,
pub forced: bool,
pub issuer: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct CertRevokedData {
pub identifier: String,
pub issuer: String,
pub reason: u8,
}
#[derive(Debug, Clone, Serialize)]
pub struct CertFailedData {
pub identifier: String,
pub issuers: Vec<String>,
pub error: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct TlsGetCertificateData {
pub server_name: Option<String>,
pub remote_addr: Option<String>,
pub alpn: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CertOcspRevokedData {
pub subjects: Vec<String>,
pub certificate_hash: String,
pub reason: Option<i64>,
pub revoked_at: Option<time::OffsetDateTime>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CachedManagedCertData {
pub subjects: Vec<String>,
pub issuer_key: String,
}
pub type OnEventFn = Arc<
dyn Fn(EventContext, EventKind) -> BoxFuture<'static, crate::error::Result<()>> + Send + Sync,
>;
pub type ShouldEmitFn = Arc<dyn Fn(&EventKind) -> bool + Send + Sync>;
pub(crate) async fn emit(
on_event: Option<&OnEventFn>,
should_emit: Option<&ShouldEmitFn>,
ct: &CancellationToken,
event: EventKind,
) -> Result<()> {
let Some(on_event) = on_event else {
return Ok(());
};
if let Some(filter) = should_emit
&& !filter(&event)
{
return Ok(());
}
let abortable = event.is_abortable();
match on_event(EventContext { ct: ct.clone() }, event).await {
Ok(()) => Ok(()),
Err(err) if abortable => Err(err),
Err(err) => {
tracing::warn!(error = %err, "event callback error (non-abortable event)");
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_names_match_go() {
let e = EventKind::CertObtaining(CertObtainingData {
identifier: "example.com".into(),
renewal: false,
forced: false,
remaining: None,
issuer: None,
});
assert_eq!(e.name(), "cert_obtaining");
assert!(e.is_abortable());
let e = EventKind::CertObtained(CertObtainedData {
identifier: "example.com".into(),
renewal: false,
storage_path: None,
private_key_path: None,
metadata_path: None,
csr_pem: None,
});
assert_eq!(e.name(), "cert_obtained");
assert!(!e.is_abortable());
let renewed = EventKind::CertRenewed(CertRenewedData {
identifier: "example.com".into(),
forced: false,
issuer: "acme".into(),
});
assert_eq!(renewed.name(), "cert_renewed");
let revoked = EventKind::CertRevoked(CertRevokedData {
identifier: "example.com".into(),
issuer: "acme".into(),
reason: 4,
});
assert_eq!(revoked.name(), "cert_revoked");
}
#[tokio::test]
async fn non_abortable_errors_are_swallowed() {
let on_event: OnEventFn = Arc::new(|_ctx, _event| {
Box::pin(async { Err(crate::error::Error::Internal("callback boom".into())) })
});
let event = EventKind::CertObtained(CertObtainedData {
identifier: "x".into(),
renewal: false,
storage_path: None,
private_key_path: None,
metadata_path: None,
csr_pem: None,
});
let ct = CancellationToken::new();
assert!(super::emit(Some(&on_event), None, &ct, event).await.is_ok());
}
#[tokio::test]
async fn abortable_errors_propagate() {
let on_event: OnEventFn = Arc::new(|_ctx, _event| {
Box::pin(async { Err(crate::error::Error::Internal("vetoed".into())) })
});
let event = EventKind::CertObtaining(CertObtainingData {
identifier: "x".into(),
renewal: false,
forced: false,
remaining: None,
issuer: None,
});
let ct = CancellationToken::new();
assert!(
super::emit(Some(&on_event), None, &ct, event)
.await
.is_err()
);
}
#[tokio::test]
async fn filter_skips_callback() {
let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
let c2 = Arc::clone(&called);
let on_event: OnEventFn = Arc::new(move |_ctx, _event| {
let c = Arc::clone(&c2);
Box::pin(async move {
c.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
})
});
let should_emit: ShouldEmitFn = Arc::new(|_e| false);
let event = EventKind::CertObtained(CertObtainedData {
identifier: "x".into(),
renewal: false,
storage_path: None,
private_key_path: None,
metadata_path: None,
csr_pem: None,
});
let ct = CancellationToken::new();
super::emit(Some(&on_event), Some(&should_emit), &ct, event)
.await
.unwrap();
assert!(!called.load(std::sync::atomic::Ordering::SeqCst));
}
}