- 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)
196 lines
6.3 KiB
Rust
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",
|
|
));
|
|
}
|
|
}
|