//! LoadBalancer service implementation use std::net::IpAddr; use std::sync::Arc; use base64::Engine as _; use crate::metadata::LbMetadataStore; use fiberlb_api::{ load_balancer_service_server::LoadBalancerService, CreateLoadBalancerRequest, CreateLoadBalancerResponse, DeleteLoadBalancerRequest, DeleteLoadBalancerResponse, GetLoadBalancerRequest, GetLoadBalancerResponse, ListLoadBalancersRequest, ListLoadBalancersResponse, UpdateLoadBalancerRequest, UpdateLoadBalancerResponse, LoadBalancer as ProtoLoadBalancer, LoadBalancerStatus as ProtoLoadBalancerStatus, }; use fiberlb_types::{LoadBalancer, LoadBalancerId, LoadBalancerStatus}; use iam_service_auth::{get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService}; use tonic::{Request, Response, Status}; use uuid::Uuid; /// LoadBalancer service implementation pub struct LoadBalancerServiceImpl { metadata: Arc, auth: Arc, } impl LoadBalancerServiceImpl { /// Create a new LoadBalancerServiceImpl pub fn new(metadata: Arc, auth: Arc) -> Self { Self { metadata, auth } } } fn normalize_requested_vip(vip_address: &str) -> Result, Status> { let trimmed = vip_address.trim(); if trimmed.is_empty() { return Ok(None); } let vip: IpAddr = trimmed .parse() .map_err(|_| Status::invalid_argument("vip_address must be a valid IP address"))?; if vip.is_unspecified() || vip.is_multicast() { return Err(Status::invalid_argument( "vip_address must be a usable unicast address", )); } Ok(Some(vip.to_string())) } async fn ensure_vip_available(metadata: &LbMetadataStore, vip: &str) -> Result<(), Status> { let lbs = metadata .list_all_lbs() .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))?; if lbs .iter() .any(|lb| lb.vip_address.as_deref() == Some(vip)) { return Err(Status::already_exists(format!( "vip_address {} is already in use", vip ))); } Ok(()) } const ACTION_LB_CREATE: &str = "network:loadbalancers:create"; const ACTION_LB_READ: &str = "network:loadbalancers:read"; const ACTION_LB_LIST: &str = "network:loadbalancers:list"; const ACTION_LB_UPDATE: &str = "network:loadbalancers:update"; const ACTION_LB_DELETE: &str = "network:loadbalancers:delete"; /// Convert domain LoadBalancer to proto fn lb_to_proto(lb: &LoadBalancer) -> ProtoLoadBalancer { ProtoLoadBalancer { id: lb.id.to_string(), name: lb.name.clone(), org_id: lb.org_id.clone(), project_id: lb.project_id.clone(), description: lb.description.clone().unwrap_or_default(), status: match lb.status { LoadBalancerStatus::Provisioning => ProtoLoadBalancerStatus::Provisioning.into(), LoadBalancerStatus::Active => ProtoLoadBalancerStatus::Active.into(), LoadBalancerStatus::Updating => ProtoLoadBalancerStatus::Updating.into(), LoadBalancerStatus::Error => ProtoLoadBalancerStatus::Error.into(), LoadBalancerStatus::Deleting => ProtoLoadBalancerStatus::Deleting.into(), }, vip_address: lb.vip_address.clone().unwrap_or_default(), created_at: lb.created_at, updated_at: lb.updated_at, } } /// 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)) } #[tonic::async_trait] impl LoadBalancerService for LoadBalancerServiceImpl { async fn create_load_balancer( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); let (org_id, project_id) = resolve_tenant_ids_from_context(&tenant, &req.org_id, &req.project_id)?; self.auth .authorize( &tenant, ACTION_LB_CREATE, &resource_for_tenant("loadbalancer", "*", &org_id, &project_id), ) .await?; // Validate required fields if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } // Create new load balancer let mut lb = LoadBalancer::new(&req.name, &org_id, &project_id); // Apply optional description if !req.description.is_empty() { lb.description = Some(req.description); } let requested_vip = normalize_requested_vip(&req.vip_address)?; let vip = if let Some(vip) = requested_vip { ensure_vip_available(&self.metadata, &vip).await?; vip } else { self.metadata .allocate_vip() .await .map_err(|e| Status::resource_exhausted(format!("failed to allocate VIP: {}", e)))? }; lb.vip_address = Some(vip); // Save load balancer self.metadata .save_lb(&lb) .await .map_err(|e| Status::internal(format!("failed to save load balancer: {}", e)))?; Ok(Response::new(CreateLoadBalancerResponse { loadbalancer: Some(lb_to_proto(&lb)), })) } async fn get_load_balancer( &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 lb_id = parse_lb_id(&req.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_LB_READ, &resource_for_tenant("loadbalancer", &lb.id.to_string(), &lb.org_id, &lb.project_id), ) .await?; Ok(Response::new(GetLoadBalancerResponse { loadbalancer: Some(lb_to_proto(&lb)), })) } async fn list_load_balancers( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); let (org_id, project_id) = resolve_tenant_ids_from_context(&tenant, &req.org_id, &req.project_id)?; self.auth .authorize( &tenant, ACTION_LB_LIST, &resource_for_tenant("loadbalancer", "*", &org_id, &project_id), ) .await?; let lbs = self .metadata .list_lbs(&org_id, Some(project_id.as_str())) .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 = lbs.len(); let end = std::cmp::min(offset + page_size, total); let paginated = lbs.iter().skip(offset).take(page_size); let loadbalancers: Vec = paginated.map(lb_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(ListLoadBalancersResponse { loadbalancers, next_page_token, })) } async fn update_load_balancer( &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 lb_id = parse_lb_id(&req.id)?; let mut 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_LB_UPDATE, &resource_for_tenant("loadbalancer", &lb.id.to_string(), &lb.org_id, &lb.project_id), ) .await?; // Apply updates if !req.name.is_empty() { lb.name = req.name; } if !req.description.is_empty() { lb.description = Some(req.description); } // Update timestamp lb.updated_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); // Save updated load balancer self.metadata .save_lb(&lb) .await .map_err(|e| Status::internal(format!("failed to save load balancer: {}", e)))?; Ok(Response::new(UpdateLoadBalancerResponse { loadbalancer: Some(lb_to_proto(&lb)), })) } async fn delete_load_balancer( &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 lb_id = parse_lb_id(&req.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_LB_DELETE, &resource_for_tenant("loadbalancer", &lb.id.to_string(), &lb.org_id, &lb.project_id), ) .await?; // Delete all associated resources (cascade delete) self.metadata .delete_lb_listeners(&lb.id) .await .map_err(|e| Status::internal(format!("failed to delete listeners: {}", e)))?; self.metadata .delete_lb_pools(&lb.id) .await .map_err(|e| Status::internal(format!("failed to delete pools: {}", e)))?; // Delete load balancer self.metadata .delete_lb(&lb) .await .map_err(|e| Status::internal(format!("failed to delete load balancer: {}", e)))?; Ok(Response::new(DeleteLoadBalancerResponse {})) } }