feat(proxy): enhance proxy service with CRUD operations and agent config binding
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
use crate::service::proxy::types::{ProxyConfig, ProxyServiceResult};
|
||||
use crate::service::proxy::types::{
|
||||
AgentConfigBinding, CreateProxyConfigParams, ProxyConfig, ProxyConfigSummary,
|
||||
ProxyServiceResult, ProxyType, UpdateProxyConfigParams,
|
||||
};
|
||||
|
||||
pub(crate) mod nginx;
|
||||
pub(crate) mod repo;
|
||||
@@ -10,6 +13,31 @@ pub mod types;
|
||||
#[async_trait::async_trait]
|
||||
pub trait ProxyServiceTrait: Send + Sync + 'static {
|
||||
async fn get_proxy_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<ProxyConfig>;
|
||||
|
||||
// CRUD
|
||||
async fn list_configs(&self) -> ProxyServiceResult<Vec<ProxyConfigSummary>>;
|
||||
async fn create_config(
|
||||
&self,
|
||||
params: CreateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary>;
|
||||
async fn update_config(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
params: UpdateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary>;
|
||||
async fn delete_config(&self, id: uuid::Uuid) -> ProxyServiceResult<bool>;
|
||||
|
||||
// Binding
|
||||
async fn get_active_agent_config(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<Option<ProxyConfigSummary>>;
|
||||
async fn bind_agent(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
config_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<AgentConfigBinding>;
|
||||
async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
pub trait ProxyConfigRenderer: Send + Sync + 'static {
|
||||
|
||||
@@ -185,3 +185,246 @@ impl ProxyConfigRenderer for NginxConfigRenderer {
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::service::proxy::types::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn make_id() -> uuid::Uuid {
|
||||
uuid::Uuid::new_v4()
|
||||
}
|
||||
|
||||
fn basic_proxy_config() -> ProxyConfig {
|
||||
let upstream_id = make_id();
|
||||
let location_id = make_id();
|
||||
let server_id = make_id();
|
||||
|
||||
ProxyConfig {
|
||||
id: make_id(),
|
||||
name: "test".to_string(),
|
||||
r#type: ProxyType::Nginx,
|
||||
description: None,
|
||||
parent_config_id: None,
|
||||
upstreams: HashMap::from([(
|
||||
upstream_id,
|
||||
UpstreamConfig {
|
||||
id: upstream_id,
|
||||
name: "backend".to_string(),
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 3000,
|
||||
metadata: None,
|
||||
override_of_id: None,
|
||||
location_blocks: vec![OverrideRef {
|
||||
id: location_id,
|
||||
override_of_id: None,
|
||||
}],
|
||||
},
|
||||
)]),
|
||||
location_blocks: HashMap::from([(
|
||||
location_id,
|
||||
LocationBlockConfig {
|
||||
id: location_id,
|
||||
server_id,
|
||||
path_pattern: "/api".to_string(),
|
||||
proxy_pass_upstream_id: Some(upstream_id),
|
||||
metadata: None,
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
limit_rules: vec![],
|
||||
proxy_settings: vec![],
|
||||
rewrite_rules: vec![],
|
||||
},
|
||||
)]),
|
||||
server_blocks: HashMap::from([(
|
||||
server_id,
|
||||
ServerBlockConfig {
|
||||
id: server_id,
|
||||
server_name: Some(vec!["example.com".to_string()]),
|
||||
listen_port: 80,
|
||||
ssl_enabled: Some(false),
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![OverrideRef {
|
||||
id: location_id,
|
||||
override_of_id: None,
|
||||
}],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
},
|
||||
)]),
|
||||
access_rules: HashMap::new(),
|
||||
cache_zones: HashMap::new(),
|
||||
limit_rules: HashMap::new(),
|
||||
limit_zones: HashMap::new(),
|
||||
log_settings: HashMap::new(),
|
||||
proxy_settings: HashMap::new(),
|
||||
rewrite_rules: HashMap::new(),
|
||||
ssl_certificates: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_basic_nginx_config() {
|
||||
let config = basic_proxy_config();
|
||||
let renderer = NginxConfigRenderer;
|
||||
let output = renderer.render(&config);
|
||||
|
||||
assert!(output.contains("upstream backend {"), "should contain upstream block");
|
||||
assert!(output.contains("server 127.0.0.1:3000;"), "should contain upstream server");
|
||||
assert!(output.contains("server {"), "should contain server block");
|
||||
assert!(output.contains("listen 80;"), "should contain listen directive");
|
||||
assert!(output.contains("server_name example.com;"), "should contain server name");
|
||||
assert!(output.contains("location /api {"), "should contain location block");
|
||||
assert!(output.contains("proxy_pass http://backend;"), "should contain proxy pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_config_with_cache_zone() {
|
||||
let zone_id = make_id();
|
||||
let mut config = basic_proxy_config();
|
||||
config.cache_zones.insert(
|
||||
zone_id,
|
||||
CacheZoneConfig {
|
||||
id: zone_id,
|
||||
name: "mycache".to_string(),
|
||||
path: "/var/cache/nginx".to_string(),
|
||||
size: "10m".to_string(),
|
||||
override_of_id: None,
|
||||
},
|
||||
);
|
||||
let renderer = NginxConfigRenderer;
|
||||
let output = renderer.render(&config);
|
||||
assert!(output.contains("proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_config_with_access_rules() {
|
||||
let rule_id = make_id();
|
||||
let mut config = basic_proxy_config();
|
||||
|
||||
// Get the server ID from the config
|
||||
let sb_id = *config.server_blocks.keys().next().unwrap();
|
||||
|
||||
let rule = AccessRuleConfig {
|
||||
id: rule_id,
|
||||
r#type: "allow".to_string(),
|
||||
ip_cidr: "192.168.1.0/24".to_string(),
|
||||
description: None,
|
||||
priority: 10,
|
||||
override_of_id: None,
|
||||
};
|
||||
config.access_rules.insert(rule_id, rule);
|
||||
|
||||
// Add access rule ref to the server block
|
||||
let sb = config.server_blocks.get_mut(&sb_id).unwrap();
|
||||
sb.access_rules.push(OverrideRef {
|
||||
id: rule_id,
|
||||
override_of_id: None,
|
||||
});
|
||||
|
||||
let renderer = NginxConfigRenderer;
|
||||
let output = renderer.render(&config);
|
||||
assert!(output.contains("allow 192.168.1.0/24;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_config_empty() {
|
||||
let config = ProxyConfig {
|
||||
id: make_id(),
|
||||
name: "empty".to_string(),
|
||||
r#type: ProxyType::Nginx,
|
||||
description: None,
|
||||
parent_config_id: None,
|
||||
server_blocks: HashMap::new(),
|
||||
upstreams: HashMap::new(),
|
||||
access_rules: HashMap::new(),
|
||||
cache_zones: HashMap::new(),
|
||||
limit_rules: HashMap::new(),
|
||||
limit_zones: HashMap::new(),
|
||||
location_blocks: HashMap::new(),
|
||||
log_settings: HashMap::new(),
|
||||
proxy_settings: HashMap::new(),
|
||||
rewrite_rules: HashMap::new(),
|
||||
ssl_certificates: HashMap::new(),
|
||||
};
|
||||
let renderer = NginxConfigRenderer;
|
||||
let output = renderer.render(&config);
|
||||
assert!(output.is_empty() || output.trim().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_config_with_limit_zone_and_rule() {
|
||||
let mut config = basic_proxy_config();
|
||||
let zone_id = make_id();
|
||||
let location_id = *config.location_blocks.keys().next().unwrap();
|
||||
let rule_id = make_id();
|
||||
|
||||
config.limit_zones.insert(
|
||||
zone_id,
|
||||
LimitZoneConfig {
|
||||
id: zone_id,
|
||||
name: "reqzone".to_string(),
|
||||
key: "$binary_remote_addr".to_string(),
|
||||
rate: "10r/s".to_string(),
|
||||
override_of_id: None,
|
||||
},
|
||||
);
|
||||
|
||||
config.limit_rules.insert(
|
||||
rule_id,
|
||||
LimitRuleConfig {
|
||||
id: rule_id,
|
||||
location_id,
|
||||
zone_id,
|
||||
burst: Some(20),
|
||||
nodelay: Some(true),
|
||||
is_deleted: false,
|
||||
override_of_id: None,
|
||||
},
|
||||
);
|
||||
|
||||
let lb = config.location_blocks.get_mut(&location_id).unwrap();
|
||||
lb.limit_rules.push(OverrideRef {
|
||||
id: rule_id,
|
||||
override_of_id: None,
|
||||
});
|
||||
|
||||
let renderer = NginxConfigRenderer;
|
||||
let output = renderer.render(&config);
|
||||
assert!(output.contains("limit_req_zone $binary_remote_addr zone=reqzone:10r/s;"));
|
||||
assert!(output.contains("limit_req zone=reqzone burst=20 nodelay;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_config_with_ssl() {
|
||||
let mut config = basic_proxy_config();
|
||||
let cert_id = make_id();
|
||||
let sb_id = *config.server_blocks.keys().next().unwrap();
|
||||
|
||||
config.ssl_certificates.insert(
|
||||
cert_id,
|
||||
SslCertificateConfig {
|
||||
id: cert_id,
|
||||
name: "test-cert".to_string(),
|
||||
cert_path: "/etc/ssl/certs/test.pem".to_string(),
|
||||
key_path: "/etc/ssl/private/test.key".to_string(),
|
||||
expiry_date: chrono::Utc::now(),
|
||||
},
|
||||
);
|
||||
|
||||
let sb = config.server_blocks.get_mut(&sb_id).unwrap();
|
||||
sb.ssl_enabled = Some(true);
|
||||
sb.ssl_certificates.push(OverrideRef {
|
||||
id: cert_id,
|
||||
override_of_id: None,
|
||||
});
|
||||
|
||||
let renderer = NginxConfigRenderer;
|
||||
let output = renderer.render(&config);
|
||||
assert!(output.contains("listen 80 ssl;"));
|
||||
assert!(output.contains("ssl_certificate /etc/ssl/certs/test.pem;"));
|
||||
assert!(output.contains("ssl_certificate_key /etc/ssl/private/test.key;"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use sea_orm::{DatabaseConnection, prelude::*};
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, prelude::*};
|
||||
|
||||
use crate::service::proxy::types::{
|
||||
Mergeable, OverrideRef, ProxyConfig, ProxyServiceError, ProxyServiceResult, ProxyType,
|
||||
AgentConfigBinding, CreateProxyConfigParams, Mergeable, OverrideRef, ProxyConfig,
|
||||
ProxyConfigSummary, ProxyServiceError, ProxyServiceResult, ProxyType, UpdateProxyConfigParams,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ProxyRepo: Send + Sync + 'static {
|
||||
// get the raw config for the given proxy_id. This should return the config of the given proxy_id without merging it with its parent configs (if any).
|
||||
async fn get_proxy_raw_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<ProxyConfig>;
|
||||
|
||||
// get the raw config for the given proxy_id. This should return the config of the given proxy_id and all its parent configs (if any) without merging them. The returned vector is ordered from the leaf (given proxy_id) down to the root config (most specific to least specific).
|
||||
async fn get_proxy_raw_configs(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<Vec<ProxyConfig>>;
|
||||
|
||||
// get the merged config for the given proxy_id. This should merge the config of the given proxy_id with its parent configs (if any) and return the final merged config.
|
||||
async fn get_merged_proxy_config(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<ProxyConfig>;
|
||||
|
||||
// CRUD
|
||||
async fn list_proxy_configs(&self) -> ProxyServiceResult<Vec<ProxyConfigSummary>>;
|
||||
async fn create_proxy_config(
|
||||
&self,
|
||||
params: CreateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary>;
|
||||
async fn update_proxy_config(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
params: UpdateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary>;
|
||||
async fn delete_proxy_config(&self, id: uuid::Uuid) -> ProxyServiceResult<bool>;
|
||||
|
||||
// Agent config binding
|
||||
async fn get_active_agent_config(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<Option<ProxyConfigSummary>>;
|
||||
async fn bind_agent_to_config(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
config_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<AgentConfigBinding>;
|
||||
async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
pub(crate) struct ProxyRepoImpl {
|
||||
@@ -441,4 +462,163 @@ impl ProxyRepo for ProxyRepoImpl {
|
||||
}
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
// ── CRUD ──
|
||||
|
||||
async fn list_proxy_configs(&self) -> ProxyServiceResult<Vec<ProxyConfigSummary>> {
|
||||
use crate::db::entities::proxy_config::Column;
|
||||
use sea_orm::QueryOrder;
|
||||
|
||||
let configs = crate::db::entities::proxy_config::Entity::find()
|
||||
.order_by(Column::UpdatedAt, sea_orm::Order::Desc)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(configs.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
async fn create_proxy_config(
|
||||
&self,
|
||||
params: CreateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary> {
|
||||
use crate::db::entities::proxy_config::ActiveModel;
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
let model = ActiveModel {
|
||||
id: Set(uuid::Uuid::new_v4()),
|
||||
name: Set(params.name),
|
||||
description: Set(params.description),
|
||||
is_template: Set(params.is_template),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
};
|
||||
let result = model.insert(&self.db).await?;
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
async fn update_proxy_config(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
params: UpdateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary> {
|
||||
use crate::db::entities::proxy_config::ActiveModel;
|
||||
use crate::db::entities::proxy_config::Entity as ProxyConfigEntity;
|
||||
|
||||
let existing = ProxyConfigEntity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(ProxyServiceError::ConfigNotFound)?;
|
||||
|
||||
let mut model: ActiveModel = existing.into();
|
||||
if let Some(name) = params.name {
|
||||
model.name = Set(name);
|
||||
}
|
||||
if let Some(description) = params.description {
|
||||
model.description = Set(Some(description));
|
||||
}
|
||||
if let Some(is_template) = params.is_template {
|
||||
model.is_template = Set(is_template);
|
||||
}
|
||||
model.updated_at = Set(chrono::Utc::now().naive_utc());
|
||||
|
||||
let result = model.update(&self.db).await?;
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
async fn delete_proxy_config(&self, id: uuid::Uuid) -> ProxyServiceResult<bool> {
|
||||
let result = crate::db::entities::proxy_config::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
// ── Agent config binding ──
|
||||
|
||||
async fn get_active_agent_config(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<Option<ProxyConfigSummary>> {
|
||||
use crate::db::entities::agent_config_binding::Column;
|
||||
use crate::db::entities::proxy_config::Entity as ProxyConfigEntity;
|
||||
use sea_orm::Condition;
|
||||
|
||||
let binding = crate::db::entities::agent_config_binding::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(Column::AgentId.eq(agent_id))
|
||||
.add(Column::IsActive.eq(true)),
|
||||
)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
match binding {
|
||||
Some(b) => {
|
||||
let config = ProxyConfigEntity::find_by_id(b.config_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or(ProxyServiceError::ConfigNotFound)?;
|
||||
Ok(Some(config.into()))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn bind_agent_to_config(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
config_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<AgentConfigBinding> {
|
||||
use crate::db::entities::agent_config_binding::ActiveModel;
|
||||
use crate::db::entities::agent_config_binding::Column;
|
||||
use sea_orm::Condition;
|
||||
|
||||
// Deactivate existing active binding for this agent
|
||||
if let Some(existing) = crate::db::entities::agent_config_binding::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(Column::AgentId.eq(agent_id))
|
||||
.add(Column::IsActive.eq(true)),
|
||||
)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
{
|
||||
let mut active: ActiveModel = existing.into();
|
||||
active.is_active = Set(false);
|
||||
active.update(&self.db).await?;
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
let model = ActiveModel {
|
||||
id: Set(uuid::Uuid::new_v4()),
|
||||
agent_id: Set(Some(agent_id)),
|
||||
group_id: Set(None),
|
||||
config_id: Set(config_id),
|
||||
is_active: Set(true),
|
||||
applied_at: Set(now),
|
||||
};
|
||||
let result = model.insert(&self.db).await?;
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult<bool> {
|
||||
use crate::db::entities::agent_config_binding::ActiveModel;
|
||||
use crate::db::entities::agent_config_binding::Column;
|
||||
use sea_orm::Condition;
|
||||
|
||||
let existing = crate::db::entities::agent_config_binding::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(Column::AgentId.eq(agent_id))
|
||||
.add(Column::IsActive.eq(true)),
|
||||
)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
if let Some(b) = existing {
|
||||
let mut active: ActiveModel = b.into();
|
||||
active.is_active = Set(false);
|
||||
active.update(&self.db).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ use sea_orm::DatabaseConnection;
|
||||
|
||||
use super::nginx::NginxConfigRenderer;
|
||||
use super::repo::{ProxyRepo, ProxyRepoImpl};
|
||||
use super::types::{ProxyConfig, ProxyServiceError, ProxyServiceResult, ProxyType};
|
||||
use super::types::{
|
||||
AgentConfigBinding, CreateProxyConfigParams, ProxyConfig, ProxyConfigSummary,
|
||||
ProxyServiceError, ProxyServiceResult, ProxyType, UpdateProxyConfigParams,
|
||||
};
|
||||
use super::{ProxyConfigRenderer, ProxyServiceTrait};
|
||||
|
||||
pub struct ProxyServiceImpl {
|
||||
@@ -28,6 +31,48 @@ impl ProxyServiceTrait for ProxyServiceImpl {
|
||||
async fn get_proxy_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<ProxyConfig> {
|
||||
self.repo.get_merged_proxy_config(proxy_id).await
|
||||
}
|
||||
|
||||
async fn list_configs(&self) -> ProxyServiceResult<Vec<ProxyConfigSummary>> {
|
||||
self.repo.list_proxy_configs().await
|
||||
}
|
||||
|
||||
async fn create_config(
|
||||
&self,
|
||||
params: CreateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary> {
|
||||
self.repo.create_proxy_config(params).await
|
||||
}
|
||||
|
||||
async fn update_config(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
params: UpdateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary> {
|
||||
self.repo.update_proxy_config(id, params).await
|
||||
}
|
||||
|
||||
async fn delete_config(&self, id: uuid::Uuid) -> ProxyServiceResult<bool> {
|
||||
self.repo.delete_proxy_config(id).await
|
||||
}
|
||||
|
||||
async fn get_active_agent_config(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<Option<ProxyConfigSummary>> {
|
||||
self.repo.get_active_agent_config(agent_id).await
|
||||
}
|
||||
|
||||
async fn bind_agent(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
config_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<AgentConfigBinding> {
|
||||
self.repo.bind_agent_to_config(agent_id, config_id).await
|
||||
}
|
||||
|
||||
async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult<bool> {
|
||||
self.repo.unbind_agent(agent_id).await
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyServiceImpl {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ProxyServiceError {
|
||||
#[error("proxy config not found")]
|
||||
ConfigNotFound,
|
||||
InvalidConfig,
|
||||
#[error("invalid proxy config: {0}")]
|
||||
InvalidConfig(String),
|
||||
#[error("no renderer registered for this proxy type")]
|
||||
RendererNotFound,
|
||||
DatabaseError(sea_orm::DbErr),
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for ProxyServiceError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
ProxyServiceError::DatabaseError(err)
|
||||
}
|
||||
#[error("database error: {0}")]
|
||||
DatabaseError(#[from] sea_orm::DbErr),
|
||||
}
|
||||
|
||||
pub type ProxyServiceResult<T> = Result<T, ProxyServiceError>;
|
||||
@@ -169,14 +167,17 @@ impl
|
||||
|
||||
impl Mergeable<ServerBlockConfig> for ServerBlockConfig {
|
||||
fn merge(&mut self, other: ServerBlockConfig) {
|
||||
if let Some(server_name) = other.server_name {
|
||||
self.server_name = Some(server_name);
|
||||
// self (child) overrides other (parent): keep child's values, fill gaps from parent
|
||||
if self.server_name.is_none() {
|
||||
self.server_name = other.server_name;
|
||||
}
|
||||
self.listen_port = other.listen_port;
|
||||
if let Some(ssl_enabled) = other.ssl_enabled {
|
||||
self.ssl_enabled = Some(ssl_enabled);
|
||||
// listen_port is non-optional, child always keeps its own
|
||||
if self.ssl_enabled.is_none() {
|
||||
self.ssl_enabled = other.ssl_enabled;
|
||||
}
|
||||
if self.override_of_id.is_none() {
|
||||
self.override_of_id = other.override_of_id;
|
||||
}
|
||||
self.override_of_id = other.override_of_id;
|
||||
|
||||
self.access_rules = merge_override_vecs(
|
||||
std::mem::take(&mut self.access_rules),
|
||||
@@ -480,3 +481,322 @@ impl Overridable for ProxySettingConfig {
|
||||
impl Overridable for RewriteRuleConfig {
|
||||
fn override_of_id(&self) -> Option<uuid::Uuid> { self.override_of_id }
|
||||
}
|
||||
|
||||
// ── CRUD types ──
|
||||
|
||||
pub struct ProxyConfigSummary {
|
||||
pub id: uuid::Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub is_template: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<crate::db::entities::proxy_config::Model> for ProxyConfigSummary {
|
||||
fn from(m: crate::db::entities::proxy_config::Model) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
is_template: m.is_template,
|
||||
created_at: m.created_at.and_utc(),
|
||||
updated_at: m.updated_at.and_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateProxyConfigParams {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub is_template: bool,
|
||||
}
|
||||
|
||||
pub struct UpdateProxyConfigParams {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub is_template: Option<bool>,
|
||||
}
|
||||
|
||||
// ── Agent config binding ──
|
||||
|
||||
pub struct AgentConfigBinding {
|
||||
pub id: uuid::Uuid,
|
||||
pub agent_id: Option<uuid::Uuid>,
|
||||
pub group_id: Option<uuid::Uuid>,
|
||||
pub config_id: uuid::Uuid,
|
||||
pub is_active: bool,
|
||||
pub applied_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<crate::db::entities::agent_config_binding::Model> for AgentConfigBinding {
|
||||
fn from(m: crate::db::entities::agent_config_binding::Model) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
agent_id: m.agent_id,
|
||||
group_id: m.group_id,
|
||||
config_id: m.config_id,
|
||||
is_active: m.is_active,
|
||||
applied_at: m.applied_at.and_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn make_id() -> uuid::Uuid {
|
||||
uuid::Uuid::new_v4()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_override_vecs_child_overrides_parent() {
|
||||
let parent_id = make_id();
|
||||
let child_id = make_id();
|
||||
let child = vec![OverrideRef {
|
||||
id: child_id,
|
||||
override_of_id: Some(parent_id),
|
||||
}];
|
||||
let parent = vec![OverrideRef {
|
||||
id: parent_id,
|
||||
override_of_id: None,
|
||||
}];
|
||||
let result = merge_override_vecs(child, parent);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].id, child_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_override_vecs_removes_overridden_parent() {
|
||||
let parent_id = make_id();
|
||||
let child_override = make_id();
|
||||
let child = vec![OverrideRef {
|
||||
id: child_override,
|
||||
override_of_id: Some(parent_id),
|
||||
}];
|
||||
let parent = vec![OverrideRef {
|
||||
id: parent_id,
|
||||
override_of_id: None,
|
||||
}];
|
||||
let result = merge_override_vecs(child, parent);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].id, child_override);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_override_vecs_empty_child() {
|
||||
let parent = vec![OverrideRef {
|
||||
id: make_id(),
|
||||
override_of_id: None,
|
||||
}];
|
||||
let result = merge_override_vecs(vec![], parent.clone());
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].id, parent[0].id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_block_merge_child_overrides_parent() {
|
||||
let id = make_id();
|
||||
let mut child = ServerBlockConfig {
|
||||
id,
|
||||
server_name: Some(vec!["child.example.com".to_string()]),
|
||||
listen_port: 443,
|
||||
ssl_enabled: Some(true),
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
let parent = ServerBlockConfig {
|
||||
id,
|
||||
server_name: Some(vec!["parent.example.com".to_string()]),
|
||||
listen_port: 80,
|
||||
ssl_enabled: Some(false),
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
child.merge(parent);
|
||||
// child keeps its own values (self overrides other)
|
||||
assert_eq!(child.server_name, Some(vec!["child.example.com".to_string()]));
|
||||
assert_eq!(child.listen_port, 443);
|
||||
assert_eq!(child.ssl_enabled, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_block_merge_fills_from_parent() {
|
||||
let id = make_id();
|
||||
let mut child = ServerBlockConfig {
|
||||
id,
|
||||
server_name: None,
|
||||
listen_port: 443,
|
||||
ssl_enabled: None,
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
let parent = ServerBlockConfig {
|
||||
id,
|
||||
server_name: Some(vec!["parent.example.com".to_string()]),
|
||||
listen_port: 80,
|
||||
ssl_enabled: Some(false),
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
child.merge(parent);
|
||||
// child fills missing optional fields from parent
|
||||
assert_eq!(child.server_name, Some(vec!["parent.example.com".to_string()]));
|
||||
assert_eq!(child.listen_port, 443); // non-optional: child keeps its own
|
||||
assert_eq!(child.ssl_enabled, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_config_merge_server_block_overrides() {
|
||||
let sb_id = make_id();
|
||||
let child_sb = ServerBlockConfig {
|
||||
id: sb_id,
|
||||
server_name: Some(vec!["child.example.com".to_string()]),
|
||||
listen_port: 443,
|
||||
ssl_enabled: Some(true),
|
||||
override_of_id: Some(make_id()),
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
let parent_sb = ServerBlockConfig {
|
||||
id: sb_id,
|
||||
server_name: Some(vec!["parent.example.com".to_string()]),
|
||||
listen_port: 80,
|
||||
ssl_enabled: Some(false),
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
let parent_id = make_id();
|
||||
let mut child_proxy = ProxyConfig {
|
||||
id: parent_id,
|
||||
name: "child".to_string(),
|
||||
r#type: ProxyType::Nginx,
|
||||
description: None,
|
||||
parent_config_id: None,
|
||||
server_blocks: HashMap::from([(child_sb.id, child_sb)]),
|
||||
upstreams: HashMap::new(),
|
||||
access_rules: HashMap::new(),
|
||||
cache_zones: HashMap::new(),
|
||||
limit_rules: HashMap::new(),
|
||||
limit_zones: HashMap::new(),
|
||||
location_blocks: HashMap::new(),
|
||||
log_settings: HashMap::new(),
|
||||
proxy_settings: HashMap::new(),
|
||||
rewrite_rules: HashMap::new(),
|
||||
ssl_certificates: HashMap::new(),
|
||||
};
|
||||
let parent_proxy = ProxyConfig {
|
||||
id: make_id(),
|
||||
name: "parent".to_string(),
|
||||
r#type: ProxyType::Nginx,
|
||||
description: None,
|
||||
parent_config_id: None,
|
||||
server_blocks: HashMap::from([(parent_sb.id, parent_sb)]),
|
||||
upstreams: HashMap::new(),
|
||||
access_rules: HashMap::new(),
|
||||
cache_zones: HashMap::new(),
|
||||
limit_rules: HashMap::new(),
|
||||
limit_zones: HashMap::new(),
|
||||
location_blocks: HashMap::new(),
|
||||
log_settings: HashMap::new(),
|
||||
proxy_settings: HashMap::new(),
|
||||
rewrite_rules: HashMap::new(),
|
||||
ssl_certificates: HashMap::new(),
|
||||
};
|
||||
child_proxy.merge(parent_proxy);
|
||||
assert_eq!(child_proxy.server_blocks.len(), 1);
|
||||
let merged_sb = &child_proxy.server_blocks[&sb_id];
|
||||
assert_eq!(merged_sb.server_name, Some(vec!["child.example.com".to_string()]));
|
||||
assert_eq!(merged_sb.listen_port, 443);
|
||||
assert_eq!(merged_sb.ssl_enabled, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_config_merge_adds_parent_server_block() {
|
||||
let child_sb_id = make_id();
|
||||
let parent_sb_id = make_id();
|
||||
let child_sb = ServerBlockConfig {
|
||||
id: child_sb_id,
|
||||
server_name: Some(vec!["child.example.com".to_string()]),
|
||||
listen_port: 443,
|
||||
ssl_enabled: Some(true),
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
let parent_sb = ServerBlockConfig {
|
||||
id: parent_sb_id,
|
||||
server_name: Some(vec!["parent.example.com".to_string()]),
|
||||
listen_port: 80,
|
||||
ssl_enabled: None,
|
||||
override_of_id: None,
|
||||
access_rules: vec![],
|
||||
location_blocks: vec![],
|
||||
log_settings: vec![],
|
||||
ssl_certificates: vec![],
|
||||
};
|
||||
let parent_id = make_id();
|
||||
let mut child_proxy = ProxyConfig {
|
||||
id: parent_id,
|
||||
name: "child".to_string(),
|
||||
r#type: ProxyType::Nginx,
|
||||
description: None,
|
||||
parent_config_id: None,
|
||||
server_blocks: HashMap::from([(child_sb.id, child_sb)]),
|
||||
upstreams: HashMap::new(),
|
||||
access_rules: HashMap::new(),
|
||||
cache_zones: HashMap::new(),
|
||||
limit_rules: HashMap::new(),
|
||||
limit_zones: HashMap::new(),
|
||||
location_blocks: HashMap::new(),
|
||||
log_settings: HashMap::new(),
|
||||
proxy_settings: HashMap::new(),
|
||||
rewrite_rules: HashMap::new(),
|
||||
ssl_certificates: HashMap::new(),
|
||||
};
|
||||
let parent_proxy = ProxyConfig {
|
||||
id: make_id(),
|
||||
name: "parent".to_string(),
|
||||
r#type: ProxyType::Nginx,
|
||||
description: None,
|
||||
parent_config_id: None,
|
||||
server_blocks: HashMap::from([(parent_sb.id, parent_sb)]),
|
||||
upstreams: HashMap::new(),
|
||||
access_rules: HashMap::new(),
|
||||
cache_zones: HashMap::new(),
|
||||
limit_rules: HashMap::new(),
|
||||
limit_zones: HashMap::new(),
|
||||
location_blocks: HashMap::new(),
|
||||
log_settings: HashMap::new(),
|
||||
proxy_settings: HashMap::new(),
|
||||
rewrite_rules: HashMap::new(),
|
||||
ssl_certificates: HashMap::new(),
|
||||
};
|
||||
child_proxy.merge(parent_proxy);
|
||||
assert_eq!(child_proxy.server_blocks.len(), 2);
|
||||
// Both child and parent server blocks should be present
|
||||
assert!(child_proxy.server_blocks.contains_key(&child_sb_id));
|
||||
assert!(child_proxy.server_blocks.contains_key(&parent_sb_id));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user