Skip to main content

xds_core/
resource.rs

1//! Resource trait and registry for xDS resources.
2//!
3//! This module provides the [`Resource`] trait for implementing custom
4//! xDS resource types, and [`ResourceRegistry`] for managing resource types.
5
6use std::any::Any;
7use std::collections::HashMap;
8use std::fmt;
9use std::sync::Arc;
10
11use dashmap::DashMap;
12
13use crate::TypeUrl;
14
15/// Trait for xDS resources.
16///
17/// Implement this trait to create custom xDS resource types that can be
18/// stored in the cache and served via xDS.
19///
20/// # Example
21///
22/// ```rust
23/// use xds_core::{Resource, TypeUrl};
24/// use prost_types::Any;
25/// use std::any::Any as StdAny;
26///
27/// #[derive(Debug)]
28/// struct MyCluster {
29///     name: String,
30///     // ... other fields
31/// }
32///
33/// impl Resource for MyCluster {
34///     fn type_url(&self) -> &str {
35///         TypeUrl::CLUSTER
36///     }
37///
38///     fn name(&self) -> &str {
39///         &self.name
40///     }
41///
42///     fn encode(&self) -> Result<Any, Box<dyn std::error::Error + Send + Sync>> {
43///         // Encode to protobuf Any
44///         Ok(Any {
45///             type_url: self.type_url().to_string(),
46///             value: vec![], // actual encoding would go here
47///         })
48///     }
49///
50///     fn as_any(&self) -> &dyn StdAny {
51///         self
52///     }
53/// }
54/// ```
55pub trait Resource: Send + Sync + fmt::Debug {
56    /// Get the type URL for this resource.
57    fn type_url(&self) -> &str;
58
59    /// Get the resource name.
60    fn name(&self) -> &str;
61
62    /// Encode the resource to a protobuf Any message.
63    fn encode(&self) -> Result<prost_types::Any, Box<dyn std::error::Error + Send + Sync>>;
64
65    /// Get the resource version, if known.
66    fn version(&self) -> Option<&str> {
67        None
68    }
69
70    /// Convert to Any for downcasting.
71    fn as_any(&self) -> &dyn Any;
72}
73
74/// Type alias for a boxed resource.
75/// Uses Arc for efficient cloning and sharing across snapshots.
76pub type BoxResource = Arc<dyn Resource>;
77
78/// A wrapped Any message that implements Resource.
79///
80/// This allows storing raw protobuf Any messages as resources
81/// without needing to decode them.
82#[derive(Debug, Clone)]
83#[allow(dead_code)] // Public API surface, will be used by consumers
84pub struct AnyResource {
85    type_url: String,
86    name: String,
87    version: Option<String>,
88    any: prost_types::Any,
89}
90
91#[allow(dead_code)] // Public API surface, will be used by consumers
92impl AnyResource {
93    /// Create a new AnyResource.
94    #[must_use]
95    pub fn new(
96        type_url: impl Into<String>,
97        name: impl Into<String>,
98        any: prost_types::Any,
99    ) -> Self {
100        Self {
101            type_url: type_url.into(),
102            name: name.into(),
103            version: None,
104            any,
105        }
106    }
107
108    /// Create a new AnyResource with a version.
109    #[must_use]
110    pub fn with_version(mut self, version: impl Into<String>) -> Self {
111        self.version = Some(version.into());
112        self
113    }
114
115    /// Get the inner Any message.
116    #[must_use]
117    pub fn inner(&self) -> &prost_types::Any {
118        &self.any
119    }
120
121    /// Consume and return the inner Any message.
122    #[must_use]
123    pub fn into_inner(self) -> prost_types::Any {
124        self.any
125    }
126}
127
128impl Resource for AnyResource {
129    fn type_url(&self) -> &str {
130        &self.type_url
131    }
132
133    fn name(&self) -> &str {
134        &self.name
135    }
136
137    fn encode(&self) -> Result<prost_types::Any, Box<dyn std::error::Error + Send + Sync>> {
138        Ok(self.any.clone())
139    }
140
141    fn version(&self) -> Option<&str> {
142        self.version.as_deref()
143    }
144
145    fn as_any(&self) -> &dyn Any {
146        self
147    }
148}
149
150/// Registry for resource types.
151///
152/// The registry maps type URLs to resource metadata and provides
153/// utilities for working with different resource types.
154#[derive(Debug, Default)]
155pub struct ResourceRegistry {
156    types: HashMap<String, ResourceTypeInfo>,
157}
158
159/// Information about a registered resource type.
160#[derive(Debug, Clone)]
161pub struct ResourceTypeInfo {
162    /// The type URL.
163    pub type_url: String,
164    /// Short name for the type.
165    pub short_name: String,
166    /// Description of the resource type.
167    pub description: String,
168}
169
170impl ResourceRegistry {
171    /// Create a new empty registry.
172    #[must_use]
173    pub fn new() -> Self {
174        Self::default()
175    }
176
177    /// Create a registry pre-populated with standard Envoy types.
178    #[must_use]
179    pub fn with_envoy_types() -> Self {
180        let mut registry = Self::new();
181
182        registry.register(ResourceTypeInfo {
183            type_url: TypeUrl::CLUSTER.to_string(),
184            short_name: "Cluster".to_string(),
185            description: "Cluster Discovery Service (CDS)".to_string(),
186        });
187
188        registry.register(ResourceTypeInfo {
189            type_url: TypeUrl::ENDPOINT.to_string(),
190            short_name: "ClusterLoadAssignment".to_string(),
191            description: "Endpoint Discovery Service (EDS)".to_string(),
192        });
193
194        registry.register(ResourceTypeInfo {
195            type_url: TypeUrl::LISTENER.to_string(),
196            short_name: "Listener".to_string(),
197            description: "Listener Discovery Service (LDS)".to_string(),
198        });
199
200        registry.register(ResourceTypeInfo {
201            type_url: TypeUrl::ROUTE.to_string(),
202            short_name: "RouteConfiguration".to_string(),
203            description: "Route Discovery Service (RDS)".to_string(),
204        });
205
206        registry.register(ResourceTypeInfo {
207            type_url: TypeUrl::SECRET.to_string(),
208            short_name: "Secret".to_string(),
209            description: "Secret Discovery Service (SDS)".to_string(),
210        });
211
212        registry.register(ResourceTypeInfo {
213            type_url: TypeUrl::RUNTIME.to_string(),
214            short_name: "Runtime".to_string(),
215            description: "Runtime Discovery Service (RTDS)".to_string(),
216        });
217
218        registry
219    }
220
221    /// Register a new resource type.
222    pub fn register(&mut self, info: ResourceTypeInfo) {
223        self.types.insert(info.type_url.clone(), info);
224    }
225
226    /// Get information about a resource type by type URL.
227    #[must_use]
228    pub fn get(&self, type_url: &str) -> Option<&ResourceTypeInfo> {
229        self.types.get(type_url)
230    }
231
232    /// Check if a type URL is registered.
233    #[must_use]
234    pub fn contains(&self, type_url: &str) -> bool {
235        self.types.contains_key(type_url)
236    }
237
238    /// Get all registered type URLs.
239    #[must_use]
240    pub fn type_urls(&self) -> Vec<&str> {
241        self.types.keys().map(String::as_str).collect()
242    }
243
244    /// Get the number of registered types.
245    #[must_use]
246    pub fn len(&self) -> usize {
247        self.types.len()
248    }
249
250    /// Check if the registry is empty.
251    #[must_use]
252    pub fn is_empty(&self) -> bool {
253        self.types.is_empty()
254    }
255
256    /// Iterate over all registered types.
257    pub fn iter(&self) -> impl Iterator<Item = (&String, &ResourceTypeInfo)> {
258        self.types.iter()
259    }
260}
261
262/// Thread-safe registry for resource types.
263///
264/// This is a concurrent version of [`ResourceRegistry`] that can be safely
265/// shared across threads without external synchronization.
266///
267/// Uses `DashMap` internally for lock-free reads and efficient concurrent writes.
268///
269/// # Example
270///
271/// ```rust
272/// use xds_core::{SharedResourceRegistry, TypeUrl};
273/// use std::sync::Arc;
274///
275/// let registry = Arc::new(SharedResourceRegistry::with_envoy_types());
276///
277/// // Can be shared across threads safely
278/// assert!(registry.contains(TypeUrl::CLUSTER));
279/// assert!(registry.contains(TypeUrl::LISTENER));
280/// ```
281#[derive(Debug)]
282pub struct SharedResourceRegistry {
283    types: DashMap<String, ResourceTypeInfo>,
284}
285
286impl Default for SharedResourceRegistry {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292impl SharedResourceRegistry {
293    /// Create a new empty thread-safe registry.
294    #[must_use]
295    pub fn new() -> Self {
296        Self {
297            types: DashMap::new(),
298        }
299    }
300
301    /// Create a thread-safe registry pre-populated with standard Envoy types.
302    #[must_use]
303    pub fn with_envoy_types() -> Self {
304        let registry = Self::new();
305
306        registry.register(ResourceTypeInfo {
307            type_url: TypeUrl::CLUSTER.to_string(),
308            short_name: "Cluster".to_string(),
309            description: "Cluster Discovery Service (CDS)".to_string(),
310        });
311
312        registry.register(ResourceTypeInfo {
313            type_url: TypeUrl::ENDPOINT.to_string(),
314            short_name: "ClusterLoadAssignment".to_string(),
315            description: "Endpoint Discovery Service (EDS)".to_string(),
316        });
317
318        registry.register(ResourceTypeInfo {
319            type_url: TypeUrl::LISTENER.to_string(),
320            short_name: "Listener".to_string(),
321            description: "Listener Discovery Service (LDS)".to_string(),
322        });
323
324        registry.register(ResourceTypeInfo {
325            type_url: TypeUrl::ROUTE.to_string(),
326            short_name: "RouteConfiguration".to_string(),
327            description: "Route Discovery Service (RDS)".to_string(),
328        });
329
330        registry.register(ResourceTypeInfo {
331            type_url: TypeUrl::SECRET.to_string(),
332            short_name: "Secret".to_string(),
333            description: "Secret Discovery Service (SDS)".to_string(),
334        });
335
336        registry.register(ResourceTypeInfo {
337            type_url: TypeUrl::RUNTIME.to_string(),
338            short_name: "Runtime".to_string(),
339            description: "Runtime Discovery Service (RTDS)".to_string(),
340        });
341
342        registry.register(ResourceTypeInfo {
343            type_url: TypeUrl::SCOPED_ROUTE.to_string(),
344            short_name: "ScopedRouteConfiguration".to_string(),
345            description: "Scoped Route Discovery Service (SRDS)".to_string(),
346        });
347
348        registry.register(ResourceTypeInfo {
349            type_url: TypeUrl::VIRTUAL_HOST.to_string(),
350            short_name: "VirtualHost".to_string(),
351            description: "Virtual Host Discovery Service (VHDS)".to_string(),
352        });
353
354        registry
355    }
356
357    /// Register a new resource type.
358    ///
359    /// This is safe to call from multiple threads concurrently.
360    pub fn register(&self, info: ResourceTypeInfo) {
361        self.types.insert(info.type_url.clone(), info);
362    }
363
364    /// Get information about a resource type by type URL.
365    ///
366    /// Returns `None` if the type is not registered.
367    #[must_use]
368    pub fn get(&self, type_url: &str) -> Option<ResourceTypeInfo> {
369        self.types.get(type_url).map(|r| r.clone())
370    }
371
372    /// Check if a type URL is registered.
373    #[must_use]
374    pub fn contains(&self, type_url: &str) -> bool {
375        self.types.contains_key(type_url)
376    }
377
378    /// Get all registered type URLs.
379    #[must_use]
380    pub fn type_urls(&self) -> Vec<String> {
381        self.types.iter().map(|r| r.key().clone()).collect()
382    }
383
384    /// Get the number of registered types.
385    #[must_use]
386    pub fn len(&self) -> usize {
387        self.types.len()
388    }
389
390    /// Check if the registry is empty.
391    #[must_use]
392    pub fn is_empty(&self) -> bool {
393        self.types.is_empty()
394    }
395
396    /// Validate that a type URL is registered.
397    ///
398    /// Returns `Ok(())` if registered, or an error with available types if not.
399    pub fn validate(&self, type_url: &str) -> Result<(), String> {
400        if self.contains(type_url) {
401            Ok(())
402        } else {
403            let available: Vec<_> = self.type_urls();
404            Err(format!(
405                "Unknown resource type: {}. Available types: {:?}",
406                type_url, available
407            ))
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn test_any_resource() {
418        let any = prost_types::Any {
419            type_url: TypeUrl::CLUSTER.to_string(),
420            value: vec![1, 2, 3],
421        };
422
423        let resource = AnyResource::new(TypeUrl::CLUSTER, "my-cluster", any);
424        assert_eq!(resource.type_url(), TypeUrl::CLUSTER);
425        assert_eq!(resource.name(), "my-cluster");
426        assert!(resource.version().is_none());
427    }
428
429    #[test]
430    fn test_any_resource_with_version() {
431        let any = prost_types::Any {
432            type_url: TypeUrl::CLUSTER.to_string(),
433            value: vec![],
434        };
435
436        let resource = AnyResource::new(TypeUrl::CLUSTER, "my-cluster", any).with_version("v1");
437        assert_eq!(resource.version(), Some("v1"));
438    }
439
440    #[test]
441    fn test_registry_new() {
442        let registry = ResourceRegistry::new();
443        assert!(registry.is_empty());
444    }
445
446    #[test]
447    fn test_registry_with_envoy_types() {
448        let registry = ResourceRegistry::with_envoy_types();
449        assert!(!registry.is_empty());
450        assert!(registry.contains(TypeUrl::CLUSTER));
451        assert!(registry.contains(TypeUrl::ENDPOINT));
452    }
453
454    #[test]
455    fn test_registry_register() {
456        let mut registry = ResourceRegistry::new();
457        registry.register(ResourceTypeInfo {
458            type_url: "custom.type".to_string(),
459            short_name: "Custom".to_string(),
460            description: "Custom type".to_string(),
461        });
462
463        assert!(registry.contains("custom.type"));
464        assert_eq!(registry.len(), 1);
465    }
466
467    #[test]
468    fn test_registry_get() {
469        let registry = ResourceRegistry::with_envoy_types();
470        let info = registry.get(TypeUrl::CLUSTER);
471        assert!(info.is_some(), "CLUSTER type should be registered");
472        // Safe because we just asserted it's Some
473        #[allow(clippy::unwrap_used)]
474        let info = info.unwrap();
475        assert_eq!(info.short_name, "Cluster");
476    }
477
478    // SharedResourceRegistry tests
479
480    #[test]
481    fn test_shared_registry_new() {
482        let registry = SharedResourceRegistry::new();
483        assert!(registry.is_empty());
484    }
485
486    #[test]
487    fn test_shared_registry_with_envoy_types() {
488        let registry = SharedResourceRegistry::with_envoy_types();
489        assert!(!registry.is_empty());
490        assert!(registry.contains(TypeUrl::CLUSTER));
491        assert!(registry.contains(TypeUrl::ENDPOINT));
492        assert!(registry.contains(TypeUrl::LISTENER));
493        assert!(registry.contains(TypeUrl::ROUTE));
494        assert!(registry.contains(TypeUrl::SECRET));
495        assert!(registry.contains(TypeUrl::RUNTIME));
496        assert!(registry.contains(TypeUrl::SCOPED_ROUTE));
497        assert!(registry.contains(TypeUrl::VIRTUAL_HOST));
498        assert_eq!(registry.len(), 8);
499    }
500
501    #[test]
502    fn test_shared_registry_register() {
503        let registry = SharedResourceRegistry::new();
504        registry.register(ResourceTypeInfo {
505            type_url: "custom.type".to_string(),
506            short_name: "Custom".to_string(),
507            description: "Custom type".to_string(),
508        });
509
510        assert!(registry.contains("custom.type"));
511        assert_eq!(registry.len(), 1);
512    }
513
514    #[test]
515    fn test_shared_registry_get() {
516        let registry = SharedResourceRegistry::with_envoy_types();
517        let info = registry.get(TypeUrl::CLUSTER);
518        assert!(info.is_some(), "CLUSTER type should be registered");
519        #[allow(clippy::unwrap_used)]
520        let info = info.unwrap();
521        assert_eq!(info.short_name, "Cluster");
522    }
523
524    #[test]
525    fn test_shared_registry_validate() {
526        let registry = SharedResourceRegistry::with_envoy_types();
527
528        // Valid type should pass
529        assert!(registry.validate(TypeUrl::CLUSTER).is_ok());
530
531        // Invalid type should fail with helpful message
532        let err = registry.validate("unknown.type").unwrap_err();
533        assert!(err.contains("Unknown resource type"));
534        assert!(err.contains("unknown.type"));
535    }
536
537    #[test]
538    fn test_shared_registry_thread_safety() {
539        use std::sync::Arc;
540        use std::thread;
541
542        let registry = Arc::new(SharedResourceRegistry::new());
543        let mut handles = vec![];
544
545        // Spawn multiple threads that register types concurrently
546        for i in 0..10 {
547            let reg = Arc::clone(&registry);
548            handles.push(thread::spawn(move || {
549                reg.register(ResourceTypeInfo {
550                    type_url: format!("type.{}", i),
551                    short_name: format!("Type{}", i),
552                    description: format!("Type {} description", i),
553                });
554            }));
555        }
556
557        for handle in handles {
558            handle.join().expect("Thread panicked");
559        }
560
561        assert_eq!(registry.len(), 10);
562    }
563}