sentinel_config/
lib.rs

1//! Configuration module for Sentinel proxy
2//!
3//! This module provides configuration parsing, validation, and hot-reload support
4//! with a focus on safety, security-first defaults, and operational clarity.
5//!
6//! # Module Organization
7//!
8//! - [`server`]: Server and listener configuration
9//! - [`routes`]: Route configuration and match conditions
10//! - [`upstreams`]: Upstream backend configuration
11//! - [`agents`]: External processing agent configuration
12//! - [`waf`]: WAF (Web Application Firewall) configuration
13//! - [`observability`]: Metrics, logging, and tracing configuration
14//! - [`filters`]: Filter types for request/response processing
15//! - [`validation`]: Configuration validation functions
16//! - [`kdl`]: KDL format parsing
17//! - [`defaults`]: Default embedded configuration
18//! - [`multi_file`]: Multi-file configuration loading
19
20use anyhow::{Context, Result};
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::path::Path;
24use tracing::{debug, info, trace, warn};
25use validator::Validate;
26
27use sentinel_common::{
28    errors::{SentinelError, SentinelResult},
29    limits::Limits,
30    types::Priority,
31};
32
33// ============================================================================
34// Module Declarations
35// ============================================================================
36
37pub mod agents;
38mod defaults;
39pub mod filters;
40mod kdl;
41pub mod multi_file;
42pub mod observability;
43pub mod routes;
44pub mod server;
45pub mod upstreams;
46pub mod validation;
47pub mod waf;
48
49// ============================================================================
50// Re-exports
51// ============================================================================
52
53// Agents
54pub use agents::{
55    AgentConfig, AgentEvent, AgentTlsConfig, AgentTransport, AgentType, BodyStreamingMode,
56};
57
58// Defaults
59pub use defaults::{create_default_config, DEFAULT_CONFIG_KDL};
60
61// Filters
62pub use filters::*;
63
64// Multi-file
65pub use multi_file::{ConfigDirectory, MultiFileLoader};
66
67// Observability
68pub use observability::{
69    AccessLogConfig, AuditLogConfig, ErrorLogConfig, LoggingConfig, MetricsConfig,
70    ObservabilityConfig, TracingBackend, TracingConfig,
71};
72
73// Routes
74pub use routes::{
75    ApiSchemaConfig, BuiltinHandler, CacheBackend, CacheStorageConfig, ErrorFormat, ErrorPage,
76    ErrorPageConfig, FailureMode, HeaderModifications, MatchCondition, RateLimitPolicy,
77    RouteCacheConfig, RouteConfig, RoutePolicies, ServiceType, StaticFileConfig,
78};
79
80// Server
81pub use server::{ListenerConfig, ListenerProtocol, ServerConfig, SniCertificate, TlsConfig};
82
83// Re-export TraceIdFormat from common for convenience
84pub use sentinel_common::TraceIdFormat;
85
86// Upstreams
87pub use upstreams::{
88    ConnectionPoolConfig, HealthCheck, HttpVersionConfig, UpstreamConfig, UpstreamPeer,
89    UpstreamTarget, UpstreamTimeouts, UpstreamTlsConfig,
90};
91
92// WAF
93pub use waf::{
94    BodyInspectionPolicy, ExclusionScope, RuleExclusion, WafConfig, WafEngine, WafMode, WafRuleset,
95};
96
97// Common types re-exported for convenience
98pub use sentinel_common::types::LoadBalancingAlgorithm;
99
100// ============================================================================
101// Main Configuration Structure
102// ============================================================================
103
104/// Current schema version supported by this build
105pub const CURRENT_SCHEMA_VERSION: &str = "1.0";
106
107/// Minimum schema version supported by this build
108pub const MIN_SCHEMA_VERSION: &str = "1.0";
109
110/// Main configuration structure for Sentinel proxy
111#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
112#[validate(schema(function = "validation::validate_config_semantics"))]
113pub struct Config {
114    /// Configuration schema version for compatibility checking
115    /// If not specified, defaults to current version
116    #[serde(default = "default_schema_version")]
117    pub schema_version: String,
118
119    /// Global server configuration
120    pub server: ServerConfig,
121
122    /// Listener configurations
123    #[validate(length(min = 1, message = "At least one listener is required"))]
124    pub listeners: Vec<ListenerConfig>,
125
126    /// Route configurations
127    pub routes: Vec<RouteConfig>,
128
129    /// Upstream pool configurations (can be empty if all routes are static)
130    #[serde(default)]
131    pub upstreams: HashMap<String, UpstreamConfig>,
132
133    /// Named filter configurations (referenced by routes)
134    #[serde(default)]
135    pub filters: HashMap<String, FilterConfig>,
136
137    /// Agent configurations
138    #[serde(default)]
139    pub agents: Vec<AgentConfig>,
140
141    /// WAF configuration
142    #[serde(default)]
143    pub waf: Option<WafConfig>,
144
145    /// Global limits configuration
146    #[serde(default)]
147    pub limits: Limits,
148
149    /// Observability configuration
150    #[serde(default)]
151    pub observability: ObservabilityConfig,
152
153    /// Global rate limit configuration
154    #[serde(default)]
155    pub rate_limits: GlobalRateLimitConfig,
156
157    /// Global cache storage configuration
158    #[serde(default)]
159    pub cache: Option<CacheStorageConfig>,
160
161    /// Default upstream for Phase 0 testing
162    #[serde(skip)]
163    pub default_upstream: Option<UpstreamPeer>,
164}
165
166/// Default schema version (current version)
167fn default_schema_version() -> String {
168    CURRENT_SCHEMA_VERSION.to_string()
169}
170
171/// Schema version compatibility result
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum SchemaCompatibility {
174    /// Version matches exactly
175    Exact,
176    /// Version is compatible (within supported range)
177    Compatible,
178    /// Version is newer than supported - may have unsupported features
179    Newer { config_version: String, max_supported: String },
180    /// Version is older than minimum supported
181    Older { config_version: String, min_supported: String },
182    /// Version format is invalid
183    Invalid { config_version: String, reason: String },
184}
185
186impl SchemaCompatibility {
187    /// Returns true if the config can be loaded (Exact, Compatible, or Newer with warning)
188    pub fn is_loadable(&self) -> bool {
189        matches!(self, Self::Exact | Self::Compatible | Self::Newer { .. })
190    }
191
192    /// Returns a warning message if applicable
193    pub fn warning(&self) -> Option<String> {
194        match self {
195            Self::Newer { config_version, max_supported } => Some(format!(
196                "Config schema version {} is newer than supported version {}. Some features may not work.",
197                config_version, max_supported
198            )),
199            _ => None,
200        }
201    }
202
203    /// Returns an error message if not loadable
204    pub fn error(&self) -> Option<String> {
205        match self {
206            Self::Older { config_version, min_supported } => Some(format!(
207                "Config schema version {} is older than minimum supported version {}. Please update your configuration.",
208                config_version, min_supported
209            )),
210            Self::Invalid { config_version, reason } => Some(format!(
211                "Invalid schema version '{}': {}",
212                config_version, reason
213            )),
214            _ => None,
215        }
216    }
217}
218
219/// Parse a version string into (major, minor) tuple
220fn parse_version(version: &str) -> Option<(u32, u32)> {
221    let parts: Vec<&str> = version.trim().split('.').collect();
222    if parts.len() != 2 {
223        return None;
224    }
225    let major = parts[0].parse().ok()?;
226    let minor = parts[1].parse().ok()?;
227    Some((major, minor))
228}
229
230/// Check schema version compatibility
231pub fn check_schema_compatibility(config_version: &str) -> SchemaCompatibility {
232    let config_ver = match parse_version(config_version) {
233        Some(v) => v,
234        None => return SchemaCompatibility::Invalid {
235            config_version: config_version.to_string(),
236            reason: "Expected format: major.minor (e.g., '1.0')".to_string(),
237        },
238    };
239
240    let current_ver = parse_version(CURRENT_SCHEMA_VERSION).unwrap();
241    let min_ver = parse_version(MIN_SCHEMA_VERSION).unwrap();
242
243    // Check if older than minimum
244    if config_ver < min_ver {
245        return SchemaCompatibility::Older {
246            config_version: config_version.to_string(),
247            min_supported: MIN_SCHEMA_VERSION.to_string(),
248        };
249    }
250
251    // Check if newer than current
252    if config_ver > current_ver {
253        return SchemaCompatibility::Newer {
254            config_version: config_version.to_string(),
255            max_supported: CURRENT_SCHEMA_VERSION.to_string(),
256        };
257    }
258
259    // Check if exact match
260    if config_ver == current_ver {
261        return SchemaCompatibility::Exact;
262    }
263
264    // Within range
265    SchemaCompatibility::Compatible
266}
267
268// ============================================================================
269// Config Implementation
270// ============================================================================
271
272impl Config {
273    /// Load configuration from a file
274    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
275        let path = path.as_ref();
276
277        trace!(
278            path = %path.display(),
279            "Loading configuration from file"
280        );
281
282        let content = std::fs::read_to_string(path)
283            .with_context(|| format!("Failed to read config file: {:?}", path))?;
284
285        let extension = path
286            .extension()
287            .and_then(|ext| ext.to_str())
288            .unwrap_or("kdl");
289
290        debug!(
291            path = %path.display(),
292            format = extension,
293            content_length = content.len(),
294            "Read configuration file"
295        );
296
297        let config = match extension {
298            "kdl" => Self::from_kdl(&content),
299            "json" => Self::from_json(&content),
300            "toml" => Self::from_toml(&content),
301            _ => Err(anyhow::anyhow!("Unsupported config format: {}", extension)),
302        }?;
303
304        info!(
305            path = %path.display(),
306            routes = config.routes.len(),
307            upstreams = config.upstreams.len(),
308            agents = config.agents.len(),
309            listeners = config.listeners.len(),
310            "Configuration loaded successfully"
311        );
312
313        Ok(config)
314    }
315
316    /// Load the default embedded configuration.
317    ///
318    /// This is used when no configuration file is provided. It parses the
319    /// embedded KDL configuration, falling back to the programmatic default
320    /// if KDL parsing fails for any reason.
321    pub fn default_embedded() -> Result<Self> {
322        trace!("Loading embedded default configuration");
323
324        Self::from_kdl(DEFAULT_CONFIG_KDL).or_else(|e| {
325            warn!(
326                error = %e,
327                "Failed to parse embedded KDL config, using programmatic default"
328            );
329            Ok(create_default_config())
330        })
331    }
332
333    /// Parse configuration from KDL format
334    pub fn from_kdl(content: &str) -> Result<Self> {
335        trace!(content_length = content.len(), "Parsing KDL configuration");
336        let doc: ::kdl::KdlDocument = content.parse().map_err(|e: ::kdl::KdlError| {
337            use miette::Diagnostic;
338
339            let mut error_msg = String::new();
340            error_msg.push_str("KDL configuration parse error:\n\n");
341
342            let mut found_details = false;
343            if let Some(related) = e.related() {
344                for diagnostic in related {
345                    let diag_str = format!("{}", diagnostic);
346                    error_msg.push_str(&format!("  {}\n", diag_str));
347                    found_details = true;
348
349                    if let Some(labels) = diagnostic.labels() {
350                        for label in labels {
351                            let offset = label.offset();
352                            let (line, col) = kdl::offset_to_line_col(content, offset);
353                            error_msg
354                                .push_str(&format!("\n  --> at line {}, column {}\n", line, col));
355
356                            let lines: Vec<&str> = content.lines().collect();
357
358                            if line > 1 {
359                                if let Some(lc) = lines.get(line.saturating_sub(2)) {
360                                    error_msg.push_str(&format!("{:>4} | {}\n", line - 1, lc));
361                                }
362                            }
363
364                            if let Some(line_content) = lines.get(line.saturating_sub(1)) {
365                                error_msg.push_str(&format!("{:>4} | {}\n", line, line_content));
366                                error_msg.push_str(&format!(
367                                    "     | {}^",
368                                    " ".repeat(col.saturating_sub(1))
369                                ));
370                                if let Some(label_msg) = label.label() {
371                                    error_msg.push_str(&format!(" {}", label_msg));
372                                }
373                                error_msg.push('\n');
374                            }
375
376                            if let Some(lc) = lines.get(line) {
377                                error_msg.push_str(&format!("{:>4} | {}\n", line + 1, lc));
378                            }
379                        }
380                    }
381
382                    if let Some(help) = diagnostic.help() {
383                        error_msg.push_str(&format!("\n  Help: {}\n", help));
384                    }
385                }
386            }
387
388            if !found_details {
389                error_msg.push_str(&format!("  {}\n", e));
390                error_msg.push_str("\n  Note: Check your KDL syntax. Common issues:\n");
391                error_msg.push_str("    - Unclosed strings (missing closing quote)\n");
392                error_msg.push_str("    - Unclosed blocks (missing closing brace)\n");
393                error_msg.push_str("    - Invalid node names or values\n");
394            }
395
396            if let Some(help) = e.help() {
397                error_msg.push_str(&format!("\n  Help: {}\n", help));
398            }
399
400            anyhow::anyhow!("{}", error_msg)
401        })?;
402
403        kdl::parse_kdl_document(doc)
404    }
405
406    /// Parse configuration from JSON format
407    pub fn from_json(content: &str) -> Result<Self> {
408        trace!(content_length = content.len(), "Parsing JSON configuration");
409        serde_json::from_str(content).context("Failed to parse JSON configuration")
410    }
411
412    /// Parse configuration from TOML format
413    pub fn from_toml(content: &str) -> Result<Self> {
414        trace!(content_length = content.len(), "Parsing TOML configuration");
415        toml::from_str(content).context("Failed to parse TOML configuration")
416    }
417
418    /// Check schema version compatibility
419    pub fn check_schema_version(&self) -> SchemaCompatibility {
420        check_schema_compatibility(&self.schema_version)
421    }
422
423    /// Validate the configuration
424    pub fn validate(&self) -> SentinelResult<()> {
425        trace!(
426            routes = self.routes.len(),
427            upstreams = self.upstreams.len(),
428            agents = self.agents.len(),
429            schema_version = %self.schema_version,
430            "Starting configuration validation"
431        );
432
433        // Check schema version compatibility
434        let compat = self.check_schema_version();
435        if let Some(warning) = compat.warning() {
436            warn!("{}", warning);
437        }
438        if !compat.is_loadable() {
439            return Err(SentinelError::Config {
440                message: compat.error().unwrap_or_else(|| "Unknown schema version error".to_string()),
441                source: None,
442            });
443        }
444        trace!(
445            schema_version = %self.schema_version,
446            compatibility = ?compat,
447            "Schema version check passed"
448        );
449
450        Validate::validate(self).map_err(|e| SentinelError::Config {
451            message: format!("Configuration validation failed: {}", e),
452            source: None,
453        })?;
454
455        trace!("Schema validation passed");
456
457        self.validate_routes()?;
458        trace!("Route validation passed");
459
460        self.validate_upstreams()?;
461        trace!("Upstream validation passed");
462
463        self.validate_agents()?;
464        trace!("Agent validation passed");
465
466        self.limits.validate()?;
467        trace!("Limits validation passed");
468
469        debug!(
470            routes = self.routes.len(),
471            upstreams = self.upstreams.len(),
472            agents = self.agents.len(),
473            "Configuration validation successful"
474        );
475
476        Ok(())
477    }
478
479    fn validate_routes(&self) -> SentinelResult<()> {
480        for route in &self.routes {
481            if let Some(upstream) = &route.upstream {
482                if !self.upstreams.contains_key(upstream) {
483                    return Err(SentinelError::Config {
484                        message: format!(
485                            "Route '{}' references non-existent upstream '{}'",
486                            route.id, upstream
487                        ),
488                        source: None,
489                    });
490                }
491            }
492
493            for filter_id in &route.filters {
494                if !self.filters.contains_key(filter_id) {
495                    return Err(SentinelError::Config {
496                        message: format!(
497                            "Route '{}' references non-existent filter '{}'",
498                            route.id, filter_id
499                        ),
500                        source: None,
501                    });
502                }
503            }
504        }
505
506        for (filter_id, filter_config) in &self.filters {
507            if let Filter::Agent(agent_filter) = &filter_config.filter {
508                if !self.agents.iter().any(|a| a.id == agent_filter.agent) {
509                    return Err(SentinelError::Config {
510                        message: format!(
511                            "Filter '{}' references non-existent agent '{}'",
512                            filter_id, agent_filter.agent
513                        ),
514                        source: None,
515                    });
516                }
517            }
518        }
519
520        Ok(())
521    }
522
523    fn validate_upstreams(&self) -> SentinelResult<()> {
524        for (id, upstream) in &self.upstreams {
525            if upstream.targets.is_empty() {
526                return Err(SentinelError::Config {
527                    message: format!("Upstream '{}' has no targets", id),
528                    source: None,
529                });
530            }
531        }
532        Ok(())
533    }
534
535    fn validate_agents(&self) -> SentinelResult<()> {
536        for agent in &self.agents {
537            if agent.timeout_ms == 0 {
538                return Err(SentinelError::Config {
539                    message: format!("Agent '{}' has invalid timeout", agent.id),
540                    source: None,
541                });
542            }
543
544            if let AgentTransport::UnixSocket { path } = &agent.transport {
545                if !path.exists() && !path.parent().is_some_and(|p| p.exists()) {
546                    return Err(SentinelError::Config {
547                        message: format!(
548                            "Agent '{}' unix socket path parent directory doesn't exist: {:?}",
549                            agent.id, path
550                        ),
551                        source: None,
552                    });
553                }
554            }
555        }
556        Ok(())
557    }
558
559    /// Create a default configuration for testing
560    pub fn default_for_testing() -> Self {
561        use sentinel_common::types::LoadBalancingAlgorithm;
562
563        let mut upstreams = HashMap::new();
564        upstreams.insert(
565            "default".to_string(),
566            UpstreamConfig {
567                id: "default".to_string(),
568                targets: vec![UpstreamTarget {
569                    address: "127.0.0.1:8081".to_string(),
570                    weight: 1,
571                    max_requests: None,
572                    metadata: HashMap::new(),
573                }],
574                load_balancing: LoadBalancingAlgorithm::RoundRobin,
575                health_check: None,
576                connection_pool: ConnectionPoolConfig::default(),
577                timeouts: UpstreamTimeouts::default(),
578                tls: None,
579                http_version: HttpVersionConfig::default(),
580            },
581        );
582
583        Self {
584            schema_version: CURRENT_SCHEMA_VERSION.to_string(),
585            server: ServerConfig {
586                worker_threads: 4,
587                max_connections: 1000,
588                graceful_shutdown_timeout_secs: 30,
589                daemon: false,
590                pid_file: None,
591                user: None,
592                group: None,
593                working_directory: None,
594                trace_id_format: Default::default(),
595                auto_reload: false,
596            },
597            listeners: vec![ListenerConfig {
598                id: "http".to_string(),
599                address: "0.0.0.0:8080".to_string(),
600                protocol: ListenerProtocol::Http,
601                tls: None,
602                default_route: Some("default".to_string()),
603                request_timeout_secs: 60,
604                keepalive_timeout_secs: 75,
605                max_concurrent_streams: 100,
606            }],
607            routes: vec![RouteConfig {
608                id: "default".to_string(),
609                priority: Priority::Normal,
610                matches: vec![MatchCondition::PathPrefix("/".to_string())],
611                upstream: Some("default".to_string()),
612                service_type: ServiceType::Web,
613                policies: RoutePolicies::default(),
614                filters: vec![],
615                builtin_handler: None,
616                waf_enabled: false,
617                circuit_breaker: None,
618                retry_policy: None,
619                static_files: None,
620                api_schema: None,
621                error_pages: None,
622                websocket: false,
623                websocket_inspection: false,
624            }],
625            upstreams,
626            filters: HashMap::new(),
627            agents: vec![],
628            waf: None,
629            limits: Limits::for_testing(),
630            observability: ObservabilityConfig::default(),
631            rate_limits: GlobalRateLimitConfig::default(),
632            cache: None,
633            default_upstream: Some(UpstreamPeer {
634                address: "127.0.0.1:8081".to_string(),
635                tls: false,
636                host: "localhost".to_string(),
637                connect_timeout_secs: 10,
638                read_timeout_secs: 30,
639                write_timeout_secs: 30,
640            }),
641        }
642    }
643
644    /// Reload configuration from the same file path
645    pub fn reload(&mut self, path: impl AsRef<Path>) -> SentinelResult<()> {
646        let path = path.as_ref();
647        debug!(
648            path = %path.display(),
649            "Reloading configuration"
650        );
651
652        let new_config = Self::from_file(path).map_err(|e| SentinelError::Config {
653            message: format!("Failed to reload configuration: {}", e),
654            source: None,
655        })?;
656
657        new_config.validate()?;
658
659        info!(
660            path = %path.display(),
661            routes = new_config.routes.len(),
662            upstreams = new_config.upstreams.len(),
663            "Configuration reloaded successfully"
664        );
665
666        *self = new_config;
667        Ok(())
668    }
669
670    /// Get a route by ID
671    pub fn get_route(&self, id: &str) -> Option<&RouteConfig> {
672        self.routes.iter().find(|r| r.id == id)
673    }
674
675    /// Get an upstream by ID
676    pub fn get_upstream(&self, id: &str) -> Option<&UpstreamConfig> {
677        self.upstreams.get(id)
678    }
679
680    /// Get an agent by ID
681    pub fn get_agent(&self, id: &str) -> Option<&AgentConfig> {
682        self.agents.iter().find(|a| a.id == id)
683    }
684}
685
686// ============================================================================
687// Tests
688// ============================================================================
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693
694    #[test]
695    fn test_parse_version() {
696        assert_eq!(parse_version("1.0"), Some((1, 0)));
697        assert_eq!(parse_version("2.5"), Some((2, 5)));
698        assert_eq!(parse_version("10.20"), Some((10, 20)));
699        assert_eq!(parse_version("1"), None);
700        assert_eq!(parse_version("1.0.0"), None);
701        assert_eq!(parse_version("abc"), None);
702        assert_eq!(parse_version(""), None);
703    }
704
705    #[test]
706    fn test_schema_compatibility_exact() {
707        let compat = check_schema_compatibility(CURRENT_SCHEMA_VERSION);
708        assert_eq!(compat, SchemaCompatibility::Exact);
709        assert!(compat.is_loadable());
710        assert!(compat.warning().is_none());
711        assert!(compat.error().is_none());
712    }
713
714    #[test]
715    fn test_schema_compatibility_newer() {
716        let compat = check_schema_compatibility("99.0");
717        assert!(matches!(compat, SchemaCompatibility::Newer { .. }));
718        assert!(compat.is_loadable()); // Newer versions are loadable with warning
719        assert!(compat.warning().is_some());
720        assert!(compat.error().is_none());
721    }
722
723    #[test]
724    fn test_schema_compatibility_older() {
725        // This test assumes MIN_SCHEMA_VERSION is "1.0"
726        let compat = check_schema_compatibility("0.5");
727        assert!(matches!(compat, SchemaCompatibility::Older { .. }));
728        assert!(!compat.is_loadable());
729        assert!(compat.warning().is_none());
730        assert!(compat.error().is_some());
731    }
732
733    #[test]
734    fn test_schema_compatibility_invalid() {
735        let compat = check_schema_compatibility("not-a-version");
736        assert!(matches!(compat, SchemaCompatibility::Invalid { .. }));
737        assert!(!compat.is_loadable());
738        assert!(compat.error().is_some());
739
740        let compat = check_schema_compatibility("1.0.0");
741        assert!(matches!(compat, SchemaCompatibility::Invalid { .. }));
742    }
743
744    #[test]
745    fn test_default_schema_version() {
746        let config = Config::default_for_testing();
747        assert_eq!(config.schema_version, CURRENT_SCHEMA_VERSION);
748    }
749
750    #[test]
751    fn test_kdl_with_schema_version() {
752        let kdl = r#"
753            schema-version "1.0"
754            server {
755                worker-threads 4
756            }
757            listeners {
758                listener "http" {
759                    address "0.0.0.0:8080"
760                    protocol "http"
761                }
762            }
763        "#;
764        let config = Config::from_kdl(kdl).unwrap();
765        assert_eq!(config.schema_version, "1.0");
766    }
767
768    #[test]
769    fn test_kdl_without_schema_version_uses_default() {
770        let kdl = r#"
771            server {
772                worker-threads 4
773            }
774            listeners {
775                listener "http" {
776                    address "0.0.0.0:8080"
777                    protocol "http"
778                }
779            }
780        "#;
781        let config = Config::from_kdl(kdl).unwrap();
782        assert_eq!(config.schema_version, CURRENT_SCHEMA_VERSION);
783    }
784}