feat: Implement ProxyHost and Location services with CRUD operations

- Added `ProxyHostInfo`, `ProxyHostCreateInfo`, and `UpdateProxyHostInfo` structs to manage proxy host data.
- Created `ProxyService` and `ProxyServiceImpl` for handling proxy host operations including create, read, update, and delete.
- Implemented `LocationService` and `LocationServiceImpl` for managing locations associated with proxy hosts.
- Introduced database transaction handling for creating proxies and locations.
- Added tests for all service methods to ensure functionality and correctness.
This commit is contained in:
GW_MC
2026-01-07 15:57:44 +08:00
parent ab840126b3
commit 1c0053207c
7 changed files with 1684 additions and 6 deletions

View File

@@ -0,0 +1,296 @@
use chrono::{DateTime, Utc};
use database::generated::entities::{location, proxy_host, upstream};
use sea_orm::ActiveValue::{Set, Unchanged};
use tracing::warn;
use uuid::Uuid;
use crate::{
errors::service_error::ServiceError,
services::nginx::{builder::NginxConfigProvider, traits::indentable::Indentable},
set_if_some,
};
use serde_json::Value as JsonValue;
#[derive(Clone)]
pub struct ProxyPassInfo {
pub protocol: String,
pub host: String,
pub port: i64,
}
#[derive(Clone)]
pub struct LocationInfo {
pub id: Uuid,
pub host_id: Uuid,
pub path: String,
pub match_type: String,
pub order: i64,
pub upstream_id: Option<Uuid>,
pub proxy_pass_info: Option<ProxyPassInfo>,
pub preserve_host_header: Option<bool>,
pub allowed_methods: Option<Vec<String>>,
pub custom_config: Option<String>,
pub enabled: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
//
pub upstream: Option<super::upstream::UpstreamInfo>,
pub proxy_host: Option<super::proxy_host::ProxyHostInfo>,
}
pub struct CreateLocationInfo {
pub host_id: Uuid,
pub path: String,
pub match_type: String,
pub order: i64,
pub upstream_id: Option<Uuid>,
pub proxy_pass_protocol: Option<String>,
pub proxy_pass_host: Option<String>,
pub proxy_pass_port: Option<i64>,
pub preserve_host_header: Option<bool>,
pub allowed_methods: Option<Vec<String>>,
pub custom_config: Option<String>,
pub enabled: bool,
}
#[derive(Clone)]
pub struct UpdateLocationInfo {
pub path: Option<String>,
pub match_type: Option<String>,
pub order: Option<i64>,
pub upstream_id: Option<Option<Uuid>>,
pub proxy_pass_protocol: Option<Option<String>>,
pub proxy_pass_host: Option<Option<String>>,
pub proxy_pass_port: Option<Option<i64>>,
pub preserve_host_header: Option<Option<bool>>,
pub allowed_methods: Option<Option<Vec<String>>>,
pub custom_config: Option<Option<String>>,
pub enabled: Option<bool>,
}
impl From<location::Model> for LocationInfo {
fn from(model: location::Model) -> Self {
let allowed_methods: Option<Vec<String>> = match model.allowed_methods {
Some(JsonValue::Array(arr)) => {
let v: Vec<String> = arr
.into_iter()
.filter_map(|val| val.as_str().map(|s| s.to_string()))
.collect();
if v.is_empty() { None } else { Some(v) }
}
_ => None,
};
Self {
id: model.id,
host_id: model.host_id,
path: model.path,
match_type: model.match_type,
order: model.order,
upstream_id: model.upstream_id,
proxy_pass_info: match (
model.proxy_pass_protocol,
model.proxy_pass_host,
model.proxy_pass_port,
) {
(Some(protocol), Some(host), Some(port)) => Some(ProxyPassInfo {
protocol,
host,
port,
}),
(Some(_), _, _) | (_, Some(_), _) | (_, _, Some(_)) => {
warn!("Incomplete proxy_pass_info for location {}", model.id);
None
}
_ => None,
},
preserve_host_header: model.preserve_host_header,
allowed_methods,
custom_config: model.custom_config,
enabled: model.enabled,
created_at: model.created_at,
updated_at: model.updated_at,
upstream: None,
proxy_host: None,
}
}
}
impl From<(location::Model, Option<proxy_host::Model>)> for LocationInfo {
fn from(data: (location::Model, Option<proxy_host::Model>)) -> Self {
let (location_model, proxy_host_model_opt) = data;
(location_model, proxy_host_model_opt, None).into()
}
}
impl
From<(
location::Model,
Option<proxy_host::Model>,
Option<upstream::Model>,
)> for LocationInfo
{
fn from(
data: (
location::Model,
Option<proxy_host::Model>,
Option<upstream::Model>,
),
) -> Self {
let (location_model, proxy_host_model_opt, upstream_model_opt) = data;
let mut location_info = LocationInfo::from(location_model);
if let Some(upstream_model) = upstream_model_opt {
location_info.upstream = Some(super::upstream::UpstreamInfo::from(upstream_model));
}
if let Some(proxy_host_model) = proxy_host_model_opt {
location_info.proxy_host =
Some(super::proxy_host::ProxyHostInfo::from(proxy_host_model));
}
location_info
}
}
impl From<CreateLocationInfo> for location::ActiveModel {
fn from(val: CreateLocationInfo) -> Self {
location::ActiveModel {
id: Set(Uuid::new_v4()),
host_id: Set(val.host_id),
path: Set(val.path),
match_type: Set(val.match_type),
order: Set(val.order),
upstream_id: Set(val.upstream_id),
proxy_pass_protocol: Set(val.proxy_pass_protocol),
proxy_pass_host: Set(val.proxy_pass_host),
proxy_pass_port: Set(val.proxy_pass_port),
preserve_host_header: Set(val.preserve_host_header),
allowed_methods: Set(val
.allowed_methods
.map(|v| JsonValue::Array(v.into_iter().map(JsonValue::String).collect()))),
custom_config: Set(val.custom_config),
enabled: Set(val.enabled),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
}
}
}
impl UpdateLocationInfo {
pub fn apply_to_model(self, current_model: location::Model) -> location::ActiveModel {
location::ActiveModel {
id: Unchanged(current_model.id),
host_id: Unchanged(current_model.host_id),
path: set_if_some!(self.path),
match_type: set_if_some!(self.match_type),
order: set_if_some!(self.order),
upstream_id: match self.upstream_id {
Some(inner) => Set(inner),
None => Unchanged(current_model.upstream_id),
},
proxy_pass_protocol: match self.proxy_pass_protocol {
Some(inner) => Set(inner),
None => Unchanged(current_model.proxy_pass_protocol),
},
proxy_pass_host: match self.proxy_pass_host {
Some(inner) => Set(inner),
None => Unchanged(current_model.proxy_pass_host),
},
proxy_pass_port: match self.proxy_pass_port {
Some(inner) => Set(inner),
None => Unchanged(current_model.proxy_pass_port),
},
preserve_host_header: match self.preserve_host_header {
Some(inner) => Set(inner),
None => Unchanged(current_model.preserve_host_header),
},
allowed_methods: match self.allowed_methods {
Some(inner) => {
let json_opt = inner
.map(|v| JsonValue::Array(v.into_iter().map(JsonValue::String).collect()));
Set(json_opt)
}
None => Unchanged(current_model.allowed_methods),
},
custom_config: match self.custom_config {
Some(inner) => Set(inner),
None => Unchanged(current_model.custom_config),
},
enabled: set_if_some!(self.enabled),
created_at: Unchanged(current_model.created_at),
updated_at: Set(chrono::Utc::now()),
}
}
}
impl NginxConfigProvider for LocationInfo {
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
let indent = indent.unwrap_or(0);
let selector = match self.match_type.as_str() {
"exact" => format!("location = {} ", self.path),
"regex" => format!("location ~ {} ", self.path),
_ => format!("location {} ", self.path),
};
let mut body_lines: Vec<String> = Vec::new();
if let Some(methods) = &self.allowed_methods
&& !methods.is_empty()
{
body_lines.push(format!(
"limit_except {} {{ deny all; }}",
methods.join(" ")
));
}
if let Some(upstream) = &self.upstream {
body_lines.push(format!(
"proxy_pass {}://{};",
upstream.protocol, upstream.name
));
} else if let Some(host) = &self.proxy_pass_info {
body_lines.push(format!(
"proxy_pass {}://{}:{};",
host.protocol, host.host, host.port
));
} else {
warn!(
"Location {} has neither upstream nor proxy_pass_host defined",
self.id
);
return Err(ServiceError::InternalError(
"Location must have either an upstream or a proxy_pass_host defined".to_string(),
));
}
if let Some(preserve) = self.preserve_host_header {
if preserve {
body_lines.push("proxy_set_header Host $host;".to_string());
} else {
body_lines.push("proxy_set_header Host $proxy_host;".to_string());
}
}
if let Some(cfg) = &self.custom_config
&& !cfg.trim().is_empty()
{
body_lines.push(cfg.clone());
}
let inner = if body_lines.is_empty() {
"# location has no config".to_string()
} else {
body_lines
.into_iter()
.map(|l| l.indent(indent + 2))
.collect::<Vec<String>>()
.join("\n")
};
Ok(format!("{}{{\n{}\n}}", selector.trim_end(), inner).indent(indent))
}
}

View File

@@ -0,0 +1,251 @@
use chrono::{DateTime, Utc};
use database::generated::entities::{location, proxy_host};
use sea_orm::ActiveValue::{Set, Unchanged};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use crate::{
errors::service_error::ServiceError,
services::nginx::{
builder::{INDENT_SIZE, NginxConfigProvider},
traits::indentable::Indentable,
},
set_if_some,
};
#[derive(Clone)]
pub struct ProxyHostInfo {
pub id: Uuid,
pub name: Option<String>,
pub domain: String,
pub scheme: String,
pub listen_port: i64,
pub forward_scheme: String,
pub forward_host: Option<String>,
pub forward_port: Option<i64>,
pub preserve_host_header: bool,
pub enable_websocket: bool,
pub meta: Option<JsonValue>,
pub enabled: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
//
pub upstream: Option<super::upstream::UpstreamInfo>,
pub locations: Vec<super::location::LocationInfo>,
}
pub struct ProxyHostCreateInfo {
pub name: Option<String>,
pub domain: String,
pub scheme: String,
pub listen_port: i64,
pub forward_scheme: String,
pub forward_host: Option<String>,
pub forward_port: Option<i64>,
pub preserve_host_header: bool,
pub enable_websocket: bool,
pub enabled: bool,
pub meta: Option<JsonValue>,
pub default_upstream_id: Option<Uuid>,
pub created_by: Option<Uuid>,
//
pub locations: Vec<super::location::CreateLocationInfo>,
}
#[derive(Clone)]
pub struct UpdateProxyHostInfo {
pub name: Option<Option<String>>,
pub domain: Option<String>,
pub scheme: Option<String>,
pub listen_port: Option<i64>,
pub forward_scheme: Option<String>,
pub forward_host: Option<Option<String>>,
pub forward_port: Option<Option<i64>>,
pub preserve_host_header: Option<bool>,
pub enable_websocket: Option<bool>,
pub enabled: Option<bool>,
pub meta: Option<Option<JsonValue>>,
pub default_upstream_id: Option<Option<Uuid>>,
}
impl From<proxy_host::Model> for ProxyHostInfo {
fn from(model: proxy_host::Model) -> Self {
Self {
id: model.id,
name: model.name,
domain: model.domain,
scheme: model.scheme,
listen_port: model.listen_port,
forward_scheme: model.forward_scheme,
forward_host: model.forward_host,
forward_port: model.forward_port,
preserve_host_header: model.preserve_host_header,
enable_websocket: model.enable_websocket,
meta: model.meta,
enabled: model.enabled,
created_at: model.created_at,
updated_at: model.updated_at,
upstream: None,
locations: Vec::new(),
}
}
}
impl From<(proxy_host::Model, Vec<location::Model>)> for ProxyHostInfo {
fn from(data: (proxy_host::Model, Vec<location::Model>)) -> Self {
let (proxy_model, location_models) = data;
let mut proxy_info = ProxyHostInfo::from(proxy_model);
let locations_info: Vec<super::location::LocationInfo> =
location_models.into_iter().map(|m| m.into()).collect();
proxy_info.locations = locations_info;
proxy_info
}
}
impl From<ProxyHostCreateInfo> for (proxy_host::ActiveModel, Vec<location::ActiveModel>) {
fn from(val: ProxyHostCreateInfo) -> Self {
let proxy_host = proxy_host::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(val.name),
domain: Set(val.domain),
scheme: Set(val.scheme),
listen_port: Set(val.listen_port),
forward_scheme: Set(val.forward_scheme),
forward_host: Set(val.forward_host),
forward_port: Set(val.forward_port),
preserve_host_header: Set(val.preserve_host_header),
enable_websocket: Set(val.enable_websocket),
enabled: Set(val.enabled),
meta: Set(val.meta),
default_upstream_id: Set(val.default_upstream_id),
created_by: Set(val.created_by),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
};
let location_models = val.locations.into_iter().map(|loc| loc.into()).collect();
(proxy_host, location_models)
}
}
impl From<ProxyHostInfo> for proxy_host::ActiveModel {
fn from(val: ProxyHostInfo) -> Self {
proxy_host::ActiveModel {
id: Set(val.id),
name: Set(val.name),
domain: Set(val.domain),
scheme: Set(val.scheme),
listen_port: Set(val.listen_port),
forward_scheme: Set(val.forward_scheme),
forward_host: Set(val.forward_host),
forward_port: Set(val.forward_port),
preserve_host_header: Set(val.preserve_host_header),
enable_websocket: Set(val.enable_websocket),
enabled: Set(val.enabled),
meta: Set(val.meta),
default_upstream_id: Set(val.upstream.as_ref().map(|u| u.id)),
created_by: Set(None),
created_at: Set(val.created_at),
updated_at: Set(val.updated_at),
}
}
}
impl UpdateProxyHostInfo {
pub fn apply_to_model(self, current_model: proxy_host::Model) -> proxy_host::ActiveModel {
proxy_host::ActiveModel {
id: Unchanged(current_model.id),
name: match self.name {
Some(inner) => Set(inner),
None => Unchanged(current_model.name),
},
domain: set_if_some!(self.domain),
scheme: set_if_some!(self.scheme),
listen_port: set_if_some!(self.listen_port),
forward_scheme: set_if_some!(self.forward_scheme),
forward_host: match self.forward_host {
Some(inner) => Set(inner),
None => Unchanged(current_model.forward_host),
},
forward_port: match self.forward_port {
Some(inner) => Set(inner),
None => Unchanged(current_model.forward_port),
},
preserve_host_header: set_if_some!(self.preserve_host_header),
enable_websocket: set_if_some!(self.enable_websocket),
enabled: set_if_some!(self.enabled),
meta: set_if_some!(self.meta),
default_upstream_id: match self.default_upstream_id {
Some(inner) => Set(inner),
None => Unchanged(current_model.default_upstream_id),
},
created_by: Unchanged(current_model.created_by),
created_at: Unchanged(current_model.created_at),
updated_at: Set(chrono::Utc::now()),
}
}
}
impl NginxConfigProvider for ProxyHostInfo {
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
let indent = indent.unwrap_or(0);
let mut body: Vec<String> = Vec::new();
// default location or fallback
let default_pass = if let Some(up) = &self.upstream {
format!("proxy_pass http://{};", up.name)
} else if let Some(host) = &self.forward_host {
if let Some(port) = self.forward_port {
format!("proxy_pass http://{}:{};", host, port)
} else {
format!("proxy_pass http://{};", host)
}
} else {
String::new()
};
// get locations's index sorted by order to prevent mutable borrow issues
let mut index_list: Vec<usize> = (0..self.locations.len()).collect();
index_list.sort_by(|&a, &b| {
let order_a = self.locations[a].order;
let order_b = self.locations[b].order;
order_a.cmp(&order_b)
});
for &index in &index_list {
let loc = &self.locations[index];
body.push(loc.to_nginx_config(Some(indent + INDENT_SIZE))?);
}
// If there is a default proxy_pass and no root location for `/`, add it
if !default_pass.is_empty() {
body.insert(
0,
format!(
"location / {{\n{}\n}}",
default_pass.indent(indent + INDENT_SIZE)
),
);
}
if self.enable_websocket {
body.push("proxy_set_header Upgrade $http_upgrade;".to_string());
body.push("proxy_set_header Connection \"upgrade\";".to_string());
}
let inner = if body.is_empty() {
"# server has no config".to_string()
} else {
body.into_iter()
.map(|l| l.indent(indent + INDENT_SIZE))
.collect::<Vec<String>>()
.join("\n")
};
Ok(format!(
"server {{\n listen {};\n server_name {};\n{}\n}}",
self.listen_port, self.domain, inner
)
.indent(indent))
}
}