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>
292 lines
10 KiB
Rust
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(),
|
|
})
|
|
}
|