Public Access
326 lines
9.7 KiB
Rust
326 lines
9.7 KiB
Rust
/// Configuration management for Charybdis
|
|
///
|
|
/// This module handles loading configuration from a TOML file with support for
|
|
/// environment variable substitution using ${VAR_NAME} syntax.
|
|
use anyhow::{Context, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use tracing::{debug, info};
|
|
|
|
/// Main Charybdis configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Config {
|
|
/// Server configuration
|
|
pub server: ServerConfig,
|
|
|
|
/// Database configuration
|
|
pub database: DatabaseConfig,
|
|
|
|
/// Security configuration (mTLS and RBAC)
|
|
#[serde(default)]
|
|
pub security: crate::security::config::SecurityConfig,
|
|
|
|
/// OpenTelemetry configuration
|
|
#[serde(default)]
|
|
pub telemetry: crate::telemetry::config::TelemetryConfig,
|
|
|
|
/// Plugin configuration
|
|
#[serde(default)]
|
|
pub plugins: PluginsConfig,
|
|
}
|
|
|
|
/// Server configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServerConfig {
|
|
/// gRPC server host
|
|
#[serde(default = "default_grpc_host")]
|
|
pub grpc_host: String,
|
|
|
|
/// gRPC server port
|
|
#[serde(default = "default_grpc_port")]
|
|
pub grpc_port: u16,
|
|
|
|
/// YAML adapter configuration
|
|
#[serde(default)]
|
|
pub yaml_adapter: YamlAdapterConfig,
|
|
}
|
|
|
|
/// YAML adapter configuration for Backstage integration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct YamlAdapterConfig {
|
|
/// Enable YAML adapter
|
|
#[serde(default = "default_yaml_enabled")]
|
|
pub enabled: bool,
|
|
|
|
/// YAML adapter host
|
|
#[serde(default = "default_yaml_host")]
|
|
pub host: String,
|
|
|
|
/// YAML adapter port
|
|
#[serde(default = "default_yaml_port")]
|
|
pub port: u16,
|
|
}
|
|
|
|
/// Database configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DatabaseConfig {
|
|
/// PostgreSQL connection URL
|
|
/// Supports environment variable substitution: ${DATABASE_URL}
|
|
pub url: String,
|
|
|
|
/// Maximum number of connections in the pool
|
|
#[serde(default = "default_max_connections")]
|
|
pub max_connections: u32,
|
|
|
|
/// Connection timeout in seconds
|
|
#[serde(default = "default_connection_timeout")]
|
|
pub connection_timeout_secs: u64,
|
|
}
|
|
|
|
/// Plugin system configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct PluginsConfig {
|
|
/// DefectDojo plugin configuration
|
|
#[serde(default)]
|
|
pub defectdojo: Option<HashMap<String, toml::Value>>,
|
|
|
|
/// Dependency-Track plugin configuration
|
|
#[serde(default)]
|
|
pub dependencytrack: Option<HashMap<String, toml::Value>>,
|
|
|
|
/// Keycloak plugin configuration
|
|
#[serde(default)]
|
|
pub keycloak: Option<HashMap<String, toml::Value>>,
|
|
|
|
/// Custom plugin configurations
|
|
#[serde(flatten)]
|
|
pub custom: HashMap<String, HashMap<String, toml::Value>>,
|
|
}
|
|
|
|
// Default values
|
|
fn default_grpc_host() -> String {
|
|
"[::1]".to_string()
|
|
}
|
|
|
|
fn default_grpc_port() -> u16 {
|
|
50051
|
|
}
|
|
|
|
fn default_yaml_enabled() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_yaml_host() -> String {
|
|
"0.0.0.0".to_string()
|
|
}
|
|
|
|
fn default_yaml_port() -> u16 {
|
|
8080
|
|
}
|
|
|
|
fn default_max_connections() -> u32 {
|
|
10
|
|
}
|
|
|
|
fn default_connection_timeout() -> u64 {
|
|
30
|
|
}
|
|
|
|
impl Default for ServerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
grpc_host: default_grpc_host(),
|
|
grpc_port: default_grpc_port(),
|
|
yaml_adapter: YamlAdapterConfig::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for YamlAdapterConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: default_yaml_enabled(),
|
|
host: default_yaml_host(),
|
|
port: default_yaml_port(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
/// Load configuration from a TOML file
|
|
///
|
|
/// Environment variables in the format ${VAR_NAME} will be substituted
|
|
/// with their values from the environment.
|
|
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
let path = path.as_ref();
|
|
info!("Loading configuration from: {}", path.display());
|
|
|
|
// Read the file
|
|
let content = fs::read_to_string(path)
|
|
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
|
|
|
|
// Substitute environment variables
|
|
let content = Self::substitute_env_vars(&content)?;
|
|
|
|
debug!("Parsing configuration");
|
|
|
|
// Parse TOML
|
|
let config: Config = toml::from_str(&content)
|
|
.with_context(|| format!("Failed to parse config file: {}", path.display()))?;
|
|
|
|
info!("Configuration loaded successfully");
|
|
Ok(config)
|
|
}
|
|
|
|
/// Load configuration from default location
|
|
///
|
|
/// Looks for config in the following order:
|
|
/// 1. ./config.toml (current directory)
|
|
/// 2. ./charybdis.toml
|
|
/// 3. /etc/charybdis/config.toml (Linux/Unix)
|
|
///
|
|
/// Falls back to environment variables if no config file is found.
|
|
pub fn load() -> Result<Self> {
|
|
let candidates = vec![
|
|
"./config.toml",
|
|
"./charybdis.toml",
|
|
"/etc/charybdis/config.toml",
|
|
];
|
|
|
|
for path in candidates {
|
|
if Path::new(path).exists() {
|
|
return Self::from_file(path);
|
|
}
|
|
}
|
|
|
|
info!("No config file found, using environment variables");
|
|
Self::from_env()
|
|
}
|
|
|
|
/// Create configuration from environment variables (legacy support)
|
|
pub fn from_env() -> Result<Self> {
|
|
use crate::security::config::SecurityConfig;
|
|
use crate::telemetry::config::TelemetryConfig;
|
|
|
|
let database_url = std::env::var("DATABASE_URL")
|
|
.context("DATABASE_URL must be set (or provide config.toml)")?;
|
|
|
|
Ok(Config {
|
|
server: ServerConfig {
|
|
grpc_host: std::env::var("GRPC_HOST").unwrap_or_else(|_| default_grpc_host()),
|
|
grpc_port: std::env::var("GRPC_PORT")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or_else(default_grpc_port),
|
|
yaml_adapter: YamlAdapterConfig::default(),
|
|
},
|
|
database: DatabaseConfig {
|
|
url: database_url,
|
|
max_connections: default_max_connections(),
|
|
connection_timeout_secs: default_connection_timeout(),
|
|
},
|
|
security: SecurityConfig::from_env(),
|
|
telemetry: TelemetryConfig::from_env(),
|
|
plugins: PluginsConfig::default(),
|
|
})
|
|
}
|
|
|
|
/// Substitute environment variables in the format ${VAR_NAME} or ${VAR_NAME:-default}
|
|
fn substitute_env_vars(content: &str) -> Result<String> {
|
|
let mut result = content.to_string();
|
|
// Match ${VAR_NAME} or ${VAR_NAME:-default_value}
|
|
let var_pattern =
|
|
regex::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)(?::-((?:[^}])*))?\}").unwrap();
|
|
|
|
for capture in var_pattern.captures_iter(content) {
|
|
let full_match = &capture[0];
|
|
let var_name = &capture[1];
|
|
let default_value = capture.get(2).map(|m| m.as_str());
|
|
|
|
match std::env::var(var_name) {
|
|
Ok(value) => {
|
|
debug!("Substituting ${{{}}}", var_name);
|
|
result = result.replace(full_match, &value);
|
|
}
|
|
Err(_) => {
|
|
if let Some(default) = default_value {
|
|
debug!(
|
|
"Environment variable not set: ${{{}}}, using default: {}",
|
|
var_name, default
|
|
);
|
|
result = result.replace(full_match, default);
|
|
} else {
|
|
debug!("Environment variable not set: ${{{}}}", var_name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_env_var_substitution() {
|
|
// Use PATH which always exists in test environment
|
|
let path_value = std::env::var("PATH").unwrap();
|
|
|
|
let input = "some_path = \"${PATH}\"";
|
|
let result = Config::substitute_env_vars(input).unwrap();
|
|
|
|
assert!(result.contains(&path_value));
|
|
assert!(!result.contains("${PATH}"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_env_var_substitution_missing() {
|
|
// Test that missing env vars are left as-is (allowing optional substitution)
|
|
let input = "url = \"postgresql://${NONEXISTENT_VAR_12345}:password@localhost\"";
|
|
let result = Config::substitute_env_vars(input).unwrap();
|
|
|
|
// Variable should remain unchanged when not found
|
|
assert!(result.contains("${NONEXISTENT_VAR_12345}"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_env_var_substitution_multiple() {
|
|
// Test multiple variable substitutions
|
|
let path_value = std::env::var("PATH").unwrap();
|
|
|
|
let input = "path = \"${PATH}\" and user = \"${USER}\"";
|
|
let result = Config::substitute_env_vars(input).unwrap();
|
|
|
|
assert!(result.contains(&path_value));
|
|
assert!(!result.contains("${PATH}"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_values() {
|
|
let server = ServerConfig::default();
|
|
assert_eq!(server.grpc_port, 50051);
|
|
assert_eq!(server.grpc_host, "[::1]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_env_var_with_default_value() {
|
|
let input = "url = \"${NONEXISTENT_VAR_99999:-http://localhost:8080}\"";
|
|
let result = Config::substitute_env_vars(input).unwrap();
|
|
assert_eq!(result, "url = \"http://localhost:8080\"");
|
|
}
|
|
|
|
#[test]
|
|
fn test_env_var_with_default_value_overridden() {
|
|
// PATH always exists — use it with a default that should be ignored
|
|
let path_value = std::env::var("PATH").unwrap();
|
|
let input = "url = \"${PATH:-http://fallback:8080}\"";
|
|
let result = Config::substitute_env_vars(input).unwrap();
|
|
assert_eq!(result, format!("url = \"{}\"", path_value));
|
|
}
|
|
}
|