Skip to main content

xds_server/
reflection.rs

1//! gRPC Server Reflection support for xDS services.
2//!
3//! This module provides gRPC reflection capabilities, allowing clients to
4//! discover available services and their message types at runtime. This is
5//! especially useful for debugging and tooling like `grpcurl`.
6//!
7//! # Example
8//!
9//! ```
10//! use xds_server::reflection::ReflectionService;
11//!
12//! // Create a reflection service with no encoded file descriptors
13//! // (pass `&[encoded_fds]` to advertise specific services).
14//! let _reflection = ReflectionService::new(&[]);
15//! ```
16
17#[cfg(feature = "reflection")]
18use tonic_reflection::server::{ServerReflection, ServerReflectionServer};
19
20/// Configuration for the gRPC reflection service.
21#[derive(Debug, Clone)]
22pub struct ReflectionConfig {
23    /// Whether reflection is enabled.
24    pub enabled: bool,
25    /// File descriptor sets to include in reflection.
26    pub file_descriptor_sets: Vec<&'static [u8]>,
27}
28
29impl Default for ReflectionConfig {
30    fn default() -> Self {
31        Self {
32            enabled: true,
33            file_descriptor_sets: Vec::new(),
34        }
35    }
36}
37
38impl ReflectionConfig {
39    /// Create a new reflection configuration.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Enable or disable reflection.
45    pub fn enabled(mut self, enabled: bool) -> Self {
46        self.enabled = enabled;
47        self
48    }
49
50    /// Add a file descriptor set.
51    pub fn add_file_descriptor_set(mut self, fds: &'static [u8]) -> Self {
52        self.file_descriptor_sets.push(fds);
53        self
54    }
55}
56
57/// Service for gRPC server reflection.
58///
59/// This wraps tonic-reflection to provide service discovery capabilities.
60#[cfg(feature = "reflection")]
61pub struct ReflectionService {
62    config: ReflectionConfig,
63}
64
65#[cfg(feature = "reflection")]
66impl ReflectionService {
67    /// Create a new reflection service with the given file descriptor sets.
68    pub fn new(file_descriptor_sets: &[&'static [u8]]) -> Self {
69        Self {
70            config: ReflectionConfig {
71                enabled: true,
72                file_descriptor_sets: file_descriptor_sets.to_vec(),
73            },
74        }
75    }
76
77    /// Create a new reflection service with a configuration.
78    pub fn with_config(config: ReflectionConfig) -> Self {
79        Self { config }
80    }
81
82    /// Build the reflection server.
83    ///
84    /// Returns `None` if reflection is disabled or no file descriptors are provided.
85    pub fn build_server(
86        &self,
87    ) -> Option<ServerReflectionServer<impl ServerReflection>> {
88        if !self.config.enabled || self.config.file_descriptor_sets.is_empty() {
89            return None;
90        }
91
92        let mut builder = tonic_reflection::server::Builder::configure();
93
94        for fds in &self.config.file_descriptor_sets {
95            builder = builder.register_encoded_file_descriptor_set(fds);
96        }
97
98        builder.build_v1().ok()
99    }
100
101    /// Build the reflection server (v1alpha version for older clients).
102    ///
103    /// Returns `None` if reflection is disabled or no file descriptors are provided.
104    pub fn build_server_v1alpha(
105        &self,
106    ) -> Option<tonic_reflection::server::v1alpha::ServerReflectionServer<impl tonic_reflection::server::v1alpha::ServerReflection>> {
107        if !self.config.enabled || self.config.file_descriptor_sets.is_empty() {
108            return None;
109        }
110
111        let mut builder = tonic_reflection::server::Builder::configure();
112
113        for fds in &self.config.file_descriptor_sets {
114            builder = builder.register_encoded_file_descriptor_set(fds);
115        }
116
117        builder.build_v1alpha().ok()
118    }
119}
120
121#[cfg(feature = "reflection")]
122impl std::fmt::Debug for ReflectionService {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("ReflectionService")
125            .field("enabled", &self.config.enabled)
126            .field(
127                "file_descriptor_count",
128                &self.config.file_descriptor_sets.len(),
129            )
130            .finish()
131    }
132}
133
134/// Placeholder for when reflection feature is disabled.
135#[cfg(not(feature = "reflection"))]
136pub struct ReflectionService {
137    _private: (),
138}
139
140#[cfg(not(feature = "reflection"))]
141impl ReflectionService {
142    /// Create a new (no-op) reflection service.
143    pub fn new(_file_descriptor_sets: &[&'static [u8]]) -> Self {
144        Self { _private: () }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn reflection_config_defaults() {
154        let config = ReflectionConfig::default();
155        assert!(config.enabled);
156        assert!(config.file_descriptor_sets.is_empty());
157    }
158
159    #[test]
160    fn reflection_config_builder() {
161        let config = ReflectionConfig::new()
162            .enabled(false);
163        assert!(!config.enabled);
164    }
165
166    #[cfg(feature = "reflection")]
167    #[test]
168    fn reflection_service_disabled_returns_none() {
169        let service = ReflectionService::with_config(
170            ReflectionConfig::new().enabled(false),
171        );
172        assert!(service.build_server().is_none());
173    }
174
175    #[cfg(feature = "reflection")]
176    #[test]
177    fn reflection_service_empty_fds_returns_none() {
178        let service = ReflectionService::new(&[]);
179        assert!(service.build_server().is_none());
180    }
181}