feat(proxy): implement Nginx configuration handling and rendering logic

This commit is contained in:
GW_MC
2026-06-21 11:47:19 +00:00
parent 9c07fce607
commit 692b17534c
16 changed files with 306 additions and 78 deletions

View File

@@ -6,7 +6,12 @@ pub(crate) mod repo;
pub mod service; pub mod service;
pub mod types; pub mod types;
#[cfg_attr(test, mockall::automock)]
#[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>;
} }
pub trait ProxyConfigRenderer: Send + Sync + 'static {
fn render(&self, config: &ProxyConfig) -> String;
}

View File

@@ -1,7 +1,7 @@
use crate::db::entities::access_rule::Model as AccessRule; use crate::service::proxy::types::AccessRuleConfig;
impl std::fmt::Display for AccessRule { impl std::fmt::Display for AccessRuleConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {};", self.r#type.clone(), self.ip_cidr.clone()) write!(f, "{} {};", self.r#type, self.ip_cidr)
} }
} }

View File

@@ -1,11 +1,11 @@
use crate::db::entities::cache_zone::Model as CacheZone; use crate::service::proxy::types::CacheZoneConfig;
impl std::fmt::Display for CacheZone { impl std::fmt::Display for CacheZoneConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
"proxy_cache_path {} levels=1:2 keys_zone={}:{};", "proxy_cache_path {} levels=1:2 keys_zone={}:{};",
self.path, self.name, self.size_limit self.path, self.name, self.size
) )
} }
} }

View File

@@ -1,11 +1,11 @@
use crate::db::entities::limit_rule::Model as LimitRule; use crate::service::proxy::types::LimitRuleConfig;
pub struct LimitRuleRender { pub struct LimitRuleRender<'a> {
pub rule: LimitRule, pub rule: &'a LimitRuleConfig,
pub zone_name: String, pub zone_name: &'a str,
} }
impl std::fmt::Display for LimitRuleRender { impl std::fmt::Display for LimitRuleRender<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "limit_req zone={}", self.zone_name)?; write!(f, "limit_req zone={}", self.zone_name)?;
if let Some(burst) = self.rule.burst { if let Some(burst) = self.rule.burst {

View File

@@ -1,6 +1,6 @@
use crate::db::entities::limit_zone::Model as LimitZone; use crate::service::proxy::types::LimitZoneConfig;
impl std::fmt::Display for LimitZone { impl std::fmt::Display for LimitZoneConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,

View File

@@ -1,25 +1,25 @@
use crate::db::entities::location_block::Model as LocationBlock; use crate::service::proxy::types::LocationBlockConfig;
pub struct LocationBlockRender { pub struct LocationBlockRender<'a> {
pub block: LocationBlock, pub block: &'a LocationBlockConfig,
pub upstream_name: Option<String>, pub upstream_name: Option<&'a str>,
pub access_rules: Vec<String>, pub access_rules: &'a [String],
pub rewrite_rules: Vec<String>, pub rewrite_rules: &'a [String],
pub proxy_setting: Option<String>, pub proxy_setting: Option<&'a str>,
pub limit_rules: Vec<String>, pub limit_rules: &'a [String],
} }
impl std::fmt::Display for LocationBlockRender { impl std::fmt::Display for LocationBlockRender<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, " location {} {{", self.block.path_pattern)?; writeln!(f, " location {} {{", self.block.path_pattern)?;
if let Some(ref upstream) = self.upstream_name { if let Some(upstream) = self.upstream_name {
writeln!(f, " proxy_pass http://{};", upstream)?; writeln!(f, " proxy_pass http://{};", upstream)?;
writeln!(f, " proxy_set_header Host $host;")?; writeln!(f, " proxy_set_header Host $host;")?;
writeln!(f, " proxy_set_header X-Real-IP $remote_addr;")?; writeln!(f, " proxy_set_header X-Real-IP $remote_addr;")?;
} }
if let Some(ref settings) = self.proxy_setting { if let Some(settings) = self.proxy_setting {
for line in settings.lines() { for line in settings.lines() {
if !line.is_empty() { if !line.is_empty() {
writeln!(f, "{}", line)?; writeln!(f, "{}", line)?;
@@ -27,15 +27,15 @@ impl std::fmt::Display for LocationBlockRender {
} }
} }
for rule in &self.access_rules { for rule in self.access_rules {
writeln!(f, " {}", rule)?; writeln!(f, " {}", rule)?;
} }
for rule in &self.rewrite_rules { for rule in self.rewrite_rules {
writeln!(f, " {}", rule)?; writeln!(f, " {}", rule)?;
} }
for rule in &self.limit_rules { for rule in self.limit_rules {
writeln!(f, " {}", rule)?; writeln!(f, " {}", rule)?;
} }

View File

@@ -1,6 +1,6 @@
use crate::db::entities::log_setting::Model as LogSetting; use crate::service::proxy::types::LogSettingConfig;
impl std::fmt::Display for LogSetting { impl std::fmt::Display for LogSettingConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ref path) = self.access_log_path { if let Some(ref path) = self.access_log_path {
if let Some(ref level) = self.log_level { if let Some(ref level) = self.log_level {

View File

@@ -1,11 +1,11 @@
mod access_rule; pub(crate) mod access_rule;
mod cache_zone; pub(crate) mod cache_zone;
mod limit_rule; pub(crate) mod limit_rule;
mod limit_zone; pub(crate) mod limit_zone;
mod location_block; pub(crate) mod location_block;
mod log_setting; pub(crate) mod log_setting;
mod proxy_setting; pub(crate) mod proxy_setting;
mod rewrite_rule; pub(crate) mod rewrite_rule;
mod server_block; pub(crate) mod server_block;
mod ssl_certificate; pub(crate) mod ssl_certificate;
mod upstream; pub(crate) mod upstream;

View File

@@ -1,32 +1,23 @@
use crate::db::entities::proxy_setting::Model as ProxySetting; use crate::service::proxy::types::ProxySettingConfig;
pub struct ProxySettingRender { pub struct ProxySettingRender<'a> {
pub setting: ProxySetting, pub setting: &'a ProxySettingConfig,
pub cache_zone_name: Option<String>, pub cache_zone_name: Option<&'a str>,
} }
impl ProxySettingRender { impl std::fmt::Display for ProxySettingRender<'_> {
fn render_timeout(value: Option<i32>, directive: &str) -> Option<String> {
value.map(|v| format!(" {} {}s;", directive, v))
}
}
impl std::fmt::Display for ProxySettingRender {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(line) = Self::render_timeout(self.setting.read_timeout, "proxy_read_timeout") if let Some(timeout) = self.setting.read_timeout {
{ writeln!(f, " proxy_read_timeout {}s;", timeout)?;
writeln!(f, "{}", line)?;
} }
if let Some(line) = if let Some(timeout) = self.setting.connect_timeout {
Self::render_timeout(self.setting.connect_timeout, "proxy_connect_timeout") writeln!(f, " proxy_connect_timeout {}s;", timeout)?;
{
writeln!(f, "{}", line)?;
} }
if let Some(buffer) = self.setting.buffer_size { if let Some(buffer) = self.setting.buffer_size {
writeln!(f, " proxy_buffer_size {};", buffer)?; writeln!(f, " proxy_buffer_size {};", buffer)?;
} }
if self.setting.cache_enabled.unwrap_or(false) { if self.setting.cache_enabled.unwrap_or(false) {
if let Some(ref zone_name) = self.cache_zone_name { if let Some(zone_name) = self.cache_zone_name {
writeln!(f, " proxy_cache {};", zone_name)?; writeln!(f, " proxy_cache {};", zone_name)?;
} }
} }

View File

@@ -1,6 +1,6 @@
use crate::db::entities::rewrite_rule::Model as RewriteRule; use crate::service::proxy::types::RewriteRuleConfig;
impl std::fmt::Display for RewriteRule { impl std::fmt::Display for RewriteRuleConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rewrite {} {}", self.pattern, self.replacement)?; write!(f, "rewrite {} {}", self.pattern, self.replacement)?;
if let Some(ref flag) = self.flag { if let Some(ref flag) = self.flag {

View File

@@ -1,14 +1,14 @@
use crate::db::entities::server_block::Model as ServerBlock; use crate::service::proxy::types::ServerBlockConfig;
pub struct ServerBlockRender { pub struct ServerBlockRender<'a> {
pub block: ServerBlock, pub block: &'a ServerBlockConfig,
pub ssl_cert: Option<String>, pub ssl_cert: Option<&'a str>,
pub locations: Vec<String>, pub locations: &'a [String],
pub access_rules: Vec<String>, pub access_rules: &'a [String],
pub log_setting: Option<String>, pub log_setting: Option<&'a str>,
} }
impl std::fmt::Display for ServerBlockRender { impl std::fmt::Display for ServerBlockRender<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "server {{")?; writeln!(f, "server {{")?;
@@ -24,11 +24,11 @@ impl std::fmt::Display for ServerBlockRender {
} }
} }
if let Some(ref cert) = self.ssl_cert { if let Some(cert) = self.ssl_cert {
writeln!(f, "{}", cert)?; writeln!(f, "{}", cert)?;
} }
if let Some(ref log) = self.log_setting { if let Some(log) = self.log_setting {
for line in log.lines() { for line in log.lines() {
if !line.is_empty() { if !line.is_empty() {
writeln!(f, "{}", line)?; writeln!(f, "{}", line)?;
@@ -36,11 +36,11 @@ impl std::fmt::Display for ServerBlockRender {
} }
} }
for rule in &self.access_rules { for rule in self.access_rules {
writeln!(f, " {}", rule)?; writeln!(f, " {}", rule)?;
} }
for loc in &self.locations { for loc in self.locations {
writeln!(f, "{}", loc)?; writeln!(f, "{}", loc)?;
} }

View File

@@ -1,6 +1,6 @@
use crate::db::entities::ssl_certificate::Model as SslCertificate; use crate::service::proxy::types::SslCertificateConfig;
impl std::fmt::Display for SslCertificate { impl std::fmt::Display for SslCertificateConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, " ssl_certificate {};", self.cert_path)?; writeln!(f, " ssl_certificate {};", self.cert_path)?;
writeln!(f, " ssl_certificate_key {};", self.key_path) writeln!(f, " ssl_certificate_key {};", self.key_path)

View File

@@ -1,6 +1,6 @@
use crate::db::entities::upstream::Model as Upstream; use crate::service::proxy::types::UpstreamConfig;
impl std::fmt::Display for Upstream { impl std::fmt::Display for UpstreamConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, " server {}:{};", self.target_host, self.target_port) write!(f, " server {}:{};", self.target_host, self.target_port)
} }

View File

@@ -1 +1,187 @@
pub mod config; pub(crate) mod config;
use std::collections::HashMap;
use std::fmt::Write;
use uuid::Uuid;
use crate::service::proxy::ProxyConfigRenderer;
use crate::service::proxy::types::{
AccessRuleConfig, CacheZoneConfig, LimitRuleConfig, LimitZoneConfig, LogSettingConfig,
OverrideRef, ProxyConfig, ProxySettingConfig, RewriteRuleConfig, SslCertificateConfig,
UpstreamConfig,
};
fn resolve_ids<'a, T>(ids: &[OverrideRef], map: &'a HashMap<Uuid, T>) -> Vec<&'a T> {
ids.iter().filter_map(|r| map.get(&r.id)).collect()
}
fn resolve_upstream_name(
upstream_id: Option<Uuid>,
upstreams: &HashMap<Uuid, UpstreamConfig>,
) -> Option<&str> {
upstream_id
.and_then(|id| upstreams.get(&id))
.map(|u| u.name.as_str())
}
fn resolve_cache_zone_name(
zone_id: Option<Uuid>,
cache_zones: &HashMap<Uuid, CacheZoneConfig>,
) -> Option<&str> {
zone_id
.and_then(|id| cache_zones.get(&id))
.map(|z| z.name.as_str())
}
fn resolve_limit_zone_name(
zone_id: Uuid,
limit_zones: &HashMap<Uuid, LimitZoneConfig>,
) -> Option<&str> {
limit_zones.get(&zone_id).map(|z| z.name.as_str())
}
fn render_access_rules(
ids: &[OverrideRef],
access_rules: &HashMap<Uuid, AccessRuleConfig>,
) -> Vec<String> {
resolve_ids(ids, access_rules)
.into_iter()
.map(|r| r.to_string())
.collect()
}
fn render_rewrite_rules(
ids: &[OverrideRef],
rewrite_rules: &HashMap<Uuid, RewriteRuleConfig>,
) -> Vec<String> {
resolve_ids(ids, rewrite_rules)
.into_iter()
.map(|r| r.to_string())
.collect()
}
fn render_limit_rules(
ids: &[OverrideRef],
limit_rules: &HashMap<Uuid, LimitRuleConfig>,
limit_zones: &HashMap<Uuid, LimitZoneConfig>,
) -> Vec<String> {
resolve_ids(ids, limit_rules)
.into_iter()
.filter_map(|rule| {
let zone_name = resolve_limit_zone_name(rule.zone_id, limit_zones)?;
Some(config::limit_rule::LimitRuleRender { rule, zone_name }.to_string())
})
.collect()
}
fn render_proxy_setting(
ids: &[OverrideRef],
proxy_settings: &HashMap<Uuid, ProxySettingConfig>,
cache_zones: &HashMap<Uuid, CacheZoneConfig>,
) -> Option<String> {
let setting = resolve_ids(ids, proxy_settings).into_iter().next()?;
let cache_zone_name = resolve_cache_zone_name(setting.cache_zone, cache_zones);
Some(
config::proxy_setting::ProxySettingRender {
setting,
cache_zone_name,
}
.to_string(),
)
}
fn render_log_setting(
ids: &[OverrideRef],
log_settings: &HashMap<Uuid, LogSettingConfig>,
) -> Option<String> {
let setting = resolve_ids(ids, log_settings).into_iter().next()?;
Some(setting.to_string())
}
fn render_ssl_cert(
ids: &[OverrideRef],
ssl_certificates: &HashMap<Uuid, SslCertificateConfig>,
) -> Option<String> {
let cert = resolve_ids(ids, ssl_certificates).into_iter().next()?;
Some(cert.to_string())
}
pub struct NginxConfigRenderer;
impl ProxyConfigRenderer for NginxConfigRenderer {
fn render(&self, config: &ProxyConfig) -> String {
let mut output = String::new();
// Global cache zones
for zone in config.cache_zones.values() {
writeln!(output, "{}", zone).ok();
}
// Limit request zones
for zone in config.limit_zones.values() {
writeln!(output, "{}", zone).ok();
}
// Upstream blocks
for upstream in config.upstreams.values() {
writeln!(output, "upstream {} {{", upstream.name).ok();
writeln!(output, "{}", upstream).ok();
writeln!(output, "}}").ok();
writeln!(output).ok();
}
// Server blocks
for sb in config.server_blocks.values() {
let access_rules = render_access_rules(&sb.access_rules, &config.access_rules);
let ssl_cert = render_ssl_cert(&sb.ssl_certificates, &config.ssl_certificates);
let log_setting = render_log_setting(&sb.log_settings, &config.log_settings);
// Render location blocks for this server
let locations: Vec<String> = resolve_ids(&sb.location_blocks, &config.location_blocks)
.into_iter()
.map(|lb| {
let upstream_name =
resolve_upstream_name(lb.proxy_pass_upstream_id, &config.upstreams);
let lb_access_rules =
render_access_rules(&lb.access_rules, &config.access_rules);
let lb_rewrite_rules =
render_rewrite_rules(&lb.rewrite_rules, &config.rewrite_rules);
let lb_proxy_setting = render_proxy_setting(
&lb.proxy_settings,
&config.proxy_settings,
&config.cache_zones,
);
let lb_limit_rules = render_limit_rules(
&lb.limit_rules,
&config.limit_rules,
&config.limit_zones,
);
config::location_block::LocationBlockRender {
block: lb,
upstream_name,
access_rules: &lb_access_rules,
rewrite_rules: &lb_rewrite_rules,
proxy_setting: lb_proxy_setting.as_deref(),
limit_rules: &lb_limit_rules,
}
.to_string()
})
.collect();
let server = config::server_block::ServerBlockRender {
block: sb,
ssl_cert: ssl_cert.as_deref(),
locations: &locations,
access_rules: &access_rules,
log_setting: log_setting.as_deref(),
};
writeln!(output, "{}", server).ok();
writeln!(output).ok();
}
output
}
}

View File

@@ -0,0 +1,42 @@
use std::collections::HashMap;
use sea_orm::DatabaseConnection;
use super::nginx::NginxConfigRenderer;
use super::repo::{ProxyRepo, ProxyRepoImpl};
use super::types::{ProxyConfig, ProxyServiceError, ProxyServiceResult, ProxyType};
use super::{ProxyConfigRenderer, ProxyServiceTrait};
pub struct ProxyServiceImpl {
repo: Box<dyn ProxyRepo>,
renderers: HashMap<ProxyType, Box<dyn ProxyConfigRenderer>>,
}
impl ProxyServiceImpl {
pub fn new(db: DatabaseConnection) -> Self {
let mut renderers: HashMap<ProxyType, Box<dyn ProxyConfigRenderer>> = HashMap::new();
renderers.insert(ProxyType::Nginx, Box::new(NginxConfigRenderer));
Self {
repo: Box::new(ProxyRepoImpl::new(db)),
renderers,
}
}
}
#[async_trait::async_trait]
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
}
}
impl ProxyServiceImpl {
pub async fn render_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<String> {
let config = self.repo.get_merged_proxy_config(proxy_id).await?;
let renderer = self
.renderers
.get(&config.r#type)
.ok_or(ProxyServiceError::RendererNotFound)?;
Ok(renderer.render(&config))
}
}

View File

@@ -4,6 +4,7 @@ use std::collections::HashMap;
pub enum ProxyServiceError { pub enum ProxyServiceError {
ConfigNotFound, ConfigNotFound,
InvalidConfig, InvalidConfig,
RendererNotFound,
DatabaseError(sea_orm::DbErr), DatabaseError(sea_orm::DbErr),
} }
@@ -15,6 +16,7 @@ impl From<sea_orm::DbErr> for ProxyServiceError {
pub type ProxyServiceResult<T> = Result<T, ProxyServiceError>; pub type ProxyServiceResult<T> = Result<T, ProxyServiceError>;
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub enum ProxyType { pub enum ProxyType {
Nginx, Nginx,
} }
@@ -247,6 +249,7 @@ impl From<crate::db::entities::access_rule::Model> for AccessRuleConfig {
pub struct CacheZoneConfig { pub struct CacheZoneConfig {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub name: String, pub name: String,
pub path: String,
pub size: String, pub size: String,
pub override_of_id: Option<uuid::Uuid>, pub override_of_id: Option<uuid::Uuid>,
} }
@@ -256,6 +259,7 @@ impl From<crate::db::entities::cache_zone::Model> for CacheZoneConfig {
CacheZoneConfig { CacheZoneConfig {
id: model.id, id: model.id,
name: model.name, name: model.name,
path: model.path,
size: model.size_limit, size: model.size_limit,
override_of_id: model.override_of_id, override_of_id: model.override_of_id,
} }