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 nginx;
|
||||||
pub(crate) mod repo;
|
pub(crate) mod repo;
|
||||||
@@ -10,6 +13,31 @@ pub mod types;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait ProxyServiceTrait: Send + Sync + 'static {
|
pub trait ProxyServiceTrait: Send + Sync + 'static {
|
||||||
async fn get_proxy_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<ProxyConfig>;
|
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 {
|
pub trait ProxyConfigRenderer: Send + Sync + 'static {
|
||||||
|
|||||||
@@ -185,3 +185,246 @@ impl ProxyConfigRenderer for NginxConfigRenderer {
|
|||||||
output
|
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 std::collections::HashMap;
|
||||||
|
|
||||||
use sea_orm::{DatabaseConnection, prelude::*};
|
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, prelude::*};
|
||||||
|
|
||||||
use crate::service::proxy::types::{
|
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]
|
#[async_trait::async_trait]
|
||||||
pub trait ProxyRepo: Send + Sync + 'static {
|
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>;
|
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(
|
async fn get_proxy_raw_configs(
|
||||||
&self,
|
&self,
|
||||||
proxy_id: uuid::Uuid,
|
proxy_id: uuid::Uuid,
|
||||||
) -> ProxyServiceResult<Vec<ProxyConfig>>;
|
) -> 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(
|
async fn get_merged_proxy_config(
|
||||||
&self,
|
&self,
|
||||||
proxy_id: uuid::Uuid,
|
proxy_id: uuid::Uuid,
|
||||||
) -> ProxyServiceResult<ProxyConfig>;
|
) -> 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 {
|
pub(crate) struct ProxyRepoImpl {
|
||||||
@@ -441,4 +462,163 @@ impl ProxyRepo for ProxyRepoImpl {
|
|||||||
}
|
}
|
||||||
Ok(merged)
|
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::nginx::NginxConfigRenderer;
|
||||||
use super::repo::{ProxyRepo, ProxyRepoImpl};
|
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};
|
use super::{ProxyConfigRenderer, ProxyServiceTrait};
|
||||||
|
|
||||||
pub struct ProxyServiceImpl {
|
pub struct ProxyServiceImpl {
|
||||||
@@ -28,6 +31,48 @@ impl ProxyServiceTrait for ProxyServiceImpl {
|
|||||||
async fn get_proxy_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<ProxyConfig> {
|
async fn get_proxy_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<ProxyConfig> {
|
||||||
self.repo.get_merged_proxy_config(proxy_id).await
|
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 {
|
impl ProxyServiceImpl {
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ProxyServiceError {
|
pub enum ProxyServiceError {
|
||||||
|
#[error("proxy config not found")]
|
||||||
ConfigNotFound,
|
ConfigNotFound,
|
||||||
InvalidConfig,
|
#[error("invalid proxy config: {0}")]
|
||||||
|
InvalidConfig(String),
|
||||||
|
#[error("no renderer registered for this proxy type")]
|
||||||
RendererNotFound,
|
RendererNotFound,
|
||||||
DatabaseError(sea_orm::DbErr),
|
#[error("database error: {0}")]
|
||||||
}
|
DatabaseError(#[from] sea_orm::DbErr),
|
||||||
|
|
||||||
impl From<sea_orm::DbErr> for ProxyServiceError {
|
|
||||||
fn from(err: sea_orm::DbErr) -> Self {
|
|
||||||
ProxyServiceError::DatabaseError(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ProxyServiceResult<T> = Result<T, ProxyServiceError>;
|
pub type ProxyServiceResult<T> = Result<T, ProxyServiceError>;
|
||||||
@@ -169,14 +167,17 @@ impl
|
|||||||
|
|
||||||
impl Mergeable<ServerBlockConfig> for ServerBlockConfig {
|
impl Mergeable<ServerBlockConfig> for ServerBlockConfig {
|
||||||
fn merge(&mut self, other: ServerBlockConfig) {
|
fn merge(&mut self, other: ServerBlockConfig) {
|
||||||
if let Some(server_name) = other.server_name {
|
// self (child) overrides other (parent): keep child's values, fill gaps from parent
|
||||||
self.server_name = Some(server_name);
|
if self.server_name.is_none() {
|
||||||
|
self.server_name = other.server_name;
|
||||||
}
|
}
|
||||||
self.listen_port = other.listen_port;
|
// listen_port is non-optional, child always keeps its own
|
||||||
if let Some(ssl_enabled) = other.ssl_enabled {
|
if self.ssl_enabled.is_none() {
|
||||||
self.ssl_enabled = Some(ssl_enabled);
|
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(
|
self.access_rules = merge_override_vecs(
|
||||||
std::mem::take(&mut self.access_rules),
|
std::mem::take(&mut self.access_rules),
|
||||||
@@ -480,3 +481,322 @@ impl Overridable for ProxySettingConfig {
|
|||||||
impl Overridable for RewriteRuleConfig {
|
impl Overridable for RewriteRuleConfig {
|
||||||
fn override_of_id(&self) -> Option<uuid::Uuid> { self.override_of_id }
|
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