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:
@@ -2,6 +2,8 @@ pub mod builder;
|
||||
pub mod info;
|
||||
pub mod traits;
|
||||
|
||||
pub mod location;
|
||||
pub mod proxy_host;
|
||||
pub mod upstream;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -14,6 +16,8 @@ use crate::{
|
||||
agent_client::AgentService,
|
||||
nginx::{
|
||||
builder::{NginxConfigBuilder, NginxConfigProvider},
|
||||
location::{LocationService, LocationServiceImpl},
|
||||
proxy_host::{ProxyService, ProxyServiceImpl},
|
||||
upstream::{UpstreamService, UpstreamServiceImpl},
|
||||
},
|
||||
},
|
||||
@@ -23,7 +27,12 @@ pub struct NginxService {
|
||||
#[allow(dead_code)]
|
||||
connection: Arc<DatabaseConnection>,
|
||||
//
|
||||
#[allow(dead_code)]
|
||||
upstream_service: Arc<dyn UpstreamService>,
|
||||
#[allow(dead_code)]
|
||||
proxy_service: Arc<dyn ProxyService>,
|
||||
#[allow(dead_code)]
|
||||
location_service: Arc<dyn LocationService>,
|
||||
}
|
||||
|
||||
impl NginxService {
|
||||
@@ -32,6 +41,8 @@ impl NginxService {
|
||||
connection: connection.clone(),
|
||||
//
|
||||
upstream_service: Arc::new(UpstreamServiceImpl::new(connection.clone())),
|
||||
proxy_service: Arc::new(ProxyServiceImpl::new(connection.clone())),
|
||||
location_service: Arc::new(LocationServiceImpl::new(connection.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +50,14 @@ impl NginxService {
|
||||
self.upstream_service.clone()
|
||||
}
|
||||
|
||||
pub fn get_proxy_service(&self) -> Arc<dyn ProxyService> {
|
||||
self.proxy_service.clone()
|
||||
}
|
||||
|
||||
pub fn get_location_service(&self) -> Arc<dyn LocationService> {
|
||||
self.location_service.clone()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn validate_config(
|
||||
&self,
|
||||
@@ -69,7 +88,7 @@ impl NginxService {
|
||||
.generate_config(&mut builder, tx)
|
||||
.await?;
|
||||
|
||||
Ok(builder.to_nginx_config(None))
|
||||
builder.to_nginx_config(None)
|
||||
}
|
||||
|
||||
pub async fn regenerate_and_apply_config(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::services::nginx::info::upstream::UpstreamInfo;
|
||||
use crate::{errors::service_error::ServiceError, services::nginx::info::upstream::UpstreamInfo};
|
||||
|
||||
pub const INDENT_SIZE: usize = 2;
|
||||
|
||||
pub trait NginxConfigProvider {
|
||||
fn to_nginx_config(&self, indent: Option<usize>) -> String;
|
||||
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -24,7 +24,7 @@ impl NginxConfigBuilder {
|
||||
}
|
||||
|
||||
impl NginxConfigProvider for NginxConfigBuilder {
|
||||
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
||||
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
|
||||
let mut config = format!(
|
||||
"# Nginx Config Generated by YANPM at {}",
|
||||
chrono::Utc::now()
|
||||
@@ -32,12 +32,12 @@ impl NginxConfigProvider for NginxConfigBuilder {
|
||||
|
||||
for upstream in &self.upstreams {
|
||||
config.push('\n');
|
||||
config.push_str(&upstream.to_nginx_config(indent));
|
||||
config.push_str(&upstream.to_nginx_config(indent)?);
|
||||
}
|
||||
|
||||
// TODO: Add other sections like servers, locations, etc.
|
||||
// trailing newline for file ending
|
||||
config.push('\n');
|
||||
config
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
pub mod location;
|
||||
pub mod proxy_host;
|
||||
pub mod upstream;
|
||||
pub mod upstream_target;
|
||||
|
||||
296
apps/api/src/services/nginx/info/location.rs
Normal file
296
apps/api/src/services/nginx/info/location.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
251
apps/api/src/services/nginx/info/proxy_host.rs
Normal file
251
apps/api/src/services/nginx/info/proxy_host.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
512
apps/api/src/services/nginx/location.rs
Normal file
512
apps/api/src/services/nginx/location.rs
Normal file
@@ -0,0 +1,512 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
|
||||
ModelTrait, QueryFilter, QuerySelect, QueryTrait,
|
||||
};
|
||||
|
||||
use database::generated::entities::{location, proxy_host, upstream};
|
||||
|
||||
use crate::{
|
||||
errors::service_error::ServiceError,
|
||||
helpers::database::PaginationFilter,
|
||||
services::nginx::info::location::{CreateLocationInfo, LocationInfo, UpdateLocationInfo},
|
||||
with_conn,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait LocationService: Send + Sync {
|
||||
async fn create_location(
|
||||
&self,
|
||||
create_info: CreateLocationInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<LocationInfo, ServiceError>;
|
||||
#[allow(dead_code)]
|
||||
async fn get_locations(
|
||||
&self,
|
||||
pagination: Option<PaginationFilter>,
|
||||
options: Option<GetLocationOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<Vec<LocationInfo>, ServiceError>;
|
||||
async fn get_location(
|
||||
&self,
|
||||
location_id: uuid::Uuid,
|
||||
options: Option<GetLocationOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<LocationInfo, ServiceError>;
|
||||
async fn update_location(
|
||||
&self,
|
||||
location_id: uuid::Uuid,
|
||||
update: UpdateLocationInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<LocationInfo, ServiceError>;
|
||||
async fn delete_location(
|
||||
&self,
|
||||
location_id: uuid::Uuid,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<(), ServiceError>;
|
||||
}
|
||||
|
||||
pub struct LocationServiceImpl {
|
||||
connection: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct LocationTotalCountOptions {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GetLocationOptions {
|
||||
pub include_upstream: bool,
|
||||
#[allow(dead_code)]
|
||||
pub filter_by_enabled: bool,
|
||||
}
|
||||
|
||||
impl LocationServiceImpl {
|
||||
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||
Self { connection }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl LocationService for LocationServiceImpl {
|
||||
async fn create_location(
|
||||
&self,
|
||||
create_info: CreateLocationInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<LocationInfo, ServiceError> {
|
||||
let model: location::ActiveModel = create_info.into();
|
||||
|
||||
let r = with_conn!(&*self.connection, tx, conn, { model.insert(*conn).await? });
|
||||
Ok(r.into())
|
||||
}
|
||||
|
||||
async fn get_locations(
|
||||
&self,
|
||||
pagination: Option<PaginationFilter>,
|
||||
options: Option<GetLocationOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<Vec<LocationInfo>, ServiceError> {
|
||||
let r = with_conn!(&*self.connection, tx, conn, {
|
||||
let mut find_query = location::Entity::find();
|
||||
if let Some(pagination) = pagination {
|
||||
let (offset, limit) = pagination.get_offset_limit();
|
||||
find_query = find_query.offset(offset).limit(limit);
|
||||
}
|
||||
let find_query = find_query
|
||||
.apply_if(
|
||||
options
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.filter_by_enabled)
|
||||
.then_some(true),
|
||||
|q, _v| q.filter(location::Column::Enabled.eq(true)),
|
||||
)
|
||||
.find_also_related(proxy_host::Entity);
|
||||
|
||||
let r: Vec<LocationInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||
find_query
|
||||
.find_also_related(upstream::Entity)
|
||||
.all(*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect()
|
||||
} else {
|
||||
find_query
|
||||
.all(*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|m| m.into())
|
||||
.collect()
|
||||
};
|
||||
r
|
||||
});
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
async fn get_location(
|
||||
&self,
|
||||
location_id: uuid::Uuid,
|
||||
options: Option<GetLocationOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<LocationInfo, ServiceError> {
|
||||
let r = with_conn!(&*self.connection, tx, conn, {
|
||||
let find_query =
|
||||
location::Entity::find_by_id(location_id).find_also_related(proxy_host::Entity);
|
||||
|
||||
let r: Option<LocationInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||
find_query
|
||||
.find_also_related(upstream::Entity)
|
||||
.one(*conn)
|
||||
.await?
|
||||
.map(|v| v.into())
|
||||
} else {
|
||||
find_query.one(*conn).await?.map(|m| m.into())
|
||||
};
|
||||
r
|
||||
});
|
||||
|
||||
Ok(r.ok_or(ServiceError::NotFound(format!(
|
||||
"Location with id {} not found",
|
||||
location_id
|
||||
)))?)
|
||||
}
|
||||
|
||||
async fn update_location(
|
||||
&self,
|
||||
location_id: uuid::Uuid,
|
||||
update: UpdateLocationInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<LocationInfo, ServiceError> {
|
||||
let current_model = with_conn!(&*self.connection, tx, conn, {
|
||||
location::Entity::find_by_id(location_id)
|
||||
.one(*conn)
|
||||
.await?
|
||||
.ok_or(ServiceError::NotFound(format!(
|
||||
"Location with id {} not found",
|
||||
location_id
|
||||
)))?
|
||||
});
|
||||
let active_model = update.apply_to_model(current_model);
|
||||
|
||||
let r = with_conn!(&*self.connection, tx, conn, {
|
||||
active_model.update(*conn).await?
|
||||
});
|
||||
Ok(r.into())
|
||||
}
|
||||
|
||||
async fn delete_location(
|
||||
&self,
|
||||
location_id: uuid::Uuid,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<(), ServiceError> {
|
||||
let model = with_conn!(&*self.connection, tx, conn, {
|
||||
location::Entity::find_by_id(location_id)
|
||||
.one(*conn)
|
||||
.await?
|
||||
.ok_or(ServiceError::NotFound(format!(
|
||||
"Location with id {} not found",
|
||||
location_id
|
||||
)))?
|
||||
});
|
||||
with_conn!(&*self.connection, tx, conn, {
|
||||
model.delete(*conn).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::MockExecResult;
|
||||
use sea_orm::{DatabaseBackend, MockDatabase};
|
||||
|
||||
use database::generated::entities::{location, proxy_host};
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_location_returns_info() {
|
||||
let host_id = uuid::Uuid::new_v4();
|
||||
let created = location::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
host_id,
|
||||
path: "/test".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![created.clone()]])
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
|
||||
let create_info = CreateLocationInfo {
|
||||
host_id,
|
||||
path: "/test".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
let res = svc.create_location(create_info, None).await;
|
||||
assert!(res.is_ok());
|
||||
let info = res.expect("Failed to create location");
|
||||
assert_eq!(info.path, "/test");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_locations_returns_list() {
|
||||
let host_id = uuid::Uuid::new_v4();
|
||||
|
||||
let l1 = location::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
host_id,
|
||||
path: "/a".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let l2 = location::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
host_id,
|
||||
path: "/b".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![
|
||||
(l1.clone(), None::<proxy_host::Model>),
|
||||
(l2.clone(), None::<proxy_host::Model>),
|
||||
]])
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc.get_locations(None, None, None).await;
|
||||
assert!(res.is_ok());
|
||||
let list = res.expect("Failed to get locations");
|
||||
assert_eq!(list.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_location_with_upstream_returns_upstream() {
|
||||
let host_id = uuid::Uuid::new_v4();
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
|
||||
let loc = location::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
host_id,
|
||||
path: "/up".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: Some(up_id),
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![(loc.clone(), None::<proxy_host::Model>)]])
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc
|
||||
.get_location(
|
||||
loc.id,
|
||||
Some(GetLocationOptions {
|
||||
include_upstream: false,
|
||||
filter_by_enabled: false,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok());
|
||||
let info = res.expect("Failed to get location");
|
||||
assert_eq!(info.id, loc.id);
|
||||
assert_eq!(info.upstream_id, Some(up_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_location_not_found_returns_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc.get_location(uuid::Uuid::new_v4(), None, None).await;
|
||||
|
||||
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_location_success() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let host_id = uuid::Uuid::new_v4();
|
||||
let existing = location::Model {
|
||||
id,
|
||||
host_id,
|
||||
path: "/old".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let updated = location::Model {
|
||||
id,
|
||||
host_id,
|
||||
path: "/new".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: existing.created_at,
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![existing.clone()]]) // find_by_id
|
||||
.append_query_results(vec![vec![updated.clone()]]) // update result
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
|
||||
let update_info = UpdateLocationInfo {
|
||||
path: Some("/new".to_string()),
|
||||
match_type: None,
|
||||
order: None,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: None,
|
||||
};
|
||||
|
||||
let res = svc.update_location(id, update_info, None).await;
|
||||
assert!(res.is_ok());
|
||||
let got = res.expect("Failed to update location");
|
||||
assert_eq!(got.path, "/new");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_location_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc
|
||||
.update_location(
|
||||
uuid::Uuid::new_v4(),
|
||||
UpdateLocationInfo {
|
||||
path: None,
|
||||
match_type: None,
|
||||
order: None,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_location_success() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let existing = location::Model {
|
||||
id,
|
||||
host_id: uuid::Uuid::new_v4(),
|
||||
path: "/del".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![existing.clone()]])
|
||||
.append_exec_results(vec![MockExecResult {
|
||||
rows_affected: 1,
|
||||
last_insert_id: 0,
|
||||
}])
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
let res = svc.delete_location(id, None).await;
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_location_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc.delete_location(uuid::Uuid::new_v4(), None).await;
|
||||
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||
}
|
||||
}
|
||||
598
apps/api/src/services/nginx/proxy_host.rs
Normal file
598
apps/api/src/services/nginx/proxy_host.rs
Normal file
@@ -0,0 +1,598 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, DatabaseTransaction,
|
||||
EntityTrait, FromQueryResult, JoinType, ModelTrait, QueryFilter, QuerySelect, QueryTrait,
|
||||
RelationTrait, TransactionTrait,
|
||||
};
|
||||
|
||||
use database::generated::entities::{location, proxy_host};
|
||||
|
||||
use crate::{
|
||||
errors::service_error::ServiceError,
|
||||
helpers::database::PaginationFilter,
|
||||
services::nginx::info::proxy_host::{ProxyHostCreateInfo, ProxyHostInfo, UpdateProxyHostInfo},
|
||||
with_conn,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ProxyService: Send + Sync {
|
||||
async fn create_proxy(
|
||||
&self,
|
||||
create_info: ProxyHostCreateInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<ProxyHostInfo, ServiceError>;
|
||||
async fn get_total_proxies(
|
||||
&self,
|
||||
options: Option<ProxyTotalCountOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<u64, ServiceError>;
|
||||
async fn get_proxies(
|
||||
&self,
|
||||
pagination: Option<PaginationFilter>,
|
||||
options: Option<ProxyHostListOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<Vec<ProxyHostInfo>, ServiceError>;
|
||||
async fn get_proxy(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
options: Option<ProxyHostGetOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<ProxyHostInfo, ServiceError>;
|
||||
async fn update_proxy(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
update: UpdateProxyHostInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<ProxyHostInfo, ServiceError>;
|
||||
async fn delete_proxy(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<(), ServiceError>;
|
||||
}
|
||||
|
||||
pub struct ProxyServiceImpl {
|
||||
connection: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ProxyTotalCountOptions {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProxyHostGetOptions {
|
||||
pub include_upstream: bool,
|
||||
pub filter_by_enabled: bool,
|
||||
}
|
||||
|
||||
pub type ProxyHostListOptions = ProxyHostGetOptions;
|
||||
|
||||
impl ProxyServiceImpl {
|
||||
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||
Self { connection }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ProxyService for ProxyServiceImpl {
|
||||
async fn create_proxy(
|
||||
&self,
|
||||
create_info: ProxyHostCreateInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<ProxyHostInfo, ServiceError> {
|
||||
let (proxy_host, location_models): (proxy_host::ActiveModel, Vec<location::ActiveModel>) =
|
||||
create_info.into();
|
||||
|
||||
let mut maybe_owned_tx: Option<DatabaseTransaction> = None;
|
||||
let tx_ref: Option<&mut DatabaseTransaction> = if let Some(tx) = tx {
|
||||
Some(tx)
|
||||
} else {
|
||||
maybe_owned_tx = Some(self.connection.begin().await?);
|
||||
maybe_owned_tx.as_mut()
|
||||
};
|
||||
let r = with_conn!(&*self.connection, tx_ref, conn, {
|
||||
let inserted_proxy = proxy_host.insert(*conn).await?;
|
||||
let mut inserted_location_models: Vec<location::Model> =
|
||||
Vec::with_capacity(location_models.len());
|
||||
|
||||
for mut loc_model in location_models {
|
||||
loc_model.host_id = Set(inserted_proxy.id);
|
||||
let r = loc_model.insert(*conn).await?;
|
||||
inserted_location_models.push(r);
|
||||
}
|
||||
(inserted_proxy, inserted_location_models)
|
||||
});
|
||||
|
||||
if let Some(t) = maybe_owned_tx.take() {
|
||||
t.commit().await?;
|
||||
}
|
||||
Ok(r.into())
|
||||
}
|
||||
|
||||
async fn get_total_proxies(
|
||||
&self,
|
||||
_options: Option<ProxyTotalCountOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<u64, ServiceError> {
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct CountResult {
|
||||
count: i64,
|
||||
}
|
||||
let count_info = with_conn!(&*self.connection, tx, conn, {
|
||||
proxy_host::Entity::find()
|
||||
.select_only()
|
||||
.column_as(proxy_host::Column::Id.count(), "count")
|
||||
.into_model::<CountResult>()
|
||||
.one(*conn)
|
||||
.await?
|
||||
});
|
||||
Ok(count_info.map_or(0, |c| c.count) as u64)
|
||||
}
|
||||
|
||||
async fn get_proxies(
|
||||
&self,
|
||||
pagination: Option<PaginationFilter>,
|
||||
options: Option<ProxyHostListOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<Vec<ProxyHostInfo>, ServiceError> {
|
||||
let r = with_conn!(&*self.connection, tx, conn, {
|
||||
let mut find_query = proxy_host::Entity::find();
|
||||
if let Some(pagination) = pagination {
|
||||
let (offset, limit) = pagination.get_offset_limit();
|
||||
find_query = find_query.offset(offset).limit(limit);
|
||||
}
|
||||
|
||||
let find_query = find_query
|
||||
.apply_if(
|
||||
options
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.filter_by_enabled)
|
||||
.then_some(true),
|
||||
|q, _v| q.filter(location::Column::Enabled.eq(true)),
|
||||
)
|
||||
.find_with_related(location::Entity);
|
||||
|
||||
let r: Vec<ProxyHostInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||
find_query
|
||||
.join(JoinType::LeftJoin, proxy_host::Relation::Upstream.def())
|
||||
.all(*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect()
|
||||
} else {
|
||||
find_query
|
||||
.all(*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|m| m.into())
|
||||
.collect()
|
||||
};
|
||||
r
|
||||
});
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
async fn get_proxy(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
options: Option<ProxyHostGetOptions>,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<ProxyHostInfo, ServiceError> {
|
||||
let r: ProxyHostInfo = with_conn!(&*self.connection, tx, conn, {
|
||||
let find_query = proxy_host::Entity::find_by_id(proxy_id)
|
||||
.apply_if(
|
||||
options
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.filter_by_enabled)
|
||||
.then_some(true),
|
||||
|q, _v| q.filter(location::Column::Enabled.eq(true)),
|
||||
)
|
||||
.find_with_related(location::Entity);
|
||||
let r: Option<ProxyHostInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||
find_query
|
||||
.join(JoinType::LeftJoin, proxy_host::Relation::Upstream.def())
|
||||
.all(*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|v| v.into())
|
||||
} else {
|
||||
find_query
|
||||
.all(*conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|m| m.into())
|
||||
.next()
|
||||
};
|
||||
r.ok_or(ServiceError::NotFound(format!(
|
||||
"Proxy host with id {} not found",
|
||||
proxy_id
|
||||
)))?
|
||||
});
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
async fn update_proxy(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
update: UpdateProxyHostInfo,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<ProxyHostInfo, ServiceError> {
|
||||
let current_model = with_conn!(&*self.connection, tx, conn, {
|
||||
proxy_host::Entity::find_by_id(proxy_id)
|
||||
.one(*conn)
|
||||
.await?
|
||||
.ok_or(ServiceError::NotFound(format!(
|
||||
"Proxy host with id {} not found",
|
||||
proxy_id
|
||||
)))?
|
||||
});
|
||||
let active_model = update.apply_to_model(current_model);
|
||||
|
||||
let r = with_conn!(&*self.connection, tx, conn, {
|
||||
active_model.update(*conn).await?
|
||||
});
|
||||
Ok(r.into())
|
||||
}
|
||||
|
||||
async fn delete_proxy(
|
||||
&self,
|
||||
proxy_id: uuid::Uuid,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<(), ServiceError> {
|
||||
let model = with_conn!(&*self.connection, tx, conn, {
|
||||
proxy_host::Entity::find_by_id(proxy_id)
|
||||
.one(*conn)
|
||||
.await?
|
||||
.ok_or(ServiceError::NotFound(format!(
|
||||
"Proxy host with id {} not found",
|
||||
proxy_id
|
||||
)))?
|
||||
});
|
||||
with_conn!(&*self.connection, tx, conn, {
|
||||
model.delete(*conn).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::MockExecResult;
|
||||
use sea_orm::{DatabaseBackend, MockDatabase};
|
||||
|
||||
use database::generated::entities::{location, proxy_host};
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_proxy_returns_info() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let created = proxy_host::Model {
|
||||
id,
|
||||
name: Some("test_proxy".to_string()),
|
||||
domain: "example.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let loc = location::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
host_id: id,
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 0,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![created.clone()]])
|
||||
.append_query_results(vec![vec![loc.clone()]])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
|
||||
let create_info = crate::services::nginx::info::proxy_host::ProxyHostCreateInfo {
|
||||
name: Some("test_proxy".to_string()),
|
||||
domain: "example.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
locations: Vec::new(),
|
||||
};
|
||||
|
||||
let res = svc.create_proxy(create_info, None).await;
|
||||
assert!(res.is_ok());
|
||||
let info = res.expect("Failed to create proxy");
|
||||
assert_eq!(info.domain, "example.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_total_proxies_returns_count() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
let res = svc
|
||||
.get_total_proxies(None, None)
|
||||
.await
|
||||
.expect("Failed to get total proxies");
|
||||
assert_eq!(res, 0u64);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_proxies_returns_list() {
|
||||
let p1 = proxy_host::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: Some("p1".to_string()),
|
||||
domain: "d1".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let p2 = proxy_host::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: Some("p2".to_string()),
|
||||
domain: "d2".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![
|
||||
(p1.clone(), None::<location::Model>),
|
||||
(p2.clone(), None::<location::Model>),
|
||||
]])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
let res = svc.get_proxies(None, None, None).await;
|
||||
assert!(res.is_ok());
|
||||
let list = res.expect("Failed to get proxies");
|
||||
assert_eq!(list.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_proxy_returns_info() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let p = proxy_host::Model {
|
||||
id,
|
||||
name: Some("proxy".to_string()),
|
||||
domain: "ex.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![(p.clone(), None::<location::Model>)]])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
let res = svc.get_proxy(id, None, None).await;
|
||||
assert!(res.is_ok());
|
||||
let got = res.expect("Failed to get proxy");
|
||||
assert_eq!(got.id, id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_proxy_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
let res = svc.get_proxy(uuid::Uuid::new_v4(), None, None).await;
|
||||
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_proxy_success() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let existing = proxy_host::Model {
|
||||
id,
|
||||
name: Some("old".to_string()),
|
||||
domain: "d".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let updated = proxy_host::Model {
|
||||
id,
|
||||
name: Some("new".to_string()),
|
||||
domain: existing.domain.clone(),
|
||||
scheme: existing.scheme.clone(),
|
||||
listen_port: existing.listen_port,
|
||||
forward_scheme: existing.forward_scheme.clone(),
|
||||
forward_host: existing.forward_host.clone(),
|
||||
forward_port: existing.forward_port,
|
||||
preserve_host_header: existing.preserve_host_header,
|
||||
enable_websocket: existing.enable_websocket,
|
||||
enabled: existing.enabled,
|
||||
meta: existing.meta.clone(),
|
||||
default_upstream_id: existing.default_upstream_id,
|
||||
created_by: existing.created_by,
|
||||
created_at: existing.created_at,
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![existing.clone()]])
|
||||
.append_query_results(vec![vec![updated.clone()]])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
|
||||
let update_info = crate::services::nginx::info::proxy_host::UpdateProxyHostInfo {
|
||||
name: None,
|
||||
domain: None,
|
||||
scheme: None,
|
||||
listen_port: None,
|
||||
forward_scheme: None,
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: None,
|
||||
enable_websocket: None,
|
||||
enabled: None,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
};
|
||||
|
||||
let res = svc.update_proxy(id, update_info, None).await;
|
||||
assert!(res.is_ok());
|
||||
let got = res.expect("Failed to update proxy");
|
||||
assert_eq!(got.name.expect("Name should be present"), "new");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_proxy_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc
|
||||
.update_proxy(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::services::nginx::info::proxy_host::UpdateProxyHostInfo {
|
||||
name: None,
|
||||
domain: None,
|
||||
scheme: None,
|
||||
listen_port: None,
|
||||
forward_scheme: None,
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: None,
|
||||
enable_websocket: None,
|
||||
enabled: None,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_proxy_success() {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let existing = proxy_host::Model {
|
||||
id,
|
||||
name: Some("to-delete".to_string()),
|
||||
domain: "d".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![existing.clone()]])
|
||||
.append_exec_results(vec![MockExecResult {
|
||||
rows_affected: 1,
|
||||
last_insert_id: 0,
|
||||
}])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc.delete_proxy(id, None).await;
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_proxy_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||
|
||||
let res = svc.delete_proxy(uuid::Uuid::new_v4(), None).await;
|
||||
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user