Files
foxhunt/services/api_gateway/src/config/endpoints.rs
jgrusewski edfe4da2e8 refactor(proto): remove duplicate config_service.proto
config_service.proto (package foxhunt.config, 4 RPCs) was a subset of
config.proto (package config, 12 RPCs). Migrated api_gateway to implement
ConfigService from config.proto directly. Removed dead config_service()
method from fxt client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:17:44 +01:00

292 lines
10 KiB
Rust

//! gRPC endpoints for configuration management
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};
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 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<tokio::sync::RwLock<ConfigurationManager>>,
}
impl GatewayConfigService {
/// Creates a new GatewayConfigService
pub fn new(manager: ConfigurationManager) -> Self {
Self {
manager: std::sync::Arc::new(tokio::sync::RwLock::new(manager)),
}
}
/// Returns the gRPC server instance
pub fn into_server(self) -> ConfigServiceServer<Self> {
ConfigServiceServer::new(self)
}
}
#[tonic::async_trait]
impl ConfigService for GatewayConfigService {
type StreamConfigChangesStream =
Pin<Box<dyn Stream<Item = Result<ConfigChangeEvent, Status>> + Send>>;
#[instrument(skip_all)]
async fn get_configuration(
&self,
request: Request<GetConfigurationRequest>,
) -> Result<Response<GetConfigurationResponse>, Status> {
let req = request.into_inner();
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<Vec<ConfigurationSetting>, Status> = configs
.into_iter()
.map(|c| config_to_setting(&c))
.collect();
return Ok(Response::new(GetConfigurationResponse {
settings: settings?,
}));
}
let manager = self.manager.read().await;
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<Vec<ConfigurationSetting>, Status> = configs
.into_iter()
.map(|c| config_to_setting(&c))
.collect();
Ok(Response::new(GetConfigurationResponse {
settings: settings?,
}))
}
}
#[instrument(skip_all)]
async fn update_configuration(
&self,
request: Request<UpdateConfigurationRequest>,
) -> Result<Response<UpdateConfigurationResponse>, Status> {
let req = request.into_inner();
info!(
"UpdateConfiguration: {}/{} by {}",
req.category, req.key, req.changed_by
);
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.category, &req.key, new_value, &req.changed_by)
.await
.map_err(|e| match e {
GatewayConfigError::Validation(msg) => Status::invalid_argument(msg),
GatewayConfigError::NotFound { .. } => Status::not_found(format!("{}", e)),
_ => Status::internal(format!("{}", e)),
})?;
Ok(Response::new(UpdateConfigurationResponse {
success: true,
message: format!("Successfully updated {}/{}", req.category, req.key),
validation_result: None,
timestamp: chrono::Utc::now().timestamp(),
}))
}
#[instrument(skip_all)]
async fn delete_configuration(
&self,
_request: Request<DeleteConfigurationRequest>,
) -> Result<Response<DeleteConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Delete not supported by gateway config",
))
}
#[instrument(skip_all)]
async fn list_categories(
&self,
request: Request<ListCategoriesRequest>,
) -> Result<Response<ListCategoriesResponse>, Status> {
let req = request.into_inner();
debug!("ListCategories: {:?}", req.parent_category);
let manager = self.manager.read().await;
let configs = manager
.list_configs(req.parent_category.as_deref())
.await
.map_err(|e| Status::internal(format!("{}", e)))?;
// 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(ListCategoriesResponse { categories }))
}
async fn stream_config_changes(
&self,
_request: Request<StreamConfigChangesRequest>,
) -> Result<Response<Self::StreamConfigChangesStream>, Status> {
Err(Status::unimplemented(
"Streaming not supported by gateway config",
))
}
async fn validate_configuration(
&self,
_request: Request<ValidateConfigurationRequest>,
) -> Result<Response<ValidateConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Validation not supported by gateway config",
))
}
async fn get_configuration_history(
&self,
_request: Request<GetConfigurationHistoryRequest>,
) -> Result<Response<GetConfigurationHistoryResponse>, Status> {
Err(Status::unimplemented(
"History not supported by gateway config",
))
}
async fn rollback_configuration(
&self,
_request: Request<RollbackConfigurationRequest>,
) -> Result<Response<RollbackConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Rollback not supported by gateway config",
))
}
async fn export_configuration(
&self,
_request: Request<ExportConfigurationRequest>,
) -> Result<Response<ExportConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Export not supported by gateway config",
))
}
async fn import_configuration(
&self,
_request: Request<ImportConfigurationRequest>,
) -> Result<Response<ImportConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Import not supported by gateway config",
))
}
async fn get_config_schema(
&self,
_request: Request<GetConfigSchemaRequest>,
) -> Result<Response<GetConfigSchemaResponse>, Status> {
Err(Status::unimplemented(
"Schema not supported by gateway config",
))
}
async fn update_config_schema(
&self,
_request: Request<UpdateConfigSchemaRequest>,
) -> Result<Response<UpdateConfigSchemaResponse>, 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<ConfigurationSetting, Status> {
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(),
})
}