initial-commit

This commit is contained in:
Guillaume GRABÉ
2026-05-12 17:06:43 +02:00
commit 051a080dfa
110 changed files with 26377 additions and 0 deletions
+325
View File
@@ -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));
}
}