diff --git a/.gitignore b/.gitignore index b627e2e..aea5b13 100644 --- a/.gitignore +++ b/.gitignore @@ -161,5 +161,6 @@ target **/mutants.out*/ .local/ +.act/ certs/ diff --git a/apps/nxmesh-agent/src/config/settings/auth.rs b/apps/nxmesh-agent/src/config/settings/auth.rs index fe9206c..1e6cf67 100644 --- a/apps/nxmesh-agent/src/config/settings/auth.rs +++ b/apps/nxmesh-agent/src/config/settings/auth.rs @@ -80,34 +80,6 @@ mod tests { assert!(result.is_ok()); } - fn create_exec_file(path: &Path) { - write_file(path); - let metadata = fs::metadata(path); - assert!(metadata.is_ok()); - let metadata = metadata.ok(); - assert!(metadata.is_some()); - let metadata = metadata.unwrap_or_else(|| unreachable!()); - - let mut perms = metadata.permissions(); - perms.set_mode(0o755); - let result = fs::set_permissions(path, perms); - assert!(result.is_ok()); - } - - fn create_non_exec_file(path: &Path) { - write_file(path); - let metadata = fs::metadata(path); - assert!(metadata.is_ok()); - let metadata = metadata.ok(); - assert!(metadata.is_some()); - let metadata = metadata.unwrap_or_else(|| unreachable!()); - - let mut perms = metadata.permissions(); - perms.set_mode(0o644); - let result = fs::set_permissions(path, perms); - assert!(result.is_ok()); - } - fn valid_tls_raw_paths(temp_dir: &TempDir) -> (PathBuf, PathBuf, PathBuf) { let ca_path = temp_dir.path().join("ca.pem"); let cert_path = temp_dir.path().join("cert.pem"); diff --git a/apps/nxmesh-agent/src/connector/master/mod.rs b/apps/nxmesh-agent/src/connector/master/mod.rs index 544d48f..392abe9 100644 --- a/apps/nxmesh-agent/src/connector/master/mod.rs +++ b/apps/nxmesh-agent/src/connector/master/mod.rs @@ -1,7 +1,3 @@ -use std::sync::Arc; - -use tokio::sync::Mutex; - pub mod ssh; pub type AgentClient = @@ -47,8 +43,6 @@ mod tests { atomic::{AtomicBool, Ordering}, }; - use tokio::sync::Mutex; - use crate::config::settings::{ GrpcSettings, LogSettings, MAuthSettings, Settings, TLSSettings, }; diff --git a/apps/nxmesh-agent/src/service/master_handler/handlers.rs b/apps/nxmesh-agent/src/service/master_handler/handlers.rs index 9f96cf3..be8c501 100644 --- a/apps/nxmesh-agent/src/service/master_handler/handlers.rs +++ b/apps/nxmesh-agent/src/service/master_handler/handlers.rs @@ -1,8 +1,6 @@ -use std::sync::{Arc, Weak}; +use std::sync::Weak; -use nxmesh_proto::{ - AgentMessage, ConfigUpdate, MasterMessage, command::Command, master_message::Payload, -}; +use nxmesh_proto::{ConfigUpdate, MasterMessage, command::Command, master_message::Payload}; use crate::service::master_handler::{MasterHandlerError, MessageResult}; diff --git a/apps/nxmesh-agent/src/service/nginx_handler/command_handler.rs b/apps/nxmesh-agent/src/service/nginx_handler/command_handler.rs index 6c45f14..2bcd4b6 100644 --- a/apps/nxmesh-agent/src/service/nginx_handler/command_handler.rs +++ b/apps/nxmesh-agent/src/service/nginx_handler/command_handler.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use thiserror::Error; use tokio::process::Command; -use tracing::{debug, warn}; +use tracing::debug; use crate::{config::settings::NginxSettings, service::master_handler::MasterHandlerError}; @@ -120,10 +120,7 @@ impl CommandHandler for CommandHandlerImpl { if !output.status.success() { let error_info = String::from_utf8_lossy(&output.stderr); return Err(CommandHandlerError::CommandExecutionError( - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to reload nginx: {}", error_info.trim()), - ), + std::io::Error::other(format!("Failed to reload nginx: {}", error_info.trim())), )); } let success_info = String::from_utf8_lossy(&output.stdout); @@ -142,10 +139,7 @@ impl CommandHandler for CommandHandlerImpl { if !output.status.success() { let error_info = String::from_utf8_lossy(&output.stderr); return Err(CommandHandlerError::CommandExecutionError( - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to stop nginx: {}", error_info.trim()), - ), + std::io::Error::other(format!("Failed to stop nginx: {}", error_info.trim())), )); } let success_info = String::from_utf8_lossy(&output.stdout); @@ -171,10 +165,10 @@ impl CommandHandler for CommandHandlerImpl { if !output.status.success() { let error_info = String::from_utf8_lossy(&output.stderr); return Err(CommandHandlerError::CommandExecutionError( - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to validate nginx config: {}", error_info.trim()), - ), + std::io::Error::other(format!( + "Failed to validate nginx config: {}", + error_info.trim() + )), )); } let success_info = String::from_utf8_lossy(&output.stdout); @@ -191,10 +185,10 @@ impl CommandHandler for CommandHandlerImpl { if !output.status.success() { let error_info = String::from_utf8_lossy(&output.stderr); return Err(CommandHandlerError::CommandExecutionError( - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to get nginx version: {}", error_info.trim()), - ), + std::io::Error::other(format!( + "Failed to get nginx version: {}", + error_info.trim() + )), )); } @@ -211,10 +205,7 @@ impl CommandHandler for CommandHandlerImpl { if !output.status.success() { let error_info = String::from_utf8_lossy(&output.stderr); return Err(CommandHandlerError::CommandExecutionError( - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to get nginx status: {}", error_info.trim()), - ), + std::io::Error::other(format!("Failed to get nginx status: {}", error_info.trim())), )); } diff --git a/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs b/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs index b9ca2f2..73a987a 100644 --- a/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs +++ b/apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use fs4::tokio::AsyncFileExt; use thiserror::Error; -use tokio::{io::AsyncWriteExt, process::Command}; +use tokio::io::AsyncWriteExt; use tracing::warn; use crate::{config::settings::NginxSettings, service::master_handler::MasterHandlerError}; @@ -222,19 +222,19 @@ impl FsHandler for FsHandlerImpl { let mut entries = tokio::fs::read_dir(&deployment_dir).await?; let mut candidates: Vec<(std::path::PathBuf, std::time::SystemTime)> = Vec::new(); while let Some(entry) = entries.next_entry().await? { - if entry.file_type().await.map_or(false, |t| t.is_dir()) { - if let Ok(mtime) = entry.metadata().await.and_then(|m| m.modified()) { - candidates.push((entry.path(), mtime)); - } + if entry.file_type().await.is_ok_and(|t| t.is_dir()) + && let Ok(mtime) = entry.metadata().await.and_then(|m| m.modified()) + { + candidates.push((entry.path(), mtime)); } } // sort descending by mtime (newest first) - candidates.sort_by(|a, b| b.1.cmp(&a.1)); + candidates.sort_by_key(|b| std::cmp::Reverse(b.1)); for (dir, _) in &candidates { let mut dir_entries = tokio::fs::read_dir(dir).await?; while let Some(file) = dir_entries.next_entry().await? { - if file.file_type().await.map_or(false, |t| t.is_file()) { + if file.file_type().await.is_ok_and(|t| t.is_file()) { let name = file.file_name().to_string_lossy().to_string(); if name == "nginx.conf" || name.ends_with(".conf") { let path = file.path().to_string_lossy().to_string(); diff --git a/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs b/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs index 51b750a..9b56a61 100644 --- a/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs +++ b/apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs @@ -107,7 +107,9 @@ impl OnConfigUpdateHandler for NginxMasterMessageHandlerImpl { // apply reload on the root config self.command_handler.reload(Some(&root_config_path)).await?; // persist deployment path so Reload/Test commands survive agent restarts - self.fs_handler.save_last_deployment(&root_config_path).await?; + self.fs_handler + .save_last_deployment(&root_config_path) + .await?; info!("Persisted last deployment path: {}", root_config_path); // Reply the master to confirm the config update is successful self.master_handler diff --git a/apps/nxmesh-master/Cargo.toml b/apps/nxmesh-master/Cargo.toml index 4fa030e..825c612 100644 --- a/apps/nxmesh-master/Cargo.toml +++ b/apps/nxmesh-master/Cargo.toml @@ -31,6 +31,9 @@ thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true +# +anyhow = { version = "1.0.102", features = ["backtrace"] } + # Web axum = { workspace = true, features = ["ws"] } tower.workspace = true @@ -87,6 +90,7 @@ zip = { workspace = true } rust-embed = { version = "8.11.0", features = [] } mime_guess = "2.0.5" axum-test = "20.0.0" +tokio-stream.workspace = true [dev-dependencies] tokio-test.workspace = true diff --git a/apps/nxmesh-master/src/connector/agent/mod.rs b/apps/nxmesh-master/src/connector/agent/mod.rs index dbeba2a..bd02726 100644 --- a/apps/nxmesh-master/src/connector/agent/mod.rs +++ b/apps/nxmesh-master/src/connector/agent/mod.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use sea_orm::DatabaseConnection; -use tonic::transport::Server; pub mod ssh; diff --git a/apps/nxmesh-master/src/db/entities/access_rule.rs b/apps/nxmesh-master/src/db/entities/access_rule.rs new file mode 100644 index 0000000..3d7b9ee --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/access_rule.rs @@ -0,0 +1,61 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "access_rule")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub server_id: Option, + pub location_id: Option, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub is_deleted: bool, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, + #[sea_orm( + belongs_to = "super::location_block::Entity", + from = "Column::LocationId", + to = "super::location_block::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + LocationBlock, + #[sea_orm( + belongs_to = "super::server_block::Entity", + from = "Column::ServerId", + to = "super::server_block::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + ServerBlock, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LocationBlock.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ServerBlock.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/agent_config_binding.rs b/apps/nxmesh-master/src/db/entities/agent_config_binding.rs new file mode 100644 index 0000000..c5d0ce4 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/agent_config_binding.rs @@ -0,0 +1,64 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "agent_config_binding")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub agent_id: Option, + pub group_id: Option, + pub config_id: Uuid, + pub is_active: bool, + pub applied_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::agent_group::Entity", + from = "Column::GroupId", + to = "super::agent_group::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + AgentGroup, + #[sea_orm( + belongs_to = "super::agents::Entity", + from = "Column::AgentId", + to = "super::agents::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Agents, + #[sea_orm( + belongs_to = "super::proxy_config::Entity", + from = "Column::ConfigId", + to = "super::proxy_config::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + ProxyConfig, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AgentGroup.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Agents.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ProxyConfig.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/agent_group.rs b/apps/nxmesh-master/src/db/entities/agent_group.rs new file mode 100644 index 0000000..8e1fdfb --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/agent_group.rs @@ -0,0 +1,35 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "agent_group")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub description: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::agent_config_binding::Entity")] + AgentConfigBinding, + #[sea_orm(has_many = "super::agents::Entity")] + Agents, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AgentConfigBinding.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Agents.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/agents.rs b/apps/nxmesh-master/src/db/entities/agents.rs index e9a681a..0f2e983 100644 --- a/apps/nxmesh-master/src/db/entities/agents.rs +++ b/apps/nxmesh-master/src/db/entities/agents.rs @@ -19,9 +19,33 @@ pub struct Model { pub labels: Option, pub created_at: DateTimeWithTimeZone, pub updated_at: DateTimeWithTimeZone, + pub group_id: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::agent_config_binding::Entity")] + AgentConfigBinding, + #[sea_orm( + belongs_to = "super::agent_group::Entity", + from = "Column::GroupId", + to = "super::agent_group::Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + AgentGroup, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AgentConfigBinding.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AgentGroup.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/cache_zone.rs b/apps/nxmesh-master/src/db/entities/cache_zone.rs new file mode 100644 index 0000000..1e15103 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/cache_zone.rs @@ -0,0 +1,37 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "cache_zone")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub path: String, + pub size_limit: String, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, + #[sea_orm(has_many = "super::proxy_setting::Entity")] + ProxySetting, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ProxySetting.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/config_inheritance.rs b/apps/nxmesh-master/src/db/entities/config_inheritance.rs new file mode 100644 index 0000000..a21bbdb --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/config_inheritance.rs @@ -0,0 +1,37 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "config_inheritance")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub child_config_id: Uuid, + pub parent_config_id: Uuid, + pub priority: Option, + pub applied_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::proxy_config::Entity", + from = "Column::ChildConfigId", + to = "super::proxy_config::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + ProxyConfig2, + #[sea_orm( + belongs_to = "super::proxy_config::Entity", + from = "Column::ParentConfigId", + to = "super::proxy_config::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + ProxyConfig1, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/limit_rule.rs b/apps/nxmesh-master/src/db/entities/limit_rule.rs new file mode 100644 index 0000000..9848ac6 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/limit_rule.rs @@ -0,0 +1,59 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "limit_rule")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub location_id: Uuid, + pub zone_id: Uuid, + pub burst: Option, + pub nodelay: Option, + pub is_deleted: bool, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, + #[sea_orm( + belongs_to = "super::limit_zone::Entity", + from = "Column::ZoneId", + to = "super::limit_zone::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + LimitZone, + #[sea_orm( + belongs_to = "super::location_block::Entity", + from = "Column::LocationId", + to = "super::location_block::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + LocationBlock, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LimitZone.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LocationBlock.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/limit_zone.rs b/apps/nxmesh-master/src/db/entities/limit_zone.rs new file mode 100644 index 0000000..f27ec88 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/limit_zone.rs @@ -0,0 +1,37 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "limit_zone")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::limit_rule::Entity")] + LimitRule, + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LimitRule.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/location_block.rs b/apps/nxmesh-master/src/db/entities/location_block.rs new file mode 100644 index 0000000..49339db --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/location_block.rs @@ -0,0 +1,91 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "location_block")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub server_id: Uuid, + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + #[sea_orm(column_type = "JsonBinary", nullable)] + pub metadata: Option, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::access_rule::Entity")] + AccessRule, + #[sea_orm(has_many = "super::limit_rule::Entity")] + LimitRule, + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, + #[sea_orm(has_many = "super::proxy_setting::Entity")] + ProxySetting, + #[sea_orm(has_many = "super::rewrite_rule::Entity")] + RewriteRule, + #[sea_orm( + belongs_to = "super::server_block::Entity", + from = "Column::ServerId", + to = "super::server_block::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + ServerBlock, + #[sea_orm( + belongs_to = "super::upstream::Entity", + from = "Column::ProxyPassUpstreamId", + to = "super::upstream::Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + Upstream, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AccessRule.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LimitRule.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ProxySetting.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::RewriteRule.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ServerBlock.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Upstream.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/log_setting.rs b/apps/nxmesh-master/src/db/entities/log_setting.rs new file mode 100644 index 0000000..e214cc0 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/log_setting.rs @@ -0,0 +1,44 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "log_setting")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub server_id: Uuid, + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, + #[sea_orm( + belongs_to = "super::server_block::Entity", + from = "Column::ServerId", + to = "super::server_block::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + ServerBlock, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ServerBlock.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/mod.rs b/apps/nxmesh-master/src/db/entities/mod.rs index 5224c47..762b8fe 100644 --- a/apps/nxmesh-master/src/db/entities/mod.rs +++ b/apps/nxmesh-master/src/db/entities/mod.rs @@ -2,5 +2,20 @@ pub mod prelude; +pub mod access_rule; +pub mod agent_config_binding; +pub mod agent_group; pub mod agents; +pub mod cache_zone; +pub mod config_inheritance; +pub mod limit_rule; +pub mod limit_zone; +pub mod location_block; +pub mod log_setting; +pub mod proxy_config; +pub mod proxy_setting; pub mod public_key_revocations; +pub mod rewrite_rule; +pub mod server_block; +pub mod ssl_certificate; +pub mod upstream; diff --git a/apps/nxmesh-master/src/db/entities/prelude.rs b/apps/nxmesh-master/src/db/entities/prelude.rs index 4fc9460..8f1e95c 100644 --- a/apps/nxmesh-master/src/db/entities/prelude.rs +++ b/apps/nxmesh-master/src/db/entities/prelude.rs @@ -1,4 +1,19 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 +pub use super::access_rule::Entity as AccessRule; +pub use super::agent_config_binding::Entity as AgentConfigBinding; +pub use super::agent_group::Entity as AgentGroup; pub use super::agents::Entity as Agents; +pub use super::cache_zone::Entity as CacheZone; +pub use super::config_inheritance::Entity as ConfigInheritance; +pub use super::limit_rule::Entity as LimitRule; +pub use super::limit_zone::Entity as LimitZone; +pub use super::location_block::Entity as LocationBlock; +pub use super::log_setting::Entity as LogSetting; +pub use super::proxy_config::Entity as ProxyConfig; +pub use super::proxy_setting::Entity as ProxySetting; pub use super::public_key_revocations::Entity as PublicKeyRevocations; +pub use super::rewrite_rule::Entity as RewriteRule; +pub use super::server_block::Entity as ServerBlock; +pub use super::ssl_certificate::Entity as SslCertificate; +pub use super::upstream::Entity as Upstream; diff --git a/apps/nxmesh-master/src/db/entities/proxy_config.rs b/apps/nxmesh-master/src/db/entities/proxy_config.rs new file mode 100644 index 0000000..0df9da2 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/proxy_config.rs @@ -0,0 +1,46 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "proxy_config")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub description: Option, + pub is_template: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::agent_config_binding::Entity")] + AgentConfigBinding, + #[sea_orm(has_many = "super::server_block::Entity")] + ServerBlock, + #[sea_orm(has_many = "super::upstream::Entity")] + Upstream, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AgentConfigBinding.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ServerBlock.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Upstream.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/proxy_setting.rs b/apps/nxmesh-master/src/db/entities/proxy_setting.rs new file mode 100644 index 0000000..d2ea4e9 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/proxy_setting.rs @@ -0,0 +1,60 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "proxy_setting")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub location_id: Uuid, + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::cache_zone::Entity", + from = "Column::CacheZone", + to = "super::cache_zone::Column::Id", + on_update = "NoAction", + on_delete = "SetNull" + )] + CacheZone, + #[sea_orm( + belongs_to = "super::location_block::Entity", + from = "Column::LocationId", + to = "super::location_block::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + LocationBlock, + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CacheZone.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LocationBlock.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/rewrite_rule.rs b/apps/nxmesh-master/src/db/entities/rewrite_rule.rs new file mode 100644 index 0000000..58143fa --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/rewrite_rule.rs @@ -0,0 +1,46 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "rewrite_rule")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub location_id: Uuid, + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub is_deleted: bool, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::location_block::Entity", + from = "Column::LocationId", + to = "super::location_block::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + LocationBlock, + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LocationBlock.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/server_block.rs b/apps/nxmesh-master/src/db/entities/server_block.rs new file mode 100644 index 0000000..3ea3006 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/server_block.rs @@ -0,0 +1,83 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "server_block")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub config_id: Uuid, + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub ssl_cert_id: Option, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::access_rule::Entity")] + AccessRule, + #[sea_orm(has_many = "super::location_block::Entity")] + LocationBlock, + #[sea_orm(has_many = "super::log_setting::Entity")] + LogSetting, + #[sea_orm( + belongs_to = "super::proxy_config::Entity", + from = "Column::ConfigId", + to = "super::proxy_config::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + ProxyConfig, + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, + #[sea_orm( + belongs_to = "super::ssl_certificate::Entity", + from = "Column::SslCertId", + to = "super::ssl_certificate::Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SslCertificate, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AccessRule.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LocationBlock.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LogSetting.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ProxyConfig.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SslCertificate.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/ssl_certificate.rs b/apps/nxmesh-master/src/db/entities/ssl_certificate.rs new file mode 100644 index 0000000..09feaba --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/ssl_certificate.rs @@ -0,0 +1,29 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "ssl_certificate")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::server_block::Entity")] + ServerBlock, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ServerBlock.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/db/entities/upstream.rs b/apps/nxmesh-master/src/db/entities/upstream.rs new file mode 100644 index 0000000..aea8e26 --- /dev/null +++ b/apps/nxmesh-master/src/db/entities/upstream.rs @@ -0,0 +1,54 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "upstream")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub config_id: Uuid, + pub name: String, + pub target_host: String, + pub target_port: i32, + #[sea_orm(column_type = "JsonBinary", nullable)] + pub metadata: Option, + pub override_of_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::location_block::Entity")] + LocationBlock, + #[sea_orm( + belongs_to = "super::proxy_config::Entity", + from = "Column::ConfigId", + to = "super::proxy_config::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + ProxyConfig, + #[sea_orm( + belongs_to = "Entity", + from = "Column::OverrideOfId", + to = "Column::Id", + on_update = "Cascade", + on_delete = "SetNull" + )] + SelfRef, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::LocationBlock.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ProxyConfig.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/apps/nxmesh-master/src/routes/api/agents/add_agent.rs b/apps/nxmesh-master/src/routes/api/agents/add_agent.rs new file mode 100644 index 0000000..9490f4c --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/add_agent.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; +use serde::{Deserialize, Serialize}; +use tracing::error; + +use crate::{ + routes::api::{agents::dto::AgentInfo, error::AppError}, + service::agent::{AgentService, CreateAgentRecord}, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CreateAgentRequest { + pub name: String, + #[serde(default)] + pub ip_address: Option, +} + +pub async fn add_agent_handler( + State(agent_service): State>, + Json(body): Json, +) -> Result { + if body.name.trim().is_empty() { + return Err(AppError::BadRequest("name is required".to_string())); + } + + let rec = CreateAgentRecord { + name: body.name, + ip_address: body.ip_address, + }; + + let agent = agent_service.create(&rec).await.map_err(|err| { + error!("Failed to create agent: {}", err); + AppError::InternalServerError + })?; + + Ok(( + StatusCode::CREATED, + Json(serde_json::json!({"agent": AgentInfo::from(agent)})), + )) +} diff --git a/apps/nxmesh-master/src/routes/api/agents/delete_agent.rs b/apps/nxmesh-master/src/routes/api/agents/delete_agent.rs new file mode 100644 index 0000000..d808347 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/delete_agent.rs @@ -0,0 +1,27 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use tracing::{error, info}; + +use crate::{routes::api::error::AppError, service::agent::AgentService}; + +pub async fn delete_agent_handler( + State(agent_service): State>, + Path(id): Path, +) -> Result { + let deleted = agent_service.delete(id).await.map_err(|err| { + error!("Failed to delete agent: {}", err); + AppError::InternalServerError + })?; + + if deleted { + info!("Agent {} deleted", id); + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/dto.rs b/apps/nxmesh-master/src/routes/api/agents/dto.rs new file mode 100644 index 0000000..6a1158f --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/dto.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use crate::service::agent::{AgentRecord, State}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentInfo { + pub id: String, + pub name: String, + pub state: State, + // + pub deployment_mode: Option, + pub ip_address: Option, + pub last_seen_at: Option, + pub labels: Option, + // + pub number_of_routes: usize, + // + pub created_at: String, + pub updated_at: String, + pub is_disabled: bool, +} + +impl From for AgentInfo { + fn from(record: AgentRecord) -> Self { + AgentInfo { + id: record.id.to_string(), + name: record.name, + ip_address: record.ip_address, + state: record.state, + deployment_mode: record.deployment_mode, + last_seen_at: record.last_seen_at, + labels: record.labels, + number_of_routes: 0, // This will be populated later + created_at: record.created_at, + updated_at: record.updated_at, + is_disabled: matches!(record.state, State::Disabled), + } + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/get_agent.rs b/apps/nxmesh-master/src/routes/api/agents/get_agent.rs new file mode 100644 index 0000000..683ac6c --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/get_agent.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, +}; +use serde::Serialize; +use tracing::error; + +use crate::{ + routes::api::{agents::dto::AgentInfo, error::AppError}, + service::agent::AgentService, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct GetAgentsResponse { + agents: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct GetAgentResponse { + agent: AgentInfo, +} + +pub async fn get_agents_handler( + State(agent_service): State>, +) -> Result, AppError> { + let agents = agent_service.list().await.map_err(|err| { + error!("Failed to get agents: {}", err); + AppError::InternalServerError + })?; + + Ok(Json(GetAgentsResponse { + agents: agents.into_iter().map(AgentInfo::from).collect(), + })) +} + +pub async fn get_agent_handler( + State(agent_service): State>, + Path(id): Path, +) -> Result, AppError> { + let agent = agent_service.get(id).await.map_err(|err| { + error!("Failed to get agent: {}", err); + AppError::InternalServerError + })?; + match agent { + Some(agent) => Ok(Json(GetAgentResponse { + agent: AgentInfo::from(agent), + })), + None => Err(AppError::NotFound), + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/mod.rs b/apps/nxmesh-master/src/routes/api/agents/mod.rs new file mode 100644 index 0000000..69d7528 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/mod.rs @@ -0,0 +1,225 @@ +use crate::routes::api::{ + ApiRouter, + agents::{ + add_agent::add_agent_handler, + delete_agent::delete_agent_handler, + get_agent::{get_agent_handler, get_agents_handler}, + update_agent::update_agent_handler, + }, +}; + +mod add_agent; +mod delete_agent; +mod dto; +mod get_agent; +mod update_agent; + +pub async fn get_router() -> ApiRouter { + ApiRouter::new() + .route( + "/agents", + axum::routing::get(get_agents_handler).post(add_agent_handler), + ) + .route( + "/agents/{id}", + axum::routing::get(get_agent_handler) + .put(update_agent_handler) + .delete(delete_agent_handler), + ) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use std::sync::Arc; + + use crate::service::agent::{AgentRecord, MockAgentService, State}; + + use super::*; + use axum_test::TestServer; + + async fn make_state(mock: MockAgentService) -> axum::Router { + use crate::service::proxy::*; + let state = crate::routes::api::LocalApiState::from(crate::routes::api::ApiState { + agent_service: Arc::new(mock), + proxy_service: Arc::new(MockProxyServiceTrait::new()), + server_block_service: Arc::new(server_block::MockServerBlockService::new()), + upstream_service: Arc::new(upstream::MockUpstreamService::new()), + location_block_service: Arc::new(location_block::MockLocationBlockService::new()), + access_rule_service: Arc::new(access_rule::MockAccessRuleService::new()), + cache_zone_service: Arc::new(cache_zone::MockCacheZoneService::new()), + limit_rule_service: Arc::new(limit_rule::MockLimitRuleService::new()), + limit_zone_service: Arc::new(limit_zone::MockLimitZoneService::new()), + log_setting_service: Arc::new(log_setting::MockLogSettingService::new()), + proxy_setting_service: Arc::new(proxy_setting::MockProxySettingService::new()), + rewrite_rule_service: Arc::new(rewrite_rule::MockRewriteRuleService::new()), + ssl_certificate_service: Arc::new(ssl_certificate::MockSslCertificateService::new()), + config_inheritance_service: Arc::new( + config_inheritance::MockConfigInheritanceService::new(), + ), + }); + get_router().await.with_state(state) + } + + fn make_record(id: uuid::Uuid) -> AgentRecord { + AgentRecord { + id, + name: "agent1".to_string(), + ip_address: Some("127.0.0.1".to_string()), + state: State::Active, + deployment_mode: None, + last_seen_at: None, + labels: None, + created_at: "created".to_string(), + updated_at: "updated".to_string(), + } + } + + #[tokio::test] + async fn test_get_agents() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_list().returning(|| Ok(vec![])); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server.get("/agents").await; + assert_eq!(response.status_code(), 200); + assert_eq!(response.text(), r#"{"agents":[]}"#); + } + + #[tokio::test] + async fn test_get_agent_not_found() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_get().returning(|_| Ok(None)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server.get(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 404); + } + + #[tokio::test] + async fn test_get_agent_found() { + let id = uuid::Uuid::new_v4(); + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock + .expect_get() + .returning(move |_| Ok(Some(make_record(id)))); + let server = TestServer::new(make_state(agent_service_mock).await); + let expected = format!( + r#"{{"agent":{{"id":"{}","name":"agent1","state":"Active","deployment_mode":null,"ip_address":"127.0.0.1","last_seen_at":null,"labels":null,"number_of_routes":0,"created_at":"created","updated_at":"updated","is_disabled":false}}}}"#, + id + ); + let response = server.get(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 200); + assert_eq!(response.text(), expected); + } + + #[tokio::test] + async fn test_create_agent() { + let id = uuid::Uuid::new_v4(); + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_create().returning(move |rec| { + Ok(AgentRecord { + id, + name: rec.name.clone(), + ip_address: rec.ip_address.clone(), + state: State::Active, + deployment_mode: None, + last_seen_at: None, + labels: None, + created_at: "created".to_string(), + updated_at: "updated".to_string(), + }) + }); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server + .post("/agents") + .json(&serde_json::json!({"name": "new-agent", "ip_address": "10.0.0.1"})) + .await; + assert_eq!(response.status_code(), 201); + let body: serde_json::Value = serde_json::from_str(&response.text()).unwrap(); + assert_eq!(body["agent"]["name"], "new-agent"); + assert_eq!(body["agent"]["ip_address"], "10.0.0.1"); + assert_eq!(body["agent"]["id"], id.to_string()); + } + + #[tokio::test] + async fn test_create_agent_empty_name() { + let agent_service_mock = MockAgentService::new(); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server + .post("/agents") + .json(&serde_json::json!({"name": ""})) + .await; + assert_eq!(response.status_code(), 400); + } + + #[tokio::test] + async fn test_create_agent_missing_name() { + let agent_service_mock = MockAgentService::new(); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server.post("/agents").json(&serde_json::json!({})).await; + assert_eq!(response.status_code(), 422); + } + + #[tokio::test] + async fn test_update_agent() { + let id = uuid::Uuid::new_v4(); + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_update().returning(move |_, _| { + Ok(Some(AgentRecord { + id, + name: "updated-agent".to_string(), + ip_address: Some("10.0.0.2".to_string()), + state: State::Inactive, + deployment_mode: None, + last_seen_at: None, + labels: None, + created_at: "created".to_string(), + updated_at: "updated".to_string(), + })) + }); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server + .put(&format!("/agents/{}", id)) + .json(&serde_json::json!({"name": "updated-agent", "state": "Inactive"})) + .await; + assert_eq!(response.status_code(), 200); + let body: serde_json::Value = serde_json::from_str(&response.text()).unwrap(); + assert_eq!(body["agent"]["name"], "updated-agent"); + assert_eq!(body["agent"]["state"], "Inactive"); + } + + #[tokio::test] + async fn test_update_agent_not_found() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock + .expect_update() + .returning(|_, _| Ok(None)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server + .put(&format!("/agents/{}", id)) + .json(&serde_json::json!({"name": "updated-agent"})) + .await; + assert_eq!(response.status_code(), 404); + } + + #[tokio::test] + async fn test_delete_agent() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_delete().returning(|_| Ok(true)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server.delete(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 204); + } + + #[tokio::test] + async fn test_delete_agent_not_found() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_delete().returning(|_| Ok(false)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server.delete(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 404); + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/update_agent.rs b/apps/nxmesh-master/src/routes/api/agents/update_agent.rs new file mode 100644 index 0000000..5128b45 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/update_agent.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::Deserialize; +use tracing::error; + +use crate::{ + routes::api::{agents::dto::AgentInfo, error::AppError}, + service::agent::{AgentService, State as AgentState, UpdateAgentRecord}, +}; + +#[derive(Debug, Deserialize)] +pub struct UpdateAgentRequest { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub ip_address: Option, + #[serde(default)] + pub state: Option, + #[serde(default)] + pub deployment_mode: Option, + #[serde(default)] + pub labels: Option, +} + +pub async fn update_agent_handler( + State(agent_service): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + if let Some(ref name) = body.name + && name.trim().is_empty() + { + return Err(AppError::BadRequest("name must not be empty".to_string())); + } + + let rec = UpdateAgentRecord { + name: body.name, + ip_address: body.ip_address, + state: body.state, + deployment_mode: body.deployment_mode, + labels: body.labels, + }; + + let agent = agent_service.update(id, &rec).await.map_err(|err| { + error!("Failed to update agent: {}", err); + AppError::InternalServerError + })?; + + match agent { + Some(agent) => Ok(( + StatusCode::OK, + Json(serde_json::json!({"agent": AgentInfo::from(agent)})), + )), + None => Err(AppError::NotFound), + } +} diff --git a/apps/nxmesh-master/src/routes/api/error.rs b/apps/nxmesh-master/src/routes/api/error.rs new file mode 100644 index 0000000..3ac159f --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/error.rs @@ -0,0 +1,55 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde_json::json; +use tracing::error; + +use crate::service::proxy::types::ProxyServiceError; + +impl From for AppError { + fn from(e: ProxyServiceError) -> Self { + match e { + ProxyServiceError::ConfigNotFound => AppError::NotFound, + ProxyServiceError::InvalidConfig(msg) => AppError::BadRequest(msg), + ProxyServiceError::RendererNotFound => AppError::InternalServerError, + ProxyServiceError::DatabaseError(_) => AppError::InternalServerError, + } + } +} + +pub enum AppError { + NotFound, + InternalServerError, + BadRequest(String), +} + +fn make_error_response(status: StatusCode, code: &str, msg: &str) -> Response { + let body = json!({"code": code, "message": msg}); + Response::builder() + .status(status) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&body).unwrap_or_default().into()) + .unwrap_or_else(|err| { + error!("Failed to build error response: {}", err); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + }) +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + match self { + AppError::BadRequest(msg) => { + make_error_response(StatusCode::BAD_REQUEST, "BAD_REQUEST", &msg) + } + AppError::NotFound => { + make_error_response(StatusCode::NOT_FOUND, "NOT_FOUND", "Not Found") + } + AppError::InternalServerError => make_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "INTERNAL_ERROR", + "Internal Server Error", + ), + } + } +} diff --git a/apps/nxmesh-master/src/routes/api/mod.rs b/apps/nxmesh-master/src/routes/api/mod.rs new file mode 100644 index 0000000..18fe8aa --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/mod.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use axum::{Router, extract::FromRef}; + +use crate::service::agent::AgentService; +use crate::service::proxy::ProxyServiceTrait; +use crate::service::proxy::access_rule::AccessRuleService; +use crate::service::proxy::cache_zone::CacheZoneService; +use crate::service::proxy::config_inheritance::ConfigInheritanceService; +use crate::service::proxy::limit_rule::LimitRuleService; +use crate::service::proxy::limit_zone::LimitZoneService; +use crate::service::proxy::location_block::LocationBlockService; +use crate::service::proxy::log_setting::LogSettingService; +use crate::service::proxy::proxy_setting::ProxySettingService; +use crate::service::proxy::rewrite_rule::RewriteRuleService; +use crate::service::proxy::server_block::ServerBlockService; +use crate::service::proxy::ssl_certificate::SslCertificateService; +use crate::service::proxy::upstream::UpstreamService; + +mod agents; +pub mod error; +pub use error::AppError; +pub(crate) mod proxy; + +pub struct ApiState { + pub agent_service: Arc, + pub proxy_service: Arc, + pub server_block_service: Arc, + pub upstream_service: Arc, + pub location_block_service: Arc, + pub access_rule_service: Arc, + pub cache_zone_service: Arc, + pub limit_rule_service: Arc, + pub limit_zone_service: Arc, + pub log_setting_service: Arc, + pub proxy_setting_service: Arc, + pub rewrite_rule_service: Arc, + pub ssl_certificate_service: Arc, + pub config_inheritance_service: Arc, +} + +#[derive(Clone)] +pub struct LocalApiState(pub Arc); + +impl From for LocalApiState { + fn from(api_state: ApiState) -> Self { + LocalApiState(Arc::new(api_state)) + } +} + +impl From> for LocalApiState { + fn from(api_state: Arc) -> Self { + LocalApiState(api_state) + } +} + +pub type ApiRouter = Router; + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.agent_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.proxy_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.server_block_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.upstream_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.location_block_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.access_rule_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.cache_zone_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.limit_rule_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.limit_zone_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.log_setting_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.proxy_setting_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.rewrite_rule_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.ssl_certificate_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.config_inheritance_service.clone() + } +} + +pub async fn get_router(state: impl Into) -> Router { + ApiRouter::new() + .nest("/agents", agents::get_router().await) + .nest("/proxy", proxy::get_router().await) + .with_state(state.into()) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/access_rules.rs b/apps/nxmesh-master/src/routes/api/proxy/access_rules.rs new file mode 100644 index 0000000..46f9257 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/access_rules.rs @@ -0,0 +1,161 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::access_rule::{ + AccessRuleService, CreateAccessRuleParams, UpdateAccessRuleParams, +}; +use crate::service::proxy::types::AccessRuleConfig; + +#[derive(Serialize)] +pub(crate) struct AccessRuleResponse { + pub id: Uuid, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for AccessRuleResponse { + fn from(c: AccessRuleConfig) -> Self { + Self { + id: c.id, + r#type: c.r#type, + ip_cidr: c.ip_cidr, + description: c.description, + priority: c.priority, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateAccessRuleRequest { + pub server_id: Option, + pub location_id: Option, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for CreateAccessRuleParams { + fn from(r: CreateAccessRuleRequest) -> Self { + Self { + server_id: r.server_id, + location_id: r.location_id, + r#type: r.r#type, + ip_cidr: r.ip_cidr, + description: r.description, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateAccessRuleRequest { + pub server_id: Option>, + pub location_id: Option>, + pub r#type: Option, + pub ip_cidr: Option, + pub description: Option>, + pub priority: Option, + pub override_of_id: Option>, +} + +impl From for UpdateAccessRuleParams { + fn from(r: UpdateAccessRuleRequest) -> Self { + Self { + server_id: r.server_id, + location_id: r.location_id, + r#type: r.r#type, + ip_cidr: r.ip_cidr, + description: r.description, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +async fn list_access_rules_by_server( + State(svc): State>, + Path(server_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_server(server_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn list_access_rules_by_location( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_location(location_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn create_access_rule( + State(svc): State>, + Json(body): Json, +) -> Result { + let rule = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(AccessRuleResponse::from(rule)))) +} + +async fn get_access_rule( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let rule = svc.get(id).await?; + Ok(Json(rule.into())) +} + +async fn update_access_rule( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let rule = svc.update(id, body.into()).await?; + Ok(Json(rule.into())) +} + +async fn delete_access_rule( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/server-blocks/{server_id}/access-rules", + axum::routing::get(list_access_rules_by_server), + ) + .route( + "/locations/{location_id}/access-rules", + axum::routing::get(list_access_rules_by_location), + ) + .route("/access-rules", axum::routing::post(create_access_rule)) + .route( + "/access-rules/{id}", + axum::routing::get(get_access_rule) + .put(update_access_rule) + .delete(delete_access_rule), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/agents.rs b/apps/nxmesh-master/src/routes/api/proxy/agents.rs new file mode 100644 index 0000000..b1c20dc --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/agents.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::{ProxyServiceTrait, types::AgentConfigBinding}; + +use super::configs::ProxyConfigResponse; + +#[derive(Serialize)] +pub(crate) struct AgentConfigResponse { + pub id: Uuid, + pub agent_id: Option, + pub group_id: Option, + pub config_id: Uuid, + pub is_active: bool, + pub applied_at: String, +} + +impl From for AgentConfigResponse { + fn from(b: AgentConfigBinding) -> Self { + Self { + id: b.id, + agent_id: b.agent_id, + group_id: b.group_id, + config_id: b.config_id, + is_active: b.is_active, + applied_at: b.applied_at.to_rfc3339(), + } + } +} + +#[derive(Deserialize)] +pub(crate) struct BindAgentRequest { + pub config_id: Uuid, +} + +async fn get_active_agent_config( + State(svc): State>, + Path(agent_id): Path, +) -> Result>, AppError> { + let config = svc.get_active_agent_config(agent_id).await?; + Ok(Json(config.map(Into::into))) +} + +async fn bind_agent( + State(svc): State>, + Path(agent_id): Path, + Json(body): Json, +) -> Result { + let binding = svc.bind_agent(agent_id, body.config_id).await?; + Ok(( + StatusCode::CREATED, + Json(AgentConfigResponse::from(binding)), + )) +} + +async fn unbind_agent( + State(svc): State>, + Path(agent_id): Path, +) -> Result { + let deleted = svc.unbind_agent(agent_id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new().route( + "/agents/{agent_id}/config", + axum::routing::get(get_active_agent_config) + .post(bind_agent) + .delete(unbind_agent), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs b/apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs new file mode 100644 index 0000000..621478b --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::cache_zone::{ + CacheZoneService, CreateCacheZoneParams, UpdateCacheZoneParams, +}; +use crate::service::proxy::types::CacheZoneConfig; + +#[derive(Serialize)] +pub(crate) struct CacheZoneResponse { + pub id: Uuid, + pub name: String, + pub path: String, + pub size: String, + pub override_of_id: Option, +} + +impl From for CacheZoneResponse { + fn from(c: CacheZoneConfig) -> Self { + Self { + id: c.id, + name: c.name, + path: c.path, + size: c.size, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateCacheZoneRequest { + pub name: String, + pub path: String, + pub size_limit: String, + pub override_of_id: Option, +} + +impl From for CreateCacheZoneParams { + fn from(r: CreateCacheZoneRequest) -> Self { + Self { + name: r.name, + path: r.path, + size_limit: r.size_limit, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateCacheZoneRequest { + pub name: Option, + pub path: Option, + pub size_limit: Option, + pub override_of_id: Option>, +} + +impl From for UpdateCacheZoneParams { + fn from(r: UpdateCacheZoneRequest) -> Self { + Self { + name: r.name, + path: r.path, + size_limit: r.size_limit, + override_of_id: r.override_of_id, + } + } +} + +async fn list_cache_zones( + State(svc): State>, +) -> Result>, AppError> { + let zones = svc.list().await?; + Ok(Json(zones.into_iter().map(Into::into).collect())) +} + +async fn create_cache_zone( + State(svc): State>, + Json(body): Json, +) -> Result { + let zone = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(CacheZoneResponse::from(zone)))) +} + +async fn get_cache_zone( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let zone = svc.get(id).await?; + Ok(Json(zone.into())) +} + +async fn update_cache_zone( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let zone = svc.update(id, body.into()).await?; + Ok(Json(zone.into())) +} + +async fn delete_cache_zone( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/cache-zones", + axum::routing::get(list_cache_zones).post(create_cache_zone), + ) + .route( + "/cache-zones/{id}", + axum::routing::get(get_cache_zone) + .put(update_cache_zone) + .delete(delete_cache_zone), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs b/apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs new file mode 100644 index 0000000..fb3222e --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::config_inheritance::{ + AddInheritanceParams, ConfigInheritanceRecord, ConfigInheritanceService, +}; + +#[derive(Deserialize)] +pub(crate) struct AddParentRequest { + pub parent_config_id: Uuid, + #[serde(default)] + pub priority: Option, +} + +#[derive(Serialize)] +pub(crate) struct InheritanceRecordResponse { + pub id: Uuid, + pub child_config_id: Uuid, + pub parent_config_id: Uuid, + pub priority: Option, + pub applied_at: String, +} + +impl From for InheritanceRecordResponse { + fn from(r: ConfigInheritanceRecord) -> Self { + Self { + id: r.id, + child_config_id: r.child_config_id, + parent_config_id: r.parent_config_id, + priority: r.priority, + applied_at: r.applied_at.and_utc().to_rfc3339(), + } + } +} + +async fn list_parents( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let records = svc.list_parents(id).await?; + let response: Vec = records.into_iter().map(Into::into).collect(); + Ok(Json(serde_json::json!(response))) +} + +async fn add_parent( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + let record = svc + .add(AddInheritanceParams { + child_config_id: id, + parent_config_id: body.parent_config_id, + priority: body.priority, + }) + .await?; + Ok(( + StatusCode::CREATED, + Json(InheritanceRecordResponse::from(record)), + )) +} + +async fn remove_parent( + State(svc): State>, + Path((id, parent_id)): Path<(Uuid, Uuid)>, +) -> Result { + let deleted = svc.remove(id, parent_id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +async fn list_children( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let records = svc.list_children(id).await?; + let response: Vec = records.into_iter().map(Into::into).collect(); + Ok(Json(serde_json::json!(response))) +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/configs/{id}/parents", + axum::routing::get(list_parents).post(add_parent), + ) + .route( + "/configs/{id}/parents/{parent_id}", + axum::routing::delete(remove_parent), + ) + .route("/configs/{id}/children", axum::routing::get(list_children)) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/configs.rs b/apps/nxmesh-master/src/routes/api/proxy/configs.rs new file mode 100644 index 0000000..03e5e2e --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/configs.rs @@ -0,0 +1,150 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::{ + ProxyServiceTrait, + types::{CreateProxyConfigParams, ProxyConfigSummary, UpdateProxyConfigParams}, +}; + +#[derive(Serialize)] +pub(crate) struct ProxyConfigResponse { + pub id: Uuid, + pub name: String, + pub description: Option, + pub is_template: bool, + pub created_at: String, + pub updated_at: String, +} + +impl From for ProxyConfigResponse { + fn from(s: ProxyConfigSummary) -> Self { + Self { + id: s.id, + name: s.name, + description: s.description, + is_template: s.is_template, + created_at: s.created_at.to_rfc3339(), + updated_at: s.updated_at.to_rfc3339(), + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateConfigRequest { + pub name: String, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub is_template: bool, +} + +#[derive(Deserialize)] +pub(crate) struct UpdateConfigRequest { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub is_template: Option, +} + +#[derive(Serialize)] +pub(crate) struct ListConfigsResponse { + pub configs: Vec, +} + +async fn list_configs( + State(svc): State>, +) -> Result, AppError> { + let configs = svc.list_configs().await?; + Ok(Json(ListConfigsResponse { + configs: configs.into_iter().map(Into::into).collect(), + })) +} + +async fn create_config( + State(svc): State>, + Json(body): Json, +) -> Result { + if body.name.trim().is_empty() { + return Err(AppError::BadRequest("name is required".to_string())); + } + let config = svc + .create_config(CreateProxyConfigParams { + name: body.name, + description: body.description, + is_template: body.is_template, + }) + .await?; + Ok((StatusCode::CREATED, Json(ProxyConfigResponse::from(config)))) +} + +async fn get_config( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let config = svc.get_proxy_config(id).await?; + Ok(Json(serde_json::to_value(config.id).unwrap_or_default())) +} + +async fn update_config( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let config = svc + .update_config( + id, + UpdateProxyConfigParams { + name: body.name, + description: body.description, + is_template: body.is_template, + }, + ) + .await?; + Ok(Json(config.into())) +} + +async fn delete_config( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete_config(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +async fn render_config( + State(svc): State>, + Path(id): Path, +) -> Result { + let output = svc.render_config(id).await?; + Ok(output) +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/configs", + axum::routing::get(list_configs).post(create_config), + ) + .route( + "/configs/{id}", + axum::routing::get(get_config) + .put(update_config) + .delete(delete_config), + ) + .route("/configs/{id}/render", axum::routing::get(render_config)) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs b/apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs new file mode 100644 index 0000000..5eadd6e --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs @@ -0,0 +1,141 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::limit_rule::{ + CreateLimitRuleParams, LimitRuleService, UpdateLimitRuleParams, +}; +use crate::service::proxy::types::LimitRuleConfig; + +#[derive(Serialize)] +pub(crate) struct LimitRuleResponse { + pub id: Uuid, + pub location_id: Uuid, + pub zone_id: Uuid, + pub burst: Option, + pub nodelay: Option, + pub override_of_id: Option, +} + +impl From for LimitRuleResponse { + fn from(c: LimitRuleConfig) -> Self { + Self { + id: c.id, + location_id: c.location_id, + zone_id: c.zone_id, + burst: c.burst, + nodelay: c.nodelay, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLimitRuleRequest { + pub location_id: Uuid, + pub zone_id: Uuid, + pub burst: Option, + pub nodelay: Option, + pub override_of_id: Option, +} + +impl From for CreateLimitRuleParams { + fn from(r: CreateLimitRuleRequest) -> Self { + Self { + location_id: r.location_id, + zone_id: r.zone_id, + burst: r.burst, + nodelay: r.nodelay, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLimitRuleRequest { + pub location_id: Option, + pub zone_id: Option, + pub burst: Option>, + pub nodelay: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateLimitRuleParams { + fn from(r: UpdateLimitRuleRequest) -> Self { + Self { + location_id: r.location_id, + zone_id: r.zone_id, + burst: r.burst, + nodelay: r.nodelay, + override_of_id: r.override_of_id, + } + } +} + +async fn list_limit_rules_by_location( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_location(location_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn create_limit_rule( + State(svc): State>, + Json(body): Json, +) -> Result { + let rule = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(LimitRuleResponse::from(rule)))) +} + +async fn get_limit_rule( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let rule = svc.get(id).await?; + Ok(Json(rule.into())) +} + +async fn update_limit_rule( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let rule = svc.update(id, body.into()).await?; + Ok(Json(rule.into())) +} + +async fn delete_limit_rule( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/locations/{location_id}/limit-rules", + axum::routing::get(list_limit_rules_by_location), + ) + .route("/limit-rules", axum::routing::post(create_limit_rule)) + .route( + "/limit-rules/{id}", + axum::routing::get(get_limit_rule) + .put(update_limit_rule) + .delete(delete_limit_rule), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs b/apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs new file mode 100644 index 0000000..cf22859 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::limit_zone::{ + CreateLimitZoneParams, LimitZoneService, UpdateLimitZoneParams, +}; +use crate::service::proxy::types::LimitZoneConfig; + +#[derive(Serialize)] +pub(crate) struct LimitZoneResponse { + pub id: Uuid, + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +impl From for LimitZoneResponse { + fn from(c: LimitZoneConfig) -> Self { + Self { + id: c.id, + name: c.name, + key: c.key, + rate: c.rate, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLimitZoneRequest { + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +impl From for CreateLimitZoneParams { + fn from(r: CreateLimitZoneRequest) -> Self { + Self { + name: r.name, + key: r.key, + rate: r.rate, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLimitZoneRequest { + pub name: Option, + pub key: Option, + pub rate: Option, + pub override_of_id: Option>, +} + +impl From for UpdateLimitZoneParams { + fn from(r: UpdateLimitZoneRequest) -> Self { + Self { + name: r.name, + key: r.key, + rate: r.rate, + override_of_id: r.override_of_id, + } + } +} + +async fn list_limit_zones( + State(svc): State>, +) -> Result>, AppError> { + let zones = svc.list().await?; + Ok(Json(zones.into_iter().map(Into::into).collect())) +} + +async fn create_limit_zone( + State(svc): State>, + Json(body): Json, +) -> Result { + let zone = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(LimitZoneResponse::from(zone)))) +} + +async fn get_limit_zone( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let zone = svc.get(id).await?; + Ok(Json(zone.into())) +} + +async fn update_limit_zone( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let zone = svc.update(id, body.into()).await?; + Ok(Json(zone.into())) +} + +async fn delete_limit_zone( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/limit-zones", + axum::routing::get(list_limit_zones).post(create_limit_zone), + ) + .route( + "/limit-zones/{id}", + axum::routing::get(get_limit_zone) + .put(update_limit_zone) + .delete(delete_limit_zone), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/locations.rs b/apps/nxmesh-master/src/routes/api/proxy/locations.rs new file mode 100644 index 0000000..6ed25f4 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/locations.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::location_block::{ + CreateLocationBlockParams, LocationBlockService, UpdateLocationBlockParams, +}; +use crate::service::proxy::types::LocationBlockConfig; + +#[derive(Serialize)] +pub(crate) struct LocationBlockResponse { + pub id: Uuid, + pub server_id: Uuid, + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for LocationBlockResponse { + fn from(c: LocationBlockConfig) -> Self { + Self { + id: c.id, + server_id: c.server_id, + path_pattern: c.path_pattern, + proxy_pass_upstream_id: c.proxy_pass_upstream_id, + metadata: c.metadata, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLocationBlockRequest { + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for CreateLocationBlockParams { + fn from(r: CreateLocationBlockRequest) -> Self { + Self { + server_id: Uuid::nil(), + path_pattern: r.path_pattern, + proxy_pass_upstream_id: r.proxy_pass_upstream_id, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLocationBlockRequest { + pub server_id: Option, + pub path_pattern: Option, + pub proxy_pass_upstream_id: Option>, + pub metadata: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateLocationBlockParams { + fn from(r: UpdateLocationBlockRequest) -> Self { + Self { + server_id: r.server_id, + path_pattern: r.path_pattern, + proxy_pass_upstream_id: r.proxy_pass_upstream_id, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +async fn list_locations( + State(svc): State>, + Path(server_id): Path, +) -> Result>, AppError> { + let locations = svc.list_by_server(server_id).await?; + Ok(Json(locations.into_iter().map(Into::into).collect())) +} + +async fn create_location( + State(svc): State>, + Path(server_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateLocationBlockParams::from(body); + params.server_id = server_id; + let location = svc.create(params).await?; + Ok(( + StatusCode::CREATED, + Json(LocationBlockResponse::from(location)), + )) +} + +async fn get_location( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let location = svc.get(id).await?; + Ok(Json(location.into())) +} + +async fn update_location( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let location = svc.update(id, body.into()).await?; + Ok(Json(location.into())) +} + +async fn delete_location( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/server-blocks/{server_id}/locations", + axum::routing::get(list_locations).post(create_location), + ) + .route( + "/locations/{id}", + axum::routing::get(get_location) + .put(update_location) + .delete(delete_location), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/log_settings.rs b/apps/nxmesh-master/src/routes/api/proxy/log_settings.rs new file mode 100644 index 0000000..18285d2 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/log_settings.rs @@ -0,0 +1,140 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::log_setting::{ + CreateLogSettingParams, LogSettingService, UpdateLogSettingParams, +}; +use crate::service::proxy::types::LogSettingConfig; + +#[derive(Serialize)] +pub(crate) struct LogSettingResponse { + pub id: Uuid, + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +impl From for LogSettingResponse { + fn from(c: LogSettingConfig) -> Self { + Self { + id: c.id, + access_log_path: c.access_log_path, + error_log_path: c.error_log_path, + log_level: c.log_level, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLogSettingRequest { + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +impl From for CreateLogSettingParams { + fn from(r: CreateLogSettingRequest) -> Self { + Self { + server_id: Uuid::nil(), + access_log_path: r.access_log_path, + error_log_path: r.error_log_path, + log_level: r.log_level, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLogSettingRequest { + pub server_id: Option, + pub access_log_path: Option>, + pub error_log_path: Option>, + pub log_level: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateLogSettingParams { + fn from(r: UpdateLogSettingRequest) -> Self { + Self { + server_id: r.server_id, + access_log_path: r.access_log_path, + error_log_path: r.error_log_path, + log_level: r.log_level, + override_of_id: r.override_of_id, + } + } +} + +async fn list_log_settings( + State(svc): State>, + Path(server_id): Path, +) -> Result>, AppError> { + let settings = svc.list_by_server(server_id).await?; + Ok(Json(settings.into_iter().map(Into::into).collect())) +} + +async fn create_log_setting( + State(svc): State>, + Path(server_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateLogSettingParams::from(body); + params.server_id = server_id; + let setting = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(LogSettingResponse::from(setting)))) +} + +async fn get_log_setting( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let setting = svc.get(id).await?; + Ok(Json(setting.into())) +} + +async fn update_log_setting( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let setting = svc.update(id, body.into()).await?; + Ok(Json(setting.into())) +} + +async fn delete_log_setting( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/server-blocks/{server_id}/log-settings", + axum::routing::get(list_log_settings).post(create_log_setting), + ) + .route( + "/log-settings/{id}", + axum::routing::get(get_log_setting) + .put(update_log_setting) + .delete(delete_log_setting), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/mod.rs b/apps/nxmesh-master/src/routes/api/proxy/mod.rs new file mode 100644 index 0000000..3d83237 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/mod.rs @@ -0,0 +1,37 @@ +use crate::routes::api::ApiRouter; + +pub(crate) mod access_rules; +pub(crate) mod agents; +pub(crate) mod cache_zones; +pub(crate) mod config_inheritance; +pub(crate) mod configs; +pub(crate) mod limit_rules; +pub(crate) mod limit_zones; +pub(crate) mod locations; +pub(crate) mod log_settings; +pub(crate) mod proxy_settings; +pub(crate) mod rewrite_rules; +pub(crate) mod server_blocks; +pub(crate) mod ssl_certificates; +pub(crate) mod upstreams; + +#[cfg(test)] +pub(crate) mod test_builder; + +pub async fn get_router() -> ApiRouter { + ApiRouter::new() + .merge(configs::routes()) + .merge(agents::routes()) + .merge(config_inheritance::routes()) + .merge(server_blocks::routes()) + .merge(upstreams::routes()) + .merge(locations::routes()) + .merge(access_rules::routes()) + .merge(cache_zones::routes()) + .merge(limit_rules::routes()) + .merge(limit_zones::routes()) + .merge(log_settings::routes()) + .merge(proxy_settings::routes()) + .merge(rewrite_rules::routes()) + .merge(ssl_certificates::routes()) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs b/apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs new file mode 100644 index 0000000..a8ac421 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs @@ -0,0 +1,157 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::proxy_setting::{ + CreateProxySettingParams, ProxySettingService, UpdateProxySettingParams, +}; +use crate::service::proxy::types::ProxySettingConfig; + +#[derive(Serialize)] +pub(crate) struct ProxySettingResponse { + pub id: Uuid, + pub location_id: Uuid, + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +impl From for ProxySettingResponse { + fn from(c: ProxySettingConfig) -> Self { + Self { + id: c.id, + location_id: c.location_id, + read_timeout: c.read_timeout, + connect_timeout: c.connect_timeout, + buffer_size: c.buffer_size, + cache_enabled: c.cache_enabled, + cache_zone: c.cache_zone, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateProxySettingRequest { + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +impl From for CreateProxySettingParams { + fn from(r: CreateProxySettingRequest) -> Self { + Self { + location_id: Uuid::nil(), + read_timeout: r.read_timeout, + connect_timeout: r.connect_timeout, + buffer_size: r.buffer_size, + cache_enabled: r.cache_enabled, + cache_zone: r.cache_zone, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateProxySettingRequest { + pub location_id: Option, + pub read_timeout: Option>, + pub connect_timeout: Option>, + pub buffer_size: Option>, + pub cache_enabled: Option>, + pub cache_zone: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateProxySettingParams { + fn from(r: UpdateProxySettingRequest) -> Self { + Self { + location_id: r.location_id, + read_timeout: r.read_timeout, + connect_timeout: r.connect_timeout, + buffer_size: r.buffer_size, + cache_enabled: r.cache_enabled, + cache_zone: r.cache_zone, + override_of_id: r.override_of_id, + } + } +} + +async fn list_proxy_settings( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let settings = svc.list_by_location(location_id).await?; + Ok(Json(settings.into_iter().map(Into::into).collect())) +} + +async fn create_proxy_setting( + State(svc): State>, + Path(location_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateProxySettingParams::from(body); + params.location_id = location_id; + let setting = svc.create(params).await?; + Ok(( + StatusCode::CREATED, + Json(ProxySettingResponse::from(setting)), + )) +} + +async fn get_proxy_setting( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let setting = svc.get(id).await?; + Ok(Json(setting.into())) +} + +async fn update_proxy_setting( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let setting = svc.update(id, body.into()).await?; + Ok(Json(setting.into())) +} + +async fn delete_proxy_setting( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/locations/{location_id}/proxy-settings", + axum::routing::get(list_proxy_settings).post(create_proxy_setting), + ) + .route( + "/proxy-settings/{id}", + axum::routing::get(get_proxy_setting) + .put(update_proxy_setting) + .delete(delete_proxy_setting), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs b/apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs new file mode 100644 index 0000000..5134d6d --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::rewrite_rule::{ + CreateRewriteRuleParams, RewriteRuleService, UpdateRewriteRuleParams, +}; +use crate::service::proxy::types::RewriteRuleConfig; + +#[derive(Serialize)] +pub(crate) struct RewriteRuleResponse { + pub id: Uuid, + pub location_id: Uuid, + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for RewriteRuleResponse { + fn from(c: RewriteRuleConfig) -> Self { + Self { + id: c.id, + location_id: c.location_id, + pattern: c.pattern, + replacement: c.replacement, + flag: c.flag, + priority: c.priority, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateRewriteRuleRequest { + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for CreateRewriteRuleParams { + fn from(r: CreateRewriteRuleRequest) -> Self { + Self { + location_id: Uuid::nil(), + pattern: r.pattern, + replacement: r.replacement, + flag: r.flag, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateRewriteRuleRequest { + pub location_id: Option, + pub pattern: Option, + pub replacement: Option, + pub flag: Option>, + pub priority: Option, + pub override_of_id: Option>, +} + +impl From for UpdateRewriteRuleParams { + fn from(r: UpdateRewriteRuleRequest) -> Self { + Self { + location_id: r.location_id, + pattern: r.pattern, + replacement: r.replacement, + flag: r.flag, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +async fn list_rewrite_rules( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_location(location_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn create_rewrite_rule( + State(svc): State>, + Path(location_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateRewriteRuleParams::from(body); + params.location_id = location_id; + let rule = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(RewriteRuleResponse::from(rule)))) +} + +async fn get_rewrite_rule( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let rule = svc.get(id).await?; + Ok(Json(rule.into())) +} + +async fn update_rewrite_rule( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let rule = svc.update(id, body.into()).await?; + Ok(Json(rule.into())) +} + +async fn delete_rewrite_rule( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/locations/{location_id}/rewrite-rules", + axum::routing::get(list_rewrite_rules).post(create_rewrite_rule), + ) + .route( + "/rewrite-rules/{id}", + axum::routing::get(get_rewrite_rule) + .put(update_rewrite_rule) + .delete(delete_rewrite_rule), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs b/apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs new file mode 100644 index 0000000..9e46e07 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs @@ -0,0 +1,142 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::server_block::{ + CreateServerBlockParams, ServerBlockService, UpdateServerBlockParams, +}; +use crate::service::proxy::types::ServerBlockConfig; + +#[derive(Serialize)] +pub(crate) struct ServerBlockResponse { + pub id: Uuid, + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub override_of_id: Option, +} + +impl From for ServerBlockResponse { + fn from(c: ServerBlockConfig) -> Self { + Self { + id: c.id, + server_name: c.server_name, + listen_port: c.listen_port, + ssl_enabled: c.ssl_enabled, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateServerBlockRequest { + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub ssl_cert_id: Option, + pub override_of_id: Option, +} + +impl From for CreateServerBlockParams { + fn from(r: CreateServerBlockRequest) -> Self { + Self { + config_id: Uuid::nil(), + server_name: r.server_name, + listen_port: r.listen_port, + ssl_enabled: r.ssl_enabled, + ssl_cert_id: r.ssl_cert_id, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateServerBlockRequest { + pub server_name: Option>>, + pub listen_port: Option, + pub ssl_enabled: Option>, + pub ssl_cert_id: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateServerBlockParams { + fn from(r: UpdateServerBlockRequest) -> Self { + Self { + server_name: r.server_name, + listen_port: r.listen_port, + ssl_enabled: r.ssl_enabled, + ssl_cert_id: r.ssl_cert_id, + override_of_id: r.override_of_id, + } + } +} + +async fn list_server_blocks( + State(svc): State>, + Path(config_id): Path, +) -> Result>, AppError> { + let blocks = svc.list_by_config(config_id).await?; + Ok(Json(blocks.into_iter().map(Into::into).collect())) +} + +async fn create_server_block( + State(svc): State>, + Path(config_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateServerBlockParams::from(body); + params.config_id = config_id; + let block = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(ServerBlockResponse::from(block)))) +} + +async fn get_server_block( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let block = svc.get(id).await?; + Ok(Json(block.into())) +} + +async fn update_server_block( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let block = svc.update(id, body.into()).await?; + Ok(Json(block.into())) +} + +async fn delete_server_block( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/configs/{config_id}/server-blocks", + axum::routing::get(list_server_blocks).post(create_server_block), + ) + .route( + "/server-blocks/{id}", + axum::routing::get(get_server_block) + .put(update_server_block) + .delete(delete_server_block), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs b/apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs new file mode 100644 index 0000000..aa7d2c9 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs @@ -0,0 +1,136 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::ssl_certificate::{ + CreateSslCertificateParams, SslCertificateService, UpdateSslCertificateParams, +}; +use crate::service::proxy::types::SslCertificateConfig; + +#[derive(Serialize)] +pub(crate) struct SslCertificateResponse { + pub id: Uuid, + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: String, +} + +impl From for SslCertificateResponse { + fn from(c: SslCertificateConfig) -> Self { + Self { + id: c.id, + name: c.name, + cert_path: c.cert_path, + key_path: c.key_path, + expiry_date: c.expiry_date.to_rfc3339(), + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateSslCertificateRequest { + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: chrono::DateTime, +} + +impl From for CreateSslCertificateParams { + fn from(r: CreateSslCertificateRequest) -> Self { + Self { + name: r.name, + cert_path: r.cert_path, + key_path: r.key_path, + expiry_date: r.expiry_date, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateSslCertificateRequest { + pub name: Option, + pub cert_path: Option, + pub key_path: Option, + pub expiry_date: Option>, +} + +impl From for UpdateSslCertificateParams { + fn from(r: UpdateSslCertificateRequest) -> Self { + Self { + name: r.name, + cert_path: r.cert_path, + key_path: r.key_path, + expiry_date: r.expiry_date, + } + } +} + +async fn list_ssl_certificates( + State(svc): State>, +) -> Result>, AppError> { + let certs = svc.list().await?; + Ok(Json(certs.into_iter().map(Into::into).collect())) +} + +async fn create_ssl_certificate( + State(svc): State>, + Json(body): Json, +) -> Result { + let cert = svc.create(body.into()).await?; + Ok(( + StatusCode::CREATED, + Json(SslCertificateResponse::from(cert)), + )) +} + +async fn get_ssl_certificate( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let cert = svc.get(id).await?; + Ok(Json(cert.into())) +} + +async fn update_ssl_certificate( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let cert = svc.update(id, body.into()).await?; + Ok(Json(cert.into())) +} + +async fn delete_ssl_certificate( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/ssl-certificates", + axum::routing::get(list_ssl_certificates).post(create_ssl_certificate), + ) + .route( + "/ssl-certificates/{id}", + axum::routing::get(get_ssl_certificate) + .put(update_ssl_certificate) + .delete(delete_ssl_certificate), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/test_builder.rs b/apps/nxmesh-master/src/routes/api/proxy/test_builder.rs new file mode 100644 index 0000000..c0c275f --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/test_builder.rs @@ -0,0 +1,178 @@ +use std::sync::Arc; + +use axum_test::TestServer; + +use crate::routes::api::{ApiState, LocalApiState}; +use crate::service::proxy::*; +use crate::service::proxy::{ + access_rule::MockAccessRuleService, cache_zone::MockCacheZoneService, + config_inheritance::MockConfigInheritanceService, limit_rule::MockLimitRuleService, + limit_zone::MockLimitZoneService, location_block::MockLocationBlockService, + log_setting::MockLogSettingService, proxy_setting::MockProxySettingService, + rewrite_rule::MockRewriteRuleService, server_block::MockServerBlockService, + ssl_certificate::MockSslCertificateService, upstream::MockUpstreamService, +}; + +pub(crate) struct TestProxyApiBuilder { + proxy_service: Option, + server_block_service: Option, + upstream_service: Option, + location_block_service: Option, + access_rule_service: Option, + cache_zone_service: Option, + limit_rule_service: Option, + limit_zone_service: Option, + log_setting_service: Option, + proxy_setting_service: Option, + rewrite_rule_service: Option, + ssl_certificate_service: Option, + config_inheritance_service: Option, +} + +impl TestProxyApiBuilder { + pub fn new() -> Self { + Self { + proxy_service: None, + server_block_service: None, + upstream_service: None, + location_block_service: None, + access_rule_service: None, + cache_zone_service: None, + limit_rule_service: None, + limit_zone_service: None, + log_setting_service: None, + proxy_setting_service: None, + rewrite_rule_service: None, + ssl_certificate_service: None, + config_inheritance_service: None, + } + } + + pub fn with_proxy(mut self, mock: MockProxyServiceTrait) -> Self { + self.proxy_service = Some(mock); + self + } + + pub fn with_server_block(mut self, mock: MockServerBlockService) -> Self { + self.server_block_service = Some(mock); + self + } + + pub fn with_upstream(mut self, mock: MockUpstreamService) -> Self { + self.upstream_service = Some(mock); + self + } + + pub fn with_location_block(mut self, mock: MockLocationBlockService) -> Self { + self.location_block_service = Some(mock); + self + } + + pub fn with_access_rule(mut self, mock: MockAccessRuleService) -> Self { + self.access_rule_service = Some(mock); + self + } + + pub fn with_cache_zone(mut self, mock: MockCacheZoneService) -> Self { + self.cache_zone_service = Some(mock); + self + } + + pub fn with_limit_rule(mut self, mock: MockLimitRuleService) -> Self { + self.limit_rule_service = Some(mock); + self + } + + pub fn with_limit_zone(mut self, mock: MockLimitZoneService) -> Self { + self.limit_zone_service = Some(mock); + self + } + + pub fn with_log_setting(mut self, mock: MockLogSettingService) -> Self { + self.log_setting_service = Some(mock); + self + } + + pub fn with_proxy_setting(mut self, mock: MockProxySettingService) -> Self { + self.proxy_setting_service = Some(mock); + self + } + + pub fn with_rewrite_rule(mut self, mock: MockRewriteRuleService) -> Self { + self.rewrite_rule_service = Some(mock); + self + } + + pub fn with_ssl_certificate(mut self, mock: MockSslCertificateService) -> Self { + self.ssl_certificate_service = Some(mock); + self + } + + pub fn with_config_inheritance(mut self, mock: MockConfigInheritanceService) -> Self { + self.config_inheritance_service = Some(mock); + self + } + + pub async fn build(self) -> TestServer { + let state = ApiState { + proxy_service: Arc::new( + self.proxy_service + .unwrap_or_else(MockProxyServiceTrait::new), + ), + server_block_service: Arc::new( + self.server_block_service + .unwrap_or_else(MockServerBlockService::new), + ), + upstream_service: Arc::new( + self.upstream_service + .unwrap_or_else(MockUpstreamService::new), + ), + location_block_service: Arc::new( + self.location_block_service + .unwrap_or_else(MockLocationBlockService::new), + ), + access_rule_service: Arc::new( + self.access_rule_service + .unwrap_or_else(MockAccessRuleService::new), + ), + cache_zone_service: Arc::new( + self.cache_zone_service + .unwrap_or_else(MockCacheZoneService::new), + ), + limit_rule_service: Arc::new( + self.limit_rule_service + .unwrap_or_else(MockLimitRuleService::new), + ), + limit_zone_service: Arc::new( + self.limit_zone_service + .unwrap_or_else(MockLimitZoneService::new), + ), + log_setting_service: Arc::new( + self.log_setting_service + .unwrap_or_else(MockLogSettingService::new), + ), + proxy_setting_service: Arc::new( + self.proxy_setting_service + .unwrap_or_else(MockProxySettingService::new), + ), + rewrite_rule_service: Arc::new( + self.rewrite_rule_service + .unwrap_or_else(MockRewriteRuleService::new), + ), + ssl_certificate_service: Arc::new( + self.ssl_certificate_service + .unwrap_or_else(MockSslCertificateService::new), + ), + config_inheritance_service: Arc::new( + self.config_inheritance_service + .unwrap_or_else(MockConfigInheritanceService::new), + ), + // Keep agent_service for ApiState completeness; not used by proxy routes + agent_service: Arc::new(crate::service::agent::MockAgentService::new()), + }; + let app = super::get_router() + .await + .with_state(LocalApiState(Arc::new(state))); + TestServer::new(app) + } +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/upstreams.rs b/apps/nxmesh-master/src/routes/api/proxy/upstreams.rs new file mode 100644 index 0000000..1f7d8e4 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/upstreams.rs @@ -0,0 +1,144 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{ApiRouter, AppError}; +use crate::service::proxy::types::UpstreamConfig; +use crate::service::proxy::upstream::{ + CreateUpstreamParams, UpdateUpstreamParams, UpstreamService, +}; + +#[derive(Serialize)] +pub(crate) struct UpstreamResponse { + pub id: Uuid, + pub name: String, + pub target_host: String, + pub target_port: i32, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for UpstreamResponse { + fn from(c: UpstreamConfig) -> Self { + Self { + id: c.id, + name: c.name, + target_host: c.target_host, + target_port: c.target_port, + metadata: c.metadata, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateUpstreamRequest { + pub name: String, + pub target_host: String, + pub target_port: i32, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for CreateUpstreamParams { + fn from(r: CreateUpstreamRequest) -> Self { + Self { + config_id: Uuid::nil(), + name: r.name, + target_host: r.target_host, + target_port: r.target_port, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateUpstreamRequest { + pub name: Option, + pub target_host: Option, + pub target_port: Option, + pub metadata: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateUpstreamParams { + fn from(r: UpdateUpstreamRequest) -> Self { + Self { + name: r.name, + target_host: r.target_host, + target_port: r.target_port, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +async fn list_upstreams( + State(svc): State>, + Path(config_id): Path, +) -> Result>, AppError> { + let upstreams = svc.list_by_config(config_id).await?; + Ok(Json(upstreams.into_iter().map(Into::into).collect())) +} + +async fn create_upstream( + State(svc): State>, + Path(config_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateUpstreamParams::from(body); + params.config_id = config_id; + let upstream = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(UpstreamResponse::from(upstream)))) +} + +async fn get_upstream( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let upstream = svc.get(id).await?; + Ok(Json(upstream.into())) +} + +async fn update_upstream( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let upstream = svc.update(id, body.into()).await?; + Ok(Json(upstream.into())) +} + +async fn delete_upstream( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/configs/{config_id}/upstreams", + axum::routing::get(list_upstreams).post(create_upstream), + ) + .route( + "/upstreams/{id}", + axum::routing::get(get_upstream) + .put(update_upstream) + .delete(delete_upstream), + ) +} diff --git a/apps/nxmesh-master/src/routes/mod.rs b/apps/nxmesh-master/src/routes/mod.rs index 0583ef2..b5361f2 100644 --- a/apps/nxmesh-master/src/routes/mod.rs +++ b/apps/nxmesh-master/src/routes/mod.rs @@ -1,21 +1,46 @@ +use std::sync::Arc; + use axum::Router; +pub mod api; mod frontend; -pub async fn get_root_router() -> Router { +pub async fn get_root_router(api_state: impl Into>) -> Router { Router::new() .merge(frontend::get_router().await) + .nest("/api", api::get_router(api_state.into()).await) .fallback(frontend::get_fallback_handler().await) } #[cfg(test)] mod tests { + use crate::service::agent::MockAgentService; + use super::*; use axum_test::TestServer; #[tokio::test] async fn test_should_return_index_html_for_root_path() { - let router = get_root_router().await; + use crate::service::proxy::*; + let state = Arc::new(api::ApiState { + agent_service: Arc::new(MockAgentService::new()), + proxy_service: Arc::new(MockProxyServiceTrait::new()), + server_block_service: Arc::new(server_block::MockServerBlockService::new()), + upstream_service: Arc::new(upstream::MockUpstreamService::new()), + location_block_service: Arc::new(location_block::MockLocationBlockService::new()), + access_rule_service: Arc::new(access_rule::MockAccessRuleService::new()), + cache_zone_service: Arc::new(cache_zone::MockCacheZoneService::new()), + limit_rule_service: Arc::new(limit_rule::MockLimitRuleService::new()), + limit_zone_service: Arc::new(limit_zone::MockLimitZoneService::new()), + log_setting_service: Arc::new(log_setting::MockLogSettingService::new()), + proxy_setting_service: Arc::new(proxy_setting::MockProxySettingService::new()), + rewrite_rule_service: Arc::new(rewrite_rule::MockRewriteRuleService::new()), + ssl_certificate_service: Arc::new(ssl_certificate::MockSslCertificateService::new()), + config_inheritance_service: Arc::new( + config_inheritance::MockConfigInheritanceService::new(), + ), + }); + let router = get_root_router(state).await; let server = TestServer::new(router); let response = server.get("/").await; assert_eq!(response.status_code(), 200); @@ -23,7 +48,26 @@ mod tests { #[tokio::test] async fn test_should_return_index_html_for_nonexistent_path() { - let router = get_root_router().await; + use crate::service::proxy::*; + let state = Arc::new(api::ApiState { + agent_service: Arc::new(MockAgentService::new()), + proxy_service: Arc::new(MockProxyServiceTrait::new()), + server_block_service: Arc::new(server_block::MockServerBlockService::new()), + upstream_service: Arc::new(upstream::MockUpstreamService::new()), + location_block_service: Arc::new(location_block::MockLocationBlockService::new()), + access_rule_service: Arc::new(access_rule::MockAccessRuleService::new()), + cache_zone_service: Arc::new(cache_zone::MockCacheZoneService::new()), + limit_rule_service: Arc::new(limit_rule::MockLimitRuleService::new()), + limit_zone_service: Arc::new(limit_zone::MockLimitZoneService::new()), + log_setting_service: Arc::new(log_setting::MockLogSettingService::new()), + proxy_setting_service: Arc::new(proxy_setting::MockProxySettingService::new()), + rewrite_rule_service: Arc::new(rewrite_rule::MockRewriteRuleService::new()), + ssl_certificate_service: Arc::new(ssl_certificate::MockSslCertificateService::new()), + config_inheritance_service: Arc::new( + config_inheritance::MockConfigInheritanceService::new(), + ), + }); + let router = get_root_router(state).await; let server = TestServer::new(router); let fallback_response = server.get("/nonexistent").await; assert_eq!(fallback_response.status_code(), 200); diff --git a/apps/nxmesh-master/src/service/agent/mod.rs b/apps/nxmesh-master/src/service/agent/mod.rs index e8f329e..eb4e489 100644 --- a/apps/nxmesh-master/src/service/agent/mod.rs +++ b/apps/nxmesh-master/src/service/agent/mod.rs @@ -1,26 +1,120 @@ -use nxmesh_proto::{AgentMessage, MasterMessage, agent_service_server::AgentService}; +use chrono::Utc; +use nxmesh_proto::{ + AgentMessage, MasterMessage, agent_service_server::AgentService as GrpcAgentService, +}; +use sea_orm::DatabaseConnection; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tracing::{error, info}; +use uuid::Uuid; -pub mod repo; +mod repo; +pub mod types; + +pub use types::{AgentRecord, CreateAgentRecord, State, UpdateAgentRecord}; + +use crate::service::error::RepoError; +use repo::{AgentRepo, AgentRepoImpl}; + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait AgentService: Send + Sync + 'static { + async fn list(&self) -> Result, RepoError>; + async fn get(&self, id: Uuid) -> Result, RepoError>; + async fn create(&self, rec: &CreateAgentRecord) -> Result; + async fn update( + &self, + id: Uuid, + rec: &UpdateAgentRecord, + ) -> Result, RepoError>; + async fn delete(&self, id: Uuid) -> Result; +} + +pub struct AgentServiceImpl { + repo: Box, +} + +impl AgentServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { + repo: Box::new(AgentRepoImpl::new(db)), + } + } +} + +#[async_trait::async_trait] +impl AgentService for AgentServiceImpl { + async fn list(&self) -> Result, RepoError> { + self.repo.list().await + } + + async fn get(&self, id: Uuid) -> Result, RepoError> { + self.repo.get(id).await + } + + async fn create(&self, rec: &CreateAgentRecord) -> Result { + self.repo.create(rec).await + } + + async fn update( + &self, + id: Uuid, + rec: &UpdateAgentRecord, + ) -> Result, RepoError> { + self.repo.update(id, rec).await + } + + async fn delete(&self, id: Uuid) -> Result { + self.repo.delete(id).await + } +} #[derive(Debug, Default)] pub struct AgentServerService {} #[async_trait::async_trait] -impl AgentService for AgentServerService { - #[doc = " Server streaming response type for the Stream method."] - type StreamStream = tonic::codec::Streaming; +impl GrpcAgentService for AgentServerService { + type StreamStream = + tokio_stream::wrappers::ReceiverStream>; - #[doc = " Stream establishes a persistent connection for real-time communication"] - #[allow( - mismatched_lifetime_syntaxes, - clippy::type_complexity, - clippy::type_repetition_in_bounds - )] async fn stream( &self, request: tonic::Request>, ) -> Result, tonic::Status> { - todo!() + let mut inbound = request.into_inner(); + + let (tx, rx) = mpsc::channel::>(32); + let outbound = ReceiverStream::new(rx); + + tokio::spawn(async move { + loop { + match inbound.message().await { + Ok(Some(msg)) => { + info!("Received AgentMessage: {:?}", msg); + + let ack = MasterMessage { + timestamp: Utc::now().timestamp_millis(), + message_id: Uuid::new_v4().to_string(), + payload: None, + }; + if let Err(e) = tx.send(Ok(ack)).await { + error!("Failed to send MasterMessage ack: {:?}", e); + return; + } + } + Ok(None) => { + info!("Agent closed the outbound stream"); + return; + } + Err(e) => { + error!("Error receiving AgentMessage: {:?}", e); + return; + } + } + } + }); + + Ok(tonic::Response::new(outbound)) } async fn connection_test( diff --git a/apps/nxmesh-master/src/service/agent/repo.rs b/apps/nxmesh-master/src/service/agent/repo.rs new file mode 100644 index 0000000..e433a16 --- /dev/null +++ b/apps/nxmesh-master/src/service/agent/repo.rs @@ -0,0 +1,146 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait}; +use uuid::Uuid; + +use super::types::{AgentRecord, CreateAgentRecord, State, UpdateAgentRecord}; +use crate::{ + db::entities::agents::{ActiveModel as AgentActiveModel, Entity as Agent}, + service::error::RepoError, +}; + +#[async_trait::async_trait] +pub(crate) trait AgentRepo: Send + Sync + 'static { + async fn list(&self) -> Result, RepoError>; + async fn get(&self, id: Uuid) -> Result, RepoError>; + async fn create(&self, rec: &CreateAgentRecord) -> Result; + async fn update( + &self, + id: Uuid, + rec: &UpdateAgentRecord, + ) -> Result, RepoError>; + async fn delete(&self, id: Uuid) -> Result; +} + +pub(crate) struct AgentRepoImpl { + db: DatabaseConnection, +} + +impl AgentRepoImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl AgentRepo for AgentRepoImpl { + async fn list(&self) -> Result, RepoError> { + let agents = Agent::find().all(&self.db).await?; + Ok(agents + .into_iter() + .map(|m| AgentRecord { + id: m.id, + name: m.name, + ip_address: m.ip_address, + state: m.state.into(), + deployment_mode: m.deployment_mode, + last_seen_at: m.last_seen_at.map(|dt| dt.to_string()), + labels: m.labels, + created_at: m.created_at.to_string(), + updated_at: m.updated_at.to_string(), + }) + .collect()) + } + + async fn get(&self, id: Uuid) -> Result, RepoError> { + let agent = Agent::find_by_id(id).one(&self.db).await?; + Ok(agent.map(|m| AgentRecord { + id: m.id, + name: m.name, + ip_address: m.ip_address, + state: m.state.into(), + deployment_mode: m.deployment_mode, + last_seen_at: m.last_seen_at.map(|dt| dt.to_string()), + labels: m.labels, + created_at: m.created_at.to_string(), + updated_at: m.updated_at.to_string(), + })) + } + + async fn create(&self, rec: &CreateAgentRecord) -> Result { + let new_agent = AgentActiveModel { + id: Set(Uuid::new_v4()), + name: Set(rec.name.clone()), + ip_address: Set(rec.ip_address.clone()), + state: Set(State::Active.into()), + deployment_mode: Set(None), + last_seen_at: Set(None), + labels: Set(None), + created_at: Set(chrono::Utc::now().into()), + updated_at: Set(chrono::Utc::now().into()), + ..Default::default() + }; + + let agent = new_agent.insert(&self.db).await?; + + Ok(AgentRecord { + id: agent.id, + name: agent.name, + ip_address: agent.ip_address, + state: agent.state.into(), + deployment_mode: agent.deployment_mode, + last_seen_at: agent.last_seen_at.map(|dt| dt.to_string()), + labels: agent.labels, + created_at: agent.created_at.to_string(), + updated_at: agent.updated_at.to_string(), + }) + } + + async fn update( + &self, + id: Uuid, + rec: &UpdateAgentRecord, + ) -> Result, RepoError> { + let existing = match Agent::find_by_id(id).one(&self.db).await? { + Some(agent) => agent, + None => return Ok(None), + }; + + let mut agent: AgentActiveModel = AgentActiveModel::from(existing); + + if let Some(name) = &rec.name { + agent.name = Set(name.clone()); + } + if let Some(ip_address) = &rec.ip_address { + agent.ip_address = Set(Some(ip_address.clone())); + } + if let Some(state) = &rec.state { + agent.state = Set(String::from(*state)); + } + if let Some(deployment_mode) = &rec.deployment_mode { + agent.deployment_mode = Set(Some(deployment_mode.clone())); + } + if let Some(labels) = &rec.labels { + agent.labels = Set(Some(labels.clone())); + } + + agent.updated_at = Set(chrono::Utc::now().into()); + + let updated = agent.update(&self.db).await?; + + Ok(Some(AgentRecord { + id: updated.id, + name: updated.name, + ip_address: updated.ip_address, + state: updated.state.into(), + deployment_mode: updated.deployment_mode, + last_seen_at: updated.last_seen_at.map(|dt| dt.to_string()), + labels: updated.labels, + created_at: updated.created_at.to_string(), + updated_at: updated.updated_at.to_string(), + })) + } + + async fn delete(&self, id: Uuid) -> Result { + let result = Agent::delete_by_id(id).exec(&self.db).await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/agent/types.rs b/apps/nxmesh-master/src/service/agent/types.rs new file mode 100644 index 0000000..c7507e3 --- /dev/null +++ b/apps/nxmesh-master/src/service/agent/types.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum State { + Active, + Inactive, + Unreachable, + Unknown, + Disabled, +} + +impl From for String { + fn from(state: State) -> Self { + match state { + State::Active => "active".to_string(), + State::Inactive => "inactive".to_string(), + State::Unreachable => "unreachable".to_string(), + State::Unknown => "unknown".to_string(), + State::Disabled => "disabled".to_string(), + } + } +} + +impl From for State { + fn from(s: String) -> Self { + match s.as_str() { + "active" => State::Active, + "inactive" => State::Inactive, + "unreachable" => State::Unreachable, + "unknown" => State::Unknown, + "disabled" => State::Disabled, + _ => State::Inactive, + } + } +} + +pub struct AgentRecord { + pub id: uuid::Uuid, + pub name: String, + pub ip_address: Option, + pub state: State, + pub deployment_mode: Option, + pub last_seen_at: Option, + pub labels: Option, + pub created_at: String, + pub updated_at: String, +} + +pub struct CreateAgentRecord { + pub name: String, + pub ip_address: Option, +} + +pub struct UpdateAgentRecord { + pub name: Option, + pub ip_address: Option, + pub state: Option, + pub deployment_mode: Option, + pub labels: Option, +} diff --git a/apps/nxmesh-master/src/service/certificate/mod.rs b/apps/nxmesh-master/src/service/certificate/mod.rs index 952ac5f..8d976c0 100644 --- a/apps/nxmesh-master/src/service/certificate/mod.rs +++ b/apps/nxmesh-master/src/service/certificate/mod.rs @@ -131,7 +131,7 @@ impl CertificateService for CertificateServiceImpl { .collect::>(), san_dns .into_iter() - .map(|dns| SanType::DnsName(dns)) + .map(SanType::DnsName) .collect::>(), ] .concat(); diff --git a/apps/nxmesh-master/src/service/error.rs b/apps/nxmesh-master/src/service/error.rs new file mode 100644 index 0000000..46bc4ab --- /dev/null +++ b/apps/nxmesh-master/src/service/error.rs @@ -0,0 +1,13 @@ +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum RepoError { + #[error("internal error: {0}")] + InternalError(String), +} + +impl From for RepoError { + fn from(err: sea_orm::DbErr) -> Self { + match err { + other => RepoError::InternalError(other.to_string()), + } + } +} diff --git a/apps/nxmesh-master/src/service/mod.rs b/apps/nxmesh-master/src/service/mod.rs index 1599a17..92c6856 100644 --- a/apps/nxmesh-master/src/service/mod.rs +++ b/apps/nxmesh-master/src/service/mod.rs @@ -6,6 +6,8 @@ use crate::{connector::agent::AgentConnectorTrait, service::certificate::Certifi pub mod agent; pub mod certificate; +pub mod error; +pub mod proxy; pub async fn start_master_server( settings: crate::config::settings::Settings, @@ -50,7 +52,60 @@ pub async fn start_master_server( } }); - let axum_router = crate::routes::get_root_router().await; + let api_state = crate::routes::api::ApiState { + agent_service: Arc::new(crate::service::agent::AgentServiceImpl::new( + db_connection.clone(), + )), + proxy_service: Arc::new(crate::service::proxy::service::ProxyServiceImpl::new( + db_connection.clone(), + )), + server_block_service: Arc::new( + crate::service::proxy::server_block::ServerBlockServiceImpl::new(db_connection.clone()), + ), + upstream_service: Arc::new(crate::service::proxy::upstream::UpstreamServiceImpl::new( + db_connection.clone(), + )), + location_block_service: Arc::new( + crate::service::proxy::location_block::LocationBlockServiceImpl::new( + db_connection.clone(), + ), + ), + access_rule_service: Arc::new( + crate::service::proxy::access_rule::AccessRuleServiceImpl::new(db_connection.clone()), + ), + cache_zone_service: Arc::new( + crate::service::proxy::cache_zone::CacheZoneServiceImpl::new(db_connection.clone()), + ), + limit_rule_service: Arc::new( + crate::service::proxy::limit_rule::LimitRuleServiceImpl::new(db_connection.clone()), + ), + limit_zone_service: Arc::new( + crate::service::proxy::limit_zone::LimitZoneServiceImpl::new(db_connection.clone()), + ), + log_setting_service: Arc::new( + crate::service::proxy::log_setting::LogSettingServiceImpl::new(db_connection.clone()), + ), + proxy_setting_service: Arc::new( + crate::service::proxy::proxy_setting::ProxySettingServiceImpl::new( + db_connection.clone(), + ), + ), + rewrite_rule_service: Arc::new( + crate::service::proxy::rewrite_rule::RewriteRuleServiceImpl::new(db_connection.clone()), + ), + ssl_certificate_service: Arc::new( + crate::service::proxy::ssl_certificate::SslCertificateServiceImpl::new( + db_connection.clone(), + ), + ), + config_inheritance_service: Arc::new( + crate::service::proxy::config_inheritance::ConfigInheritanceServiceImpl::new( + db_connection.clone(), + ), + ), + }; + + let axum_router = crate::routes::get_root_router(Arc::new(api_state)).await; // Start the HTTP server let addr = format!("{}:{}", settings.server.bind_address, settings.server.port) diff --git a/apps/nxmesh-master/src/service/proxy/access_rule/mod.rs b/apps/nxmesh-master/src/service/proxy/access_rule/mod.rs new file mode 100644 index 0000000..bf26c52 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/access_rule/mod.rs @@ -0,0 +1,156 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{AccessRuleConfig, ProxyServiceError, ProxyServiceResult}; + +pub struct CreateAccessRuleParams { + pub server_id: Option, + pub location_id: Option, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub override_of_id: Option, +} + +pub struct UpdateAccessRuleParams { + pub server_id: Option>, + pub location_id: Option>, + pub r#type: Option, + pub ip_cidr: Option, + pub description: Option>, + pub priority: Option, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait AccessRuleService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_server(&self, server_id: Uuid) -> ProxyServiceResult>; + async fn list_by_location( + &self, + location_id: Uuid, + ) -> ProxyServiceResult>; + async fn create(&self, params: CreateAccessRuleParams) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateAccessRuleParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct AccessRuleServiceImpl { + db: DatabaseConnection, +} + +impl AccessRuleServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl AccessRuleService for AccessRuleServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::access_rule; + + let model = access_rule::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list_by_server(&self, server_id: Uuid) -> ProxyServiceResult> { + use crate::db::entities::access_rule; + + let models = access_rule::Entity::find() + .filter(access_rule::Column::ServerId.eq(server_id)) + .all(&self.db) + .await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn list_by_location( + &self, + location_id: Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::access_rule; + + let models = access_rule::Entity::find() + .filter(access_rule::Column::LocationId.eq(location_id)) + .all(&self.db) + .await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create(&self, params: CreateAccessRuleParams) -> ProxyServiceResult { + use crate::db::entities::access_rule::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + server_id: Set(params.server_id), + location_id: Set(params.location_id), + r#type: Set(params.r#type), + ip_cidr: Set(params.ip_cidr), + description: Set(params.description), + priority: Set(params.priority), + is_deleted: Set(false), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateAccessRuleParams, + ) -> ProxyServiceResult { + use crate::db::entities::access_rule::{ActiveModel, Entity as AccessRuleEntity}; + + let existing = AccessRuleEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(server_id) = params.server_id { + model.server_id = Set(server_id); + } + if let Some(location_id) = params.location_id { + model.location_id = Set(location_id); + } + if let Some(r#type) = params.r#type { + model.r#type = Set(r#type); + } + if let Some(ip_cidr) = params.ip_cidr { + model.ip_cidr = Set(ip_cidr); + } + if let Some(description) = params.description { + model.description = Set(description); + } + if let Some(priority) = params.priority { + model.priority = Set(priority); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::access_rule::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/cache_zone/mod.rs b/apps/nxmesh-master/src/service/proxy/cache_zone/mod.rs new file mode 100644 index 0000000..3b95ff7 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/cache_zone/mod.rs @@ -0,0 +1,116 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{CacheZoneConfig, ProxyServiceError, ProxyServiceResult}; + +pub struct CreateCacheZoneParams { + pub name: String, + pub path: String, + pub size_limit: String, + pub override_of_id: Option, +} + +pub struct UpdateCacheZoneParams { + pub name: Option, + pub path: Option, + pub size_limit: Option, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait CacheZoneService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list(&self) -> ProxyServiceResult>; + async fn create(&self, params: CreateCacheZoneParams) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateCacheZoneParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct CacheZoneServiceImpl { + db: DatabaseConnection, +} + +impl CacheZoneServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl CacheZoneService for CacheZoneServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::cache_zone; + + let model = cache_zone::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list(&self) -> ProxyServiceResult> { + use crate::db::entities::cache_zone; + + let models = cache_zone::Entity::find().all(&self.db).await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create(&self, params: CreateCacheZoneParams) -> ProxyServiceResult { + use crate::db::entities::cache_zone::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + name: Set(params.name), + path: Set(params.path), + size_limit: Set(params.size_limit), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateCacheZoneParams, + ) -> ProxyServiceResult { + use crate::db::entities::cache_zone::{ActiveModel, Entity as CacheZoneEntity}; + + let existing = CacheZoneEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(name) = params.name { + model.name = Set(name); + } + if let Some(path) = params.path { + model.path = Set(path); + } + if let Some(size_limit) = params.size_limit { + model.size_limit = Set(size_limit); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::cache_zone::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/config_inheritance/mod.rs b/apps/nxmesh-master/src/service/proxy/config_inheritance/mod.rs new file mode 100644 index 0000000..1ad00d5 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/config_inheritance/mod.rs @@ -0,0 +1,144 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::ProxyServiceResult; + +pub struct AddInheritanceParams { + pub child_config_id: Uuid, + pub parent_config_id: Uuid, + pub priority: Option, +} + +pub struct ConfigInheritanceRecord { + pub id: Uuid, + pub child_config_id: Uuid, + pub parent_config_id: Uuid, + pub priority: Option, + pub applied_at: chrono::NaiveDateTime, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait ConfigInheritanceService: Send + Sync + 'static { + async fn add( + &self, + params: AddInheritanceParams, + ) -> ProxyServiceResult; + async fn remove( + &self, + child_config_id: Uuid, + parent_config_id: Uuid, + ) -> ProxyServiceResult; + async fn list_parents( + &self, + child_config_id: Uuid, + ) -> ProxyServiceResult>; + async fn list_children( + &self, + parent_config_id: Uuid, + ) -> ProxyServiceResult>; +} + +pub(crate) struct ConfigInheritanceServiceImpl { + db: DatabaseConnection, +} + +impl ConfigInheritanceServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl ConfigInheritanceService for ConfigInheritanceServiceImpl { + async fn add( + &self, + params: AddInheritanceParams, + ) -> ProxyServiceResult { + use crate::db::entities::config_inheritance::ActiveModel; + + let now = chrono::Utc::now().naive_utc(); + let model = ActiveModel { + id: Set(Uuid::new_v4()), + child_config_id: Set(params.child_config_id), + parent_config_id: Set(params.parent_config_id), + priority: Set(params.priority), + applied_at: Set(now), + }; + + let result = model.insert(&self.db).await?; + Ok(ConfigInheritanceRecord { + id: result.id, + child_config_id: result.child_config_id, + parent_config_id: result.parent_config_id, + priority: result.priority, + applied_at: result.applied_at, + }) + } + + async fn remove( + &self, + child_config_id: Uuid, + parent_config_id: Uuid, + ) -> ProxyServiceResult { + use crate::db::entities::config_inheritance::{Column, Entity as ConfigInheritanceEntity}; + use sea_orm::Condition; + + let result = ConfigInheritanceEntity::delete_many() + .filter( + Condition::all() + .add(Column::ChildConfigId.eq(child_config_id)) + .add(Column::ParentConfigId.eq(parent_config_id)), + ) + .exec(&self.db) + .await?; + + Ok(result.rows_affected > 0) + } + + async fn list_parents( + &self, + child_config_id: Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::config_inheritance; + + let results = config_inheritance::Entity::find() + .filter(config_inheritance::Column::ChildConfigId.eq(child_config_id)) + .all(&self.db) + .await?; + + Ok(results + .into_iter() + .map(|m| ConfigInheritanceRecord { + id: m.id, + child_config_id: m.child_config_id, + parent_config_id: m.parent_config_id, + priority: m.priority, + applied_at: m.applied_at, + }) + .collect()) + } + + async fn list_children( + &self, + parent_config_id: Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::config_inheritance; + + let results = config_inheritance::Entity::find() + .filter(config_inheritance::Column::ParentConfigId.eq(parent_config_id)) + .all(&self.db) + .await?; + + Ok(results + .into_iter() + .map(|m| ConfigInheritanceRecord { + id: m.id, + child_config_id: m.child_config_id, + parent_config_id: m.parent_config_id, + priority: m.priority, + applied_at: m.applied_at, + }) + .collect()) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/limit_rule/mod.rs b/apps/nxmesh-master/src/service/proxy/limit_rule/mod.rs new file mode 100644 index 0000000..10ed722 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/limit_rule/mod.rs @@ -0,0 +1,130 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{LimitRuleConfig, ProxyServiceError, ProxyServiceResult}; + +pub struct CreateLimitRuleParams { + pub location_id: Uuid, + pub zone_id: Uuid, + pub burst: Option, + pub nodelay: Option, + pub override_of_id: Option, +} + +pub struct UpdateLimitRuleParams { + pub location_id: Option, + pub zone_id: Option, + pub burst: Option>, + pub nodelay: Option>, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait LimitRuleService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_location(&self, location_id: Uuid) + -> ProxyServiceResult>; + async fn create(&self, params: CreateLimitRuleParams) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateLimitRuleParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct LimitRuleServiceImpl { + db: DatabaseConnection, +} + +impl LimitRuleServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl LimitRuleService for LimitRuleServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::limit_rule; + + let model = limit_rule::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list_by_location( + &self, + location_id: Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::limit_rule; + + let models = limit_rule::Entity::find() + .filter(limit_rule::Column::LocationId.eq(location_id)) + .all(&self.db) + .await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create(&self, params: CreateLimitRuleParams) -> ProxyServiceResult { + use crate::db::entities::limit_rule::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + location_id: Set(params.location_id), + zone_id: Set(params.zone_id), + burst: Set(params.burst), + nodelay: Set(params.nodelay), + is_deleted: Set(false), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateLimitRuleParams, + ) -> ProxyServiceResult { + use crate::db::entities::limit_rule::{ActiveModel, Entity as LimitRuleEntity}; + + let existing = LimitRuleEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(location_id) = params.location_id { + model.location_id = Set(location_id); + } + if let Some(zone_id) = params.zone_id { + model.zone_id = Set(zone_id); + } + if let Some(burst) = params.burst { + model.burst = Set(burst); + } + if let Some(nodelay) = params.nodelay { + model.nodelay = Set(nodelay); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::limit_rule::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/limit_zone/mod.rs b/apps/nxmesh-master/src/service/proxy/limit_zone/mod.rs new file mode 100644 index 0000000..02f5958 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/limit_zone/mod.rs @@ -0,0 +1,116 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{LimitZoneConfig, ProxyServiceError, ProxyServiceResult}; + +pub struct CreateLimitZoneParams { + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +pub struct UpdateLimitZoneParams { + pub name: Option, + pub key: Option, + pub rate: Option, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait LimitZoneService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list(&self) -> ProxyServiceResult>; + async fn create(&self, params: CreateLimitZoneParams) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateLimitZoneParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct LimitZoneServiceImpl { + db: DatabaseConnection, +} + +impl LimitZoneServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl LimitZoneService for LimitZoneServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::limit_zone; + + let model = limit_zone::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list(&self) -> ProxyServiceResult> { + use crate::db::entities::limit_zone; + + let models = limit_zone::Entity::find().all(&self.db).await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create(&self, params: CreateLimitZoneParams) -> ProxyServiceResult { + use crate::db::entities::limit_zone::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + name: Set(params.name), + key: Set(params.key), + rate: Set(params.rate), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateLimitZoneParams, + ) -> ProxyServiceResult { + use crate::db::entities::limit_zone::{ActiveModel, Entity as LimitZoneEntity}; + + let existing = LimitZoneEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(name) = params.name { + model.name = Set(name); + } + if let Some(key) = params.key { + model.key = Set(key); + } + if let Some(rate) = params.rate { + model.rate = Set(rate); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::limit_zone::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/location_block/mod.rs b/apps/nxmesh-master/src/service/proxy/location_block/mod.rs new file mode 100644 index 0000000..421b56d --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/location_block/mod.rs @@ -0,0 +1,206 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{ + LocationBlockConfig, OverrideRef, ProxyServiceError, ProxyServiceResult, +}; + +pub struct CreateLocationBlockParams { + pub server_id: Uuid, + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + pub metadata: Option, + pub override_of_id: Option, +} + +pub struct UpdateLocationBlockParams { + pub server_id: Option, + pub path_pattern: Option, + pub proxy_pass_upstream_id: Option>, + pub metadata: Option>, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait LocationBlockService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_server(&self, server_id: Uuid) + -> ProxyServiceResult>; + async fn create( + &self, + params: CreateLocationBlockParams, + ) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateLocationBlockParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct LocationBlockServiceImpl { + db: DatabaseConnection, +} + +impl LocationBlockServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } + + async fn build_with_children( + &self, + model: crate::db::entities::location_block::Model, + ) -> ProxyServiceResult { + use crate::db::entities::{access_rule, limit_rule, proxy_setting, rewrite_rule}; + + let access_rules = access_rule::Entity::find() + .filter(access_rule::Column::LocationId.eq(model.id)) + .all(&self.db) + .await? + .into_iter() + .map(|a| OverrideRef { + id: a.id, + override_of_id: a.override_of_id, + }) + .collect(); + + let limit_rules = limit_rule::Entity::find() + .filter(limit_rule::Column::LocationId.eq(model.id)) + .all(&self.db) + .await? + .into_iter() + .map(|l| OverrideRef { + id: l.id, + override_of_id: l.override_of_id, + }) + .collect(); + + let proxy_settings = proxy_setting::Entity::find() + .filter(proxy_setting::Column::LocationId.eq(model.id)) + .all(&self.db) + .await? + .into_iter() + .map(|p| OverrideRef { + id: p.id, + override_of_id: p.override_of_id, + }) + .collect(); + + let rewrite_rules = rewrite_rule::Entity::find() + .filter(rewrite_rule::Column::LocationId.eq(model.id)) + .all(&self.db) + .await? + .into_iter() + .map(|r| OverrideRef { + id: r.id, + override_of_id: r.override_of_id, + }) + .collect(); + + Ok(LocationBlockConfig { + id: model.id, + server_id: model.server_id, + path_pattern: model.path_pattern, + proxy_pass_upstream_id: model.proxy_pass_upstream_id, + metadata: model.metadata, + override_of_id: model.override_of_id, + access_rules, + limit_rules, + proxy_settings, + rewrite_rules, + }) + } +} + +#[async_trait::async_trait] +impl LocationBlockService for LocationBlockServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::location_block; + + let model = location_block::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + self.build_with_children(model).await + } + + async fn list_by_server( + &self, + server_id: Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::location_block; + + let models = location_block::Entity::find() + .filter(location_block::Column::ServerId.eq(server_id)) + .all(&self.db) + .await?; + + let mut results = Vec::with_capacity(models.len()); + for m in models { + results.push(self.build_with_children(m).await?); + } + Ok(results) + } + + async fn create( + &self, + params: CreateLocationBlockParams, + ) -> ProxyServiceResult { + use crate::db::entities::location_block::ActiveModel; + + let id = Uuid::new_v4(); + let model = ActiveModel { + id: Set(id), + server_id: Set(params.server_id), + path_pattern: Set(params.path_pattern), + proxy_pass_upstream_id: Set(params.proxy_pass_upstream_id), + metadata: Set(params.metadata), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + self.build_with_children(result).await + } + + async fn update( + &self, + id: Uuid, + params: UpdateLocationBlockParams, + ) -> ProxyServiceResult { + use crate::db::entities::location_block::{ActiveModel, Entity as LocationBlockEntity}; + + let existing = LocationBlockEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(server_id) = params.server_id { + model.server_id = Set(server_id); + } + if let Some(path_pattern) = params.path_pattern { + model.path_pattern = Set(path_pattern); + } + if let Some(proxy_pass_upstream_id) = params.proxy_pass_upstream_id { + model.proxy_pass_upstream_id = Set(proxy_pass_upstream_id); + } + if let Some(metadata) = params.metadata { + model.metadata = Set(metadata); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + self.build_with_children(result).await + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::location_block::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/log_setting/mod.rs b/apps/nxmesh-master/src/service/proxy/log_setting/mod.rs new file mode 100644 index 0000000..5c8713e --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/log_setting/mod.rs @@ -0,0 +1,125 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{LogSettingConfig, ProxyServiceError, ProxyServiceResult}; + +pub struct CreateLogSettingParams { + pub server_id: Uuid, + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +pub struct UpdateLogSettingParams { + pub server_id: Option, + pub access_log_path: Option>, + pub error_log_path: Option>, + pub log_level: Option>, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait LogSettingService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_server(&self, server_id: Uuid) -> ProxyServiceResult>; + async fn create(&self, params: CreateLogSettingParams) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateLogSettingParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct LogSettingServiceImpl { + db: DatabaseConnection, +} + +impl LogSettingServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl LogSettingService for LogSettingServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::log_setting; + + let model = log_setting::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list_by_server(&self, server_id: Uuid) -> ProxyServiceResult> { + use crate::db::entities::log_setting; + + let models = log_setting::Entity::find() + .filter(log_setting::Column::ServerId.eq(server_id)) + .all(&self.db) + .await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create(&self, params: CreateLogSettingParams) -> ProxyServiceResult { + use crate::db::entities::log_setting::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + server_id: Set(params.server_id), + access_log_path: Set(params.access_log_path), + error_log_path: Set(params.error_log_path), + log_level: Set(params.log_level), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateLogSettingParams, + ) -> ProxyServiceResult { + use crate::db::entities::log_setting::{ActiveModel, Entity as LogSettingEntity}; + + let existing = LogSettingEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(server_id) = params.server_id { + model.server_id = Set(server_id); + } + if let Some(access_log_path) = params.access_log_path { + model.access_log_path = Set(access_log_path); + } + if let Some(error_log_path) = params.error_log_path { + model.error_log_path = Set(error_log_path); + } + if let Some(log_level) = params.log_level { + model.log_level = Set(log_level); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::log_setting::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/mod.rs b/apps/nxmesh-master/src/service/proxy/mod.rs new file mode 100644 index 0000000..3d5e893 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/mod.rs @@ -0,0 +1,60 @@ +use crate::service::proxy::types::{ + AgentConfigBinding, CreateProxyConfigParams, ProxyConfig, ProxyConfigSummary, + ProxyServiceResult, UpdateProxyConfigParams, +}; + +pub(crate) mod access_rule; +pub(crate) mod cache_zone; +pub(crate) mod config_inheritance; +pub(crate) mod limit_rule; +pub(crate) mod limit_zone; +pub(crate) mod location_block; +pub(crate) mod log_setting; +pub(crate) mod nginx; +pub(crate) mod proxy_setting; +pub(crate) mod repo; +pub(crate) mod rewrite_rule; +pub(crate) mod server_block; +pub(crate) mod ssl_certificate; +pub(crate) mod upstream; + +pub mod service; +pub mod types; + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait ProxyServiceTrait: Send + Sync + 'static { + async fn get_proxy_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult; + + // CRUD + async fn list_configs(&self) -> ProxyServiceResult>; + async fn create_config( + &self, + params: CreateProxyConfigParams, + ) -> ProxyServiceResult; + async fn update_config( + &self, + id: uuid::Uuid, + params: UpdateProxyConfigParams, + ) -> ProxyServiceResult; + async fn delete_config(&self, id: uuid::Uuid) -> ProxyServiceResult; + + // Render + async fn render_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult; + + // Binding + async fn get_active_agent_config( + &self, + agent_id: uuid::Uuid, + ) -> ProxyServiceResult>; + async fn bind_agent( + &self, + agent_id: uuid::Uuid, + config_id: uuid::Uuid, + ) -> ProxyServiceResult; + async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult; +} + +pub trait ProxyConfigRenderer: Send + Sync + 'static { + fn render(&self, config: &ProxyConfig) -> String; +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/access_rule.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/access_rule.rs new file mode 100644 index 0000000..dda3fc4 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/access_rule.rs @@ -0,0 +1,7 @@ +use crate::service::proxy::types::AccessRuleConfig; + +impl std::fmt::Display for AccessRuleConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {};", self.r#type, self.ip_cidr) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/cache_zone.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/cache_zone.rs new file mode 100644 index 0000000..330872c --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/cache_zone.rs @@ -0,0 +1,11 @@ +use crate::service::proxy::types::CacheZoneConfig; + +impl std::fmt::Display for CacheZoneConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "proxy_cache_path {} levels=1:2 keys_zone={}:{};", + self.path, self.name, self.size + ) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/limit_rule.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_rule.rs new file mode 100644 index 0000000..73e02f8 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_rule.rs @@ -0,0 +1,19 @@ +use crate::service::proxy::types::LimitRuleConfig; + +pub struct LimitRuleRender<'a> { + pub rule: &'a LimitRuleConfig, + pub zone_name: &'a str, +} + +impl std::fmt::Display for LimitRuleRender<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "limit_req zone={}", self.zone_name)?; + if let Some(burst) = self.rule.burst { + write!(f, " burst={}", burst)?; + } + if self.rule.nodelay.unwrap_or(false) { + write!(f, " nodelay")?; + } + write!(f, ";") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/limit_zone.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_zone.rs new file mode 100644 index 0000000..9ddb9aa --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_zone.rs @@ -0,0 +1,11 @@ +use crate::service::proxy::types::LimitZoneConfig; + +impl std::fmt::Display for LimitZoneConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "limit_req_zone {} zone={}:{};", + self.key, self.name, self.rate + ) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/location_block.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/location_block.rs new file mode 100644 index 0000000..a758e67 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/location_block.rs @@ -0,0 +1,44 @@ +use crate::service::proxy::types::LocationBlockConfig; + +pub struct LocationBlockRender<'a> { + pub block: &'a LocationBlockConfig, + pub upstream_name: Option<&'a str>, + pub access_rules: &'a [String], + pub rewrite_rules: &'a [String], + pub proxy_setting: Option<&'a str>, + pub limit_rules: &'a [String], +} + +impl std::fmt::Display for LocationBlockRender<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, " location {} {{", self.block.path_pattern)?; + + if let Some(upstream) = self.upstream_name { + writeln!(f, " proxy_pass http://{};", upstream)?; + writeln!(f, " proxy_set_header Host $host;")?; + writeln!(f, " proxy_set_header X-Real-IP $remote_addr;")?; + } + + if let Some(settings) = self.proxy_setting { + for line in settings.lines() { + if !line.is_empty() { + writeln!(f, "{}", line)?; + } + } + } + + for rule in self.access_rules { + writeln!(f, " {}", rule)?; + } + + for rule in self.rewrite_rules { + writeln!(f, " {}", rule)?; + } + + for rule in self.limit_rules { + writeln!(f, " {}", rule)?; + } + + writeln!(f, " }}") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/log_setting.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/log_setting.rs new file mode 100644 index 0000000..2366f8f --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/log_setting.rs @@ -0,0 +1,21 @@ +use crate::service::proxy::types::LogSettingConfig; + +impl std::fmt::Display for LogSettingConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(ref path) = self.access_log_path { + if let Some(ref level) = self.log_level { + writeln!(f, " access_log {} {};", path, level)?; + } else { + writeln!(f, " access_log {};", path)?; + } + } + if let Some(ref path) = self.error_log_path { + if let Some(ref level) = self.log_level { + writeln!(f, " error_log {} {};", path, level)?; + } else { + writeln!(f, " error_log {};", path)?; + } + } + Ok(()) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs new file mode 100644 index 0000000..6db9d51 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs @@ -0,0 +1,11 @@ +pub(crate) mod access_rule; +pub(crate) mod cache_zone; +pub(crate) mod limit_rule; +pub(crate) mod limit_zone; +pub(crate) mod location_block; +pub(crate) mod log_setting; +pub(crate) mod proxy_setting; +pub(crate) mod rewrite_rule; +pub(crate) mod server_block; +pub(crate) mod ssl_certificate; +pub(crate) mod upstream; diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/proxy_setting.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/proxy_setting.rs new file mode 100644 index 0000000..18d4440 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/proxy_setting.rs @@ -0,0 +1,26 @@ +use crate::service::proxy::types::ProxySettingConfig; + +pub struct ProxySettingRender<'a> { + pub setting: &'a ProxySettingConfig, + pub cache_zone_name: Option<&'a str>, +} + +impl std::fmt::Display for ProxySettingRender<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(timeout) = self.setting.read_timeout { + writeln!(f, " proxy_read_timeout {}s;", timeout)?; + } + if let Some(timeout) = self.setting.connect_timeout { + writeln!(f, " proxy_connect_timeout {}s;", timeout)?; + } + if let Some(buffer) = self.setting.buffer_size { + writeln!(f, " proxy_buffer_size {};", buffer)?; + } + if self.setting.cache_enabled.unwrap_or(false) + && let Some(zone_name) = self.cache_zone_name + { + writeln!(f, " proxy_cache {};", zone_name)?; + } + Ok(()) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/rewrite_rule.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/rewrite_rule.rs new file mode 100644 index 0000000..1fd9473 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/rewrite_rule.rs @@ -0,0 +1,11 @@ +use crate::service::proxy::types::RewriteRuleConfig; + +impl std::fmt::Display for RewriteRuleConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "rewrite {} {}", self.pattern, self.replacement)?; + if let Some(ref flag) = self.flag { + write!(f, " {}", flag)?; + } + write!(f, ";") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/server_block.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/server_block.rs new file mode 100644 index 0000000..3661126 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/server_block.rs @@ -0,0 +1,49 @@ +use crate::service::proxy::types::ServerBlockConfig; + +pub struct ServerBlockRender<'a> { + pub block: &'a ServerBlockConfig, + pub ssl_cert: Option<&'a str>, + pub locations: &'a [String], + pub access_rules: &'a [String], + pub log_setting: Option<&'a str>, +} + +impl std::fmt::Display for ServerBlockRender<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "server {{")?; + + if self.block.ssl_enabled.unwrap_or(false) { + writeln!(f, " listen {} ssl;", self.block.listen_port)?; + } else { + writeln!(f, " listen {};", self.block.listen_port)?; + } + + if let Some(ref names) = self.block.server_name + && !names.is_empty() + { + writeln!(f, " server_name {};", names.join(" "))?; + } + + if let Some(cert) = self.ssl_cert { + writeln!(f, "{}", cert)?; + } + + if let Some(log) = self.log_setting { + for line in log.lines() { + if !line.is_empty() { + writeln!(f, "{}", line)?; + } + } + } + + for rule in self.access_rules { + writeln!(f, " {}", rule)?; + } + + for loc in self.locations { + writeln!(f, "{}", loc)?; + } + + write!(f, "}}") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/ssl_certificate.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/ssl_certificate.rs new file mode 100644 index 0000000..7b7a5ad --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/ssl_certificate.rs @@ -0,0 +1,8 @@ +use crate::service::proxy::types::SslCertificateConfig; + +impl std::fmt::Display for SslCertificateConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, " ssl_certificate {};", self.cert_path)?; + writeln!(f, " ssl_certificate_key {};", self.key_path) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/upstream.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/upstream.rs new file mode 100644 index 0000000..5e00db0 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/upstream.rs @@ -0,0 +1,7 @@ +use crate::service::proxy::types::UpstreamConfig; + +impl std::fmt::Display for UpstreamConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, " server {}:{};", self.target_host, self.target_port) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/mod.rs b/apps/nxmesh-master/src/service/proxy/nginx/mod.rs new file mode 100644 index 0000000..96ef86f --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/mod.rs @@ -0,0 +1,451 @@ +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) -> Vec<&'a T> { + ids.iter().filter_map(|r| map.get(&r.id)).collect() +} + +fn resolve_upstream_name( + upstream_id: Option, + upstreams: &HashMap, +) -> Option<&str> { + upstream_id + .and_then(|id| upstreams.get(&id)) + .map(|u| u.name.as_str()) +} + +fn resolve_cache_zone_name( + zone_id: Option, + cache_zones: &HashMap, +) -> 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, +) -> Option<&str> { + limit_zones.get(&zone_id).map(|z| z.name.as_str()) +} + +fn render_access_rules( + ids: &[OverrideRef], + access_rules: &HashMap, +) -> Vec { + resolve_ids(ids, access_rules) + .into_iter() + .map(|r| r.to_string()) + .collect() +} + +fn render_rewrite_rules( + ids: &[OverrideRef], + rewrite_rules: &HashMap, +) -> Vec { + resolve_ids(ids, rewrite_rules) + .into_iter() + .map(|r| r.to_string()) + .collect() +} + +fn render_limit_rules( + ids: &[OverrideRef], + limit_rules: &HashMap, + limit_zones: &HashMap, +) -> Vec { + 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, + cache_zones: &HashMap, +) -> Option { + 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, +) -> Option { + let setting = resolve_ids(ids, log_settings).into_iter().next()?; + Some(setting.to_string()) +} + +fn render_ssl_cert( + ids: &[OverrideRef], + ssl_certificates: &HashMap, +) -> Option { + 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 = 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 + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use crate::service::proxy::types::*; + use std::collections::HashMap; + + fn make_id() -> uuid::Uuid { + uuid::Uuid::new_v4() + } + + fn basic_proxy_config() -> ProxyConfig { + let upstream_id = make_id(); + let location_id = make_id(); + let server_id = make_id(); + + ProxyConfig { + id: make_id(), + name: "test".to_string(), + r#type: ProxyType::Nginx, + description: None, + parent_config_id: None, + upstreams: HashMap::from([( + upstream_id, + UpstreamConfig { + id: upstream_id, + name: "backend".to_string(), + target_host: "127.0.0.1".to_string(), + target_port: 3000, + metadata: None, + override_of_id: None, + location_blocks: vec![OverrideRef { + id: location_id, + override_of_id: None, + }], + }, + )]), + location_blocks: HashMap::from([( + location_id, + LocationBlockConfig { + id: location_id, + server_id, + path_pattern: "/api".to_string(), + proxy_pass_upstream_id: Some(upstream_id), + metadata: None, + override_of_id: None, + access_rules: vec![], + limit_rules: vec![], + proxy_settings: vec![], + rewrite_rules: vec![], + }, + )]), + server_blocks: HashMap::from([( + server_id, + ServerBlockConfig { + id: server_id, + server_name: Some(vec!["example.com".to_string()]), + listen_port: 80, + ssl_enabled: Some(false), + override_of_id: None, + access_rules: vec![], + location_blocks: vec![OverrideRef { + id: location_id, + override_of_id: None, + }], + log_settings: vec![], + ssl_certificates: vec![], + }, + )]), + access_rules: HashMap::new(), + cache_zones: HashMap::new(), + limit_rules: HashMap::new(), + limit_zones: HashMap::new(), + log_settings: HashMap::new(), + proxy_settings: HashMap::new(), + rewrite_rules: HashMap::new(), + ssl_certificates: HashMap::new(), + } + } + + #[test] + fn test_render_basic_nginx_config() { + let config = basic_proxy_config(); + let renderer = NginxConfigRenderer; + let output = renderer.render(&config); + + assert!( + output.contains("upstream backend {"), + "should contain upstream block" + ); + assert!( + output.contains("server 127.0.0.1:3000;"), + "should contain upstream server" + ); + assert!(output.contains("server {"), "should contain server block"); + assert!( + output.contains("listen 80;"), + "should contain listen directive" + ); + assert!( + output.contains("server_name example.com;"), + "should contain server name" + ); + assert!( + output.contains("location /api {"), + "should contain location block" + ); + assert!( + output.contains("proxy_pass http://backend;"), + "should contain proxy pass" + ); + } + + #[test] + fn test_render_config_with_cache_zone() { + let zone_id = make_id(); + let mut config = basic_proxy_config(); + config.cache_zones.insert( + zone_id, + CacheZoneConfig { + id: zone_id, + name: "mycache".to_string(), + path: "/var/cache/nginx".to_string(), + size: "10m".to_string(), + override_of_id: None, + }, + ); + let renderer = NginxConfigRenderer; + let output = renderer.render(&config); + assert!( + output.contains("proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m;") + ); + } + + #[test] + fn test_render_config_with_access_rules() { + let rule_id = make_id(); + let mut config = basic_proxy_config(); + + // Get the server ID from the config + let sb_id = *config.server_blocks.keys().next().unwrap(); + + let rule = AccessRuleConfig { + id: rule_id, + r#type: "allow".to_string(), + ip_cidr: "192.168.1.0/24".to_string(), + description: None, + priority: 10, + override_of_id: None, + }; + config.access_rules.insert(rule_id, rule); + + // Add access rule ref to the server block + let sb = config.server_blocks.get_mut(&sb_id).unwrap(); + sb.access_rules.push(OverrideRef { + id: rule_id, + override_of_id: None, + }); + + let renderer = NginxConfigRenderer; + let output = renderer.render(&config); + assert!(output.contains("allow 192.168.1.0/24;")); + } + + #[test] + fn test_render_config_empty() { + let config = ProxyConfig { + id: make_id(), + name: "empty".to_string(), + r#type: ProxyType::Nginx, + description: None, + parent_config_id: None, + server_blocks: HashMap::new(), + upstreams: HashMap::new(), + access_rules: HashMap::new(), + cache_zones: HashMap::new(), + limit_rules: HashMap::new(), + limit_zones: HashMap::new(), + location_blocks: HashMap::new(), + log_settings: HashMap::new(), + proxy_settings: HashMap::new(), + rewrite_rules: HashMap::new(), + ssl_certificates: HashMap::new(), + }; + let renderer = NginxConfigRenderer; + let output = renderer.render(&config); + assert!(output.is_empty() || output.trim().is_empty()); + } + + #[test] + fn test_render_config_with_limit_zone_and_rule() { + let mut config = basic_proxy_config(); + let zone_id = make_id(); + let location_id = *config.location_blocks.keys().next().unwrap(); + let rule_id = make_id(); + + config.limit_zones.insert( + zone_id, + LimitZoneConfig { + id: zone_id, + name: "reqzone".to_string(), + key: "$binary_remote_addr".to_string(), + rate: "10r/s".to_string(), + override_of_id: None, + }, + ); + + config.limit_rules.insert( + rule_id, + LimitRuleConfig { + id: rule_id, + location_id, + zone_id, + burst: Some(20), + nodelay: Some(true), + is_deleted: false, + override_of_id: None, + }, + ); + + let lb = config.location_blocks.get_mut(&location_id).unwrap(); + lb.limit_rules.push(OverrideRef { + id: rule_id, + override_of_id: None, + }); + + let renderer = NginxConfigRenderer; + let output = renderer.render(&config); + assert!(output.contains("limit_req_zone $binary_remote_addr zone=reqzone:10r/s;")); + assert!(output.contains("limit_req zone=reqzone burst=20 nodelay;")); + } + + #[test] + fn test_render_config_with_ssl() { + let mut config = basic_proxy_config(); + let cert_id = make_id(); + let sb_id = *config.server_blocks.keys().next().unwrap(); + + config.ssl_certificates.insert( + cert_id, + SslCertificateConfig { + id: cert_id, + name: "test-cert".to_string(), + cert_path: "/etc/ssl/certs/test.pem".to_string(), + key_path: "/etc/ssl/private/test.key".to_string(), + expiry_date: chrono::Utc::now(), + }, + ); + + let sb = config.server_blocks.get_mut(&sb_id).unwrap(); + sb.ssl_enabled = Some(true); + sb.ssl_certificates.push(OverrideRef { + id: cert_id, + override_of_id: None, + }); + + let renderer = NginxConfigRenderer; + let output = renderer.render(&config); + assert!(output.contains("listen 80 ssl;")); + assert!(output.contains("ssl_certificate /etc/ssl/certs/test.pem;")); + assert!(output.contains("ssl_certificate_key /etc/ssl/private/test.key;")); + } +} diff --git a/apps/nxmesh-master/src/service/proxy/proxy_setting/mod.rs b/apps/nxmesh-master/src/service/proxy/proxy_setting/mod.rs new file mode 100644 index 0000000..8a79547 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/proxy_setting/mod.rs @@ -0,0 +1,149 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{ProxyServiceError, ProxyServiceResult, ProxySettingConfig}; + +pub struct CreateProxySettingParams { + pub location_id: Uuid, + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +pub struct UpdateProxySettingParams { + pub location_id: Option, + pub read_timeout: Option>, + pub connect_timeout: Option>, + pub buffer_size: Option>, + pub cache_enabled: Option>, + pub cache_zone: Option>, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait ProxySettingService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_location( + &self, + location_id: Uuid, + ) -> ProxyServiceResult>; + async fn create( + &self, + params: CreateProxySettingParams, + ) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateProxySettingParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct ProxySettingServiceImpl { + db: DatabaseConnection, +} + +impl ProxySettingServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl ProxySettingService for ProxySettingServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::proxy_setting; + + let model = proxy_setting::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list_by_location( + &self, + location_id: Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::proxy_setting; + + let models = proxy_setting::Entity::find() + .filter(proxy_setting::Column::LocationId.eq(location_id)) + .all(&self.db) + .await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create( + &self, + params: CreateProxySettingParams, + ) -> ProxyServiceResult { + use crate::db::entities::proxy_setting::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + location_id: Set(params.location_id), + read_timeout: Set(params.read_timeout), + connect_timeout: Set(params.connect_timeout), + buffer_size: Set(params.buffer_size), + cache_enabled: Set(params.cache_enabled), + cache_zone: Set(params.cache_zone), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateProxySettingParams, + ) -> ProxyServiceResult { + use crate::db::entities::proxy_setting::{ActiveModel, Entity as ProxySettingEntity}; + + let existing = ProxySettingEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(location_id) = params.location_id { + model.location_id = Set(location_id); + } + if let Some(read_timeout) = params.read_timeout { + model.read_timeout = Set(read_timeout); + } + if let Some(connect_timeout) = params.connect_timeout { + model.connect_timeout = Set(connect_timeout); + } + if let Some(buffer_size) = params.buffer_size { + model.buffer_size = Set(buffer_size); + } + if let Some(cache_enabled) = params.cache_enabled { + model.cache_enabled = Set(cache_enabled); + } + if let Some(cache_zone) = params.cache_zone { + model.cache_zone = Set(cache_zone); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::proxy_setting::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/repo.rs b/apps/nxmesh-master/src/service/proxy/repo.rs new file mode 100644 index 0000000..1605e45 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/repo.rs @@ -0,0 +1,624 @@ +use std::collections::HashMap; + +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, prelude::*}; + +use crate::service::proxy::types::{ + AgentConfigBinding, CreateProxyConfigParams, Mergeable, OverrideRef, ProxyConfig, + ProxyConfigSummary, ProxyServiceError, ProxyServiceResult, ProxyType, UpdateProxyConfigParams, +}; + +#[async_trait::async_trait] +pub trait ProxyRepo: Send + Sync + 'static { + async fn get_proxy_raw_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult; + async fn get_proxy_raw_configs( + &self, + proxy_id: uuid::Uuid, + ) -> ProxyServiceResult>; + async fn get_merged_proxy_config( + &self, + proxy_id: uuid::Uuid, + ) -> ProxyServiceResult; + + // CRUD + async fn list_proxy_configs(&self) -> ProxyServiceResult>; + async fn create_proxy_config( + &self, + params: CreateProxyConfigParams, + ) -> ProxyServiceResult; + async fn update_proxy_config( + &self, + id: uuid::Uuid, + params: UpdateProxyConfigParams, + ) -> ProxyServiceResult; + async fn delete_proxy_config(&self, id: uuid::Uuid) -> ProxyServiceResult; + + // Agent config binding + async fn get_active_agent_config( + &self, + agent_id: uuid::Uuid, + ) -> ProxyServiceResult>; + async fn bind_agent_to_config( + &self, + agent_id: uuid::Uuid, + config_id: uuid::Uuid, + ) -> ProxyServiceResult; + async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult; +} + +pub(crate) struct ProxyRepoImpl { + db: DatabaseConnection, +} + +impl ProxyRepoImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl ProxyRepo for ProxyRepoImpl { + async fn get_proxy_raw_configs( + &self, + proxy_config_id: uuid::Uuid, + ) -> ProxyServiceResult> { + let mut proxy_config_id_frontier = vec![proxy_config_id]; + let mut visited = std::collections::HashSet::new(); + + let mut configs: Vec = Vec::new(); + + while let Some(current_id) = proxy_config_id_frontier.pop() { + if visited.contains(¤t_id) { + continue; + } + visited.insert(current_id); + // + let config = self.get_proxy_raw_config(current_id).await?; + // + if let Some(parent_ids) = &config.parent_config_id { + proxy_config_id_frontier.extend(parent_ids); + } + configs.push(config); + } + + Ok(configs) + } + + async fn get_proxy_raw_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult { + let proxy_entity = crate::db::entities::proxy_config::Entity::find_by_id(proxy_id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + // Resolve parent configs from config_inheritance + let parent_config_id = { + let inheritance = crate::db::entities::config_inheritance::Entity::find() + .filter(crate::db::entities::config_inheritance::Column::ChildConfigId.eq(proxy_id)) + .all(&self.db) + .await?; + if inheritance.is_empty() { + None + } else { + Some( + inheritance + .into_iter() + .map(|ci| ci.parent_config_id) + .collect(), + ) + } + }; + + // Direct children of proxy_config + let server_blocks = crate::db::entities::server_block::Entity::find() + .filter(crate::db::entities::server_block::Column::ConfigId.eq(proxy_id)) + .all(&self.db) + .await?; + + let upstreams = crate::db::entities::upstream::Entity::find() + .filter(crate::db::entities::upstream::Column::ConfigId.eq(proxy_id)) + .all(&self.db) + .await?; + + // Children of server_blocks + let server_block_ids: Vec = server_blocks.iter().map(|sb| sb.id).collect(); + + let (location_blocks, log_settings, server_access_rules) = if server_block_ids.is_empty() { + (vec![], vec![], vec![]) + } else { + ( + crate::db::entities::location_block::Entity::find() + .filter( + crate::db::entities::location_block::Column::ServerId + .is_in(server_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::log_setting::Entity::find() + .filter( + crate::db::entities::log_setting::Column::ServerId + .is_in(server_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::access_rule::Entity::find() + .filter( + crate::db::entities::access_rule::Column::ServerId.is_in(server_block_ids), + ) + .all(&self.db) + .await?, + ) + }; + + // Children of location_blocks + let location_block_ids: Vec = location_blocks.iter().map(|lb| lb.id).collect(); + + let (location_access_rules, limit_rules, proxy_settings, rewrite_rules) = + if location_block_ids.is_empty() { + (vec![], vec![], vec![], vec![]) + } else { + ( + crate::db::entities::access_rule::Entity::find() + .filter( + crate::db::entities::access_rule::Column::LocationId + .is_in(location_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::limit_rule::Entity::find() + .filter( + crate::db::entities::limit_rule::Column::LocationId + .is_in(location_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::proxy_setting::Entity::find() + .filter( + crate::db::entities::proxy_setting::Column::LocationId + .is_in(location_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::rewrite_rule::Entity::find() + .filter( + crate::db::entities::rewrite_rule::Column::LocationId + .is_in(location_block_ids), + ) + .all(&self.db) + .await?, + ) + }; + + // Collect referenced IDs for zone and cert lookups + let limit_zone_ids: Vec = limit_rules.iter().map(|lr| lr.zone_id).collect(); + let cache_zone_ids: Vec = proxy_settings + .iter() + .filter_map(|ps| ps.cache_zone) + .collect(); + let ssl_cert_ids: Vec = server_blocks + .iter() + .filter_map(|sb| sb.ssl_cert_id) + .collect(); + + let limit_zones = if limit_zone_ids.is_empty() { + vec![] + } else { + crate::db::entities::limit_zone::Entity::find() + .filter(crate::db::entities::limit_zone::Column::Id.is_in(limit_zone_ids)) + .all(&self.db) + .await? + }; + + let cache_zones = if cache_zone_ids.is_empty() { + vec![] + } else { + crate::db::entities::cache_zone::Entity::find() + .filter(crate::db::entities::cache_zone::Column::Id.is_in(cache_zone_ids)) + .all(&self.db) + .await? + }; + + let ssl_certificates = if ssl_cert_ids.is_empty() { + vec![] + } else { + crate::db::entities::ssl_certificate::Entity::find() + .filter(crate::db::entities::ssl_certificate::Column::Id.is_in(ssl_cert_ids)) + .all(&self.db) + .await? + }; + + // ── Group child IDs by parent ──────────────────────────────────────── + + // location_block IDs grouped by server_id + let loc_block_ids_by_server: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for lb in &location_blocks { + map.entry(lb.server_id).or_default().push(OverrideRef { + id: lb.id, + override_of_id: lb.override_of_id, + }); + } + map + }; + + // log_setting IDs grouped by server_id + let log_setting_ids_by_server: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ls in &log_settings { + map.entry(ls.server_id).or_default().push(OverrideRef { + id: ls.id, + override_of_id: ls.override_of_id, + }); + } + map + }; + + // server-level access_rule IDs grouped by server_id + let server_ar_ids_by_server: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ar in &server_access_rules { + if let Some(sid) = ar.server_id { + map.entry(sid).or_default().push(OverrideRef { + id: ar.id, + override_of_id: ar.override_of_id, + }); + } + } + map + }; + + // location-level access_rule IDs grouped by location_id + let loc_ar_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ar in &location_access_rules { + if let Some(lid) = ar.location_id { + map.entry(lid).or_default().push(OverrideRef { + id: ar.id, + override_of_id: ar.override_of_id, + }); + } + } + map + }; + + // limit_rule IDs grouped by location_id + let lr_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for lr in &limit_rules { + map.entry(lr.location_id).or_default().push(OverrideRef { + id: lr.id, + override_of_id: lr.override_of_id, + }); + } + map + }; + + // proxy_setting IDs grouped by location_id + let ps_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ps in &proxy_settings { + map.entry(ps.location_id).or_default().push(OverrideRef { + id: ps.id, + override_of_id: ps.override_of_id, + }); + } + map + }; + + // rewrite_rule IDs grouped by location_id + let rr_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for rr in &rewrite_rules { + map.entry(rr.location_id).or_default().push(OverrideRef { + id: rr.id, + override_of_id: rr.override_of_id, + }); + } + map + }; + + // location_block IDs grouped by proxy_pass_upstream_id (reverse FK) + let loc_block_ids_by_upstream: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for lb in &location_blocks { + if let Some(up_id) = lb.proxy_pass_upstream_id { + map.entry(up_id).or_default().push(OverrideRef { + id: lb.id, + override_of_id: lb.override_of_id, + }); + } + } + map + }; + + // ── Build child-ID tuples for each config type ─────────────────────── + + let location_block_tuples: Vec<( + crate::db::entities::location_block::Model, + Vec, + Vec, + Vec, + Vec, + )> = location_blocks + .into_iter() + .map(|lb| { + let ar_ids = loc_ar_ids_by_location + .get(&lb.id) + .cloned() + .unwrap_or_default(); + let lr_ids = lr_ids_by_location.get(&lb.id).cloned().unwrap_or_default(); + let ps_ids = ps_ids_by_location.get(&lb.id).cloned().unwrap_or_default(); + let rr_ids = rr_ids_by_location.get(&lb.id).cloned().unwrap_or_default(); + (lb, ar_ids, lr_ids, ps_ids, rr_ids) + }) + .collect(); + + let server_block_tuples: Vec<( + crate::db::entities::server_block::Model, + Vec, + Vec, + Vec, + Vec, + )> = server_blocks + .into_iter() + .map(|sb| { + let ar_ids = server_ar_ids_by_server + .get(&sb.id) + .cloned() + .unwrap_or_default(); + let lb_ids = loc_block_ids_by_server + .get(&sb.id) + .cloned() + .unwrap_or_default(); + let ls_ids = log_setting_ids_by_server + .get(&sb.id) + .cloned() + .unwrap_or_default(); + let sc_ids: Vec = sb + .ssl_cert_id + .into_iter() + .map(|id| OverrideRef { + id, + override_of_id: None, + }) + .collect(); + (sb, ar_ids, lb_ids, ls_ids, sc_ids) + }) + .collect(); + + let upstream_tuples: Vec<(crate::db::entities::upstream::Model, Vec)> = + upstreams + .into_iter() + .map(|u| { + let lb_ids = loc_block_ids_by_upstream + .get(&u.id) + .cloned() + .unwrap_or_default(); + (u, lb_ids) + }) + .collect(); + + // Combine server-level and location-level access rules into one flat list + let all_access_rules: Vec = { + let mut ars = + Vec::with_capacity(server_access_rules.len() + location_access_rules.len()); + ars.extend(server_access_rules); + ars.extend(location_access_rules); + ars + }; + + Ok(ProxyConfig { + id: proxy_entity.id, + name: proxy_entity.name, + r#type: ProxyType::Nginx, + description: proxy_entity.description, + parent_config_id, + server_blocks: server_block_tuples + .into_iter() + .map(|m| (m.0.id, m.into())) + .collect(), + upstreams: upstream_tuples + .into_iter() + .map(|m| (m.0.id, m.into())) + .collect(), + location_blocks: location_block_tuples + .into_iter() + .map(|m| (m.0.id, m.into())) + .collect(), + access_rules: all_access_rules + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + cache_zones: cache_zones.into_iter().map(|m| (m.id, m.into())).collect(), + limit_rules: limit_rules.into_iter().map(|m| (m.id, m.into())).collect(), + limit_zones: limit_zones.into_iter().map(|m| (m.id, m.into())).collect(), + log_settings: log_settings.into_iter().map(|m| (m.id, m.into())).collect(), + proxy_settings: proxy_settings + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + rewrite_rules: rewrite_rules + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + ssl_certificates: ssl_certificates + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + }) + } + + async fn get_merged_proxy_config( + &self, + proxy_id: uuid::Uuid, + ) -> ProxyServiceResult { + let configs = self.get_proxy_raw_configs(proxy_id).await?; + + // configs is ordered [leaf, ..., root] (most specific first) + // self.merge(other) means self overrides other + // So start with leaf and merge each ancestor into it + let mut iter = configs.into_iter(); + let mut merged = iter.next().ok_or(ProxyServiceError::ConfigNotFound)?; + for config in iter { + merged.merge(config); + } + Ok(merged) + } + + // ── CRUD ── + + async fn list_proxy_configs(&self) -> ProxyServiceResult> { + use crate::db::entities::proxy_config::Column; + use sea_orm::QueryOrder; + + let configs = crate::db::entities::proxy_config::Entity::find() + .order_by(Column::UpdatedAt, sea_orm::Order::Desc) + .all(&self.db) + .await?; + Ok(configs.into_iter().map(Into::into).collect()) + } + + async fn create_proxy_config( + &self, + params: CreateProxyConfigParams, + ) -> ProxyServiceResult { + use crate::db::entities::proxy_config::ActiveModel; + let now = chrono::Utc::now().naive_utc(); + let model = ActiveModel { + id: Set(uuid::Uuid::new_v4()), + name: Set(params.name), + description: Set(params.description), + is_template: Set(params.is_template), + created_at: Set(now), + updated_at: Set(now), + }; + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update_proxy_config( + &self, + id: uuid::Uuid, + params: UpdateProxyConfigParams, + ) -> ProxyServiceResult { + use crate::db::entities::proxy_config::ActiveModel; + use crate::db::entities::proxy_config::Entity as ProxyConfigEntity; + + let existing = ProxyConfigEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(name) = params.name { + model.name = Set(name); + } + if let Some(description) = params.description { + model.description = Set(Some(description)); + } + if let Some(is_template) = params.is_template { + model.is_template = Set(is_template); + } + model.updated_at = Set(chrono::Utc::now().naive_utc()); + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete_proxy_config(&self, id: uuid::Uuid) -> ProxyServiceResult { + let result = crate::db::entities::proxy_config::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } + + // ── Agent config binding ── + + async fn get_active_agent_config( + &self, + agent_id: uuid::Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::agent_config_binding::Column; + use crate::db::entities::proxy_config::Entity as ProxyConfigEntity; + use sea_orm::Condition; + + let binding = crate::db::entities::agent_config_binding::Entity::find() + .filter( + Condition::all() + .add(Column::AgentId.eq(agent_id)) + .add(Column::IsActive.eq(true)), + ) + .one(&self.db) + .await?; + + match binding { + Some(b) => { + let config = ProxyConfigEntity::find_by_id(b.config_id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + Ok(Some(config.into())) + } + None => Ok(None), + } + } + + async fn bind_agent_to_config( + &self, + agent_id: uuid::Uuid, + config_id: uuid::Uuid, + ) -> ProxyServiceResult { + use crate::db::entities::agent_config_binding::ActiveModel; + use crate::db::entities::agent_config_binding::Column; + use sea_orm::Condition; + + // Deactivate existing active binding for this agent + if let Some(existing) = crate::db::entities::agent_config_binding::Entity::find() + .filter( + Condition::all() + .add(Column::AgentId.eq(agent_id)) + .add(Column::IsActive.eq(true)), + ) + .one(&self.db) + .await? + { + let mut active: ActiveModel = existing.into(); + active.is_active = Set(false); + active.update(&self.db).await?; + } + + let now = chrono::Utc::now().naive_utc(); + let model = ActiveModel { + id: Set(uuid::Uuid::new_v4()), + agent_id: Set(Some(agent_id)), + group_id: Set(None), + config_id: Set(config_id), + is_active: Set(true), + applied_at: Set(now), + }; + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult { + use crate::db::entities::agent_config_binding::ActiveModel; + use crate::db::entities::agent_config_binding::Column; + use sea_orm::Condition; + + let existing = crate::db::entities::agent_config_binding::Entity::find() + .filter( + Condition::all() + .add(Column::AgentId.eq(agent_id)) + .add(Column::IsActive.eq(true)), + ) + .one(&self.db) + .await?; + + if let Some(b) = existing { + let mut active: ActiveModel = b.into(); + active.is_active = Set(false); + active.update(&self.db).await?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/apps/nxmesh-master/src/service/proxy/rewrite_rule/mod.rs b/apps/nxmesh-master/src/service/proxy/rewrite_rule/mod.rs new file mode 100644 index 0000000..17298c9 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/rewrite_rule/mod.rs @@ -0,0 +1,144 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{ProxyServiceError, ProxyServiceResult, RewriteRuleConfig}; + +pub struct CreateRewriteRuleParams { + pub location_id: Uuid, + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub override_of_id: Option, +} + +pub struct UpdateRewriteRuleParams { + pub location_id: Option, + pub pattern: Option, + pub replacement: Option, + pub flag: Option>, + pub priority: Option, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait RewriteRuleService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_location( + &self, + location_id: Uuid, + ) -> ProxyServiceResult>; + async fn create( + &self, + params: CreateRewriteRuleParams, + ) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateRewriteRuleParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct RewriteRuleServiceImpl { + db: DatabaseConnection, +} + +impl RewriteRuleServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl RewriteRuleService for RewriteRuleServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::rewrite_rule; + + let model = rewrite_rule::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list_by_location( + &self, + location_id: Uuid, + ) -> ProxyServiceResult> { + use crate::db::entities::rewrite_rule; + + let models = rewrite_rule::Entity::find() + .filter(rewrite_rule::Column::LocationId.eq(location_id)) + .all(&self.db) + .await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create( + &self, + params: CreateRewriteRuleParams, + ) -> ProxyServiceResult { + use crate::db::entities::rewrite_rule::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + location_id: Set(params.location_id), + pattern: Set(params.pattern), + replacement: Set(params.replacement), + flag: Set(params.flag), + priority: Set(params.priority), + is_deleted: Set(false), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateRewriteRuleParams, + ) -> ProxyServiceResult { + use crate::db::entities::rewrite_rule::{ActiveModel, Entity as RewriteRuleEntity}; + + let existing = RewriteRuleEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(location_id) = params.location_id { + model.location_id = Set(location_id); + } + if let Some(pattern) = params.pattern { + model.pattern = Set(pattern); + } + if let Some(replacement) = params.replacement { + model.replacement = Set(replacement); + } + if let Some(flag) = params.flag { + model.flag = Set(flag); + } + if let Some(priority) = params.priority { + model.priority = Set(priority); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::rewrite_rule::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/server_block/mod.rs b/apps/nxmesh-master/src/service/proxy/server_block/mod.rs new file mode 100644 index 0000000..23ed4e7 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/server_block/mod.rs @@ -0,0 +1,202 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{ + OverrideRef, ProxyServiceError, ProxyServiceResult, ServerBlockConfig, +}; + +pub struct CreateServerBlockParams { + pub config_id: Uuid, + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub ssl_cert_id: Option, + pub override_of_id: Option, +} + +pub struct UpdateServerBlockParams { + pub server_name: Option>>, + pub listen_port: Option, + pub ssl_enabled: Option>, + pub ssl_cert_id: Option>, + pub override_of_id: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait ServerBlockService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_config(&self, config_id: Uuid) -> ProxyServiceResult>; + async fn create( + &self, + params: CreateServerBlockParams, + ) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateServerBlockParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct ServerBlockServiceImpl { + db: DatabaseConnection, +} + +impl ServerBlockServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } + + async fn build_with_children( + &self, + model: crate::db::entities::server_block::Model, + ) -> ProxyServiceResult { + use crate::db::entities::{access_rule, location_block, log_setting}; + + let access_rules = access_rule::Entity::find() + .filter(access_rule::Column::ServerId.eq(model.id)) + .all(&self.db) + .await? + .into_iter() + .map(|a| OverrideRef { + id: a.id, + override_of_id: a.override_of_id, + }) + .collect(); + + let location_blocks = location_block::Entity::find() + .filter(location_block::Column::ServerId.eq(model.id)) + .all(&self.db) + .await? + .into_iter() + .map(|l| OverrideRef { + id: l.id, + override_of_id: l.override_of_id, + }) + .collect(); + + let log_settings = log_setting::Entity::find() + .filter(log_setting::Column::ServerId.eq(model.id)) + .all(&self.db) + .await? + .into_iter() + .map(|l| OverrideRef { + id: l.id, + override_of_id: l.override_of_id, + }) + .collect(); + + let ssl_certificates = model + .ssl_cert_id + .map(|id| { + vec![OverrideRef { + id, + override_of_id: None, + }] + }) + .unwrap_or_default(); + + Ok(ServerBlockConfig { + id: model.id, + server_name: model.server_name, + listen_port: model.listen_port, + ssl_enabled: model.ssl_enabled, + override_of_id: model.override_of_id, + access_rules, + location_blocks, + log_settings, + ssl_certificates, + }) + } +} + +#[async_trait::async_trait] +impl ServerBlockService for ServerBlockServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::server_block; + + let model = server_block::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + self.build_with_children(model).await + } + + async fn list_by_config(&self, config_id: Uuid) -> ProxyServiceResult> { + use crate::db::entities::server_block; + + let models = server_block::Entity::find() + .filter(server_block::Column::ConfigId.eq(config_id)) + .all(&self.db) + .await?; + + let mut results = Vec::with_capacity(models.len()); + for m in models { + results.push(self.build_with_children(m).await?); + } + Ok(results) + } + + async fn create( + &self, + params: CreateServerBlockParams, + ) -> ProxyServiceResult { + use crate::db::entities::server_block::ActiveModel; + + let id = Uuid::new_v4(); + let model = ActiveModel { + id: Set(id), + config_id: Set(params.config_id), + server_name: Set(params.server_name), + listen_port: Set(params.listen_port), + ssl_enabled: Set(params.ssl_enabled), + ssl_cert_id: Set(params.ssl_cert_id), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + self.build_with_children(result).await + } + + async fn update( + &self, + id: Uuid, + params: UpdateServerBlockParams, + ) -> ProxyServiceResult { + use crate::db::entities::server_block::{ActiveModel, Entity as ServerBlockEntity}; + + let existing = ServerBlockEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(server_name) = params.server_name { + model.server_name = Set(server_name); + } + if let Some(listen_port) = params.listen_port { + model.listen_port = Set(listen_port); + } + if let Some(ssl_enabled) = params.ssl_enabled { + model.ssl_enabled = Set(ssl_enabled); + } + if let Some(ssl_cert_id) = params.ssl_cert_id { + model.ssl_cert_id = Set(ssl_cert_id); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + self.build_with_children(result).await + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::server_block::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/service.rs b/apps/nxmesh-master/src/service/proxy/service.rs new file mode 100644 index 0000000..95b779e --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/service.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; + +use sea_orm::DatabaseConnection; + +use super::nginx::NginxConfigRenderer; +use super::repo::{ProxyRepo, ProxyRepoImpl}; +use super::types::{ + AgentConfigBinding, CreateProxyConfigParams, ProxyConfig, ProxyConfigSummary, + ProxyServiceError, ProxyServiceResult, ProxyType, UpdateProxyConfigParams, +}; +use super::{ProxyConfigRenderer, ProxyServiceTrait}; + +pub struct ProxyServiceImpl { + repo: Box, + renderers: HashMap>, +} + +impl ProxyServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + let mut renderers: HashMap> = 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 { + self.repo.get_merged_proxy_config(proxy_id).await + } + + async fn render_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult { + 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)) + } + + async fn list_configs(&self) -> ProxyServiceResult> { + self.repo.list_proxy_configs().await + } + + async fn create_config( + &self, + params: CreateProxyConfigParams, + ) -> ProxyServiceResult { + self.repo.create_proxy_config(params).await + } + + async fn update_config( + &self, + id: uuid::Uuid, + params: UpdateProxyConfigParams, + ) -> ProxyServiceResult { + self.repo.update_proxy_config(id, params).await + } + + async fn delete_config(&self, id: uuid::Uuid) -> ProxyServiceResult { + self.repo.delete_proxy_config(id).await + } + + async fn get_active_agent_config( + &self, + agent_id: uuid::Uuid, + ) -> ProxyServiceResult> { + self.repo.get_active_agent_config(agent_id).await + } + + async fn bind_agent( + &self, + agent_id: uuid::Uuid, + config_id: uuid::Uuid, + ) -> ProxyServiceResult { + self.repo.bind_agent_to_config(agent_id, config_id).await + } + + async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult { + self.repo.unbind_agent(agent_id).await + } +} + +impl ProxyServiceImpl { + pub async fn render_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult { + 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)) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/ssl_certificate/mod.rs b/apps/nxmesh-master/src/service/proxy/ssl_certificate/mod.rs new file mode 100644 index 0000000..ec367f6 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/ssl_certificate/mod.rs @@ -0,0 +1,123 @@ +use chrono::Utc; +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{ProxyServiceError, ProxyServiceResult, SslCertificateConfig}; + +pub struct CreateSslCertificateParams { + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: chrono::DateTime, +} + +pub struct UpdateSslCertificateParams { + pub name: Option, + pub cert_path: Option, + pub key_path: Option, + pub expiry_date: Option>, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait SslCertificateService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list(&self) -> ProxyServiceResult>; + async fn create( + &self, + params: CreateSslCertificateParams, + ) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateSslCertificateParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct SslCertificateServiceImpl { + db: DatabaseConnection, +} + +impl SslCertificateServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl SslCertificateService for SslCertificateServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::ssl_certificate; + + let model = ssl_certificate::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model.into()) + } + + async fn list(&self) -> ProxyServiceResult> { + use crate::db::entities::ssl_certificate; + + let models = ssl_certificate::Entity::find().all(&self.db).await?; + + Ok(models.into_iter().map(Into::into).collect()) + } + + async fn create( + &self, + params: CreateSslCertificateParams, + ) -> ProxyServiceResult { + use crate::db::entities::ssl_certificate::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + name: Set(params.name), + cert_path: Set(params.cert_path), + key_path: Set(params.key_path), + expiry_date: Set(params.expiry_date.naive_utc()), + }; + + let result = model.insert(&self.db).await?; + Ok(result.into()) + } + + async fn update( + &self, + id: Uuid, + params: UpdateSslCertificateParams, + ) -> ProxyServiceResult { + use crate::db::entities::ssl_certificate::{ActiveModel, Entity as SslCertificateEntity}; + + let existing = SslCertificateEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(name) = params.name { + model.name = Set(name); + } + if let Some(cert_path) = params.cert_path { + model.cert_path = Set(cert_path); + } + if let Some(key_path) = params.key_path { + model.key_path = Set(key_path); + } + if let Some(expiry_date) = params.expiry_date { + model.expiry_date = Set(expiry_date.naive_utc()); + } + + let result = model.update(&self.db).await?; + Ok(result.into()) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::ssl_certificate::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/types.rs b/apps/nxmesh-master/src/service/proxy/types.rs new file mode 100644 index 0000000..f578192 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/types.rs @@ -0,0 +1,827 @@ +use std::collections::HashMap; + +#[derive(Debug, thiserror::Error)] +pub enum ProxyServiceError { + #[error("proxy config not found")] + ConfigNotFound, + #[error("invalid proxy config: {0}")] + InvalidConfig(String), + #[error("no renderer registered for this proxy type")] + RendererNotFound, + #[error("database error: {0}")] + DatabaseError(#[from] sea_orm::DbErr), +} + +pub type ProxyServiceResult = Result; + +#[derive(Hash, Eq, PartialEq, Clone, Debug)] +pub enum ProxyType { + Nginx, +} + +pub trait Mergeable { + // merge with other, self overrides other + fn merge(&mut self, other: T); +} + +#[derive(Debug, Clone)] +pub struct OverrideRef { + pub id: uuid::Uuid, + pub override_of_id: Option, +} + +pub fn merge_override_vecs( + mut child: Vec, + parent: Vec, +) -> Vec { + let overridden_ids: std::collections::HashSet = + child.iter().filter_map(|r| r.override_of_id).collect(); + for item in parent { + if !overridden_ids.contains(&item.id) { + child.push(item); + } + } + child +} + +pub trait Overridable { + fn override_of_id(&self) -> Option; +} + +pub struct ProxyConfig { + pub id: uuid::Uuid, + pub name: String, + pub r#type: ProxyType, + pub description: Option, + pub parent_config_id: Option>, + // + pub server_blocks: HashMap, + pub upstreams: HashMap, + pub access_rules: HashMap, + pub cache_zones: HashMap, + pub limit_rules: HashMap, + pub limit_zones: HashMap, + pub location_blocks: HashMap, + pub log_settings: HashMap, + pub proxy_settings: HashMap, + pub rewrite_rules: HashMap, + pub ssl_certificates: HashMap, +} + +impl Mergeable for ProxyConfig { + fn merge(&mut self, other: ProxyConfig) { + use std::collections::HashSet; + + macro_rules! merge_overridable_field { + ($field:ident) => { + let overridden: HashSet = self + .$field + .values() + .filter_map(|v| v.override_of_id()) + .collect(); + for (id, value) in other.$field { + if !overridden.contains(&id) && !self.$field.contains_key(&id) { + self.$field.insert(id, value); + } + } + }; + } + + // server_blocks: merge matching entries (field-level), handle overrides + { + let overridden: HashSet = self + .server_blocks + .values() + .filter_map(|sb| sb.override_of_id) + .collect(); + for (id, block) in other.server_blocks { + if let Some(self_block) = self.server_blocks.get_mut(&id) { + self_block.merge(block); + } else if !overridden.contains(&id) { + self.server_blocks.insert(id, block); + } + } + } + + merge_overridable_field!(upstreams); + merge_overridable_field!(access_rules); + merge_overridable_field!(cache_zones); + merge_overridable_field!(limit_rules); + merge_overridable_field!(limit_zones); + merge_overridable_field!(location_blocks); + merge_overridable_field!(log_settings); + merge_overridable_field!(proxy_settings); + merge_overridable_field!(rewrite_rules); + + // ssl_certificates: simple merge (no override_of_id) + for (id, cert) in other.ssl_certificates { + self.ssl_certificates.entry(id).or_insert(cert); + } + } +} + +pub struct ServerBlockConfig { + pub id: uuid::Uuid, + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub override_of_id: Option, + // + pub access_rules: Vec, + pub location_blocks: Vec, + pub log_settings: Vec, + pub ssl_certificates: Vec, +} + +impl + From<( + crate::db::entities::server_block::Model, + Vec, + Vec, + Vec, + Vec, + )> for ServerBlockConfig +{ + fn from( + (model, access_rules, location_blocks, log_settings, ssl_certificates): ( + crate::db::entities::server_block::Model, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + ServerBlockConfig { + id: model.id, + server_name: model.server_name, + listen_port: model.listen_port, + ssl_enabled: model.ssl_enabled, + override_of_id: model.override_of_id, + access_rules, + location_blocks, + log_settings, + ssl_certificates, + } + } +} + +impl Mergeable for ServerBlockConfig { + fn merge(&mut self, other: ServerBlockConfig) { + // self (child) overrides other (parent): keep child's values, fill gaps from parent + if self.server_name.is_none() { + self.server_name = other.server_name; + } + // listen_port is non-optional, child always keeps its own + if self.ssl_enabled.is_none() { + self.ssl_enabled = other.ssl_enabled; + } + if self.override_of_id.is_none() { + self.override_of_id = other.override_of_id; + } + + self.access_rules = + merge_override_vecs(std::mem::take(&mut self.access_rules), other.access_rules); + self.location_blocks = merge_override_vecs( + std::mem::take(&mut self.location_blocks), + other.location_blocks, + ); + self.log_settings = + merge_override_vecs(std::mem::take(&mut self.log_settings), other.log_settings); + self.ssl_certificates = merge_override_vecs( + std::mem::take(&mut self.ssl_certificates), + other.ssl_certificates, + ); + } +} + +pub struct UpstreamConfig { + pub id: uuid::Uuid, + pub name: String, + pub target_host: String, + pub target_port: i32, + pub metadata: Option, + pub override_of_id: Option, + // + pub location_blocks: Vec, +} + +impl From<(crate::db::entities::upstream::Model, Vec)> for UpstreamConfig { + fn from( + (model, location_blocks): (crate::db::entities::upstream::Model, Vec), + ) -> Self { + UpstreamConfig { + id: model.id, + name: model.name, + target_host: model.target_host, + target_port: model.target_port, + metadata: model.metadata, + override_of_id: model.override_of_id, + location_blocks, + } + } +} + +pub struct AccessRuleConfig { + pub id: uuid::Uuid, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for AccessRuleConfig { + fn from(model: crate::db::entities::access_rule::Model) -> Self { + AccessRuleConfig { + id: model.id, + r#type: model.r#type, + ip_cidr: model.ip_cidr, + description: model.description, + priority: model.priority, + override_of_id: model.override_of_id, + } + } +} + +pub struct CacheZoneConfig { + pub id: uuid::Uuid, + pub name: String, + pub path: String, + pub size: String, + pub override_of_id: Option, +} + +impl From for CacheZoneConfig { + fn from(model: crate::db::entities::cache_zone::Model) -> Self { + CacheZoneConfig { + id: model.id, + name: model.name, + path: model.path, + size: model.size_limit, + override_of_id: model.override_of_id, + } + } +} + +pub struct LimitRuleConfig { + pub id: uuid::Uuid, + pub location_id: uuid::Uuid, + pub zone_id: uuid::Uuid, + pub burst: Option, + pub nodelay: Option, + pub is_deleted: bool, + + pub override_of_id: Option, +} + +impl From for LimitRuleConfig { + fn from(model: crate::db::entities::limit_rule::Model) -> Self { + LimitRuleConfig { + id: model.id, + location_id: model.location_id, + zone_id: model.zone_id, + burst: model.burst, + nodelay: model.nodelay, + is_deleted: model.is_deleted, + override_of_id: model.override_of_id, + } + } +} + +pub struct LimitZoneConfig { + pub id: uuid::Uuid, + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +impl From for LimitZoneConfig { + fn from(model: crate::db::entities::limit_zone::Model) -> Self { + LimitZoneConfig { + id: model.id, + name: model.name, + key: model.key, + rate: model.rate, + override_of_id: model.override_of_id, + } + } +} + +pub struct LocationBlockConfig { + pub id: uuid::Uuid, + pub server_id: uuid::Uuid, + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + pub metadata: Option, + pub override_of_id: Option, + // + pub access_rules: Vec, + pub limit_rules: Vec, + pub proxy_settings: Vec, + pub rewrite_rules: Vec, +} + +impl + From<( + crate::db::entities::location_block::Model, + Vec, + Vec, + Vec, + Vec, + )> for LocationBlockConfig +{ + fn from( + (model, access_rules, limit_rules, proxy_settings, rewrite_rules): ( + crate::db::entities::location_block::Model, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + LocationBlockConfig { + id: model.id, + server_id: model.server_id, + path_pattern: model.path_pattern, + proxy_pass_upstream_id: model.proxy_pass_upstream_id, + metadata: model.metadata, + override_of_id: model.override_of_id, + access_rules, + limit_rules, + proxy_settings, + rewrite_rules, + } + } +} + +pub struct LogSettingConfig { + pub id: uuid::Uuid, + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +impl From for LogSettingConfig { + fn from(model: crate::db::entities::log_setting::Model) -> Self { + LogSettingConfig { + id: model.id, + access_log_path: model.access_log_path, + error_log_path: model.error_log_path, + log_level: model.log_level, + override_of_id: model.override_of_id, + } + } +} + +pub struct SslCertificateConfig { + pub id: uuid::Uuid, + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: chrono::DateTime, +} + +impl From for SslCertificateConfig { + fn from(model: crate::db::entities::ssl_certificate::Model) -> Self { + SslCertificateConfig { + id: model.id, + name: model.name, + cert_path: model.cert_path, + key_path: model.key_path, + expiry_date: model.expiry_date.and_utc(), + } + } +} + +pub struct ProxySettingConfig { + pub id: uuid::Uuid, + pub location_id: uuid::Uuid, + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +impl From for ProxySettingConfig { + fn from(model: crate::db::entities::proxy_setting::Model) -> Self { + ProxySettingConfig { + id: model.id, + location_id: model.location_id, + read_timeout: model.read_timeout, + connect_timeout: model.connect_timeout, + buffer_size: model.buffer_size, + cache_enabled: model.cache_enabled, + cache_zone: model.cache_zone, + override_of_id: model.override_of_id, + } + } +} + +pub struct RewriteRuleConfig { + pub id: uuid::Uuid, + pub location_id: uuid::Uuid, + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for RewriteRuleConfig { + fn from(model: crate::db::entities::rewrite_rule::Model) -> Self { + RewriteRuleConfig { + id: model.id, + location_id: model.location_id, + pattern: model.pattern, + replacement: model.replacement, + flag: model.flag, + priority: model.priority, + override_of_id: model.override_of_id, + } + } +} + +// ── Overridable implementations ── + +impl Overridable for ServerBlockConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for UpstreamConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for AccessRuleConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for CacheZoneConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for LimitRuleConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for LimitZoneConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for LocationBlockConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for LogSettingConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for ProxySettingConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} +impl Overridable for RewriteRuleConfig { + fn override_of_id(&self) -> Option { + self.override_of_id + } +} + +// ── CRUD types ── + +pub struct ProxyConfigSummary { + pub id: uuid::Uuid, + pub name: String, + pub description: Option, + pub is_template: bool, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +impl From for ProxyConfigSummary { + fn from(m: crate::db::entities::proxy_config::Model) -> Self { + Self { + id: m.id, + name: m.name, + description: m.description, + is_template: m.is_template, + created_at: m.created_at.and_utc(), + updated_at: m.updated_at.and_utc(), + } + } +} + +pub struct CreateProxyConfigParams { + pub name: String, + pub description: Option, + pub is_template: bool, +} + +pub struct UpdateProxyConfigParams { + pub name: Option, + pub description: Option, + pub is_template: Option, +} + +// ── Agent config binding ── + +pub struct AgentConfigBinding { + pub id: uuid::Uuid, + pub agent_id: Option, + pub group_id: Option, + pub config_id: uuid::Uuid, + pub is_active: bool, + pub applied_at: chrono::DateTime, +} + +impl From for AgentConfigBinding { + fn from(m: crate::db::entities::agent_config_binding::Model) -> Self { + Self { + id: m.id, + agent_id: m.agent_id, + group_id: m.group_id, + config_id: m.config_id, + is_active: m.is_active, + applied_at: m.applied_at.and_utc(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + fn make_id() -> uuid::Uuid { + uuid::Uuid::new_v4() + } + + #[test] + fn test_merge_override_vecs_child_overrides_parent() { + let parent_id = make_id(); + let child_id = make_id(); + let child = vec![OverrideRef { + id: child_id, + override_of_id: Some(parent_id), + }]; + let parent = vec![OverrideRef { + id: parent_id, + override_of_id: None, + }]; + let result = merge_override_vecs(child, parent); + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, child_id); + } + + #[test] + fn test_merge_override_vecs_removes_overridden_parent() { + let parent_id = make_id(); + let child_override = make_id(); + let child = vec![OverrideRef { + id: child_override, + override_of_id: Some(parent_id), + }]; + let parent = vec![OverrideRef { + id: parent_id, + override_of_id: None, + }]; + let result = merge_override_vecs(child, parent); + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, child_override); + } + + #[test] + fn test_merge_override_vecs_empty_child() { + let parent = vec![OverrideRef { + id: make_id(), + override_of_id: None, + }]; + let result = merge_override_vecs(vec![], parent.clone()); + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, parent[0].id); + } + + #[test] + fn test_server_block_merge_child_overrides_parent() { + let id = make_id(); + let mut child = ServerBlockConfig { + id, + server_name: Some(vec!["child.example.com".to_string()]), + listen_port: 443, + ssl_enabled: Some(true), + override_of_id: None, + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + let parent = ServerBlockConfig { + id, + server_name: Some(vec!["parent.example.com".to_string()]), + listen_port: 80, + ssl_enabled: Some(false), + override_of_id: None, + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + child.merge(parent); + // child keeps its own values (self overrides other) + assert_eq!( + child.server_name, + Some(vec!["child.example.com".to_string()]) + ); + assert_eq!(child.listen_port, 443); + assert_eq!(child.ssl_enabled, Some(true)); + } + + #[test] + fn test_server_block_merge_fills_from_parent() { + let id = make_id(); + let mut child = ServerBlockConfig { + id, + server_name: None, + listen_port: 443, + ssl_enabled: None, + override_of_id: None, + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + let parent = ServerBlockConfig { + id, + server_name: Some(vec!["parent.example.com".to_string()]), + listen_port: 80, + ssl_enabled: Some(false), + override_of_id: None, + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + child.merge(parent); + // child fills missing optional fields from parent + assert_eq!( + child.server_name, + Some(vec!["parent.example.com".to_string()]) + ); + assert_eq!(child.listen_port, 443); // non-optional: child keeps its own + assert_eq!(child.ssl_enabled, Some(false)); + } + + #[test] + fn test_proxy_config_merge_server_block_overrides() { + let sb_id = make_id(); + let child_sb = ServerBlockConfig { + id: sb_id, + server_name: Some(vec!["child.example.com".to_string()]), + listen_port: 443, + ssl_enabled: Some(true), + override_of_id: Some(make_id()), + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + let parent_sb = ServerBlockConfig { + id: sb_id, + server_name: Some(vec!["parent.example.com".to_string()]), + listen_port: 80, + ssl_enabled: Some(false), + override_of_id: None, + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + let parent_id = make_id(); + let mut child_proxy = ProxyConfig { + id: parent_id, + name: "child".to_string(), + r#type: ProxyType::Nginx, + description: None, + parent_config_id: None, + server_blocks: HashMap::from([(child_sb.id, child_sb)]), + upstreams: HashMap::new(), + access_rules: HashMap::new(), + cache_zones: HashMap::new(), + limit_rules: HashMap::new(), + limit_zones: HashMap::new(), + location_blocks: HashMap::new(), + log_settings: HashMap::new(), + proxy_settings: HashMap::new(), + rewrite_rules: HashMap::new(), + ssl_certificates: HashMap::new(), + }; + let parent_proxy = ProxyConfig { + id: make_id(), + name: "parent".to_string(), + r#type: ProxyType::Nginx, + description: None, + parent_config_id: None, + server_blocks: HashMap::from([(parent_sb.id, parent_sb)]), + upstreams: HashMap::new(), + access_rules: HashMap::new(), + cache_zones: HashMap::new(), + limit_rules: HashMap::new(), + limit_zones: HashMap::new(), + location_blocks: HashMap::new(), + log_settings: HashMap::new(), + proxy_settings: HashMap::new(), + rewrite_rules: HashMap::new(), + ssl_certificates: HashMap::new(), + }; + child_proxy.merge(parent_proxy); + assert_eq!(child_proxy.server_blocks.len(), 1); + let merged_sb = &child_proxy.server_blocks[&sb_id]; + assert_eq!( + merged_sb.server_name, + Some(vec!["child.example.com".to_string()]) + ); + assert_eq!(merged_sb.listen_port, 443); + assert_eq!(merged_sb.ssl_enabled, Some(true)); + } + + #[test] + fn test_proxy_config_merge_adds_parent_server_block() { + let child_sb_id = make_id(); + let parent_sb_id = make_id(); + let child_sb = ServerBlockConfig { + id: child_sb_id, + server_name: Some(vec!["child.example.com".to_string()]), + listen_port: 443, + ssl_enabled: Some(true), + override_of_id: None, + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + let parent_sb = ServerBlockConfig { + id: parent_sb_id, + server_name: Some(vec!["parent.example.com".to_string()]), + listen_port: 80, + ssl_enabled: None, + override_of_id: None, + access_rules: vec![], + location_blocks: vec![], + log_settings: vec![], + ssl_certificates: vec![], + }; + let parent_id = make_id(); + let mut child_proxy = ProxyConfig { + id: parent_id, + name: "child".to_string(), + r#type: ProxyType::Nginx, + description: None, + parent_config_id: None, + server_blocks: HashMap::from([(child_sb.id, child_sb)]), + upstreams: HashMap::new(), + access_rules: HashMap::new(), + cache_zones: HashMap::new(), + limit_rules: HashMap::new(), + limit_zones: HashMap::new(), + location_blocks: HashMap::new(), + log_settings: HashMap::new(), + proxy_settings: HashMap::new(), + rewrite_rules: HashMap::new(), + ssl_certificates: HashMap::new(), + }; + let parent_proxy = ProxyConfig { + id: make_id(), + name: "parent".to_string(), + r#type: ProxyType::Nginx, + description: None, + parent_config_id: None, + server_blocks: HashMap::from([(parent_sb.id, parent_sb)]), + upstreams: HashMap::new(), + access_rules: HashMap::new(), + cache_zones: HashMap::new(), + limit_rules: HashMap::new(), + limit_zones: HashMap::new(), + location_blocks: HashMap::new(), + log_settings: HashMap::new(), + proxy_settings: HashMap::new(), + rewrite_rules: HashMap::new(), + ssl_certificates: HashMap::new(), + }; + child_proxy.merge(parent_proxy); + assert_eq!(child_proxy.server_blocks.len(), 2); + // Both child and parent server blocks should be present + assert!(child_proxy.server_blocks.contains_key(&child_sb_id)); + assert!(child_proxy.server_blocks.contains_key(&parent_sb_id)); + } +} diff --git a/apps/nxmesh-master/src/service/proxy/upstream/mod.rs b/apps/nxmesh-master/src/service/proxy/upstream/mod.rs new file mode 100644 index 0000000..86d1d5f --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/upstream/mod.rs @@ -0,0 +1,139 @@ +use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, prelude::*}; +use uuid::Uuid; + +use crate::service::proxy::types::{ProxyServiceError, ProxyServiceResult, UpstreamConfig}; + +pub struct CreateUpstreamParams { + pub config_id: Uuid, + pub name: String, + pub target_host: String, + pub target_port: i32, + pub metadata: Option, + pub override_of_id: Option, +} + +pub struct UpdateUpstreamParams { + pub name: Option, + pub target_host: Option, + pub target_port: Option, + pub metadata: Option>, + pub override_of_id: Option>, +} + +fn model_to_config(model: crate::db::entities::upstream::Model) -> UpstreamConfig { + UpstreamConfig { + id: model.id, + name: model.name, + target_host: model.target_host, + target_port: model.target_port, + metadata: model.metadata, + override_of_id: model.override_of_id, + location_blocks: vec![], + } +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait UpstreamService: Send + Sync + 'static { + async fn get(&self, id: Uuid) -> ProxyServiceResult; + async fn list_by_config(&self, config_id: Uuid) -> ProxyServiceResult>; + async fn create(&self, params: CreateUpstreamParams) -> ProxyServiceResult; + async fn update( + &self, + id: Uuid, + params: UpdateUpstreamParams, + ) -> ProxyServiceResult; + async fn delete(&self, id: Uuid) -> ProxyServiceResult; +} + +pub(crate) struct UpstreamServiceImpl { + db: DatabaseConnection, +} + +impl UpstreamServiceImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl UpstreamService for UpstreamServiceImpl { + async fn get(&self, id: Uuid) -> ProxyServiceResult { + use crate::db::entities::upstream; + + let model = upstream::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + Ok(model_to_config(model)) + } + + async fn list_by_config(&self, config_id: Uuid) -> ProxyServiceResult> { + use crate::db::entities::upstream; + + let models = upstream::Entity::find() + .filter(upstream::Column::ConfigId.eq(config_id)) + .all(&self.db) + .await?; + + Ok(models.into_iter().map(model_to_config).collect()) + } + + async fn create(&self, params: CreateUpstreamParams) -> ProxyServiceResult { + use crate::db::entities::upstream::ActiveModel; + + let model = ActiveModel { + id: Set(Uuid::new_v4()), + config_id: Set(params.config_id), + name: Set(params.name), + target_host: Set(params.target_host), + target_port: Set(params.target_port), + metadata: Set(params.metadata), + override_of_id: Set(params.override_of_id), + }; + + let result = model.insert(&self.db).await?; + Ok(model_to_config(result)) + } + + async fn update( + &self, + id: Uuid, + params: UpdateUpstreamParams, + ) -> ProxyServiceResult { + use crate::db::entities::upstream::{ActiveModel, Entity as UpstreamEntity}; + + let existing = UpstreamEntity::find_by_id(id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + let mut model: ActiveModel = existing.into(); + if let Some(name) = params.name { + model.name = Set(name); + } + if let Some(target_host) = params.target_host { + model.target_host = Set(target_host); + } + if let Some(target_port) = params.target_port { + model.target_port = Set(target_port); + } + if let Some(metadata) = params.metadata { + model.metadata = Set(metadata); + } + if let Some(override_of_id) = params.override_of_id { + model.override_of_id = Set(override_of_id); + } + + let result = model.update(&self.db).await?; + Ok(model_to_config(result)) + } + + async fn delete(&self, id: Uuid) -> ProxyServiceResult { + let result = crate::db::entities::upstream::Entity::delete_by_id(id) + .exec(&self.db) + .await?; + Ok(result.rows_affected > 0) + } +} diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index fb7c7c5..ba9bd77 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -2,6 +2,7 @@ pub use sea_orm_migration::prelude::*; mod m20260301_000001_create_agents; mod m20260301_000002_create_public_key_revokaction; +mod m20260620_111325_create_proxy_tables; pub struct Migrator; @@ -11,6 +12,7 @@ impl MigratorTrait for Migrator { vec![ Box::new(m20260301_000001_create_agents::Migration), Box::new(m20260301_000002_create_public_key_revokaction::Migration), + Box::new(m20260620_111325_create_proxy_tables::Migration), ] } } diff --git a/crates/migration/src/m20260620_111325_create_proxy_tables.rs b/crates/migration/src/m20260620_111325_create_proxy_tables.rs new file mode 100644 index 0000000..9f9416c --- /dev/null +++ b/crates/migration/src/m20260620_111325_create_proxy_tables.rs @@ -0,0 +1,902 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(AgentGroup::Table) + .if_not_exists() + .col( + ColumnDef::new(AgentGroup::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AgentGroup::Name).string().not_null()) + .col(ColumnDef::new(AgentGroup::Description).string()) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(Agents::Table) + .add_column(ColumnDef::new(Agents::GroupId).uuid().null()) + .add_foreign_key( + TableForeignKey::new() + .name("fk_agents_group_id") + .from_tbl(Agents::Table) + .to_tbl(AgentGroup::Table) + .from_col(Agents::GroupId) + .to_col(AgentGroup::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(ProxyConfig::Table) + .if_not_exists() + .col( + ColumnDef::new(ProxyConfig::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(ProxyConfig::Name).string().not_null()) + .col(ColumnDef::new(ProxyConfig::Description).string()) + .col(ColumnDef::new(ProxyConfig::IsTemplate).boolean().not_null()) + .col( + ColumnDef::new(ProxyConfig::CreatedAt) + .timestamp() + .not_null(), + ) + .col( + ColumnDef::new(ProxyConfig::UpdatedAt) + .timestamp() + .not_null(), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(SSLCertificate::Table) + .if_not_exists() + .col( + ColumnDef::new(SSLCertificate::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(SSLCertificate::Name).string().not_null()) + .col(ColumnDef::new(SSLCertificate::CertPath).string().not_null()) + .col(ColumnDef::new(SSLCertificate::KeyPath).string().not_null()) + .col( + ColumnDef::new(SSLCertificate::ExpiryDate) + .timestamp() + .not_null(), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(CacheZone::Table) + .if_not_exists() + .col( + ColumnDef::new(CacheZone::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(CacheZone::Name).string().not_null()) + .col(ColumnDef::new(CacheZone::Path).string().not_null()) + .col(ColumnDef::new(CacheZone::SizeLimit).string().not_null()) + .col(ColumnDef::new(CacheZone::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_cache_zone_override_of") + .from_tbl(CacheZone::Table) + .to_tbl(CacheZone::Table) + .from_col(CacheZone::OverrideOfId) + .to_col(CacheZone::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(LimitZone::Table) + .if_not_exists() + .col( + ColumnDef::new(LimitZone::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(LimitZone::Name).string().not_null()) + .col(ColumnDef::new(LimitZone::Key).string().not_null()) + .col(ColumnDef::new(LimitZone::Rate).string().not_null()) + .col(ColumnDef::new(LimitZone::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_limit_zone_override_of") + .from_tbl(LimitZone::Table) + .to_tbl(LimitZone::Table) + .from_col(LimitZone::OverrideOfId) + .to_col(LimitZone::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(AgentConfigBinding::Table) + .if_not_exists() + .col( + ColumnDef::new(AgentConfigBinding::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AgentConfigBinding::AgentId).uuid()) + .col(ColumnDef::new(AgentConfigBinding::GroupId).uuid()) + .col( + ColumnDef::new(AgentConfigBinding::ConfigId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(AgentConfigBinding::IsActive) + .boolean() + .not_null(), + ) + .col( + ColumnDef::new(AgentConfigBinding::AppliedAt) + .timestamp() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk_agent_config_binding_agent") + .from_tbl(AgentConfigBinding::Table) + .to_tbl(Agents::Table) + .from_col(AgentConfigBinding::AgentId) + .to_col(Agents::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_agent_config_binding_group") + .from_tbl(AgentConfigBinding::Table) + .to_tbl(AgentGroup::Table) + .from_col(AgentConfigBinding::GroupId) + .to_col(AgentGroup::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_agent_config_binding_config") + .from_tbl(AgentConfigBinding::Table) + .to_tbl(ProxyConfig::Table) + .from_col(AgentConfigBinding::ConfigId) + .to_col(ProxyConfig::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(ConfigInheritance::Table) + .if_not_exists() + .col( + ColumnDef::new(ConfigInheritance::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col( + ColumnDef::new(ConfigInheritance::ChildConfigId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(ConfigInheritance::ParentConfigId) + .uuid() + .not_null(), + ) + .col(ColumnDef::new(ConfigInheritance::Priority).integer()) + .col( + ColumnDef::new(ConfigInheritance::AppliedAt) + .timestamp() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk_config_inheritance_child") + .from_tbl(ConfigInheritance::Table) + .to_tbl(ProxyConfig::Table) + .from_col(ConfigInheritance::ChildConfigId) + .to_col(ProxyConfig::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_config_inheritance_parent") + .from_tbl(ConfigInheritance::Table) + .to_tbl(ProxyConfig::Table) + .from_col(ConfigInheritance::ParentConfigId) + .to_col(ProxyConfig::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(Upstream::Table) + .if_not_exists() + .col(ColumnDef::new(Upstream::Id).uuid().not_null().primary_key()) + .col(ColumnDef::new(Upstream::ConfigId).uuid().not_null()) + .col(ColumnDef::new(Upstream::Name).string().not_null()) + .col(ColumnDef::new(Upstream::TargetHost).string().not_null()) + .col(ColumnDef::new(Upstream::TargetPort).integer().not_null()) + .col(ColumnDef::new(Upstream::Metadata).json_binary()) + .col(ColumnDef::new(Upstream::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_upstream_config") + .from_tbl(Upstream::Table) + .to_tbl(ProxyConfig::Table) + .from_col(Upstream::ConfigId) + .to_col(ProxyConfig::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_upstream_override_of") + .from_tbl(Upstream::Table) + .to_tbl(Upstream::Table) + .from_col(Upstream::OverrideOfId) + .to_col(Upstream::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(ServerBlock::Table) + .if_not_exists() + .col( + ColumnDef::new(ServerBlock::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(ServerBlock::ConfigId).uuid().not_null()) + .col( + ColumnDef::new(ServerBlock::ServerName) + .array(ColumnType::String(StringLen::None)), + ) + .col(ColumnDef::new(ServerBlock::ListenPort).integer().not_null()) + .col(ColumnDef::new(ServerBlock::SslEnabled).boolean()) + .col(ColumnDef::new(ServerBlock::SslCertId).uuid()) + .col(ColumnDef::new(ServerBlock::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_server_block_config") + .from_tbl(ServerBlock::Table) + .to_tbl(ProxyConfig::Table) + .from_col(ServerBlock::ConfigId) + .to_col(ProxyConfig::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_server_block_ssl_cert") + .from_tbl(ServerBlock::Table) + .to_tbl(SSLCertificate::Table) + .from_col(ServerBlock::SslCertId) + .to_col(SSLCertificate::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_server_block_override_of") + .from_tbl(ServerBlock::Table) + .to_tbl(ServerBlock::Table) + .from_col(ServerBlock::OverrideOfId) + .to_col(ServerBlock::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(LocationBlock::Table) + .if_not_exists() + .col( + ColumnDef::new(LocationBlock::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(LocationBlock::ServerId).uuid().not_null()) + .col( + ColumnDef::new(LocationBlock::PathPattern) + .string() + .not_null(), + ) + .col(ColumnDef::new(LocationBlock::ProxyPassUpstreamId).uuid()) + .col(ColumnDef::new(LocationBlock::Metadata).json_binary()) + .col(ColumnDef::new(LocationBlock::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_location_block_server") + .from_tbl(LocationBlock::Table) + .to_tbl(ServerBlock::Table) + .from_col(LocationBlock::ServerId) + .to_col(ServerBlock::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_location_block_upstream") + .from_tbl(LocationBlock::Table) + .to_tbl(Upstream::Table) + .from_col(LocationBlock::ProxyPassUpstreamId) + .to_col(Upstream::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_location_block_override_of") + .from_tbl(LocationBlock::Table) + .to_tbl(LocationBlock::Table) + .from_col(LocationBlock::OverrideOfId) + .to_col(LocationBlock::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(AccessRule::Table) + .if_not_exists() + .col( + ColumnDef::new(AccessRule::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(AccessRule::ServerId).uuid()) + .col(ColumnDef::new(AccessRule::LocationId).uuid()) + .col( + ColumnDef::new(AccessRule::Type) + .string() + .not_null() + .comment("allow or deny"), + ) + .col( + ColumnDef::new(AccessRule::IpCidr) + .string() + .not_null() + .comment("IP address or CIDR range. 0.0.0.0/0 means all IPs"), + ) + .col(ColumnDef::new(AccessRule::Description).string()) + .col( + ColumnDef::new(AccessRule::Priority) + .integer() + .not_null() + .comment( + "Priority of the access rule. Lower number means higher priority.", + ), + ) + .col(ColumnDef::new(AccessRule::IsDeleted).boolean().not_null()) + .col(ColumnDef::new(AccessRule::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_access_rule_server") + .from_tbl(AccessRule::Table) + .to_tbl(ServerBlock::Table) + .from_col(AccessRule::ServerId) + .to_col(ServerBlock::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_access_rule_location") + .from_tbl(AccessRule::Table) + .to_tbl(LocationBlock::Table) + .from_col(AccessRule::LocationId) + .to_col(LocationBlock::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_access_rule_override_of") + .from_tbl(AccessRule::Table) + .to_tbl(AccessRule::Table) + .from_col(AccessRule::OverrideOfId) + .to_col(AccessRule::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(RewriteRule::Table) + .if_not_exists() + .col( + ColumnDef::new(RewriteRule::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(RewriteRule::LocationId).uuid().not_null()) + .col(ColumnDef::new(RewriteRule::Pattern).string().not_null()) + .col(ColumnDef::new(RewriteRule::Replacement).string().not_null()) + .col(ColumnDef::new(RewriteRule::Flag).string()) + .col(ColumnDef::new(RewriteRule::Priority).integer().not_null()) + .col(ColumnDef::new(RewriteRule::IsDeleted).boolean().not_null()) + .col(ColumnDef::new(RewriteRule::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_rewrite_rule_location") + .from_tbl(RewriteRule::Table) + .to_tbl(LocationBlock::Table) + .from_col(RewriteRule::LocationId) + .to_col(LocationBlock::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_rewrite_rule_override_of") + .from_tbl(RewriteRule::Table) + .to_tbl(RewriteRule::Table) + .from_col(RewriteRule::OverrideOfId) + .to_col(RewriteRule::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(ProxySetting::Table) + .if_not_exists() + .col( + ColumnDef::new(ProxySetting::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(ProxySetting::LocationId).uuid().not_null()) + .col(ColumnDef::new(ProxySetting::ReadTimeout).integer()) + .col(ColumnDef::new(ProxySetting::ConnectTimeout).integer()) + .col(ColumnDef::new(ProxySetting::BufferSize).integer()) + .col(ColumnDef::new(ProxySetting::CacheEnabled).boolean()) + .col(ColumnDef::new(ProxySetting::CacheZone).uuid()) + .col(ColumnDef::new(ProxySetting::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_proxy_setting_location") + .from_tbl(ProxySetting::Table) + .to_tbl(LocationBlock::Table) + .from_col(ProxySetting::LocationId) + .to_col(LocationBlock::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_proxy_setting_cache_zone") + .from_tbl(ProxySetting::Table) + .to_tbl(CacheZone::Table) + .from_col(ProxySetting::CacheZone) + .to_col(CacheZone::Id) + .on_delete(ForeignKeyAction::SetNull), + ) + .foreign_key( + ForeignKey::create() + .name("fk_proxy_setting_override_of") + .from_tbl(ProxySetting::Table) + .to_tbl(ProxySetting::Table) + .from_col(ProxySetting::OverrideOfId) + .to_col(ProxySetting::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(LimitRule::Table) + .if_not_exists() + .col( + ColumnDef::new(LimitRule::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(LimitRule::LocationId).uuid().not_null()) + .col(ColumnDef::new(LimitRule::ZoneId).uuid().not_null()) + .col(ColumnDef::new(LimitRule::Burst).integer()) + .col(ColumnDef::new(LimitRule::Nodelay).boolean()) + .col(ColumnDef::new(LimitRule::IsDeleted).boolean().not_null()) + .col(ColumnDef::new(LimitRule::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_limit_rule_location") + .from_tbl(LimitRule::Table) + .to_tbl(LocationBlock::Table) + .from_col(LimitRule::LocationId) + .to_col(LocationBlock::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_limit_rule_zone") + .from_tbl(LimitRule::Table) + .to_tbl(LimitZone::Table) + .from_col(LimitRule::ZoneId) + .to_col(LimitZone::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_limit_rule_override_of") + .from_tbl(LimitRule::Table) + .to_tbl(LimitRule::Table) + .from_col(LimitRule::OverrideOfId) + .to_col(LimitRule::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(LogSetting::Table) + .if_not_exists() + .col( + ColumnDef::new(LogSetting::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(LogSetting::ServerId).uuid().not_null()) + .col(ColumnDef::new(LogSetting::AccessLogPath).string()) + .col(ColumnDef::new(LogSetting::ErrorLogPath).string()) + .col(ColumnDef::new(LogSetting::LogLevel).string()) + .col(ColumnDef::new(LogSetting::OverrideOfId).uuid()) + .foreign_key( + ForeignKey::create() + .name("fk_log_setting_server") + .from_tbl(LogSetting::Table) + .to_tbl(ServerBlock::Table) + .from_col(LogSetting::ServerId) + .to_col(ServerBlock::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_log_setting_override_of") + .from_tbl(LogSetting::Table) + .to_tbl(LogSetting::Table) + .from_col(LogSetting::OverrideOfId) + .to_col(LogSetting::Id) + .on_delete(ForeignKeyAction::SetNull) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(LogSetting::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(LimitRule::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(ProxySetting::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(RewriteRule::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(AccessRule::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(LocationBlock::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(ServerBlock::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Upstream::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(ConfigInheritance::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(AgentConfigBinding::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(LimitZone::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(CacheZone::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(SSLCertificate::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(ProxyConfig::Table).to_owned()) + .await?; + manager + .alter_table( + Table::alter() + .table(Agents::Table) + .drop_foreign_key(Alias::new("fk_agents_group_id")) + .to_owned(), + ) + .await?; + manager + .alter_table( + Table::alter() + .table(Agents::Table) + .drop_column(Agents::GroupId) + .to_owned(), + ) + .await?; + manager + .drop_table(Table::drop().table(AgentGroup::Table).to_owned()) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Agents { + Table, + Id, + GroupId, +} + +#[derive(DeriveIden)] +enum AgentGroup { + Table, + Id, + Name, + Description, +} + +#[derive(DeriveIden)] +enum AgentConfigBinding { + Table, + Id, + AgentId, + GroupId, + ConfigId, + IsActive, + AppliedAt, +} + +#[derive(DeriveIden)] +enum ProxyConfig { + Table, + Id, + Name, + Description, + IsTemplate, + CreatedAt, + UpdatedAt, +} + +#[derive(DeriveIden)] +enum ConfigInheritance { + Table, + Id, + ChildConfigId, + ParentConfigId, + Priority, + AppliedAt, +} + +#[derive(DeriveIden)] +enum SSLCertificate { + Table, + Id, + Name, + CertPath, + KeyPath, + ExpiryDate, +} + +#[derive(DeriveIden)] +enum CacheZone { + Table, + Id, + Name, + Path, + SizeLimit, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum LimitZone { + Table, + Id, + Name, + Key, + Rate, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum Upstream { + Table, + Id, + ConfigId, + Name, + TargetHost, + TargetPort, + Metadata, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum ServerBlock { + Table, + Id, + ConfigId, + ServerName, + ListenPort, + SslEnabled, + SslCertId, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum LocationBlock { + Table, + Id, + ServerId, + PathPattern, + ProxyPassUpstreamId, + Metadata, + OverrideOfId, +} +#[derive(DeriveIden)] +enum AccessRule { + Table, + Id, + ServerId, + LocationId, + Type, + IpCidr, + Priority, + Description, + IsDeleted, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum RewriteRule { + Table, + Id, + LocationId, + Pattern, + Replacement, + Flag, + Priority, + IsDeleted, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum ProxySetting { + Table, + Id, + LocationId, + ReadTimeout, + ConnectTimeout, + BufferSize, + CacheEnabled, + CacheZone, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum LimitRule { + Table, + Id, + LocationId, + ZoneId, + Burst, + Nodelay, + IsDeleted, + OverrideOfId, +} + +#[derive(DeriveIden)] +enum LogSetting { + Table, + Id, + ServerId, + AccessLogPath, + ErrorLogPath, + LogLevel, + OverrideOfId, +} diff --git a/crates/nxmesh-proto/src/lib.rs b/crates/nxmesh-proto/src/lib.rs index 249fd51..686206d 100644 --- a/crates/nxmesh-proto/src/lib.rs +++ b/crates/nxmesh-proto/src/lib.rs @@ -12,114 +12,3 @@ pub use agent::*; pub mod auth; #[allow(ambiguous_glob_reexports)] pub use tonic_async_interceptor::*; - -#[cfg(test)] -mod tests { - use prost::Message; - - use crate::agent::{ - AgentMessage, ConfigApplyStatus, ConfigStatus, DeploymentMode, Error, MasterMessage, - MetricType, RegistrationRequest, agent_message, master_message, - }; - - #[test] - fn agent_message_round_trip_with_registration_payload() { - let msg = AgentMessage { - agent_id: "agent-1".to_string(), - timestamp: 123, - payload: Some(agent_message::Payload::Registration(RegistrationRequest { - hostname: "node-1".to_string(), - ip_address: "127.0.0.1".to_string(), - version: "1.0.0".to_string(), - capabilities: vec!["reload".to_string(), "metrics".to_string()], - labels: std::collections::HashMap::from([ - ("region".to_string(), "dev".to_string()), - ("tier".to_string(), "edge".to_string()), - ]), - deployment_mode: DeploymentMode::Standalone as i32, - })), - }; - - let encoded = msg.encode_to_vec(); - let decoded = AgentMessage::decode(encoded.as_slice()); - assert!(decoded.is_ok()); - let decoded = decoded.unwrap_or_else(|_| unreachable!()); - - assert_eq!(decoded.agent_id, "agent-1"); - assert_eq!(decoded.timestamp, 123); - - match decoded.payload { - Some(agent_message::Payload::Registration(payload)) => { - assert_eq!(payload.hostname, "node-1"); - assert_eq!(payload.ip_address, "127.0.0.1"); - assert_eq!(payload.version, "1.0.0"); - assert_eq!(payload.capabilities.len(), 2); - assert_eq!(payload.labels.get("region"), Some(&"dev".to_string())); - assert_eq!(payload.deployment_mode, DeploymentMode::Standalone as i32); - } - _ => unreachable!(), - } - } - - #[test] - fn master_message_round_trip_with_error_payload() { - let msg = MasterMessage { - timestamp: 999, - payload: Some(master_message::Payload::Error(Error { - code: "E_CONFIG_INVALID".to_string(), - message: "invalid config".to_string(), - details: std::collections::HashMap::from([ - ("file".to_string(), "site.conf".to_string()), - ("line".to_string(), "42".to_string()), - ]), - })), - }; - - let encoded = msg.encode_to_vec(); - let decoded = MasterMessage::decode(encoded.as_slice()); - assert!(decoded.is_ok()); - let decoded = decoded.unwrap_or_else(|_| unreachable!()); - - assert_eq!(decoded.timestamp, 999); - match decoded.payload { - Some(master_message::Payload::Error(err)) => { - assert_eq!(err.code, "E_CONFIG_INVALID"); - assert_eq!(err.message, "invalid config"); - assert_eq!(err.details.get("line"), Some(&"42".to_string())); - } - _ => unreachable!(), - } - } - - #[test] - fn enum_integer_mappings_are_stable() { - assert_eq!(DeploymentMode::Unspecified as i32, 0); - assert_eq!(DeploymentMode::DockerSidecar as i32, 1); - assert_eq!(DeploymentMode::KubernetesSidecar as i32, 2); - assert_eq!(DeploymentMode::Standalone as i32, 3); - - assert_eq!(ConfigApplyStatus::Unspecified as i32, 0); - assert_eq!(ConfigApplyStatus::Pending as i32, 1); - assert_eq!(ConfigApplyStatus::Validating as i32, 2); - assert_eq!(ConfigApplyStatus::Applying as i32, 3); - assert_eq!(ConfigApplyStatus::Success as i32, 4); - assert_eq!(ConfigApplyStatus::Failed as i32, 5); - assert_eq!(ConfigApplyStatus::RolledBack as i32, 6); - - assert_eq!(MetricType::Unspecified as i32, 0); - assert_eq!(MetricType::Gauge as i32, 1); - assert_eq!(MetricType::Counter as i32, 2); - assert_eq!(MetricType::Histogram as i32, 3); - } - - #[test] - fn config_status_defaults_are_proto3_zero_values() { - let status = ConfigStatus::default(); - - assert_eq!(status.config_id, ""); - assert_eq!(status.version, 0); - assert_eq!(status.status, ConfigApplyStatus::Unspecified as i32); - assert_eq!(status.error_message, ""); - assert_eq!(status.applied_at, 0); - } -} diff --git a/justfile b/justfile index 4330597..6928399 100644 --- a/justfile +++ b/justfile @@ -54,7 +54,7 @@ dev-master *ARGS: dev-agent *ARGS: @echo "🔧 Starting Rust agent..." - cargo watch -w apps/nxmesh-agent -x 'run --bin nxmesh-agent -- {{ ARGS }}' + cargo watch -w apps/nxmesh-agent -x 'run --bin nxmesh-agent --release -- {{ ARGS }}' # Start Vite frontend development server dev-frontend: