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:
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user