sentinel_proxy/reload/validators.rs
1//! Configuration validators for hot reload.
2//!
3//! These validators perform runtime-specific validation that complements
4//! the schema-level validation in sentinel-config.
5
6use sentinel_common::errors::{SentinelError, SentinelResult};
7use sentinel_config::Config;
8
9use super::ConfigValidator;
10
11/// Route configuration validator
12///
13/// Performs runtime-specific route validation that complements the schema-level
14/// validation in sentinel-config. This validator focuses on aspects that may
15/// change during hot reload.
16pub struct RouteValidator;
17
18#[async_trait::async_trait]
19impl ConfigValidator for RouteValidator {
20 async fn validate(&self, config: &Config) -> SentinelResult<()> {
21 // Most validation is now handled by Config's validate_config_semantics
22 // This validator handles runtime-specific checks
23
24 // Check for routes with both upstream and static-files (ambiguous config)
25 for route in &config.routes {
26 if route.upstream.is_some() && route.static_files.is_some() {
27 return Err(SentinelError::Config {
28 message: format!(
29 "Route '{}' has both 'upstream' and 'static-files' configured.\n\
30 A route can only be one type. Choose either:\n\
31 - Remove 'upstream' to serve static files\n\
32 - Remove 'static-files' to proxy to upstream",
33 route.id
34 ),
35 source: None,
36 });
37 }
38 }
39
40 // Check for static routes with non-existent root directories
41 for route in &config.routes {
42 if let Some(ref static_config) = route.static_files {
43 if !static_config.root.exists() {
44 return Err(SentinelError::Config {
45 message: format!(
46 "Route '{}' static files root directory '{}' does not exist.\n\
47 Hint: Create the directory or update the path:\n\
48 \n\
49 mkdir -p {}\n\
50 \n\
51 Or change the configuration:\n\
52 static-files {{\n\
53 root \"/path/to/existing/directory\"\n\
54 }}",
55 route.id,
56 static_config.root.display(),
57 static_config.root.display()
58 ),
59 source: None,
60 });
61 }
62
63 if !static_config.root.is_dir() {
64 return Err(SentinelError::Config {
65 message: format!(
66 "Route '{}' static files root '{}' is not a directory.\n\
67 The 'root' must be a directory path, not a file.",
68 route.id,
69 static_config.root.display()
70 ),
71 source: None,
72 });
73 }
74 }
75 }
76
77 Ok(())
78 }
79
80 fn name(&self) -> &str {
81 "RouteValidator"
82 }
83}
84
85/// Upstream configuration validator
86///
87/// Performs runtime-specific upstream validation. The schema-level validation
88/// in sentinel-config handles most checks; this focuses on network reachability
89/// and runtime concerns.
90pub struct UpstreamValidator;
91
92#[async_trait::async_trait]
93impl ConfigValidator for UpstreamValidator {
94 async fn validate(&self, config: &Config) -> SentinelResult<()> {
95 // Most validation is now handled by Config's validate_config_semantics
96 // This validator handles runtime-specific checks
97
98 for (name, upstream) in &config.upstreams {
99 // Validate target addresses can be parsed (supports hostnames)
100 for (i, target) in upstream.targets.iter().enumerate() {
101 // Try as socket address first
102 if target.address.parse::<std::net::SocketAddr>().is_ok() {
103 continue;
104 }
105
106 // Try as host:port format
107 let parts: Vec<&str> = target.address.rsplitn(2, ':').collect();
108 if parts.len() == 2 {
109 if let Ok(port) = parts[0].parse::<u16>() {
110 if port > 0 {
111 continue; // Valid host:port format
112 }
113 }
114 }
115
116 return Err(SentinelError::Config {
117 message: format!(
118 "Upstream '{}' target #{} has invalid address '{}'.\n\
119 \n\
120 Expected format: HOST:PORT\n\
121 \n\
122 Valid examples:\n\
123 - 127.0.0.1:8080 (IPv4)\n\
124 - [::1]:8080 (IPv6)\n\
125 - backend.local:8080 (hostname)\n\
126 - api-server:3000 (service name)",
127 name,
128 i + 1,
129 target.address
130 ),
131 source: None,
132 });
133 }
134
135 // Warn about upstreams with only one target (no redundancy)
136 if upstream.targets.len() == 1 {
137 tracing::warn!(
138 upstream = %name,
139 "Upstream '{}' has only one target. Consider adding more targets for redundancy.",
140 name
141 );
142 }
143 }
144
145 Ok(())
146 }
147
148 fn name(&self) -> &str {
149 "UpstreamValidator"
150 }
151}