Public Access
initial-commit
This commit is contained in:
+325
@@ -0,0 +1,325 @@
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user