429 lines
15 KiB
Rust
429 lines
15 KiB
Rust
//! 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<LbMetadataStore>,
|
|
auth: Arc<AuthService>,
|
|
}
|
|
|
|
impl PoolServiceImpl {
|
|
/// Create a new PoolServiceImpl
|
|
pub fn new(metadata: Arc<LbMetadataStore>, auth: Arc<AuthService>) -> 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<PoolId, Status> {
|
|
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<LoadBalancerId, Status> {
|
|
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<ProtoSessionPersistence>) -> Option<SessionPersistence> {
|
|
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<CreatePoolRequest>,
|
|
) -> Result<Response<CreatePoolResponse>, 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<GetPoolRequest>,
|
|
) -> Result<Response<GetPoolResponse>, 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<ListPoolsRequest>,
|
|
) -> Result<Response<ListPoolsResponse>, 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::<usize>()
|
|
.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<ProtoPool> = 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<UpdatePoolRequest>,
|
|
) -> Result<Response<UpdatePoolResponse>, 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<DeletePoolRequest>,
|
|
) -> Result<Response<DeletePoolResponse>, 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 {}))
|
|
}
|
|
}
|