//! Pool service implementation use std::sync::Arc; use base64::Engine as _; use crate::metadata::LbMetadataStore; use fiberlb_api::{ pool_service_server::PoolService, CreatePoolRequest, CreatePoolResponse, DeletePoolRequest, DeletePoolResponse, GetPoolRequest, GetPoolResponse, ListPoolsRequest, ListPoolsResponse, UpdatePoolRequest, UpdatePoolResponse, Pool as ProtoPool, PoolAlgorithm as ProtoPoolAlgorithm, PoolProtocol as ProtoPoolProtocol, SessionPersistence as ProtoSessionPersistence, PersistenceType as ProtoPersistenceType, }; use fiberlb_types::{ LoadBalancerId, Pool, PoolAlgorithm, PoolId, PoolProtocol, SessionPersistence, PersistenceType, }; use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService}; use tonic::{Request, Response, Status}; use uuid::Uuid; /// Pool service implementation pub struct PoolServiceImpl { metadata: Arc, auth: Arc, } impl PoolServiceImpl { /// Create a new PoolServiceImpl pub fn new(metadata: Arc, auth: Arc) -> Self { Self { metadata, auth } } } const ACTION_POOLS_CREATE: &str = "network:pools:create"; const ACTION_POOLS_READ: &str = "network:pools:read"; const ACTION_POOLS_LIST: &str = "network:pools:list"; const ACTION_POOLS_UPDATE: &str = "network:pools:update"; const ACTION_POOLS_DELETE: &str = "network:pools:delete"; /// Convert domain Pool to proto fn pool_to_proto(pool: &Pool) -> ProtoPool { ProtoPool { id: pool.id.to_string(), name: pool.name.clone(), loadbalancer_id: pool.loadbalancer_id.to_string(), algorithm: match pool.algorithm { PoolAlgorithm::RoundRobin => ProtoPoolAlgorithm::RoundRobin.into(), PoolAlgorithm::LeastConnections => ProtoPoolAlgorithm::LeastConnections.into(), PoolAlgorithm::IpHash => ProtoPoolAlgorithm::IpHash.into(), PoolAlgorithm::WeightedRoundRobin => ProtoPoolAlgorithm::WeightedRoundRobin.into(), PoolAlgorithm::Random => ProtoPoolAlgorithm::Random.into(), PoolAlgorithm::Maglev => ProtoPoolAlgorithm::Maglev.into(), }, protocol: match pool.protocol { PoolProtocol::Tcp => ProtoPoolProtocol::Tcp.into(), PoolProtocol::Udp => ProtoPoolProtocol::Udp.into(), PoolProtocol::Http => ProtoPoolProtocol::Http.into(), PoolProtocol::Https => ProtoPoolProtocol::Https.into(), }, session_persistence: pool.session_persistence.as_ref().map(|sp| { ProtoSessionPersistence { r#type: match sp.persistence_type { PersistenceType::SourceIp => ProtoPersistenceType::SourceIp.into(), PersistenceType::Cookie => ProtoPersistenceType::Cookie.into(), PersistenceType::AppCookie => ProtoPersistenceType::AppCookie.into(), }, cookie_name: sp.cookie_name.clone().unwrap_or_default(), timeout_seconds: sp.timeout_seconds, } }), created_at: pool.created_at, updated_at: pool.updated_at, } } /// Parse PoolId from string fn parse_pool_id(id: &str) -> Result { let uuid: Uuid = id .parse() .map_err(|_| Status::invalid_argument("invalid pool ID"))?; Ok(PoolId::from_uuid(uuid)) } /// Parse LoadBalancerId from string fn parse_lb_id(id: &str) -> Result { let uuid: Uuid = id .parse() .map_err(|_| Status::invalid_argument("invalid load balancer ID"))?; Ok(LoadBalancerId::from_uuid(uuid)) } /// Convert proto algorithm to domain fn proto_to_algorithm(algo: i32) -> PoolAlgorithm { match ProtoPoolAlgorithm::try_from(algo) { Ok(ProtoPoolAlgorithm::RoundRobin) => PoolAlgorithm::RoundRobin, Ok(ProtoPoolAlgorithm::LeastConnections) => PoolAlgorithm::LeastConnections, Ok(ProtoPoolAlgorithm::IpHash) => PoolAlgorithm::IpHash, Ok(ProtoPoolAlgorithm::WeightedRoundRobin) => PoolAlgorithm::WeightedRoundRobin, Ok(ProtoPoolAlgorithm::Random) => PoolAlgorithm::Random, _ => PoolAlgorithm::RoundRobin, } } /// Convert proto protocol to domain fn proto_to_protocol(proto: i32) -> PoolProtocol { match ProtoPoolProtocol::try_from(proto) { Ok(ProtoPoolProtocol::Tcp) => PoolProtocol::Tcp, Ok(ProtoPoolProtocol::Udp) => PoolProtocol::Udp, Ok(ProtoPoolProtocol::Http) => PoolProtocol::Http, Ok(ProtoPoolProtocol::Https) => PoolProtocol::Https, _ => PoolProtocol::Tcp, } } /// Convert proto session persistence to domain fn proto_to_session_persistence(sp: Option) -> Option { sp.map(|s| { let persistence_type = match ProtoPersistenceType::try_from(s.r#type) { Ok(ProtoPersistenceType::SourceIp) => PersistenceType::SourceIp, Ok(ProtoPersistenceType::Cookie) => PersistenceType::Cookie, Ok(ProtoPersistenceType::AppCookie) => PersistenceType::AppCookie, _ => PersistenceType::SourceIp, }; SessionPersistence { persistence_type, cookie_name: if s.cookie_name.is_empty() { None } else { Some(s.cookie_name) }, timeout_seconds: s.timeout_seconds, } }) } #[tonic::async_trait] impl PoolService for PoolServiceImpl { async fn create_pool( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); // Validate required fields if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } if req.loadbalancer_id.is_empty() { return Err(Status::invalid_argument("loadbalancer_id is required")); } let lb_id = parse_lb_id(&req.loadbalancer_id)?; // Verify load balancer exists let lb = self.metadata .load_lb_by_id(&lb_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("load balancer not in tenant scope")); } self.auth .authorize( &tenant, ACTION_POOLS_CREATE, &resource_for_tenant("pool", "*", &lb.org_id, &lb.project_id), ) .await?; // Create new pool let algorithm = proto_to_algorithm(req.algorithm); let protocol = proto_to_protocol(req.protocol); let mut pool = Pool::new(&req.name, lb_id, algorithm, protocol); pool.session_persistence = proto_to_session_persistence(req.session_persistence); // Save pool self.metadata .save_pool(&pool) .await .map_err(|e| Status::internal(format!("failed to save pool: {}", e)))?; Ok(Response::new(CreatePoolResponse { pool: Some(pool_to_proto(&pool)), })) } async fn get_pool( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); if req.id.is_empty() { return Err(Status::invalid_argument("id is required")); } let pool_id = parse_pool_id(&req.id)?; let pool = self .metadata .load_pool_by_id(&pool_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("pool not found"))?; let lb = self .metadata .load_lb_by_id(&pool.loadbalancer_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("pool not in tenant scope")); } self.auth .authorize( &tenant, ACTION_POOLS_READ, &resource_for_tenant("pool", &pool.id.to_string(), &lb.org_id, &lb.project_id), ) .await?; Ok(Response::new(GetPoolResponse { pool: Some(pool_to_proto(&pool)), })) } async fn list_pools( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); if req.loadbalancer_id.is_empty() { return Err(Status::invalid_argument("loadbalancer_id is required")); } let lb_id = parse_lb_id(&req.loadbalancer_id)?; let lb = self .metadata .load_lb_by_id(&lb_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("load balancer not in tenant scope")); } self.auth .authorize( &tenant, ACTION_POOLS_LIST, &resource_for_tenant("pool", "*", &lb.org_id, &lb.project_id), ) .await?; let pools = self .metadata .list_pools(&lb_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))?; let page_size = if req.page_size == 0 { 50 } else { req.page_size as usize }; let offset = if req.page_token.is_empty() { 0 } else { let decoded = base64::engine::general_purpose::STANDARD .decode(&req.page_token) .map_err(|_| Status::invalid_argument("invalid page_token"))?; let offset_str = String::from_utf8(decoded) .map_err(|_| Status::invalid_argument("invalid page_token encoding"))?; offset_str .parse::() .map_err(|_| Status::invalid_argument("invalid page_token format"))? }; let total = pools.len(); let end = std::cmp::min(offset + page_size, total); let paginated = pools.iter().skip(offset).take(page_size); let proto_pools: Vec = paginated.map(pool_to_proto).collect(); let next_page_token = if end < total { base64::engine::general_purpose::STANDARD.encode(end.to_string().as_bytes()) } else { String::new() }; Ok(Response::new(ListPoolsResponse { pools: proto_pools, next_page_token, })) } async fn update_pool( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); if req.id.is_empty() { return Err(Status::invalid_argument("id is required")); } let pool_id = parse_pool_id(&req.id)?; let mut pool = self .metadata .load_pool_by_id(&pool_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("pool not found"))?; let lb = self .metadata .load_lb_by_id(&pool.loadbalancer_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("pool not in tenant scope")); } let lb_org_id = lb.org_id; let lb_project_id = lb.project_id; self.auth .authorize( &tenant, ACTION_POOLS_UPDATE, &resource_for_tenant("pool", &pool.id.to_string(), &lb_org_id, &lb_project_id), ) .await?; // Apply updates if !req.name.is_empty() { pool.name = req.name; } if req.algorithm != 0 { pool.algorithm = proto_to_algorithm(req.algorithm); } if req.session_persistence.is_some() { pool.session_persistence = proto_to_session_persistence(req.session_persistence); } // Update timestamp pool.updated_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); // Save updated pool self.metadata .save_pool(&pool) .await .map_err(|e| Status::internal(format!("failed to save pool: {}", e)))?; Ok(Response::new(UpdatePoolResponse { pool: Some(pool_to_proto(&pool)), })) } async fn delete_pool( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); if req.id.is_empty() { return Err(Status::invalid_argument("id is required")); } let pool_id = parse_pool_id(&req.id)?; let pool = self .metadata .load_pool_by_id(&pool_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("pool not found"))?; let lb = self .metadata .load_lb_by_id(&pool.loadbalancer_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("pool not in tenant scope")); } let lb_org_id = lb.org_id; let lb_project_id = lb.project_id; self.auth .authorize( &tenant, ACTION_POOLS_DELETE, &resource_for_tenant("pool", &pool.id.to_string(), &lb_org_id, &lb_project_id), ) .await?; // Delete all backends first self.metadata .delete_pool_backends(&pool.id) .await .map_err(|e| Status::internal(format!("failed to delete backends: {}", e)))?; // Delete pool self.metadata .delete_pool(&pool) .await .map_err(|e| Status::internal(format!("failed to delete pool: {}", e)))?; Ok(Response::new(DeletePoolResponse {})) } }