Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/auto-diagnostic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Auto Diagnostic Bundle

on:
push:
branches:
- 'feat/**'
- 'fix/**'
- 'chore/**'

# Skip bot commits to avoid infinite loop
concurrency:
group: diagnostic-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write

jobs:
build-diagnostic:
name: Run build.py and commit diagnostic bundle
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.author.name, 'github-actions')"

steps:
- name: Checkout branch
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
gcc g++ cmake make lua5.4 luajit ruby ghc

- name: Make encryptly executable
run: |
chmod +x tools/encryptly/linux-x64/encryptly
chmod +x tools/encryptly/linux-arm64/encryptly || true

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Run build.py
run: python3 build.py
continue-on-error: true

- name: Commit and push diagnostic bundle
run: |
git add diagnostic/ || true
if git diff --cached --quiet; then
echo "No diagnostic files to commit"
exit 0
fi
git commit -m "ci: add diagnostic bundle [skip ci]"
git push origin HEAD
32 changes: 5 additions & 27 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,36 +1,14 @@
[package]
name = "tent-backend"
name = "backend"
version = "0.1.0"
edition = "2021"
description = "Tent of Trials - Backend Microservices Orchestration Framework"
authors = ["TentOfTrials"]

[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
thiserror = "2"
anyhow = "1"
async-trait = "0.1"
futures = "0.3"
dashmap = "6"
parking_lot = "0.12"
bytes = "1"
regex = "1"
sha2 = "0.10"
reqwest = { version = "0.12", features = ["json"] }
lazy_static = "1"
log = "0.4"
thiserror = "1"

[build-dependencies]
tonic-build = "0.12"

[profile.release]
opt-level = 3
debug = false
[dev-dependencies]
tempfile = "3"
251 changes: 170 additions & 81 deletions backend/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,106 +1,195 @@
use anyhow::Result;
//! Configuration loader module
//!
//! Provides multi-format configuration loading with environment variable overrides.

use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::Path;
use thiserror::Error;

/// Configuration error types
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Failed to read config file: {0}")]
FileRead(#[from] std::io::Error),

#[error("Failed to parse TOML: {0}")]
TomlParse(#[from] toml::de::Error),

#[error("Failed to parse JSON: {0}")]
JsonParse(#[from] serde_json::Error),

#[error("Failed to parse YAML: {0}")]
YamlParse(#[from] serde_yaml::Error),

#[error("Unsupported config format: {0}")]
UnsupportedFormat(String),

#[error("Validation error: {0}")]
Validation(String),
}

/// Application configuration structure
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AppConfig {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub database: DatabaseConfig,
#[serde(default)]
pub ai: AiConfig,
}

/// Server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceConfig {
pub name: String,
pub version: String,
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
pub tls_enabled: bool,
pub tls_cert_path: Option<String>,
pub tls_key_path: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryConfig {
pub backend: String,
pub endpoints: Vec<String>,
pub heartbeat_interval_ms: u64,
pub ttl_seconds: u64,
pub replication_factor: u32,
fn default_host() -> String { "127.0.0.1".to_string() }
fn default_port() -> u16 { 8080 }

impl Default for ServerConfig {
fn default() -> Self {
Self { host: default_host(), port: default_port() }
}
}

/// Database configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoveryConfig {
pub provider: String,
pub namespace: String,
pub tags: Vec<String>,
pub health_check_path: String,
pub health_check_interval_ms: u64,
pub struct DatabaseConfig {
#[serde(default = "default_db_url")]
pub url: String,
#[serde(default = "default_pool_size")]
pub pool_size: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessagingConfig {
pub broker_type: String,
pub uris: Vec<String>,
pub consumer_group: String,
pub max_retries: u32,
pub retry_backoff_ms: u64,
pub batch_size: u32,
pub compression: String,
fn default_db_url() -> String { "sqlite://:memory:".to_string() }
fn default_pool_size() -> u32 { 5 }

impl Default for DatabaseConfig {
fn default() -> Self {
Self { url: default_db_url(), pool_size: default_pool_size() }
}
}

/// AI configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootConfig {
pub service: ServiceConfig,
pub registry: RegistryConfig,
pub discovery: DiscoveryConfig,
pub messaging: MessagingConfig,
pub struct AiConfig {
#[serde(default)]
pub model_path: Option<String>,
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
}

impl Default for RootConfig {
fn default_max_tokens() -> usize { 2048 }

impl Default for AiConfig {
fn default() -> Self {
Self {
service: ServiceConfig {
name: "tent-backend".into(),
version: "0.1.0".into(),
host: "0.0.0.0".into(),
port: 8080,
tls_enabled: false,
tls_cert_path: None,
tls_key_path: None,
},
registry: RegistryConfig {
backend: "etcd".into(),
endpoints: vec!["localhost:2379".into()],
heartbeat_interval_ms: 5000,
ttl_seconds: 30,
replication_factor: 3,
},
discovery: DiscoveryConfig {
provider: "consul".into(),
namespace: "tent".into(),
tags: vec!["microservice".into(), "orchestration".into()],
health_check_path: "/health".into(),
health_check_interval_ms: 10000,
},
messaging: MessagingConfig {
broker_type: "kafka".into(),
uris: vec!["localhost:9092".into()],
consumer_group: "tent-consumers".into(),
max_retries: 3,
retry_backoff_ms: 1000,
batch_size: 500,
compression: "snappy".into(),
},
}
Self { model_path: None, max_tokens: default_max_tokens() }
}
}

pub async fn load_config(path: &str) -> Result<RootConfig> {
let path = Path::new(path);
if path.exists() {
let contents = tokio::fs::read_to_string(path).await?;
let config: RootConfig = toml::from_str(&contents)?;
tracing::info!("configuration loaded from {}", path.display());
/// Configuration loader with multi-format support
pub struct ConfigLoader;

impl ConfigLoader {
/// Load configuration from a file with environment variable overrides
pub fn load<P: AsRef<Path>>(path: P) -> Result<AppConfig, ConfigError> {
let path = path.as_ref();
let mut config = if path.exists() {
Self::load_from_file(path)?
} else {
AppConfig::default()
};
Self::apply_env_overrides(&mut config);
Self::validate(&config)?;
Ok(config)
} else {
tracing::warn!(
"config file {} not found, using defaults",
path.display()
);
Ok(RootConfig::default())
}

/// Load configuration from a file based on extension
fn load_from_file(path: &Path) -> Result<AppConfig, ConfigError> {
let content = fs::read_to_string(path)?;
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");

match ext.to_lowercase().as_str() {
"toml" => Ok(toml::from_str(&content)?),
"json" => Ok(serde_json::from_str(&content)?),
"yaml" | "yml" => Ok(serde_yaml::from_str(&content)?),
_ => Err(ConfigError::UnsupportedFormat(ext.to_string())),
}
}

/// Apply environment variable overrides
fn apply_env_overrides(config: &mut AppConfig) {
if let Ok(host) = env::var("APP_SERVER_HOST") {
config.server.host = host;
}
if let Ok(port) = env::var("APP_SERVER_PORT") {
if let Ok(p) = port.parse() {
config.server.port = p;
}
}
if let Ok(url) = env::var("APP_DATABASE_URL") {
config.database.url = url;
}
}

/// Validate configuration
fn validate(config: &AppConfig) -> Result<(), ConfigError> {
if config.server.port == 0 {
return Err(ConfigError::Validation("Port cannot be 0".to_string()));
}
if config.database.pool_size == 0 {
return Err(ConfigError::Validation("Pool size cannot be 0".to_string()));
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;

#[test]
fn test_default_config() {
let config = AppConfig::default();
assert_eq!(config.server.host, "127.0.0.1");
assert_eq!(config.server.port, 8080);
}

#[test]
fn test_load_toml() {
let mut file = NamedTempFile::with_suffix(".toml").unwrap();
writeln!(file, "[server]\nhost = \"0.0.0.0\"\nport = 3000").unwrap();

let config = ConfigLoader::load(file.path()).unwrap();
assert_eq!(config.server.host, "0.0.0.0");
assert_eq!(config.server.port, 3000);
}

#[test]
fn test_load_json() {
let mut file = NamedTempFile::with_suffix(".json").unwrap();
writeln!(file, r{{"server": {{"host": "localhost", "port": 9000}}}}).unwrap();

let config = ConfigLoader::load(file.path()).unwrap();
assert_eq!(config.server.host, "localhost");
assert_eq!(config.server.port, 9000);
}

#[test]
fn test_validation_fails() {
let mut file = NamedTempFile::with_suffix(".toml").unwrap();
writeln!(file, "[server]\nport = 0").unwrap();

let result = ConfigLoader::load(file.path());
assert!(result.is_err());
}
}
Loading