diff --git a/bin/fxt/build.rs b/bin/fxt/build.rs index 74c0694ce..81cb54891 100644 --- a/bin/fxt/build.rs +++ b/bin/fxt/build.rs @@ -14,7 +14,6 @@ fn main() -> Result<(), Box> { "monitoring", "risk", "data_acquisition", - "config_service", ] .iter() .map(|p| format!("{proto_root}/{p}.proto")) @@ -40,7 +39,6 @@ fn main() -> Result<(), Box> { "monitoring", "risk", "data_acquisition", - "config_service", ] { println!("cargo:rerun-if-changed={proto_root}/{proto}.proto"); } diff --git a/bin/fxt/src/grpc.rs b/bin/fxt/src/grpc.rs index b1652a529..637ab890f 100644 --- a/bin/fxt/src/grpc.rs +++ b/bin/fxt/src/grpc.rs @@ -176,17 +176,4 @@ impl FoxhuntClient { ) } - /// API Gateway configuration service client (package `foxhunt.config`). - /// - /// Distinct from [`Self::config`] which targets the trading service's - /// configuration proto (package `config`). - pub fn config_service( - &self, - ) -> crate::proto::config_service::configuration_service_client::ConfigurationServiceClient - { - crate::proto::config_service::configuration_service_client::ConfigurationServiceClient::with_interceptor( - self.channel.clone(), - self.interceptor.clone(), - ) - } } diff --git a/bin/fxt/src/lib.rs b/bin/fxt/src/lib.rs index ef22122ec..7d7b4c6a3 100644 --- a/bin/fxt/src/lib.rs +++ b/bin/fxt/src/lib.rs @@ -122,12 +122,4 @@ pub mod proto { tonic::include_proto!("data_acquisition"); } - /// Config service protobuf definitions (package `foxhunt.config`) - /// - /// Note: This is from `config_service.proto` (API Gateway config service), - /// distinct from `config.proto` (trading service config, package `config`). - #[allow(clippy::shadow_reuse, clippy::impl_trait_in_params, clippy::empty_structs_with_brackets)] - pub mod config_service { - tonic::include_proto!("foxhunt.config"); - } } diff --git a/proto/config_service.proto b/proto/config_service.proto deleted file mode 100644 index de96bb785..000000000 --- a/proto/config_service.proto +++ /dev/null @@ -1,81 +0,0 @@ -syntax = "proto3"; - -package foxhunt.config; - -// Configuration Management Service -service ConfigurationService { - // Get a single configuration value - rpc GetConfig(GetConfigRequest) returns (GetConfigResponse); - - // Update a configuration value - rpc UpdateConfig(UpdateConfigRequest) returns (UpdateConfigResponse); - - // List all configurations for a service scope - rpc ListConfigs(ListConfigsRequest) returns (ListConfigsResponse); - - // Trigger configuration reload - rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse); -} - -// Get configuration request -message GetConfigRequest { - string service_scope = 1; - string config_key = 2; -} - -// Get configuration response -message GetConfigResponse { - string config_value = 1; // JSON-serialized value - string data_type = 2; - string description = 3; - int64 updated_at = 4; // Unix timestamp - string updated_by = 5; -} - -// Update configuration request -message UpdateConfigRequest { - string service_scope = 1; - string config_key = 2; - string new_value = 3; // JSON-serialized value - string updated_by = 4; -} - -// Update configuration response -message UpdateConfigResponse { - bool success = 1; - string message = 2; -} - -// List configurations request -message ListConfigsRequest { - optional string service_scope = 1; // If not provided, lists all scopes -} - -// Configuration item -message ConfigItem { - string service_scope = 1; - string config_key = 2; - string config_value = 3; // JSON-serialized value - string data_type = 4; - string description = 5; - int64 created_at = 6; - int64 updated_at = 7; - string updated_by = 8; -} - -// List configurations response -message ListConfigsResponse { - repeated ConfigItem configs = 1; -} - -// Reload configuration request -message ReloadConfigRequest { - optional string service_scope = 1; - optional string config_key = 2; -} - -// Reload configuration response -message ReloadConfigResponse { - bool success = 1; - string message = 2; -} diff --git a/services/api_gateway/build.rs b/services/api_gateway/build.rs index dd2a3d0e8..b82355483 100644 --- a/services/api_gateway/build.rs +++ b/services/api_gateway/build.rs @@ -4,10 +4,9 @@ //! Proto packages: //! - `foxhunt.tli` (fxt_trading.proto) - Fat-client unified interface (server+client) //! - `trading` (trading.proto) - Backend trading service (client-only) -//! - `foxhunt.config` (config_service.proto) - Config service (server+client) //! - `risk` (risk.proto) - Risk service (client-only) //! - `monitoring` (monitoring.proto) - Monitoring service (server+client) -//! - `config` (config.proto) - Config backend (client-only) +//! - `config` (config.proto) - Config service (server+client) //! - `ml_training` (ml_training.proto) - ML Training service (server+client) //! - `trading_agent` (trading_agent.proto) - Trading Agent service (server+client) @@ -17,19 +16,6 @@ fn main() -> Result<(), Box> { let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); - // Compile Config Service proto (foxhunt.config package) - config - .clone() - .build_server(true) - .build_client(true) - .file_descriptor_set_path(out_dir.join("config_service_descriptor.bin")) - .compile_well_known_types(true) - .extern_path(".google.protobuf", "::prost_types") - .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") - .server_mod_attribute(".", "#[allow(unused_qualifications)]") - .client_mod_attribute(".", "#[allow(unused_qualifications)]") - .compile_protos(&["../../proto/config_service.proto"], &["../../proto"])?; - // Compile fat-client TLI proto (package: foxhunt.tli) — server+client // Contains TradingService, BacktestingService, and MLService // API Gateway acts as server for incoming client requests @@ -193,7 +179,6 @@ fn main() -> Result<(), Box> { &["../../proto"], )?; - println!("cargo:rerun-if-changed=../../proto/config_service.proto"); println!("cargo:rerun-if-changed=../../proto/fxt_trading.proto"); println!("cargo:rerun-if-changed=../../proto/trading.proto"); println!("cargo:rerun-if-changed=../../proto/risk.proto"); diff --git a/services/api_gateway/src/config/endpoints.rs b/services/api_gateway/src/config/endpoints.rs index 025befc60..6f95c525b 100644 --- a/services/api_gateway/src/config/endpoints.rs +++ b/services/api_gateway/src/config/endpoints.rs @@ -2,24 +2,35 @@ use crate::config::ConfigurationManager; use crate::error::GatewayConfigError; +use futures::Stream; +use std::pin::Pin; use tonic::{Request, Response, Status}; use tracing::{debug, info, instrument}; -// Import generated proto code from crate root -use crate::foxhunt::config_proto::{ - configuration_service_server::{ConfigurationService, ConfigurationServiceServer}, - ConfigItem as ProtoConfigItem, GetConfigRequest, GetConfigResponse, ListConfigsRequest, - ListConfigsResponse, ReloadConfigRequest, ReloadConfigResponse, UpdateConfigRequest, - UpdateConfigResponse, +use crate::config_backend::{ + config_service_server::{ConfigService, ConfigServiceServer}, + ConfigChangeEvent, ConfigDataType, ConfigurationCategory, ConfigurationSetting, + DeleteConfigurationRequest, DeleteConfigurationResponse, ExportConfigurationRequest, + ExportConfigurationResponse, GetConfigSchemaRequest, GetConfigSchemaResponse, + GetConfigurationHistoryRequest, GetConfigurationHistoryResponse, GetConfigurationRequest, + GetConfigurationResponse, ImportConfigurationRequest, ImportConfigurationResponse, + ListCategoriesRequest, ListCategoriesResponse, RollbackConfigurationRequest, + RollbackConfigurationResponse, StreamConfigChangesRequest, UpdateConfigSchemaRequest, + UpdateConfigSchemaResponse, UpdateConfigurationRequest, UpdateConfigurationResponse, + ValidateConfigurationRequest, ValidateConfigurationResponse, }; -/// gRPC service implementation for configuration management -pub struct ConfigurationServiceImpl { +/// gRPC service implementation for gateway-local configuration management. +/// +/// Implements the `config.ConfigService` trait from config.proto. +/// Handles GetConfiguration, UpdateConfiguration, and ListCategories locally; +/// all other RPCs return `Status::unimplemented`. +pub struct GatewayConfigService { manager: std::sync::Arc>, } -impl ConfigurationServiceImpl { - /// Creates a new ConfigurationServiceImpl +impl GatewayConfigService { + /// Creates a new GatewayConfigService pub fn new(manager: ConfigurationManager) -> Self { Self { manager: std::sync::Arc::new(tokio::sync::RwLock::new(manager)), @@ -27,63 +38,94 @@ impl ConfigurationServiceImpl { } /// Returns the gRPC server instance - pub fn into_server(self) -> ConfigurationServiceServer { - ConfigurationServiceServer::new(self) + pub fn into_server(self) -> ConfigServiceServer { + ConfigServiceServer::new(self) } } #[tonic::async_trait] -impl ConfigurationService for ConfigurationServiceImpl { +impl ConfigService for GatewayConfigService { + type StreamConfigChangesStream = + Pin> + Send>>; + #[instrument(skip_all)] - async fn get_config( + async fn get_configuration( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { let req = request.into_inner(); - debug!("GetConfig: {}/{}", req.service_scope, req.config_key); + let category = req.category.unwrap_or_default(); + let key = req.key.unwrap_or_default(); + debug!("GetConfiguration: {}/{}", category, key); + + if category.is_empty() && key.is_empty() { + // List all configs + let manager = self.manager.read().await; + let configs = manager + .list_configs(None) + .await + .map_err(|e| Status::internal(format!("{}", e)))?; + + let settings: Result, Status> = configs + .into_iter() + .map(|c| config_to_setting(&c)) + .collect(); + + return Ok(Response::new(GetConfigurationResponse { + settings: settings?, + })); + } let manager = self.manager.read().await; - let config = manager - .get_config(&req.service_scope, &req.config_key) - .await - .map_err(|e| match e { - GatewayConfigError::NotFound { .. } => Status::not_found(format!("{}", e)), - _ => Status::internal(format!("{}", e)), - })?; - Ok(Response::new(GetConfigResponse { - config_value: serde_json::to_string(&config.config_value) - .map_err(|e| Status::internal(format!("Serialization error: {}", e)))?, - data_type: config.data_type, - description: config.description.unwrap_or_default(), - updated_at: config.updated_at.timestamp(), - updated_by: config.updated_by.unwrap_or_default(), - })) + if !key.is_empty() { + // Get specific config + let config = manager + .get_config(&category, &key) + .await + .map_err(|e| match e { + GatewayConfigError::NotFound { .. } => Status::not_found(format!("{}", e)), + _ => Status::internal(format!("{}", e)), + })?; + + Ok(Response::new(GetConfigurationResponse { + settings: vec![config_to_setting(&config)?], + })) + } else { + // List by category + let configs = manager + .list_configs(Some(&category)) + .await + .map_err(|e| Status::internal(format!("{}", e)))?; + + let settings: Result, Status> = configs + .into_iter() + .map(|c| config_to_setting(&c)) + .collect(); + + Ok(Response::new(GetConfigurationResponse { + settings: settings?, + })) + } } #[instrument(skip_all)] - async fn update_config( + async fn update_configuration( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { let req = request.into_inner(); info!( - "UpdateConfig: {}/{} by {}", - req.service_scope, req.config_key, req.updated_by + "UpdateConfiguration: {}/{} by {}", + req.category, req.key, req.changed_by ); - // Parse new value as JSON - let new_value: serde_json::Value = serde_json::from_str(&req.new_value) + let new_value: serde_json::Value = serde_json::from_str(&req.value) .map_err(|e| Status::invalid_argument(format!("Invalid JSON: {}", e)))?; let manager = self.manager.read().await; manager - .update_config( - &req.service_scope, - &req.config_key, - new_value, - &req.updated_by, - ) + .update_config(&req.category, &req.key, new_value, &req.changed_by) .await .map_err(|e| match e { GatewayConfigError::Validation(msg) => Status::invalid_argument(msg), @@ -91,65 +133,159 @@ impl ConfigurationService for ConfigurationServiceImpl { _ => Status::internal(format!("{}", e)), })?; - Ok(Response::new(UpdateConfigResponse { + Ok(Response::new(UpdateConfigurationResponse { success: true, - message: format!( - "Successfully updated {}/{}", - req.service_scope, req.config_key - ), + message: format!("Successfully updated {}/{}", req.category, req.key), + validation_result: None, + timestamp: chrono::Utc::now().timestamp(), })) } #[instrument(skip_all)] - async fn list_configs( + async fn delete_configuration( &self, - request: Request, - ) -> Result, Status> { + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Delete not supported by gateway config", + )) + } + + #[instrument(skip_all)] + async fn list_categories( + &self, + request: Request, + ) -> Result, Status> { let req = request.into_inner(); - debug!("ListConfigs: {:?}", req.service_scope); + debug!("ListCategories: {:?}", req.parent_category); let manager = self.manager.read().await; let configs = manager - .list_configs(req.service_scope.as_deref()) + .list_configs(req.parent_category.as_deref()) .await .map_err(|e| Status::internal(format!("{}", e)))?; - let proto_configs: Result, Status> = configs - .into_iter() - .map(|c| { - Ok(ProtoConfigItem { - service_scope: c.service_scope, - config_key: c.config_key, - config_value: serde_json::to_string(&c.config_value) - .map_err(|e| Status::internal(format!("{}", e)))?, - data_type: c.data_type, - description: c.description.unwrap_or_default(), - created_at: c.created_at.timestamp(), - updated_at: c.updated_at.timestamp(), - updated_by: c.updated_by.unwrap_or_default(), - }) + // Extract unique categories from configs + let mut seen = std::collections::HashSet::new(); + let categories = configs + .iter() + .filter_map(|c| { + if seen.insert(c.service_scope.clone()) { + Some(ConfigurationCategory { + id: 0, + name: c.service_scope.clone(), + description: String::new(), + parent_id: None, + display_order: 0, + icon: None, + created_at: c.created_at.timestamp(), + children: vec![], + }) + } else { + None + } }) .collect(); - Ok(Response::new(ListConfigsResponse { - configs: proto_configs?, - })) + Ok(Response::new(ListCategoriesResponse { categories })) } - #[instrument(skip_all)] - async fn reload_config( + async fn stream_config_changes( &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - info!("ReloadConfig: {:?}/{:?}", req.service_scope, req.config_key); + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Streaming not supported by gateway config", + )) + } - // For now, this is a no-op since hot-reload is automatic via NOTIFY/LISTEN - // In the future, this could force a cache invalidation + async fn validate_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Validation not supported by gateway config", + )) + } - Ok(Response::new(ReloadConfigResponse { - success: true, - message: "Configuration reload triggered (hot-reload active)".to_string(), - })) + async fn get_configuration_history( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "History not supported by gateway config", + )) + } + + async fn rollback_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Rollback not supported by gateway config", + )) + } + + async fn export_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Export not supported by gateway config", + )) + } + + async fn import_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Import not supported by gateway config", + )) + } + + async fn get_config_schema( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Schema not supported by gateway config", + )) + } + + async fn update_config_schema( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Schema update not supported by gateway config", + )) } } + +/// Convert a ConfigItem (from ConfigurationManager) to a ConfigurationSetting proto message +fn config_to_setting(c: &crate::config::ConfigItem) -> Result { + Ok(ConfigurationSetting { + id: 0, + category: c.service_scope.clone(), + key: c.config_key.clone(), + value: serde_json::to_string(&c.config_value) + .map_err(|e| Status::internal(format!("Serialization error: {}", e)))?, + data_type: ConfigDataType::String as i32, + hot_reload: false, + description: c.description.clone().unwrap_or_default(), + default_value: None, + required: false, + sensitive: false, + validation_rule: None, + environment_override: None, + min_value: None, + max_value: None, + enum_values: None, + depends_on: vec![], + tags: vec![], + display_order: 0, + created_at: c.created_at.timestamp(), + modified_at: c.updated_at.timestamp(), + }) +} diff --git a/services/api_gateway/src/config/mod.rs b/services/api_gateway/src/config/mod.rs index d4b7dad15..4b5136c3b 100644 --- a/services/api_gateway/src/config/mod.rs +++ b/services/api_gateway/src/config/mod.rs @@ -5,7 +5,7 @@ pub mod endpoints; pub mod manager; pub mod validator; -pub use endpoints::ConfigurationServiceImpl; +pub use endpoints::GatewayConfigService; pub use manager::{ConfigItem, ConfigurationManager}; pub use validator::{ConfigValidator, ValidationRules}; diff --git a/services/api_gateway/src/lib.rs b/services/api_gateway/src/lib.rs index 256cf0c77..7eac202f6 100644 --- a/services/api_gateway/src/lib.rs +++ b/services/api_gateway/src/lib.rs @@ -26,8 +26,6 @@ pub const ML_TRAINING_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("ml_training_descriptor"); pub const TRADING_AGENT_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("trading_agent_descriptor"); -pub const CONFIG_SERVICE_FILE_DESCRIPTOR_SET: &[u8] = - tonic::include_file_descriptor_set!("config_service_descriptor"); pub const TRADING_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("trading_descriptor"); pub const RISK_FILE_DESCRIPTOR_SET: &[u8] = @@ -46,9 +44,6 @@ pub mod foxhunt { pub mod tli { tonic::include_proto!("foxhunt.tli"); } - pub mod config_proto { - tonic::include_proto!("foxhunt.config"); - } } pub mod ml_training { @@ -113,7 +108,7 @@ pub use error::{GatewayConfigError, GatewayConfigResult}; // Re-export configuration management types pub use config::{ AuthzMetrics, AuthzService, ConfigItem, ConfigValidator, ConfigurationManager, - ConfigurationServiceImpl, PermissionResult, + GatewayConfigService, PermissionResult, }; // Re-export routing and rate limiting types diff --git a/services/api_gateway/src/main.rs b/services/api_gateway/src/main.rs index 97cf86cd1..5cb5161c9 100644 --- a/services/api_gateway/src/main.rs +++ b/services/api_gateway/src/main.rs @@ -682,7 +682,6 @@ async fn main() -> Result<()> { .register_encoded_file_descriptor_set(api_gateway::MONITORING_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::ML_TRAINING_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::TRADING_AGENT_FILE_DESCRIPTOR_SET) - .register_encoded_file_descriptor_set(api_gateway::CONFIG_SERVICE_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::TRADING_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::RISK_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::CONFIG_FILE_DESCRIPTOR_SET)