photoncloud-monorepo/fiberlb/crates/fiberlb-server/src/services/backend.rs
centra a7ec7e2158 Add T026 practical test + k8shost to flake + workspace files
- Created T026-practical-test task.yaml for MVP smoke testing
- Added k8shost-server to flake.nix (packages, apps, overlays)
- Staged all workspace directories for nix flake build
- Updated flake.nix shellHook to include k8shost

Resolves: T026.S1 blocker (R8 - nix submodule visibility)
2025-12-09 06:07:50 +09:00

196 lines
6.3 KiB
Rust

//! Backend service implementation
use std::sync::Arc;
use crate::metadata::LbMetadataStore;
use fiberlb_api::{
backend_service_server::BackendService,
CreateBackendRequest, CreateBackendResponse,
DeleteBackendRequest, DeleteBackendResponse,
GetBackendRequest, GetBackendResponse,
ListBackendsRequest, ListBackendsResponse,
UpdateBackendRequest, UpdateBackendResponse,
Backend as ProtoBackend, BackendAdminState as ProtoBackendAdminState,
BackendStatus as ProtoBackendStatus,
};
use fiberlb_types::{Backend, BackendAdminState, BackendId, BackendStatus, PoolId};
use tonic::{Request, Response, Status};
use uuid::Uuid;
/// Backend service implementation
pub struct BackendServiceImpl {
metadata: Arc<LbMetadataStore>,
}
impl BackendServiceImpl {
/// Create a new BackendServiceImpl
pub fn new(metadata: Arc<LbMetadataStore>) -> Self {
Self { metadata }
}
}
/// Convert domain Backend to proto
fn backend_to_proto(backend: &Backend) -> ProtoBackend {
ProtoBackend {
id: backend.id.to_string(),
name: backend.name.clone(),
pool_id: backend.pool_id.to_string(),
address: backend.address.clone(),
port: backend.port as u32,
weight: backend.weight,
admin_state: match backend.admin_state {
BackendAdminState::Enabled => ProtoBackendAdminState::Enabled.into(),
BackendAdminState::Disabled => ProtoBackendAdminState::Disabled.into(),
BackendAdminState::Drain => ProtoBackendAdminState::Drain.into(),
},
status: match backend.status {
BackendStatus::Online => ProtoBackendStatus::Online.into(),
BackendStatus::Offline => ProtoBackendStatus::Offline.into(),
BackendStatus::Checking => ProtoBackendStatus::Checking.into(),
BackendStatus::Disabled => ProtoBackendStatus::Offline.into(),
BackendStatus::Unknown => ProtoBackendStatus::Unknown.into(),
},
created_at: backend.created_at,
updated_at: backend.updated_at,
}
}
/// Parse BackendId from string
fn parse_backend_id(id: &str) -> Result<BackendId, Status> {
let uuid: Uuid = id
.parse()
.map_err(|_| Status::invalid_argument("invalid backend ID"))?;
Ok(BackendId::from_uuid(uuid))
}
/// Parse PoolId from string
fn parse_pool_id(id: &str) -> Result<PoolId, Status> {
let uuid: Uuid = id
.parse()
.map_err(|_| Status::invalid_argument("invalid pool ID"))?;
Ok(PoolId::from_uuid(uuid))
}
#[tonic::async_trait]
impl BackendService for BackendServiceImpl {
async fn create_backend(
&self,
request: Request<CreateBackendRequest>,
) -> Result<Response<CreateBackendResponse>, Status> {
let req = request.into_inner();
// Validate required fields
if req.name.is_empty() {
return Err(Status::invalid_argument("name is required"));
}
if req.pool_id.is_empty() {
return Err(Status::invalid_argument("pool_id is required"));
}
if req.address.is_empty() {
return Err(Status::invalid_argument("address is required"));
}
if req.port == 0 {
return Err(Status::invalid_argument("port is required"));
}
let pool_id = parse_pool_id(&req.pool_id)?;
// Create new backend
let mut backend = Backend::new(&req.name, pool_id, &req.address, req.port as u16);
// Apply weight if specified
if req.weight > 0 {
backend.weight = req.weight;
}
// Save backend
self.metadata
.save_backend(&backend)
.await
.map_err(|e| Status::internal(format!("failed to save backend: {}", e)))?;
Ok(Response::new(CreateBackendResponse {
backend: Some(backend_to_proto(&backend)),
}))
}
async fn get_backend(
&self,
request: Request<GetBackendRequest>,
) -> Result<Response<GetBackendResponse>, Status> {
let req = request.into_inner();
if req.id.is_empty() {
return Err(Status::invalid_argument("id is required"));
}
let _backend_id = parse_backend_id(&req.id)?;
// Need pool_id context to efficiently look up backend
// The proto doesn't include pool_id in GetBackendRequest
Err(Status::unimplemented(
"get_backend by ID requires pool_id context; use list_backends instead",
))
}
async fn list_backends(
&self,
request: Request<ListBackendsRequest>,
) -> Result<Response<ListBackendsResponse>, Status> {
let req = request.into_inner();
if req.pool_id.is_empty() {
return Err(Status::invalid_argument("pool_id is required"));
}
let pool_id = parse_pool_id(&req.pool_id)?;
let backends = self
.metadata
.list_backends(&pool_id)
.await
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?;
let proto_backends: Vec<ProtoBackend> = backends.iter().map(backend_to_proto).collect();
Ok(Response::new(ListBackendsResponse {
backends: proto_backends,
next_page_token: String::new(),
}))
}
async fn update_backend(
&self,
request: Request<UpdateBackendRequest>,
) -> Result<Response<UpdateBackendResponse>, Status> {
let req = request.into_inner();
if req.id.is_empty() {
return Err(Status::invalid_argument("id is required"));
}
// For update, we need to know the pool_id to load the backend
// This is a limitation - the proto doesn't require pool_id for update
// We'll need to scan or require pool_id in a future update
return Err(Status::unimplemented(
"update_backend requires pool_id context; include pool_id in request",
));
}
async fn delete_backend(
&self,
request: Request<DeleteBackendRequest>,
) -> Result<Response<DeleteBackendResponse>, Status> {
let req = request.into_inner();
if req.id.is_empty() {
return Err(Status::invalid_argument("id is required"));
}
// Same limitation as update - need pool_id context
return Err(Status::unimplemented(
"delete_backend requires pool_id context; include pool_id in request",
));
}
}