368 lines
12 KiB
Rust
368 lines
12 KiB
Rust
//! 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<LbMetadataStore>,
|
|
auth: Arc<AuthService>,
|
|
}
|
|
|
|
impl LoadBalancerServiceImpl {
|
|
/// Create a new LoadBalancerServiceImpl
|
|
pub fn new(metadata: Arc<LbMetadataStore>, auth: Arc<AuthService>) -> Self {
|
|
Self { metadata, auth }
|
|
}
|
|
}
|
|
|
|
fn normalize_requested_vip(vip_address: &str) -> Result<Option<String>, 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<LoadBalancerId, Status> {
|
|
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<CreateLoadBalancerRequest>,
|
|
) -> Result<Response<CreateLoadBalancerResponse>, 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<GetLoadBalancerRequest>,
|
|
) -> Result<Response<GetLoadBalancerResponse>, 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<ListLoadBalancersRequest>,
|
|
) -> Result<Response<ListLoadBalancersResponse>, 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::<usize>()
|
|
.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<ProtoLoadBalancer> = 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<UpdateLoadBalancerRequest>,
|
|
) -> Result<Response<UpdateLoadBalancerResponse>, 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<DeleteLoadBalancerRequest>,
|
|
) -> Result<Response<DeleteLoadBalancerResponse>, 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 {}))
|
|
}
|
|
}
|