feat(proxy): enhance proxy service with CRUD operations and agent config binding

This commit is contained in:
GW_MC
2026-06-21 11:59:23 +00:00
parent 692b17534c
commit 2e758c67fc
5 changed files with 840 additions and 24 deletions

View File

@@ -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 {

View File

@@ -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;"));
}
}

View File

@@ -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)
}
}
} }

View File

@@ -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 {

View File

@@ -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));
}
}