1use std::sync::Arc;
8
9use tokio::sync::mpsc;
10use tonic::{Status, Streaming};
11use tracing::{error, info};
12
13use xds_core::{NodeHash, TypeUrl};
14
15use crate::sotw::{SotwHandler, SotwResponse};
16use crate::stream::StreamContext;
17
18pub use xds_types::envoy::service::discovery::v3::{DiscoveryRequest, DiscoveryResponse};
19
20#[derive(Debug, Clone, Copy)]
22pub struct StreamConfig {
23 pub type_url: &'static str,
25 pub service_name: &'static str,
27}
28
29impl StreamConfig {
30 pub const fn new(type_url: &'static str, service_name: &'static str) -> Self {
32 Self {
33 type_url,
34 service_name,
35 }
36 }
37}
38
39pub mod configs {
41 use super::*;
42
43 pub const CDS: StreamConfig = StreamConfig::new(TypeUrl::CLUSTER, "CDS");
45 pub const EDS: StreamConfig = StreamConfig::new(TypeUrl::ENDPOINT, "EDS");
47 pub const LDS: StreamConfig = StreamConfig::new(TypeUrl::LISTENER, "LDS");
49 pub const RDS: StreamConfig = StreamConfig::new(TypeUrl::ROUTE, "RDS");
51 pub const SDS: StreamConfig = StreamConfig::new(TypeUrl::SECRET, "SDS");
53}
54
55pub enum StreamAction {
57 SendResponse(DiscoveryResponse),
59 NoResponse,
61 Error(Status),
63 Break,
65}
66
67pub async fn handle_discovery_stream<F>(
84 mut stream: Streaming<DiscoveryRequest>,
85 tx: mpsc::Sender<Result<DiscoveryResponse, Status>>,
86 handler: Arc<SotwHandler>,
87 config: StreamConfig,
88 convert_response: F,
89) where
90 F: Fn(SotwResponse) -> Result<DiscoveryResponse, Status> + Send + 'static,
91{
92 let mut ctx = StreamContext::new();
93 let mut node_hash: Option<NodeHash> = None;
94
95 info!(
96 stream = %ctx.id(),
97 service = config.service_name,
98 "{} stream started",
99 config.service_name
100 );
101
102 while let Some(result) = tokio_stream::StreamExt::next(&mut stream).await {
103 match result {
104 Ok(request) => {
105 if !request.type_url.is_empty() && request.type_url != config.type_url {
107 error!(
108 stream = %ctx.id(),
109 expected = config.type_url,
110 got = %request.type_url,
111 "invalid type URL for {}",
112 config.service_name
113 );
114 continue;
115 }
116
117 if node_hash.is_none() {
119 if let Some(ref node) = request.node {
120 let hash = NodeHash::from_id(&node.id);
121 ctx.set_node(node.id.clone(), hash);
122 node_hash = Some(hash);
123 }
124 }
125
126 let hash = match node_hash {
127 Some(h) => h,
128 None => {
129 error!(
131 stream = %ctx.id(),
132 service = config.service_name,
133 "first request missing required node information"
134 );
135 let _ = tx
136 .send(Err(Status::invalid_argument(
137 "first request must include node information",
138 )))
139 .await;
140 break;
141 }
142 };
143
144 match handler.process_request(
146 &ctx,
147 config.type_url.into(),
148 &request.version_info,
149 &request.resource_names,
150 hash,
151 ) {
152 Ok(Some(response)) => match convert_response(response) {
153 Ok(discovery_response) => {
154 if tx.send(Ok(discovery_response)).await.is_err() {
155 break;
156 }
157 }
158 Err(e) => {
159 error!(
160 stream = %ctx.id(),
161 error = %e,
162 "failed to convert response"
163 );
164 let _ = tx.send(Err(e)).await;
165 break;
166 }
167 },
168 Ok(None) => {
169 }
171 Err(e) => {
172 error!(
173 stream = %ctx.id(),
174 error = %e,
175 "{} request failed",
176 config.service_name
177 );
178 break;
179 }
180 }
181 }
182 Err(e) => {
183 error!(
184 stream = %ctx.id(),
185 error = %e,
186 "stream error"
187 );
188 break;
189 }
190 }
191 }
192
193 info!(
194 stream = %ctx.id(),
195 service = config.service_name,
196 "{} stream ended",
197 config.service_name
198 );
199}
200
201pub fn convert_sotw_response(
206 response: SotwResponse,
207 type_url: &str,
208) -> Result<DiscoveryResponse, Status> {
209 use xds_types::google::protobuf::Any;
210
211 let resources: Vec<Any> = response
212 .resources
213 .iter()
214 .map(|r| {
215 r.encode().map(|encoded| Any {
216 type_url: encoded.type_url.clone(),
217 value: encoded.value.clone(),
218 })
219 })
220 .collect::<Result<Vec<_>, _>>()
221 .map_err(|e| Status::internal(format!("failed to encode resource: {}", e)))?;
222
223 Ok(DiscoveryResponse {
224 version_info: response.version_info,
225 resources,
226 type_url: type_url.to_string(),
227 nonce: response.nonce,
228 canary: false,
229 control_plane: None,
230 resource_errors: vec![],
231 })
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn stream_config_creation() {
240 let config = StreamConfig::new(TypeUrl::CLUSTER, "CDS");
241 assert_eq!(config.type_url, TypeUrl::CLUSTER);
242 assert_eq!(config.service_name, "CDS");
243 }
244
245 #[test]
246 fn predefined_configs() {
247 assert_eq!(configs::CDS.type_url, TypeUrl::CLUSTER);
248 assert_eq!(configs::EDS.type_url, TypeUrl::ENDPOINT);
249 assert_eq!(configs::LDS.type_url, TypeUrl::LISTENER);
250 assert_eq!(configs::RDS.type_url, TypeUrl::ROUTE);
251 assert_eq!(configs::SDS.type_url, TypeUrl::SECRET);
252 }
253}