use std::sync::Arc;
use tokio::sync::mpsc;
use tonic::{Status, Streaming};
use tracing::{error, info};
use xds_core::{NodeHash, TypeUrl};
use crate::sotw::{SotwHandler, SotwResponse};
use crate::stream::StreamContext;
pub use xds_types::envoy::service::discovery::v3::{DiscoveryRequest, DiscoveryResponse};
#[derive(Debug, Clone, Copy)]
pub struct StreamConfig {
pub type_url: &'static str,
pub service_name: &'static str,
}
impl StreamConfig {
pub const fn new(type_url: &'static str, service_name: &'static str) -> Self {
Self {
type_url,
service_name,
}
}
}
pub mod configs {
use super::*;
pub const CDS: StreamConfig = StreamConfig::new(TypeUrl::CLUSTER, "CDS");
pub const EDS: StreamConfig = StreamConfig::new(TypeUrl::ENDPOINT, "EDS");
pub const LDS: StreamConfig = StreamConfig::new(TypeUrl::LISTENER, "LDS");
pub const RDS: StreamConfig = StreamConfig::new(TypeUrl::ROUTE, "RDS");
pub const SDS: StreamConfig = StreamConfig::new(TypeUrl::SECRET, "SDS");
}
pub enum StreamAction {
SendResponse(DiscoveryResponse),
NoResponse,
Error(Status),
Break,
}
pub async fn handle_discovery_stream<F>(
mut stream: Streaming<DiscoveryRequest>,
tx: mpsc::Sender<Result<DiscoveryResponse, Status>>,
handler: Arc<SotwHandler>,
config: StreamConfig,
convert_response: F,
) where
F: Fn(SotwResponse) -> Result<DiscoveryResponse, Status> + Send + 'static,
{
let mut ctx = StreamContext::new();
let mut node_hash: Option<NodeHash> = None;
info!(
stream = %ctx.id(),
service = config.service_name,
"{} stream started",
config.service_name
);
while let Some(result) = tokio_stream::StreamExt::next(&mut stream).await {
match result {
Ok(request) => {
if !request.type_url.is_empty() && request.type_url != config.type_url {
error!(
stream = %ctx.id(),
expected = config.type_url,
got = %request.type_url,
"invalid type URL for {}",
config.service_name
);
continue;
}
if node_hash.is_none() {
if let Some(ref node) = request.node {
let hash = NodeHash::from_id(&node.id);
ctx.set_node(node.id.clone(), hash);
node_hash = Some(hash);
}
}
let hash = match node_hash {
Some(h) => h,
None => {
error!(
stream = %ctx.id(),
service = config.service_name,
"first request missing required node information"
);
let _ = tx
.send(Err(Status::invalid_argument(
"first request must include node information",
)))
.await;
break;
}
};
match handler.process_request(
&ctx,
config.type_url.into(),
&request.version_info,
&request.resource_names,
hash,
) {
Ok(Some(response)) => match convert_response(response) {
Ok(discovery_response) => {
if tx.send(Ok(discovery_response)).await.is_err() {
break;
}
}
Err(e) => {
error!(
stream = %ctx.id(),
error = %e,
"failed to convert response"
);
let _ = tx.send(Err(e)).await;
break;
}
},
Ok(None) => {
}
Err(e) => {
error!(
stream = %ctx.id(),
error = %e,
"{} request failed",
config.service_name
);
break;
}
}
}
Err(e) => {
error!(
stream = %ctx.id(),
error = %e,
"stream error"
);
break;
}
}
}
info!(
stream = %ctx.id(),
service = config.service_name,
"{} stream ended",
config.service_name
);
}
pub fn convert_sotw_response(
response: SotwResponse,
type_url: &str,
) -> Result<DiscoveryResponse, Status> {
use xds_types::google::protobuf::Any;
let resources: Vec<Any> = response
.resources
.iter()
.map(|r| {
r.encode().map(|encoded| Any {
type_url: encoded.type_url.clone(),
value: encoded.value.clone(),
})
})
.collect::<Result<Vec<_>, _>>()
.map_err(|e| Status::internal(format!("failed to encode resource: {}", e)))?;
Ok(DiscoveryResponse {
version_info: response.version_info,
resources,
type_url: type_url.to_string(),
nonce: response.nonce,
canary: false,
control_plane: None,
resource_errors: vec![],
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stream_config_creation() {
let config = StreamConfig::new(TypeUrl::CLUSTER, "CDS");
assert_eq!(config.type_url, TypeUrl::CLUSTER);
assert_eq!(config.service_name, "CDS");
}
#[test]
fn predefined_configs() {
assert_eq!(configs::CDS.type_url, TypeUrl::CLUSTER);
assert_eq!(configs::EDS.type_url, TypeUrl::ENDPOINT);
assert_eq!(configs::LDS.type_url, TypeUrl::LISTENER);
assert_eq!(configs::RDS.type_url, TypeUrl::ROUTE);
assert_eq!(configs::SDS.type_url, TypeUrl::SECRET);
}
}