Compare commits
42 Commits
feature/da
...
feature/ng
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f560c981b | ||
|
|
a7863b9e87 | ||
|
|
b7891ec200 | ||
|
|
e44a67f5a8 | ||
|
|
6d1df8dcca | ||
|
|
c7cc006212 | ||
|
|
d976c22683 | ||
|
|
7c233b5f77 | ||
|
|
2e758c67fc | ||
|
|
692b17534c | ||
|
|
9c07fce607 | ||
|
|
2dbab85c87 | ||
| be1d2e3fa9 | |||
| 143a280d26 | |||
| 4376167d74 | |||
| 24d3f46a5c | |||
| 3c2cda88f1 | |||
|
|
c0d243f661 | ||
|
|
f7162f0c17 | ||
|
|
d6a829c529 | ||
|
|
9991b20b28 | ||
|
|
8dbbeb4b24 | ||
|
|
93a36cb101 | ||
|
|
f65cae7f33 | ||
|
|
5f439f6231 | ||
|
|
c52c0b3103 | ||
|
|
153c092abe | ||
|
|
cb4ad27e89 | ||
|
|
9f72cd4bbb | ||
|
|
7173f504ed | ||
|
|
3b7916d66c | ||
|
|
94d5e178d2 | ||
|
|
f428f18f87 | ||
|
|
4252154420 | ||
|
|
984334c232 | ||
|
|
98e07715fa | ||
|
|
7a0111c4c5 | ||
|
|
0575c34fd6 | ||
|
|
1daac01583 | ||
|
|
60af2c5b80 | ||
|
|
e831640540 | ||
|
|
a023cbc082 |
39
.devcontainer/devcontainer-lock.json
Normal file
39
.devcontainer/devcontainer-lock.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-extra/features/act": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "ghcr.io/devcontainers-extra/features/act@sha256:db4a2194930d1f7ec62822d4f600dd2fa4aff3c33b98cdb0b578b64ffb10924c",
|
||||
"integrity": "sha256:db4a2194930d1f7ec62822d4f600dd2fa4aff3c33b98cdb0b578b64ffb10924c"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/bun": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/devcontainers-extra/features/bun@sha256:0624284ecaead9dd4c6654616a7f939cfa4ebcbc60593700a74e35b1767befa5",
|
||||
"integrity": "sha256:0624284ecaead9dd4c6654616a7f939cfa4ebcbc60593700a74e35b1767befa5"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a",
|
||||
"integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"version": "2.17.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c",
|
||||
"integrity": "sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6",
|
||||
"integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/rust@sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c",
|
||||
"integrity": "sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c"
|
||||
},
|
||||
"ghcr.io/guiyomh/features/just:0": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "ghcr.io/guiyomh/features/just@sha256:8311dff976bd153a54a879021353a7e149963e580022b25af49c45cfc5f13bec",
|
||||
"integrity": "sha256:8311dff976bd153a54a879021353a7e149963e580022b25af49c45cfc5f13bec"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,14 @@
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"nefrob.vscode-just-syntax"
|
||||
"nefrob.vscode-just-syntax",
|
||||
"zxh404.vscode-proto3",
|
||||
"mhutchie.git-graph",
|
||||
"qwtel.sqlite-viewer",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"christian-kohler.path-intellisense",
|
||||
"redhat.vscode-yaml"
|
||||
],
|
||||
"settings": {
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -161,5 +161,6 @@ target
|
||||
**/mutants.out*/
|
||||
|
||||
.local/
|
||||
.act/
|
||||
|
||||
certs/
|
||||
|
||||
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -1091,6 +1091,20 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
@@ -1443,6 +1457,17 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
||||
|
||||
[[package]]
|
||||
name = "fs4"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"tokio",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@@ -2558,10 +2583,13 @@ dependencies = [
|
||||
name = "nxmesh-agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
"dashmap",
|
||||
"fs4",
|
||||
"futures",
|
||||
"hex",
|
||||
"hostname",
|
||||
@@ -2576,6 +2604,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-test",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tonic",
|
||||
"tracing",
|
||||
@@ -2600,6 +2629,7 @@ dependencies = [
|
||||
name = "nxmesh-master"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -2628,6 +2658,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-test",
|
||||
"toml",
|
||||
"tonic",
|
||||
@@ -2672,9 +2703,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@@ -5380,6 +5411,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
|
||||
@@ -56,9 +56,6 @@ futures = "0.3"
|
||||
toml = "0.9"
|
||||
config = "0.15"
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["json"] }
|
||||
|
||||
# Crypto
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
@@ -33,6 +33,7 @@ tonic.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
tokio-util = "0.7"
|
||||
|
||||
# Config
|
||||
config.workspace = true
|
||||
@@ -56,6 +57,9 @@ zip = { workspace = true }
|
||||
|
||||
# CLI
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
anyhow = { version = "1.0.102", features = ["backtrace"] }
|
||||
fs4 = { version = "0.13.1", features = ["tokio"] }
|
||||
dashmap = "6.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test.workspace = true
|
||||
|
||||
119
apps/nxmesh-agent/src/cli/import_certs.rs
Normal file
119
apps/nxmesh-agent/src/cli/import_certs.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Import certificates for agent from zip file or separate cert and key files")]
|
||||
pub struct ImportCertsCommand {
|
||||
/// Zip file containing ca.pem cert.pem and key.pem
|
||||
#[arg(value_name = "ZIP_FILE", group = "input_source")]
|
||||
zip: Option<String>,
|
||||
/// Certificate name in zip file, required if using zip input
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "zip",
|
||||
default_value = "cert.pem",
|
||||
value_name = "CERT_NAME"
|
||||
)]
|
||||
cert_name: Option<String>,
|
||||
/// Key name in zip file, required if using zip input
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "zip",
|
||||
default_value = "key.pem",
|
||||
value_name = "KEY_NAME"
|
||||
)]
|
||||
key_name: Option<String>,
|
||||
/// CA certificate name in zip file, required if using zip input
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "zip",
|
||||
default_value = "ca.pem",
|
||||
value_name = "CA_NAME"
|
||||
)]
|
||||
ca_name: Option<String>,
|
||||
|
||||
// Separate cert and key file inputs, required if not using zip input
|
||||
/// Certificate file path
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "key",
|
||||
conflicts_with = "zip",
|
||||
value_name = "CERT_FILE"
|
||||
)]
|
||||
cert: Option<String>,
|
||||
|
||||
/// Key file path
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "cert",
|
||||
conflicts_with = "zip",
|
||||
value_name = "KEY_FILE"
|
||||
)]
|
||||
key: Option<String>,
|
||||
|
||||
/// Master CA certificate file path for verifying master identity, optional if the CA certificate is already trusted by the system
|
||||
/// This is required if the master server uses a self-signed certificate that is not trusted by the system
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
conflicts_with = "zip",
|
||||
value_name = "CA_CERT_FILE"
|
||||
)]
|
||||
ca_cert: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_import_certs_with_zip_defaults() {
|
||||
let parsed = ImportCertsCommand::try_parse_from(["import-certs", "bundle.zip"]);
|
||||
assert!(parsed.is_ok());
|
||||
|
||||
let parsed = parsed.ok();
|
||||
assert!(parsed.is_some());
|
||||
let parsed = parsed.unwrap_or_else(|| unreachable!());
|
||||
|
||||
assert_eq!(parsed.zip.as_deref(), Some("bundle.zip"));
|
||||
assert_eq!(parsed.cert_name.as_deref(), Some("cert.pem"));
|
||||
assert_eq!(parsed.key_name.as_deref(), Some("key.pem"));
|
||||
assert_eq!(parsed.ca_name.as_deref(), Some("ca.pem"));
|
||||
assert!(parsed.cert.is_none());
|
||||
assert!(parsed.key.is_none());
|
||||
assert!(parsed.ca_cert.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_import_certs_with_separate_files() {
|
||||
let parsed = ImportCertsCommand::try_parse_from([
|
||||
"import-certs",
|
||||
"--cert",
|
||||
"agent.crt",
|
||||
"--key",
|
||||
"agent.key",
|
||||
"--ca-cert",
|
||||
"ca.crt",
|
||||
]);
|
||||
assert!(parsed.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_conflicting_zip_and_separate_inputs() {
|
||||
let parsed = ImportCertsCommand::try_parse_from([
|
||||
"import-certs",
|
||||
"bundle.zip",
|
||||
"--cert",
|
||||
"agent.crt",
|
||||
"--key",
|
||||
"agent.key",
|
||||
]);
|
||||
|
||||
assert!(parsed.is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
pub mod import_certs;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
@@ -13,78 +15,14 @@ pub struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
#[command(about = "Import certificates for agent from zip file or separate cert and key files")]
|
||||
ImportCerts {
|
||||
// Zip file input, mutually exclusive with separate cert and key file inputs
|
||||
/// Zip file containing ca.pem cert.pem and key.pem
|
||||
#[arg(value_name = "ZIP_FILE", group = "input_source")]
|
||||
zip: Option<String>,
|
||||
/// Certificate name in zip file, required if using zip input
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "zip",
|
||||
default_value = "cert.pem",
|
||||
value_name = "CERT_NAME"
|
||||
)]
|
||||
cert_name: Option<String>,
|
||||
/// Key name in zip file, required if using zip input
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "zip",
|
||||
default_value = "key.pem",
|
||||
value_name = "KEY_NAME"
|
||||
)]
|
||||
key_name: Option<String>,
|
||||
/// CA certificate name in zip file, required if using zip input
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "zip",
|
||||
default_value = "ca.pem",
|
||||
value_name = "CA_NAME"
|
||||
)]
|
||||
ca_name: Option<String>,
|
||||
|
||||
// Separate cert and key file inputs, required if not using zip input
|
||||
/// Certificate file path
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "key",
|
||||
conflicts_with = "zip",
|
||||
value_name = "CERT_FILE"
|
||||
)]
|
||||
cert: Option<String>,
|
||||
|
||||
/// Key file path
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
requires = "cert",
|
||||
conflicts_with = "zip",
|
||||
value_name = "KEY_FILE"
|
||||
)]
|
||||
key: Option<String>,
|
||||
|
||||
/// Master CA certificate file path for verifying master identity, optional if the CA certificate is already trusted by the system
|
||||
/// This is required if the master server uses a self-signed certificate that is not trusted by the system
|
||||
#[arg(
|
||||
long,
|
||||
group = "input_source",
|
||||
conflicts_with = "zip",
|
||||
value_name = "CA_CERT_FILE"
|
||||
)]
|
||||
ca_cert: Option<String>,
|
||||
},
|
||||
ImportCerts(import_certs::ImportCertsCommand),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::Parser;
|
||||
|
||||
use super::{Cli, Commands};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_serve_flag_without_subcommand() {
|
||||
@@ -98,65 +36,4 @@ mod tests {
|
||||
assert!(parsed.serve);
|
||||
assert!(parsed.command.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_import_certs_with_zip_defaults() {
|
||||
let parsed = Cli::try_parse_from(["nxmesh-agent", "import-certs", "bundle.zip"]);
|
||||
assert!(parsed.is_ok());
|
||||
|
||||
let parsed = parsed.ok();
|
||||
assert!(parsed.is_some());
|
||||
let parsed = parsed.unwrap_or_else(|| unreachable!());
|
||||
|
||||
match parsed.command {
|
||||
Some(Commands::ImportCerts {
|
||||
zip,
|
||||
cert_name,
|
||||
key_name,
|
||||
ca_name,
|
||||
cert,
|
||||
key,
|
||||
ca_cert,
|
||||
}) => {
|
||||
assert_eq!(zip.as_deref(), Some("bundle.zip"));
|
||||
assert_eq!(cert_name.as_deref(), Some("cert.pem"));
|
||||
assert_eq!(key_name.as_deref(), Some("key.pem"));
|
||||
assert_eq!(ca_name.as_deref(), Some("ca.pem"));
|
||||
assert!(cert.is_none());
|
||||
assert!(key.is_none());
|
||||
assert!(ca_cert.is_none());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_import_certs_with_separate_files() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
"nxmesh-agent",
|
||||
"import-certs",
|
||||
"--cert",
|
||||
"agent.crt",
|
||||
"--key",
|
||||
"agent.key",
|
||||
"--ca-cert",
|
||||
"ca.crt",
|
||||
]);
|
||||
assert!(parsed.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_conflicting_zip_and_separate_inputs() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
"nxmesh-agent",
|
||||
"import-certs",
|
||||
"bundle.zip",
|
||||
"--cert",
|
||||
"agent.crt",
|
||||
"--key",
|
||||
"agent.key",
|
||||
]);
|
||||
|
||||
assert!(parsed.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::{os::unix::fs::PermissionsExt, str::FromStr};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
const NGINX_BINARY_PATH_TEMPLATE: &str = "{{nginx_binary_path}}";
|
||||
const NGINX_DEFAULT_BINARY: &str = "nginx";
|
||||
|
||||
type ValidationError = String;
|
||||
|
||||
trait Validate {
|
||||
fn validate(&self) -> Result<(), ValidationError>;
|
||||
}
|
||||
|
||||
/// Agent settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub grpc: GrpcSettings,
|
||||
#[serde(default)]
|
||||
pub log: LogSettings,
|
||||
pub nginx: Option<NginxSettings>,
|
||||
}
|
||||
|
||||
/// gRPC client settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GrpcSettings {
|
||||
pub connection_string: String,
|
||||
pub m_auth: MAuthSettings,
|
||||
#[serde(default)]
|
||||
pub cors: Option<CorsSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MAuthSettings {
|
||||
Tls(TLSSettings),
|
||||
}
|
||||
|
||||
/// TLS certificate settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TLSSettings {
|
||||
RawPath {
|
||||
ca_path: String,
|
||||
cert_path: String,
|
||||
key_path: String,
|
||||
},
|
||||
ZipPath {
|
||||
cert_zip_path: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// CORS settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CorsSettings {
|
||||
#[serde(default)]
|
||||
pub allowed_origins: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_methods: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_headers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_credentials: bool,
|
||||
}
|
||||
|
||||
/// Logging settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogSettings {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
pub level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Default for LogSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: default_log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NginxSettings {
|
||||
#[serde(default = "default_nginx_config_path")]
|
||||
pub nginx_config_path: String,
|
||||
// #[serde(default = "default_nginx_binary_path")]
|
||||
#[serde(default)]
|
||||
pub nginx_binary_path: Option<String>,
|
||||
// commands
|
||||
#[serde(default = "default_nginx_reload_command")]
|
||||
pub override_nginx_reload_command: Vec<String>,
|
||||
#[serde(default = "default_nginx_test_command")]
|
||||
pub override_nginx_test_command: Vec<String>,
|
||||
// timeouts
|
||||
#[serde(default = "default_nginx_reload_timeout_seconds")]
|
||||
pub nginx_reload_timeout_seconds: u64,
|
||||
#[serde(default = "default_nginx_test_timeout_seconds")]
|
||||
pub nginx_test_timeout_seconds: u64,
|
||||
}
|
||||
|
||||
impl Validate for Settings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
self.grpc.validate()?;
|
||||
if let Some(nginx) = &self.nginx {
|
||||
nginx.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Load settings from config files and environment
|
||||
pub fn load() -> Result<Self, ConfigError> {
|
||||
let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
|
||||
|
||||
let settings = Config::builder()
|
||||
.add_source(File::with_name("config/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
|
||||
.add_source(File::with_name("config/agent/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/agent/{}", run_mode)).required(false))
|
||||
.add_source(Environment::with_prefix("NXMESH").separator("__"))
|
||||
.build()?;
|
||||
|
||||
let mut settings: Self = settings.try_deserialize()?;
|
||||
|
||||
settings.validate().map_err(ConfigError::Message)?;
|
||||
|
||||
if let Some(nginx) = &mut settings.nginx {
|
||||
nginx.validate().map_err(ConfigError::Message)?;
|
||||
|
||||
// replace binary path template in commands with actual binary path, if the template is present
|
||||
nginx
|
||||
.override_nginx_reload_command
|
||||
.iter_mut()
|
||||
.for_each(|cmd| {
|
||||
*cmd = cmd.replace(
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
&nginx
|
||||
.nginx_binary_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
||||
);
|
||||
});
|
||||
nginx
|
||||
.override_nginx_test_command
|
||||
.iter_mut()
|
||||
.for_each(|cmd| {
|
||||
*cmd = cmd.replace(
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
&nginx
|
||||
.nginx_binary_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for GrpcSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.connection_string.is_empty() {
|
||||
return Err("gRPC connection string cannot be empty".into());
|
||||
}
|
||||
self.m_auth.validate()?;
|
||||
if let Some(cors) = &self.cors {
|
||||
cors.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for MAuthSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
match self {
|
||||
MAuthSettings::Tls(tls_settings) => tls_settings.validate()?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for TLSSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
match self {
|
||||
TLSSettings::RawPath {
|
||||
ca_path,
|
||||
cert_path,
|
||||
key_path,
|
||||
} => {
|
||||
if !std::path::Path::new(ca_path).exists() {
|
||||
return Err(format!("CA file not found: {}", ca_path));
|
||||
}
|
||||
if !std::path::Path::new(cert_path).exists() {
|
||||
return Err(format!("Certificate file not found: {}", cert_path));
|
||||
}
|
||||
if !std::path::Path::new(key_path).exists() {
|
||||
return Err(format!("Key file not found: {}", key_path));
|
||||
}
|
||||
}
|
||||
TLSSettings::ZipPath { cert_zip_path } => {
|
||||
if !std::path::Path::new(cert_zip_path).exists() {
|
||||
return Err(format!("Certificate zip file not found: {}", cert_zip_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for CorsSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for NginxSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
match &self.nginx_binary_path {
|
||||
Some(path) if path.is_empty() => {
|
||||
return Err("Nginx binary path cannot be empty".into());
|
||||
}
|
||||
Some(path) if !std::path::Path::new(path).exists() => {
|
||||
return Err(format!("Nginx binary not found: {}", path));
|
||||
}
|
||||
Some(path)
|
||||
if !std::fs::metadata(path)
|
||||
.map_err(|e| format!("Failed to read nginx binary metadata: {}", e))?
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o111
|
||||
!= 0 =>
|
||||
{
|
||||
return Err(format!("Nginx binary is not executable: {}", path));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.nginx_config_path.is_empty() {
|
||||
return Err("Nginx config path cannot be empty".into());
|
||||
}
|
||||
if !std::path::Path::new(&self.nginx_config_path).exists() {
|
||||
return Err(format!(
|
||||
"Nginx config file not found: {}",
|
||||
self.nginx_config_path
|
||||
));
|
||||
}
|
||||
|
||||
// ensure reload and test commands contain the binary path template
|
||||
if !&self
|
||||
.override_nginx_reload_command
|
||||
.join(" ")
|
||||
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
||||
{
|
||||
return Err(format!(
|
||||
"Nginx reload command must contain the binary path template '{}': {}",
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
self.override_nginx_reload_command.join(" ")
|
||||
));
|
||||
}
|
||||
if !&self
|
||||
.override_nginx_test_command
|
||||
.join(" ")
|
||||
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
||||
{
|
||||
return Err(format!(
|
||||
"Nginx test command must contain the binary path template '{}': {}",
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
self.override_nginx_test_command.join(" ")
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_log_level() -> LevelFilter {
|
||||
LevelFilter::INFO
|
||||
}
|
||||
|
||||
fn default_nginx_config_path() -> String {
|
||||
"/etc/nginx/nginx.conf".into()
|
||||
}
|
||||
|
||||
fn default_nginx_reload_command() -> Vec<String> {
|
||||
vec![
|
||||
NGINX_BINARY_PATH_TEMPLATE.to_string(),
|
||||
"-s".to_string(),
|
||||
"reload".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn default_nginx_test_command() -> Vec<String> {
|
||||
vec![NGINX_BINARY_PATH_TEMPLATE.to_string(), "-t".to_string()]
|
||||
}
|
||||
|
||||
fn default_nginx_reload_timeout_seconds() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_nginx_test_timeout_seconds() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
fn deserialize_level_filter<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
LevelFilter::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn serialize_level_filter<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&level.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Settings>();
|
||||
assert_send_sync::<GrpcSettings>();
|
||||
assert_send_sync::<TLSSettings>();
|
||||
assert_send_sync::<CorsSettings>();
|
||||
assert_send_sync::<LogSettings>();
|
||||
assert_send_sync::<NginxSettings>();
|
||||
}
|
||||
|
||||
fn write_file(path: &Path) {
|
||||
let result = fs::write(path, b"content");
|
||||
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");
|
||||
let key_path = temp_dir.path().join("key.pem");
|
||||
|
||||
write_file(&ca_path);
|
||||
write_file(&cert_path);
|
||||
write_file(&key_path);
|
||||
|
||||
(ca_path, cert_path, key_path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_raw_path_validate_succeeds_when_all_files_exist() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let (ca_path, cert_path, key_path) = valid_tls_raw_paths(&temp_dir);
|
||||
let settings = TLSSettings::RawPath {
|
||||
ca_path: ca_path.to_string_lossy().to_string(),
|
||||
cert_path: cert_path.to_string_lossy().to_string(),
|
||||
key_path: key_path.to_string_lossy().to_string(),
|
||||
};
|
||||
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_raw_path_validate_fails_when_ca_missing() {
|
||||
let settings = TLSSettings::RawPath {
|
||||
ca_path: "/tmp/does-not-exist-ca.pem".into(),
|
||||
cert_path: "/tmp/does-not-exist-cert.pem".into(),
|
||||
key_path: "/tmp/does-not-exist-key.pem".into(),
|
||||
};
|
||||
|
||||
let result = settings.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("CA file not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_zip_path_validate_fails_when_zip_missing() {
|
||||
let settings = TLSSettings::ZipPath {
|
||||
cert_zip_path: "/tmp/missing-certs.zip".into(),
|
||||
};
|
||||
|
||||
let result = settings.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("Certificate zip file not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_validate_fails_when_connection_string_empty() {
|
||||
let settings = GrpcSettings {
|
||||
connection_string: "".into(),
|
||||
m_auth: MAuthSettings::Tls(TLSSettings::ZipPath {
|
||||
cert_zip_path: "/tmp/does-not-exist.zip".into(),
|
||||
}),
|
||||
cors: None,
|
||||
};
|
||||
|
||||
let result = settings.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("gRPC connection string cannot be empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nginx_validate_succeeds_for_valid_paths_and_commands() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let nginx_binary = temp_dir.path().join("nginx");
|
||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||
|
||||
create_exec_file(&nginx_binary);
|
||||
write_file(&nginx_config);
|
||||
|
||||
let nginx = NginxSettings {
|
||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||
override_nginx_reload_command: default_nginx_reload_command(),
|
||||
override_nginx_test_command: default_nginx_test_command(),
|
||||
nginx_reload_timeout_seconds: 30,
|
||||
nginx_test_timeout_seconds: 30,
|
||||
};
|
||||
|
||||
assert!(nginx.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nginx_validate_fails_for_non_executable_binary() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let nginx_binary = temp_dir.path().join("nginx");
|
||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||
|
||||
create_non_exec_file(&nginx_binary);
|
||||
write_file(&nginx_config);
|
||||
|
||||
let nginx = NginxSettings {
|
||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||
override_nginx_reload_command: default_nginx_reload_command(),
|
||||
override_nginx_test_command: default_nginx_test_command(),
|
||||
nginx_reload_timeout_seconds: 30,
|
||||
nginx_test_timeout_seconds: 30,
|
||||
};
|
||||
|
||||
let result = nginx.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("Nginx binary is not executable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nginx_validate_fails_when_reload_command_lacks_template() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let nginx_binary = temp_dir.path().join("nginx");
|
||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||
|
||||
create_exec_file(&nginx_binary);
|
||||
write_file(&nginx_config);
|
||||
|
||||
let nginx = NginxSettings {
|
||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||
override_nginx_reload_command: vec!["nginx".into(), "-s".into(), "reload".into()],
|
||||
override_nginx_test_command: default_nginx_test_command(),
|
||||
nginx_reload_timeout_seconds: 30,
|
||||
nginx_test_timeout_seconds: 30,
|
||||
};
|
||||
|
||||
let result = nginx.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("Nginx reload command must contain the binary path template"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_filter_round_trip_serialization() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Wrapper {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
let original = Wrapper {
|
||||
level: LevelFilter::DEBUG,
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_string(&original);
|
||||
assert!(encoded.is_ok());
|
||||
let encoded = encoded.ok();
|
||||
assert!(encoded.is_some());
|
||||
let encoded = encoded.unwrap_or_else(|| unreachable!());
|
||||
assert!(encoded.to_lowercase().contains("debug"));
|
||||
|
||||
let decoded = serde_json::from_str::<Wrapper>(&encoded);
|
||||
assert!(decoded.is_ok());
|
||||
let decoded = decoded.ok();
|
||||
assert!(decoded.is_some());
|
||||
let decoded = decoded.unwrap_or_else(|| unreachable!());
|
||||
assert_eq!(decoded.level, LevelFilter::DEBUG);
|
||||
}
|
||||
}
|
||||
166
apps/nxmesh-agent/src/config/settings/auth.rs
Normal file
166
apps/nxmesh-agent/src/config/settings/auth.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{Validate, ValidationError};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MAuthSettings {
|
||||
Tls(TLSSettings),
|
||||
}
|
||||
|
||||
/// TLS certificate settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TLSSettings {
|
||||
RawPath {
|
||||
ca_path: String,
|
||||
cert_path: String,
|
||||
key_path: String,
|
||||
},
|
||||
ZipPath {
|
||||
cert_zip_path: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Validate for MAuthSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
match self {
|
||||
MAuthSettings::Tls(tls_settings) => tls_settings.validate()?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for TLSSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
match self {
|
||||
TLSSettings::RawPath {
|
||||
ca_path,
|
||||
cert_path,
|
||||
key_path,
|
||||
} => {
|
||||
if !std::path::Path::new(ca_path).exists() {
|
||||
return Err(format!("CA file not found: {}", ca_path));
|
||||
}
|
||||
if !std::path::Path::new(cert_path).exists() {
|
||||
return Err(format!("Certificate file not found: {}", cert_path));
|
||||
}
|
||||
if !std::path::Path::new(key_path).exists() {
|
||||
return Err(format!("Key file not found: {}", key_path));
|
||||
}
|
||||
}
|
||||
TLSSettings::ZipPath { cert_zip_path } => {
|
||||
if !std::path::Path::new(cert_zip_path).exists() {
|
||||
return Err(format!("Certificate zip file not found: {}", cert_zip_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<TLSSettings>();
|
||||
}
|
||||
|
||||
fn write_file(path: &Path) {
|
||||
let result = fs::write(path, b"content");
|
||||
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");
|
||||
let key_path = temp_dir.path().join("key.pem");
|
||||
|
||||
write_file(&ca_path);
|
||||
write_file(&cert_path);
|
||||
write_file(&key_path);
|
||||
|
||||
(ca_path, cert_path, key_path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_raw_path_validate_succeeds_when_all_files_exist() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let (ca_path, cert_path, key_path) = valid_tls_raw_paths(&temp_dir);
|
||||
let settings = TLSSettings::RawPath {
|
||||
ca_path: ca_path.to_string_lossy().to_string(),
|
||||
cert_path: cert_path.to_string_lossy().to_string(),
|
||||
key_path: key_path.to_string_lossy().to_string(),
|
||||
};
|
||||
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_raw_path_validate_fails_when_ca_missing() {
|
||||
let settings = TLSSettings::RawPath {
|
||||
ca_path: "/tmp/does-not-exist-ca.pem".into(),
|
||||
cert_path: "/tmp/does-not-exist-cert.pem".into(),
|
||||
key_path: "/tmp/does-not-exist-key.pem".into(),
|
||||
};
|
||||
|
||||
let result = settings.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("CA file not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_zip_path_validate_fails_when_zip_missing() {
|
||||
let settings = TLSSettings::ZipPath {
|
||||
cert_zip_path: "/tmp/missing-certs.zip".into(),
|
||||
};
|
||||
|
||||
let result = settings.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("Certificate zip file not found"));
|
||||
}
|
||||
}
|
||||
34
apps/nxmesh-agent/src/config/settings/cors.rs
Normal file
34
apps/nxmesh-agent/src/config/settings/cors.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{Validate, ValidationError};
|
||||
|
||||
/// CORS settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CorsSettings {
|
||||
#[serde(default)]
|
||||
pub allowed_origins: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_methods: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_headers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_credentials: bool,
|
||||
}
|
||||
|
||||
impl Validate for CorsSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<CorsSettings>();
|
||||
}
|
||||
}
|
||||
56
apps/nxmesh-agent/src/config/settings/grpc.rs
Normal file
56
apps/nxmesh-agent/src/config/settings/grpc.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::super::settings::{Validate, ValidationError};
|
||||
use super::{auth::MAuthSettings, cors::CorsSettings};
|
||||
|
||||
/// gRPC client settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GrpcSettings {
|
||||
pub connection_string: String,
|
||||
pub m_auth: MAuthSettings,
|
||||
#[serde(default)]
|
||||
pub cors: Option<CorsSettings>,
|
||||
}
|
||||
|
||||
impl Validate for GrpcSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.connection_string.is_empty() {
|
||||
return Err("gRPC connection string cannot be empty".into());
|
||||
}
|
||||
self.m_auth.validate()?;
|
||||
if let Some(cors) = &self.cors {
|
||||
cors.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::config::settings::TLSSettings;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<GrpcSettings>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grpc_validate_fails_when_connection_string_empty() {
|
||||
let settings = GrpcSettings {
|
||||
connection_string: "".into(),
|
||||
m_auth: MAuthSettings::Tls(TLSSettings::ZipPath {
|
||||
cert_zip_path: "/tmp/does-not-exist.zip".into(),
|
||||
}),
|
||||
cors: None,
|
||||
};
|
||||
|
||||
let result = settings.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("gRPC connection string cannot be empty"));
|
||||
}
|
||||
}
|
||||
82
apps/nxmesh-agent/src/config/settings/log.rs
Normal file
82
apps/nxmesh-agent/src/config/settings/log.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
/// Logging settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogSettings {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
pub level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Default for LogSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: default_log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_log_level() -> LevelFilter {
|
||||
LevelFilter::INFO
|
||||
}
|
||||
|
||||
fn deserialize_level_filter<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
LevelFilter::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn serialize_level_filter<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&level.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<LogSettings>();
|
||||
}
|
||||
#[test]
|
||||
fn level_filter_round_trip_serialization() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Wrapper {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
let original = Wrapper {
|
||||
level: LevelFilter::DEBUG,
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_string(&original);
|
||||
assert!(encoded.is_ok());
|
||||
let encoded = encoded.ok();
|
||||
assert!(encoded.is_some());
|
||||
let encoded = encoded.unwrap_or_else(|| unreachable!());
|
||||
assert!(encoded.to_lowercase().contains("debug"));
|
||||
|
||||
let decoded = serde_json::from_str::<Wrapper>(&encoded);
|
||||
assert!(decoded.is_ok());
|
||||
let decoded = decoded.ok();
|
||||
assert!(decoded.is_some());
|
||||
let decoded = decoded.unwrap_or_else(|| unreachable!());
|
||||
assert_eq!(decoded.level, LevelFilter::DEBUG);
|
||||
}
|
||||
}
|
||||
76
apps/nxmesh-agent/src/config/settings/mod.rs
Normal file
76
apps/nxmesh-agent/src/config/settings/mod.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod auth;
|
||||
mod cors;
|
||||
mod grpc;
|
||||
mod log;
|
||||
mod nginx;
|
||||
|
||||
pub use auth::*;
|
||||
pub use cors::*;
|
||||
pub use grpc::*;
|
||||
pub use log::*;
|
||||
pub use nginx::*;
|
||||
|
||||
pub type ValidationError = String;
|
||||
|
||||
pub trait Validate {
|
||||
fn validate(&self) -> Result<(), ValidationError>;
|
||||
}
|
||||
|
||||
/// Agent settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub agent_id: String,
|
||||
|
||||
pub grpc: GrpcSettings,
|
||||
#[serde(default)]
|
||||
pub log: LogSettings,
|
||||
#[serde(default)]
|
||||
pub nginx: NginxSettings,
|
||||
}
|
||||
|
||||
impl Validate for Settings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
self.grpc.validate()?;
|
||||
self.nginx.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Load settings from config files and environment
|
||||
pub fn load() -> Result<Self, ConfigError> {
|
||||
let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
|
||||
|
||||
let settings = Config::builder()
|
||||
.add_source(File::with_name("config/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
|
||||
.add_source(File::with_name("config/agent/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/agent/{}", run_mode)).required(false))
|
||||
.add_source(Environment::with_prefix("NXMESH").separator("__"))
|
||||
.build()?;
|
||||
|
||||
let mut settings: Self = settings.try_deserialize()?;
|
||||
|
||||
settings.validate().map_err(ConfigError::Message)?;
|
||||
|
||||
settings.nginx.validate().map_err(ConfigError::Message)?;
|
||||
settings.nginx.transform_commands();
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ensure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Settings>();
|
||||
}
|
||||
}
|
||||
280
apps/nxmesh-agent/src/config/settings/nginx.rs
Normal file
280
apps/nxmesh-agent/src/config/settings/nginx.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{Validate, ValidationError};
|
||||
|
||||
const NGINX_BINARY_PATH_TEMPLATE: &str = "{{nginx_binary_path}}";
|
||||
const NGINX_DEFAULT_BINARY: &str = "nginx";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct NginxSettings {
|
||||
#[serde(default = "default_nginx_config_path")]
|
||||
pub nginx_config_path: String,
|
||||
// #[serde(default = "default_nginx_binary_path")]
|
||||
#[serde(default)]
|
||||
pub nginx_binary_path: Option<String>,
|
||||
// commands
|
||||
#[serde(default = "default_nginx_reload_command")]
|
||||
pub override_nginx_reload_command: Vec<String>,
|
||||
#[serde(default = "default_nginx_test_command")]
|
||||
pub override_nginx_test_command: Vec<String>,
|
||||
// timeouts
|
||||
#[serde(default = "default_nginx_reload_timeout_seconds")]
|
||||
pub nginx_reload_timeout_seconds: u64,
|
||||
#[serde(default = "default_nginx_test_timeout_seconds")]
|
||||
pub nginx_test_timeout_seconds: u64,
|
||||
}
|
||||
|
||||
impl NginxSettings {
|
||||
/// Transforms the reload and test commands by replacing the binary path template with the actual binary path if provided.
|
||||
/// This MUST be called after validation to ensure the binary path is valid and the commands contain the template.
|
||||
pub fn transform_commands(&mut self) {
|
||||
self.override_nginx_reload_command = self.transformed_reload_command();
|
||||
self.override_nginx_test_command = self.transformed_test_command();
|
||||
}
|
||||
|
||||
fn transformed_reload_command(&self) -> Vec<String> {
|
||||
self.override_nginx_reload_command
|
||||
.iter()
|
||||
.map(|cmd| {
|
||||
cmd.replace(
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
&self
|
||||
.nginx_binary_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn transformed_test_command(&self) -> Vec<String> {
|
||||
self.override_nginx_test_command
|
||||
.iter()
|
||||
.map(|cmd| {
|
||||
cmd.replace(
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
&self
|
||||
.nginx_binary_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for NginxSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
match &self.nginx_binary_path {
|
||||
Some(path) if path.is_empty() => {
|
||||
return Err("Nginx binary path cannot be empty".into());
|
||||
}
|
||||
Some(path) if !std::path::Path::new(path).exists() => {
|
||||
return Err(format!("Nginx binary not found: {}", path));
|
||||
}
|
||||
Some(path)
|
||||
if !std::fs::metadata(path)
|
||||
.map_err(|e| format!("Failed to read nginx binary metadata: {}", e))?
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o111
|
||||
!= 0 =>
|
||||
{
|
||||
return Err(format!("Nginx binary is not executable: {}", path));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.nginx_config_path.is_empty() {
|
||||
return Err("Nginx config path cannot be empty".into());
|
||||
}
|
||||
if !std::path::Path::new(&self.nginx_config_path).exists() {
|
||||
return Err(format!(
|
||||
"Nginx config file not found: {}",
|
||||
self.nginx_config_path
|
||||
));
|
||||
}
|
||||
|
||||
// ensure reload and test commands contain the binary path template
|
||||
if !&self
|
||||
.override_nginx_reload_command
|
||||
.join(" ")
|
||||
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
||||
{
|
||||
return Err(format!(
|
||||
"Nginx reload command must contain the binary path template '{}': {}",
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
self.override_nginx_reload_command.join(" ")
|
||||
));
|
||||
}
|
||||
if !&self
|
||||
.override_nginx_test_command
|
||||
.join(" ")
|
||||
.contains(NGINX_BINARY_PATH_TEMPLATE)
|
||||
{
|
||||
return Err(format!(
|
||||
"Nginx test command must contain the binary path template '{}': {}",
|
||||
NGINX_BINARY_PATH_TEMPLATE,
|
||||
self.override_nginx_test_command.join(" ")
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_nginx_config_path() -> String {
|
||||
"/etc/nginx/nginx.conf".into()
|
||||
}
|
||||
|
||||
fn default_nginx_reload_command() -> Vec<String> {
|
||||
vec![
|
||||
NGINX_BINARY_PATH_TEMPLATE.to_string(),
|
||||
"-s".to_string(),
|
||||
"reload".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn default_nginx_test_command() -> Vec<String> {
|
||||
vec![NGINX_BINARY_PATH_TEMPLATE.to_string(), "-t".to_string()]
|
||||
}
|
||||
|
||||
fn default_nginx_reload_timeout_seconds() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_nginx_test_timeout_seconds() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, os::unix::fs::PermissionsExt, path::Path};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<NginxSettings>();
|
||||
}
|
||||
|
||||
fn write_file(path: &Path) {
|
||||
let result = fs::write(path, b"content");
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nginx_validate_succeeds_for_valid_paths_and_commands() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let nginx_binary = temp_dir.path().join("nginx");
|
||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||
|
||||
create_exec_file(&nginx_binary);
|
||||
write_file(&nginx_config);
|
||||
|
||||
let nginx = NginxSettings {
|
||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||
override_nginx_reload_command: default_nginx_reload_command(),
|
||||
override_nginx_test_command: default_nginx_test_command(),
|
||||
nginx_reload_timeout_seconds: 30,
|
||||
nginx_test_timeout_seconds: 30,
|
||||
};
|
||||
|
||||
assert!(nginx.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nginx_validate_fails_for_non_executable_binary() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let nginx_binary = temp_dir.path().join("nginx");
|
||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||
|
||||
create_non_exec_file(&nginx_binary);
|
||||
write_file(&nginx_config);
|
||||
|
||||
let nginx = NginxSettings {
|
||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||
override_nginx_reload_command: default_nginx_reload_command(),
|
||||
override_nginx_test_command: default_nginx_test_command(),
|
||||
nginx_reload_timeout_seconds: 30,
|
||||
nginx_test_timeout_seconds: 30,
|
||||
};
|
||||
|
||||
let result = nginx.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("Nginx binary is not executable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nginx_validate_fails_when_reload_command_lacks_template() {
|
||||
let temp_dir = TempDir::new();
|
||||
assert!(temp_dir.is_ok());
|
||||
let temp_dir = temp_dir.ok();
|
||||
assert!(temp_dir.is_some());
|
||||
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||
|
||||
let nginx_binary = temp_dir.path().join("nginx");
|
||||
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||
|
||||
create_exec_file(&nginx_binary);
|
||||
write_file(&nginx_config);
|
||||
|
||||
let nginx = NginxSettings {
|
||||
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||
override_nginx_reload_command: vec!["nginx".into(), "-s".into(), "reload".into()],
|
||||
override_nginx_test_command: default_nginx_test_command(),
|
||||
nginx_reload_timeout_seconds: 30,
|
||||
nginx_test_timeout_seconds: 30,
|
||||
};
|
||||
|
||||
let result = nginx.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||
assert!(msg.contains("Nginx reload command must contain the binary path template"));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ pub trait MasterConnectorTrait: Send + Sync {
|
||||
&mut self,
|
||||
settings: &crate::config::settings::Settings,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
fn get_client(&self) -> Arc<Mutex<AgentClient>>;
|
||||
fn get_client(&self) -> AgentClient;
|
||||
}
|
||||
|
||||
pub struct MasterConnector {
|
||||
@@ -35,7 +35,7 @@ impl MasterConnectorTrait for MasterConnector {
|
||||
self.connector.connect(settings).await
|
||||
}
|
||||
|
||||
fn get_client(&self) -> Arc<Mutex<AgentClient>> {
|
||||
fn get_client(&self) -> AgentClient {
|
||||
self.connector.get_client()
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ mod tests {
|
||||
struct FakeConnector {
|
||||
called: Arc<AtomicBool>,
|
||||
fail: bool,
|
||||
client: Arc<Mutex<AgentClient>>,
|
||||
client: AgentClient,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -74,13 +74,14 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_client(&self) -> Arc<Mutex<AgentClient>> {
|
||||
fn get_client(&self) -> AgentClient {
|
||||
self.client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn test_settings() -> Settings {
|
||||
Settings {
|
||||
agent_id: "test-agent".to_string(),
|
||||
grpc: GrpcSettings {
|
||||
connection_string: "https://localhost:50051".to_string(),
|
||||
m_auth: MAuthSettings::Tls(TLSSettings::ZipPath {
|
||||
@@ -89,14 +90,14 @@ mod tests {
|
||||
cors: None,
|
||||
},
|
||||
log: LogSettings::default(),
|
||||
nginx: None,
|
||||
nginx: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_client() -> Arc<Mutex<AgentClient>> {
|
||||
fn test_client() -> AgentClient {
|
||||
let channel =
|
||||
tonic::transport::Channel::from_static("http://127.0.0.1:50051").connect_lazy();
|
||||
Arc::new(Mutex::new(AgentClient::new(channel)))
|
||||
AgentClient::new(channel)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -126,18 +127,4 @@ mod tests {
|
||||
let result = master.connect(&test_settings()).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn master_connector_returns_underlying_client() {
|
||||
let shared_client = test_client();
|
||||
let fake = FakeConnector {
|
||||
called: Arc::new(AtomicBool::new(false)),
|
||||
fail: false,
|
||||
client: shared_client.clone(),
|
||||
};
|
||||
let master = MasterConnector::new(Box::new(fake));
|
||||
|
||||
let client = master.get_client();
|
||||
assert!(Arc::ptr_eq(&client, &shared_client));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{fs::File, io::Read, sync::Arc};
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use tokio::{fs::read, sync::Mutex};
|
||||
use tokio::fs::read;
|
||||
|
||||
use nxmesh_proto::agent_service_client::AgentServiceClient;
|
||||
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
|
||||
@@ -11,7 +11,7 @@ use crate::config::settings::{MAuthSettings, TLSSettings};
|
||||
use super::{AgentClient, MasterConnectorTrait};
|
||||
|
||||
pub struct SshMasterConnector {
|
||||
client: Arc<Mutex<AgentClient>>,
|
||||
client: AgentClient,
|
||||
}
|
||||
|
||||
impl SshMasterConnector {
|
||||
@@ -34,7 +34,7 @@ impl SshMasterConnector {
|
||||
.connect_lazy();
|
||||
|
||||
// Create the gRPC client
|
||||
let client = Arc::new(Mutex::new(AgentServiceClient::new(endpoint)));
|
||||
let client = AgentServiceClient::new(endpoint);
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ impl MasterConnectorTrait for SshMasterConnector {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_client(&self) -> Arc<Mutex<AgentClient>> {
|
||||
fn get_client(&self) -> AgentClient {
|
||||
self.client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "128"]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
|
||||
|
||||
use std::process::exit;
|
||||
use std::{process::exit, sync::Arc};
|
||||
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::{
|
||||
@@ -9,11 +10,12 @@ use tracing_subscriber::{
|
||||
util::SubscriberInitExt,
|
||||
};
|
||||
|
||||
use crate::connector::master::{MasterConnector, MasterConnectorTrait, ssh::SshMasterConnector};
|
||||
use crate::service::get_services;
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
mod connector;
|
||||
mod service;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -43,38 +45,26 @@ async fn main() {
|
||||
// info!("Loaded settings: {:#?}", settings);
|
||||
|
||||
info!("Starting NxMesh Agent...");
|
||||
// install grpc client
|
||||
#[expect(clippy::expect_used)]
|
||||
let ssh_connector = SshMasterConnector::new(settings.grpc.clone())
|
||||
let services = get_services(Arc::new(settings))
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!("Failed to create SSH Master Connector: {}", e);
|
||||
exit(1);
|
||||
.map_err(|e| {
|
||||
error!("Failed to initialize services: {}", e);
|
||||
e
|
||||
})
|
||||
.expect("Failed to create SSH Master Connector");
|
||||
let mut master_connector = MasterConnector::new(Box::new(ssh_connector));
|
||||
.unwrap_or_else(|_| {
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if let Err(e) = master_connector.connect(&settings).await {
|
||||
error!("Failed to connect to master: {}", e);
|
||||
exit(1);
|
||||
let master_handler = services.master_handler.clone();
|
||||
// spawn the long-running handler so main can wait for shutdown signal
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = master_handler.start_handle_master_message().await {
|
||||
error!("Master message handler exited with error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// send a dummy heartbeat to verify the connection is working
|
||||
let client = master_connector.get_client();
|
||||
|
||||
let request = nxmesh_proto::HealthReport {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match client.lock().await.report_health(request).await {
|
||||
Ok(_) => info!("Successfully sent health report to master."),
|
||||
Err(e) => {
|
||||
error!("Failed to send health report to master: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Successfully connected to master. Agent is running.");
|
||||
info!("Agent is running. Waiting for shutdown signal.");
|
||||
shutdown_handler(services.master_handler.clone()).await;
|
||||
}
|
||||
|
||||
fn install_tracing_subscriber()
|
||||
@@ -91,3 +81,20 @@ fn install_tracing_subscriber()
|
||||
|
||||
reload_handle
|
||||
}
|
||||
|
||||
async fn wait_for_shutdown_signal() {
|
||||
#[expect(clippy::expect_used)]
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to listen for ctrl_c");
|
||||
info!("Shutdown signal received, stopping handler.");
|
||||
}
|
||||
|
||||
async fn shutdown_handler(master_handler: Arc<dyn service::master_handler::MasterHandler>) {
|
||||
wait_for_shutdown_signal().await;
|
||||
//
|
||||
let _ = master_handler.stop_handle_master_message().await;
|
||||
//
|
||||
info!("Agent stopped.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nxmesh_proto::ConfigUpdate;
|
||||
use tracing::info;
|
||||
|
||||
use crate::connector::master::MasterConnector;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait MasterHandler {
|
||||
async fn on_config_update(
|
||||
&self,
|
||||
config_info: ConfigUpdate,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
}
|
||||
|
||||
pub struct MasterHandlerImpl {
|
||||
settings: Arc<crate::config::settings::Settings>,
|
||||
}
|
||||
|
||||
impl MasterHandlerImpl {
|
||||
pub fn new(settings: impl Into<Arc<crate::config::settings::Settings>>) -> Self {
|
||||
Self {
|
||||
settings: settings.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MasterHandler for MasterHandlerImpl {
|
||||
async fn on_config_update(
|
||||
&self,
|
||||
config_info: ConfigUpdate,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Received config update from master: {:?}", config_info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
106
apps/nxmesh-agent/src/service/master_handler/handlers.rs
Normal file
106
apps/nxmesh-agent/src/service/master_handler/handlers.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use nxmesh_proto::{
|
||||
AgentMessage, ConfigUpdate, MasterMessage, command::Command, master_message::Payload,
|
||||
};
|
||||
|
||||
use crate::service::master_handler::{MasterHandlerError, MessageResult};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait MasterMessageHandler: Send + Sync + 'static {
|
||||
async fn handle_master_message(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
message: MasterMessage,
|
||||
) -> MessageResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait OnConfigUpdateHandler: Send + Sync + 'static {
|
||||
// Handle the config update message from master, write the config content to files, validate the new config and reload nginx
|
||||
async fn on_config_update(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
timestamp: i64,
|
||||
message_id: &str,
|
||||
config_info: ConfigUpdate,
|
||||
) -> MessageResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait OnCommandHandler: Send + Sync + 'static {
|
||||
// Handle the command message from master, execute the command and return the result
|
||||
async fn on_command(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
timestamp: i64,
|
||||
message_id: &str,
|
||||
command: Command,
|
||||
) -> MessageResult<()>;
|
||||
}
|
||||
|
||||
pub struct HandlerImpl<OCUH, OCH>
|
||||
where
|
||||
OCUH: OnConfigUpdateHandler + ?Sized,
|
||||
OCH: OnCommandHandler + ?Sized,
|
||||
{
|
||||
on_config_update_handler: Weak<OCUH>,
|
||||
on_command_handler: Weak<OCH>,
|
||||
}
|
||||
|
||||
impl<OCUH, OCH> HandlerImpl<OCUH, OCH>
|
||||
where
|
||||
OCUH: OnConfigUpdateHandler + ?Sized,
|
||||
OCH: OnCommandHandler + ?Sized,
|
||||
{
|
||||
pub fn new(on_config_update_handler: Weak<OCUH>, on_command_handler: Weak<OCH>) -> Self {
|
||||
Self {
|
||||
on_config_update_handler,
|
||||
on_command_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<OCUH, OCH> MasterMessageHandler for HandlerImpl<OCUH, OCH>
|
||||
where
|
||||
OCUH: OnConfigUpdateHandler + ?Sized,
|
||||
OCH: OnCommandHandler + ?Sized,
|
||||
{
|
||||
async fn handle_master_message(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
message: MasterMessage,
|
||||
) -> MessageResult<()> {
|
||||
match message.payload {
|
||||
Some(Payload::ConfigUpdate(config_info)) => {
|
||||
let on_config_update_handler =
|
||||
self.on_config_update_handler.upgrade().ok_or_else(|| {
|
||||
MasterHandlerError::MessageHandlingError(
|
||||
"Failed to upgrade weak reference to config update handler".to_string(),
|
||||
)
|
||||
})?;
|
||||
on_config_update_handler
|
||||
.on_config_update(
|
||||
agent_id,
|
||||
message.timestamp,
|
||||
&message.message_id,
|
||||
config_info,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(_) => {
|
||||
// We should never receive other types of messages from the master, but we should handle it anyway
|
||||
Err(MasterHandlerError::MessageHandlingError(
|
||||
"Received unsupported master message type".to_string(),
|
||||
))
|
||||
}
|
||||
None => {
|
||||
// This should never happen as the master should always send a valid message, but we should handle it anyway
|
||||
return Err(MasterHandlerError::MessageHandlingError(
|
||||
"Received master message with empty payload".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
224
apps/nxmesh-agent/src/service/master_handler/mod.rs
Normal file
224
apps/nxmesh-agent/src/service/master_handler/mod.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nxmesh_proto::AgentMessage;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
connector::master::{MasterConnector, MasterConnectorTrait},
|
||||
service::master_handler::handlers::MasterMessageHandler,
|
||||
};
|
||||
|
||||
pub mod handlers;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MasterHandlerError {
|
||||
ConnectionError(String),
|
||||
// TODO: should be protobuf error to transmit the error to master
|
||||
MessageHandlingError(String),
|
||||
RetryLimitExceeded(String),
|
||||
SendMessageError(String),
|
||||
}
|
||||
|
||||
pub type MessageResult<T> = std::result::Result<T, MasterHandlerError>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait MasterHandler: Send + Sync + 'static {
|
||||
// Create a new routine to handle incoming messages from the master
|
||||
// This method will auto-reconnect if the connection is lost, so it should run indefinitely until the agent is shut down
|
||||
async fn start_handle_master_message(&self) -> MessageResult<()>;
|
||||
async fn stop_handle_master_message(&self) -> MessageResult<()>;
|
||||
|
||||
// Send a message to the master, response should be handled by the agent message handler registered
|
||||
async fn send_message_to_master(&self, message: AgentMessage) -> MessageResult<()>;
|
||||
}
|
||||
|
||||
struct MessageHandleInfo {
|
||||
tx: mpsc::Sender<AgentMessage>,
|
||||
// used to signal the running handler/connection to stop
|
||||
cancel: CancellationToken,
|
||||
}
|
||||
|
||||
pub struct MasterHandlerImpl<MMH>
|
||||
where
|
||||
MMH: MasterMessageHandler + ?Sized,
|
||||
{
|
||||
agent_id: String,
|
||||
connector: Arc<MasterConnector>,
|
||||
message_handler: Arc<MMH>,
|
||||
message_handle_lock: tokio::sync::RwLock<Option<MessageHandleInfo>>,
|
||||
}
|
||||
|
||||
impl<MMH> MasterHandlerImpl<MMH>
|
||||
where
|
||||
MMH: MasterMessageHandler + ?Sized,
|
||||
{
|
||||
pub fn new(agent_id: &str, connector: Arc<MasterConnector>, message_handler: Arc<MMH>) -> Self {
|
||||
Self {
|
||||
agent_id: agent_id.to_string(),
|
||||
connector,
|
||||
message_handler,
|
||||
message_handle_lock: tokio::sync::RwLock::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<MMH> MasterHandler for MasterHandlerImpl<MMH>
|
||||
where
|
||||
MMH: MasterMessageHandler + ?Sized,
|
||||
{
|
||||
async fn start_handle_master_message(&self) -> MessageResult<()> {
|
||||
info!("Starting master message handler...");
|
||||
let mut client = self.connector.get_client();
|
||||
|
||||
// ensure only one caller can start the handler
|
||||
// create the cancel token for the lifetime of this handler invocation
|
||||
let cancel_token = CancellationToken::new();
|
||||
{
|
||||
let mut guard = self.message_handle_lock.write().await;
|
||||
if guard.is_some() {
|
||||
warn!("Master message handler is already running");
|
||||
return Ok(());
|
||||
}
|
||||
// placeholder tx; will be replaced per-connection
|
||||
let (tx, _rx) = mpsc::channel(1);
|
||||
*guard = Some(MessageHandleInfo {
|
||||
tx,
|
||||
cancel: cancel_token.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
'connection_loop: loop {
|
||||
// fresh outbound channel per connection
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let outbound_stream = ReceiverStream::new(rx);
|
||||
|
||||
// try to connect
|
||||
let mut stream = match client.stream(outbound_stream).await {
|
||||
Ok(s) => s.into_inner(),
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to connect to master: {}. Retrying in 5 seconds...",
|
||||
e
|
||||
);
|
||||
// update stored sender so any callers see the current tx
|
||||
{
|
||||
let mut guard = self.message_handle_lock.write().await;
|
||||
if let Some(info) = guard.as_mut() {
|
||||
info.tx = tx.clone();
|
||||
}
|
||||
}
|
||||
let conn_token = cancel_token.child_token();
|
||||
tokio::select! {
|
||||
_ = conn_token.cancelled() => break 'connection_loop,
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => continue 'connection_loop,
|
||||
}
|
||||
}
|
||||
};
|
||||
// store current tx so senders can use it
|
||||
{
|
||||
let mut guard = self.message_handle_lock.write().await;
|
||||
if let Some(info) = guard.as_mut() {
|
||||
info.tx = tx.clone();
|
||||
}
|
||||
}
|
||||
// connection-level token to observe stop requests
|
||||
let conn_token = cancel_token.child_token();
|
||||
info!("Connected to master, starting to receive messages...");
|
||||
// process messages inline so we can clear the slot on exit
|
||||
'message_processing: loop {
|
||||
tokio::select! {
|
||||
_ = conn_token.cancelled() => {
|
||||
info!("Stop requested for master handler");
|
||||
break 'connection_loop;
|
||||
}
|
||||
message = stream.message() => {
|
||||
match message {
|
||||
Ok(Some(msg)) => {
|
||||
if let Err(e) = self.message_handler.handle_master_message(&self.agent_id, msg).await {
|
||||
error!("Failed to handle master message: {:?}", e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("Master closed the connection");
|
||||
break 'message_processing;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error receiving message from master: {:?}", e);
|
||||
break 'message_processing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connection ended — clear stored info
|
||||
{
|
||||
let mut guard = self.message_handle_lock.write().await;
|
||||
guard.take();
|
||||
}
|
||||
|
||||
// if stop requested, exit
|
||||
if cancel_token.is_cancelled() {
|
||||
break 'connection_loop;
|
||||
}
|
||||
|
||||
// otherwise reconnect after backoff
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
|
||||
// final cleanup
|
||||
let mut guard = self.message_handle_lock.write().await;
|
||||
guard.take();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_handle_master_message(&self) -> MessageResult<()> {
|
||||
// Signal the running handler to stop and wait for it to clear
|
||||
let mut maybe_cancel = None;
|
||||
{
|
||||
let mut guard = self.message_handle_lock.write().await;
|
||||
if let Some(info) = guard.take() {
|
||||
maybe_cancel = Some(info.cancel);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cancel) = maybe_cancel {
|
||||
cancel.cancel();
|
||||
|
||||
// wait for the handler to clear (with timeout)
|
||||
for _ in 0..50 {
|
||||
if self.message_handle_lock.read().await.is_none() {
|
||||
info!("Master message handler task stopped successfully");
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
warn!("Timed out waiting for master message handler to stop");
|
||||
} else {
|
||||
warn!("Master message handler is not running");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_message_to_master(&self, message: AgentMessage) -> MessageResult<()> {
|
||||
let guard = self.message_handle_lock.read().await;
|
||||
if let Some(handle_info) = guard.as_ref() {
|
||||
handle_info.tx.send(message).await.map_err(|e| {
|
||||
MasterHandlerError::SendMessageError(format!(
|
||||
"Failed to send message to master: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
} else {
|
||||
return Err(MasterHandlerError::SendMessageError(
|
||||
"Master message handler is not running".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
63
apps/nxmesh-agent/src/service/mod.rs
Normal file
63
apps/nxmesh-agent/src/service/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
config::settings::Settings,
|
||||
connector::master::{MasterConnector, ssh::SshMasterConnector},
|
||||
service::{
|
||||
master_handler::{MasterHandler, MasterHandlerImpl, handlers::HandlerImpl},
|
||||
nginx_handler::{NginxHandler, NginxHandlerImpl},
|
||||
},
|
||||
};
|
||||
|
||||
pub mod master_handler;
|
||||
pub mod nginx_handler;
|
||||
|
||||
pub struct Services {
|
||||
pub master_handler: Arc<dyn MasterHandler>,
|
||||
pub nginx_handler: Arc<dyn NginxHandler>,
|
||||
}
|
||||
|
||||
pub async fn get_services(settings: Arc<Settings>) -> anyhow::Result<Services> {
|
||||
let master_connector = initialize_master_connector(settings.clone()).await?;
|
||||
let master_connector = Arc::new(master_connector);
|
||||
|
||||
let master_handler_slot = Arc::new(Mutex::new(None));
|
||||
let slot = master_handler_slot.clone();
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let nginx_handler = Arc::new_cyclic(|nginx_handler_weak| {
|
||||
let message_handler = Arc::new(HandlerImpl::new(
|
||||
nginx_handler_weak.clone(),
|
||||
nginx_handler_weak.clone(),
|
||||
));
|
||||
let master_handler = Arc::new(MasterHandlerImpl::new(
|
||||
settings.agent_id.as_str(),
|
||||
master_connector.clone(),
|
||||
message_handler,
|
||||
));
|
||||
*slot.lock().expect("master handler slot lock poisoned") = Some(master_handler.clone());
|
||||
|
||||
NginxHandlerImpl::new(settings.nginx.clone().into(), master_handler)
|
||||
});
|
||||
#[expect(clippy::expect_used)]
|
||||
let master_handler = master_handler_slot
|
||||
.lock()
|
||||
.expect("master handler slot lock poisoned")
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to initialize master handler"))?;
|
||||
|
||||
Ok(Services {
|
||||
master_handler,
|
||||
nginx_handler,
|
||||
})
|
||||
}
|
||||
|
||||
async fn initialize_master_connector(settings: Arc<Settings>) -> anyhow::Result<MasterConnector> {
|
||||
let ssh_connector = SshMasterConnector::new(settings.grpc.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize SSH connector: {}", e))?;
|
||||
|
||||
let master_connector = MasterConnector::new(Box::new(ssh_connector));
|
||||
|
||||
Ok(master_connector)
|
||||
}
|
||||
252
apps/nxmesh-agent/src/service/nginx_handler/command_handler.rs
Normal file
252
apps/nxmesh-agent/src/service/nginx_handler/command_handler.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::process::Command;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{config::settings::NginxSettings, service::master_handler::MasterHandlerError};
|
||||
|
||||
#[cfg(test)]
|
||||
use mockall::predicate::*;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CommandHandlerError {
|
||||
#[error("Failed to execute command: {0}")]
|
||||
CommandExecutionError(#[from] std::io::Error),
|
||||
#[error("Invalid config path: {0}")]
|
||||
InvalidConfigPath(String),
|
||||
#[error("Invalid output path: {0}")]
|
||||
InvalidOutputPath(String),
|
||||
#[error("Permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
#[error("Other error: {0}")]
|
||||
OtherError(String),
|
||||
}
|
||||
|
||||
impl From<CommandHandlerError> for MasterHandlerError {
|
||||
fn from(err: CommandHandlerError) -> Self {
|
||||
MasterHandlerError::MessageHandlingError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type CommandHandlerResult<T> = std::result::Result<T, CommandHandlerError>;
|
||||
type Result<T> = CommandHandlerResult<T>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
pub trait CommandHandler: Send + Sync + 'static {
|
||||
// Reload nginx to apply new config. The config_path is an optional parameter that specifies the path to the nginx config file to be used for this reload operation. If not provided, the default config path will be used.
|
||||
async fn reload(&self, config_path: Option<&str>) -> Result<()>;
|
||||
async fn stop(&self) -> Result<()>;
|
||||
async fn validate(&self, config_path: Option<&str>) -> Result<()>;
|
||||
async fn get_version(&self) -> Result<String>;
|
||||
async fn get_status(&self) -> Result<String>;
|
||||
}
|
||||
|
||||
pub struct CommandHandlerImpl {
|
||||
settings: Arc<NginxSettings>,
|
||||
}
|
||||
|
||||
impl CommandHandlerImpl {
|
||||
pub fn new(settings: Arc<NginxSettings>) -> Self {
|
||||
Self { settings }
|
||||
}
|
||||
|
||||
fn get_nginx_command(&self) -> String {
|
||||
// TODO: rename the setting for better clarity, it can be a binary path or a custom command
|
||||
self.settings
|
||||
.nginx_binary_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| "nginx".to_string())
|
||||
}
|
||||
|
||||
fn validate_config_path(config_path: &str) -> Result<()> {
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
return Err(CommandHandlerError::InvalidConfigPath(format!(
|
||||
"Config file not found at path: {}",
|
||||
config_path
|
||||
)));
|
||||
}
|
||||
if !std::path::Path::new(config_path).is_file() {
|
||||
return Err(CommandHandlerError::InvalidConfigPath(format!(
|
||||
"Config path is not a file: {}",
|
||||
config_path
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_config_path_to_command_vecs<'a>(
|
||||
command: &'a mut Vec<String>,
|
||||
config_path: &str,
|
||||
) -> Result<&'a mut Vec<String>> {
|
||||
// if given a config path, add it to the end of the command arguments to override the default config path used
|
||||
Self::validate_config_path(config_path)?;
|
||||
let parent_dir = match std::path::Path::new(config_path).parent() {
|
||||
Some(dir) => dir,
|
||||
// return root
|
||||
None => std::path::Path::new("/"),
|
||||
};
|
||||
// set prefix path to the parent directory of the config file to ensure nginx can find all related files (e.g. certs, conf.d, etc.)
|
||||
command.push("-p".to_string());
|
||||
command.push(parent_dir.to_string_lossy().to_string());
|
||||
// add the config file path to the command arguments to override the default config path used by nginx
|
||||
command.push("-c".to_string());
|
||||
command.push(config_path.to_string());
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CommandHandler for CommandHandlerImpl {
|
||||
async fn reload(&self, config_path: Option<&str>) -> Result<()> {
|
||||
// TODO: add timeout for the command execution
|
||||
let reload_command_str = self.settings.override_nginx_reload_command.clone();
|
||||
let program = match reload_command_str.first() {
|
||||
Some(cmd) => cmd,
|
||||
None => &self.get_nginx_command(),
|
||||
};
|
||||
|
||||
let mut reload_command_vec = reload_command_str[1..].to_vec();
|
||||
// if given a config path, add it to the end of the command arguments to override the default config path used
|
||||
if let Some(path) = config_path {
|
||||
Self::apply_config_path_to_command_vecs(&mut reload_command_vec, path)?;
|
||||
}
|
||||
|
||||
let output = Command::new(program)
|
||||
.args(&reload_command_vec)
|
||||
.output()
|
||||
.await?;
|
||||
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()),
|
||||
),
|
||||
));
|
||||
}
|
||||
let success_info = String::from_utf8_lossy(&output.stdout);
|
||||
debug!("Nginx reloaded successfully: {}", success_info.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(&self) -> Result<()> {
|
||||
let output = Command::new(self.get_nginx_command())
|
||||
.arg("-s")
|
||||
.arg("stop")
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
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()),
|
||||
),
|
||||
));
|
||||
}
|
||||
let success_info = String::from_utf8_lossy(&output.stdout);
|
||||
debug!("Nginx stopped successfully: {}", success_info.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn validate(&self, config_path: Option<&str>) -> Result<()> {
|
||||
// TODO: add timeout for the command execution
|
||||
let validate_command_str = self.settings.override_nginx_test_command.clone();
|
||||
let program = match validate_command_str.first() {
|
||||
Some(cmd) => cmd,
|
||||
None => &self.get_nginx_command(),
|
||||
};
|
||||
let mut validate_args = validate_command_str[1..].to_vec();
|
||||
// if given a config path, add it to the end of the command arguments to override the default config path used
|
||||
if let Some(path) = config_path {
|
||||
Self::apply_config_path_to_command_vecs(&mut validate_args, path)?;
|
||||
}
|
||||
|
||||
let output = Command::new(program).args(&validate_args).output().await?;
|
||||
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()),
|
||||
),
|
||||
));
|
||||
}
|
||||
let success_info = String::from_utf8_lossy(&output.stdout);
|
||||
debug!("Nginx config validation succeeded: {}", success_info.trim());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_version(&self) -> Result<String> {
|
||||
let output = Command::new(self.get_nginx_command())
|
||||
.arg("-v")
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
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()),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let version_info = String::from_utf8_lossy(&output.stderr);
|
||||
Ok(version_info.trim().to_string())
|
||||
}
|
||||
|
||||
async fn get_status(&self) -> Result<String> {
|
||||
let output = Command::new(self.get_nginx_command())
|
||||
.arg("-t")
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
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()),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let status_info = String::from_utf8_lossy(&output.stderr);
|
||||
Ok(status_info.trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::expect_used)]
|
||||
mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_config_path_to_command_vecs_appends_prefix_and_config() -> Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let cfg_file = temp.path().join("nginx.conf");
|
||||
tokio::fs::write(&cfg_file, b"data").await?;
|
||||
|
||||
let mut args: Vec<String> = vec!["base".to_string()];
|
||||
let result = CommandHandlerImpl::apply_config_path_to_command_vecs(
|
||||
&mut args,
|
||||
&cfg_file.to_string_lossy(),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
let args = result.expect("Failed to apply config path to command vecs");
|
||||
// expect -p <parent_dir> -c <config>
|
||||
assert!(args.contains(&"-p".to_string()));
|
||||
assert!(args.contains(&"-c".to_string()));
|
||||
assert!(args.contains(&cfg_file.to_string_lossy().to_string()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
448
apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs
Normal file
448
apps/nxmesh-agent/src/service/nginx_handler/fs_handler.rs
Normal file
@@ -0,0 +1,448 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fs4::tokio::AsyncFileExt;
|
||||
use thiserror::Error;
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{config::settings::NginxSettings, service::master_handler::MasterHandlerError};
|
||||
|
||||
#[cfg(test)]
|
||||
use mockall::predicate::*;
|
||||
// TODO: custom error type
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FsHandlerError {
|
||||
#[error("Invalid output path: {0}")]
|
||||
InvalidOutputPath(String),
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl From<FsHandlerError> for MasterHandlerError {
|
||||
fn from(err: FsHandlerError) -> Self {
|
||||
MasterHandlerError::MessageHandlingError(format!("File system handling error: {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
pub type FsResult<T> = std::result::Result<T, FsHandlerError>;
|
||||
type Result<T> = FsResult<T>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
pub trait FsHandler: Send + Sync + 'static {
|
||||
fn get_deployment_id(config_id: &str, version: &str) -> String
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
format!("{}-{}", config_id, version)
|
||||
}
|
||||
// Write a new config file for nginx.
|
||||
// The output_path is a relative path to the nginx config directory of the deployment folder. The actual path to the config should not be assumed by the caller, as it can be different in different environments, but will be promised to be relative to the deployment folder for each the corresponding deployment_id. Path traversal is not allowed.
|
||||
async fn write_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> Result<String>;
|
||||
// Append a new config content to an existing config file for nginx. This is useful for some use cases where we want to keep the existing config and just add some new config content to it. The output_path is a relative path to the nginx config directory of the deployment folder, which should be the same as the one used in write_config function. Path traversal is not allowed.
|
||||
async fn append_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> Result<String>;
|
||||
|
||||
// clean up old config files that are applied to nginx
|
||||
// keep only latest n deployments.
|
||||
async fn cleanup_config(&self, n: usize) -> Result<()>;
|
||||
|
||||
// Persist the root config path of the last successful deployment.
|
||||
// Survives agent restarts so Reload/Test commands work without a new ConfigUpdate.
|
||||
async fn save_last_deployment(&self, root_config_path: &str) -> Result<()>;
|
||||
|
||||
// Load the last persisted root config path, if any.
|
||||
// Returns Ok(None) when no state file exists or it is empty/corrupt.
|
||||
async fn load_last_deployment(&self) -> Result<Option<String>>;
|
||||
}
|
||||
|
||||
pub struct FsHandlerImpl {
|
||||
settings: Arc<NginxSettings>,
|
||||
}
|
||||
|
||||
impl FsHandlerImpl {
|
||||
pub fn new(settings: Arc<NginxSettings>) -> Self {
|
||||
Self { settings }
|
||||
}
|
||||
|
||||
fn validate_config_path(config_path: &str) -> Result<()> {
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
return Err(FsHandlerError::InvalidOutputPath(format!(
|
||||
"Config file not found at path: {}",
|
||||
config_path
|
||||
)));
|
||||
}
|
||||
if !std::path::Path::new(config_path).is_file() {
|
||||
return Err(FsHandlerError::InvalidOutputPath(format!(
|
||||
"Config path is not a file: {}",
|
||||
config_path
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_deployment_dir(&self) -> std::path::PathBuf {
|
||||
std::path::Path::new(&self.settings.nginx_config_path).join("deployments")
|
||||
}
|
||||
|
||||
fn get_deployment_dir_path(&self, deployment_id: &str) -> std::path::PathBuf {
|
||||
self.get_deployment_dir().join(deployment_id)
|
||||
}
|
||||
|
||||
fn get_state_file_path(&self) -> std::path::PathBuf {
|
||||
std::path::Path::new(&self.settings.nginx_config_path).join(".last_deployment")
|
||||
}
|
||||
|
||||
async fn get_deployment_config_path(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
output_path: &str,
|
||||
create_dir_if_not_exists: bool,
|
||||
) -> Result<std::path::PathBuf> {
|
||||
let output_path_obj = std::path::Path::new(output_path);
|
||||
if output_path_obj.is_absolute() {
|
||||
return Err(FsHandlerError::InvalidOutputPath(
|
||||
"Output path must be a relative path".into(),
|
||||
));
|
||||
}
|
||||
if output_path_obj
|
||||
.components()
|
||||
.any(|comp| comp == std::path::Component::ParentDir)
|
||||
{
|
||||
return Err(FsHandlerError::InvalidOutputPath(
|
||||
"Output path must not contain parent directory traversal".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let deployment_config_dir = self.get_deployment_dir_path(deployment_id);
|
||||
let full_path = deployment_config_dir.join(output_path);
|
||||
if create_dir_if_not_exists {
|
||||
if let Some(parent) = full_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
} else {
|
||||
tokio::fs::create_dir_all(&deployment_config_dir).await?;
|
||||
}
|
||||
}
|
||||
Ok(full_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FsHandler for FsHandlerImpl {
|
||||
async fn write_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> Result<String> {
|
||||
let full_output_path = self
|
||||
.get_deployment_config_path(deployment_id, output_path, true)
|
||||
.await?;
|
||||
let parent_dir = full_output_path.parent().ok_or_else(|| {
|
||||
FsHandlerError::InvalidOutputPath(format!(
|
||||
"Failed to get parent directory of output path: {:?}",
|
||||
full_output_path
|
||||
))
|
||||
})?;
|
||||
// ensure the parent directory exists before creating the file
|
||||
tokio::fs::create_dir_all(parent_dir).await?;
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(full_output_path.clone())
|
||||
.await?;
|
||||
// lock the file for writing to prevent concurrent write issue
|
||||
file.lock_exclusive()?;
|
||||
file.write_all(config_content.as_bytes()).await?;
|
||||
file.unlock()?;
|
||||
file.flush().await?;
|
||||
|
||||
Ok(full_output_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn append_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> Result<String> {
|
||||
let full_output_path = self
|
||||
.get_deployment_config_path(deployment_id, output_path, true)
|
||||
.await?;
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(full_output_path.clone())
|
||||
.await?;
|
||||
// lock the file for writing to prevent concurrent write issue
|
||||
file.lock_exclusive()?;
|
||||
file.write_all(config_content.as_bytes()).await?;
|
||||
file.unlock()?;
|
||||
file.flush().await?;
|
||||
|
||||
Ok(full_output_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn save_last_deployment(&self, root_config_path: &str) -> Result<()> {
|
||||
let state_path = self.get_state_file_path();
|
||||
let tmp_path = state_path.with_extension("tmp");
|
||||
tokio::fs::write(&tmp_path, format!("{}\n", root_config_path)).await?;
|
||||
tokio::fs::rename(&tmp_path, &state_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_last_deployment(&self) -> Result<Option<String>> {
|
||||
// primary: try state file
|
||||
let state_path = self.get_state_file_path();
|
||||
if state_path.exists() {
|
||||
let content = tokio::fs::read_to_string(&state_path).await?;
|
||||
let path = content.trim().to_string();
|
||||
if !path.is_empty() {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: scan deployments directory for the newest deployment
|
||||
let deployment_dir = self.get_deployment_dir();
|
||||
if !deployment_dir.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
// sort descending by mtime (newest first)
|
||||
candidates.sort_by(|a, b| b.1.cmp(&a.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()) {
|
||||
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();
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn cleanup_config(&self, n: usize) -> Result<()> {
|
||||
let deployment_dir = self.get_deployment_dir();
|
||||
// loop through all files in the deployment dir and delete them
|
||||
let mut entries = tokio::fs::read_dir(&deployment_dir).await?;
|
||||
let mut deployment_ids = Vec::new();
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let file_type = entry.file_type().await?;
|
||||
if file_type.is_dir()
|
||||
&& let Some(deployment_id) = entry.file_name().to_str()
|
||||
{
|
||||
deployment_ids.push(deployment_id.to_string());
|
||||
}
|
||||
}
|
||||
// sort the deployment ids by modified time in descending order and keep the latest n deployments, delete the rest
|
||||
deployment_ids.sort_by_key(|id| {
|
||||
let path = self.get_deployment_dir_path(id);
|
||||
std::fs::metadata(path)
|
||||
.and_then(|meta| meta.modified())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
});
|
||||
for deployment_id in deployment_ids.into_iter().skip(n) {
|
||||
let path = self.get_deployment_dir_path(&deployment_id);
|
||||
// ensure path is within the deplyment and nginx directory to prevent accidental deletion of other files
|
||||
if !path.starts_with(&deployment_dir)
|
||||
|| !path.starts_with(&self.settings.nginx_config_path)
|
||||
{
|
||||
warn!(
|
||||
"Skipping deletion of path outside of deployment or nginx config directory: {:?}",
|
||||
path
|
||||
);
|
||||
continue;
|
||||
}
|
||||
tokio::fs::remove_dir_all(path).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::expect_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_and_append_config_roundtrip() -> Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let settings = NginxSettings {
|
||||
nginx_config_path: temp.path().to_string_lossy().to_string(),
|
||||
nginx_binary_path: None,
|
||||
override_nginx_reload_command: vec![],
|
||||
override_nginx_test_command: vec![],
|
||||
nginx_reload_timeout_seconds: 1,
|
||||
nginx_test_timeout_seconds: 1,
|
||||
};
|
||||
|
||||
let handler = FsHandlerImpl::new(Arc::new(settings));
|
||||
|
||||
handler
|
||||
.write_config("deployment1", "hello", "conf/nginx.conf")
|
||||
.await?;
|
||||
|
||||
let full_path = temp
|
||||
.path()
|
||||
.join("deployments")
|
||||
.join("deployment1")
|
||||
.join("conf/nginx.conf");
|
||||
|
||||
let content = tokio::fs::read_to_string(&full_path).await?;
|
||||
assert_eq!(content, "hello");
|
||||
|
||||
handler
|
||||
.append_config("deployment1", " world", "conf/nginx.conf")
|
||||
.await?;
|
||||
|
||||
let content = tokio::fs::read_to_string(&full_path).await?;
|
||||
assert_eq!(content, "hello world");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_config_rejects_absolute_and_traversal_paths() -> Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let settings = NginxSettings {
|
||||
nginx_config_path: temp.path().to_string_lossy().to_string(),
|
||||
nginx_binary_path: None,
|
||||
override_nginx_reload_command: vec![],
|
||||
override_nginx_test_command: vec![],
|
||||
nginx_reload_timeout_seconds: 1,
|
||||
nginx_test_timeout_seconds: 1,
|
||||
};
|
||||
|
||||
let handler = FsHandlerImpl::new(Arc::new(settings));
|
||||
|
||||
let err = handler
|
||||
.write_config("d", "x", "/absolute/path.conf")
|
||||
.await
|
||||
.err();
|
||||
assert!(err.is_some());
|
||||
|
||||
let err = handler.write_config("d", "x", "../escape.conf").await.err();
|
||||
assert!(err.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_config_path_checks_file_exists_and_is_file() {
|
||||
// missing file
|
||||
let res = FsHandlerImpl::validate_config_path("/this/path/does/not/exist.conf");
|
||||
assert!(res.is_err());
|
||||
|
||||
// create a temp dir and ensure a directory is rejected
|
||||
let temp = TempDir::new().expect("Failed to create temp dir");
|
||||
let dir_path = temp.path();
|
||||
let res = FsHandlerImpl::validate_config_path(dir_path.to_string_lossy().as_ref());
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_deployment_config_path_create_flag_behaviour() -> Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let settings = NginxSettings {
|
||||
nginx_config_path: temp.path().to_string_lossy().to_string(),
|
||||
nginx_binary_path: None,
|
||||
override_nginx_reload_command: vec![],
|
||||
override_nginx_test_command: vec![],
|
||||
nginx_reload_timeout_seconds: 1,
|
||||
nginx_test_timeout_seconds: 1,
|
||||
};
|
||||
|
||||
let handler = FsHandlerImpl::new(Arc::new(settings));
|
||||
|
||||
// when create_dir_if_not_exists = false, directory shouldn't be created
|
||||
let path = handler
|
||||
.get_deployment_config_path("did", "conf/nginx.conf", false)
|
||||
.await?;
|
||||
assert!(
|
||||
!path
|
||||
.parent()
|
||||
.expect("Failed to get parent directory of deployment config path")
|
||||
.exists()
|
||||
);
|
||||
|
||||
// when create_dir_if_not_exists = true, directory should be created
|
||||
let path = handler
|
||||
.get_deployment_config_path("did", "conf/nginx.conf", true)
|
||||
.await?;
|
||||
assert!(
|
||||
path.parent()
|
||||
.expect("Failed to get parent directory of deployment config path")
|
||||
.exists()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cleanup_config_deletes_expected_deployments() -> Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let settings = NginxSettings {
|
||||
nginx_config_path: temp.path().to_string_lossy().to_string(),
|
||||
nginx_binary_path: None,
|
||||
override_nginx_reload_command: vec![],
|
||||
override_nginx_test_command: vec![],
|
||||
nginx_reload_timeout_seconds: 1,
|
||||
nginx_test_timeout_seconds: 1,
|
||||
};
|
||||
|
||||
let handler = FsHandlerImpl::new(Arc::new(settings));
|
||||
let base = temp.path().join("deployments");
|
||||
|
||||
// create three deployments sequentially so mtimes differ
|
||||
for id in &["d1", "d2", "d3"] {
|
||||
let p = base.join(id);
|
||||
std::fs::create_dir_all(&p)?;
|
||||
std::fs::write(p.join("file"), b"x")?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
}
|
||||
|
||||
// call cleanup keeping 1; current implementation keeps the oldest n, so expect only d1 remains
|
||||
handler.cleanup_config(1).await?;
|
||||
|
||||
let mut exists = vec![];
|
||||
for id in &["d1", "d2", "d3"] {
|
||||
exists.push((id.to_string(), base.join(id).exists()));
|
||||
}
|
||||
|
||||
// d1 should remain, others removed (matches current implementation behavior)
|
||||
assert!(exists.iter().find(|(id, e)| id == "d1" && *e).is_some());
|
||||
assert!(exists.iter().find(|(id, e)| id == "d2" && !*e).is_some());
|
||||
assert!(exists.iter().find(|(id, e)| id == "d3" && !*e).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
185
apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs
Normal file
185
apps/nxmesh-agent/src/service/nginx_handler/message_handler.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use nxmesh_proto::{
|
||||
ConfigUpdate, ConfigUpdateResult,
|
||||
agent_message::Payload::ConfigUpdateResult as ConfigUpdateResultPayload, command::Command,
|
||||
command_result,
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::{
|
||||
config::settings::NginxSettings,
|
||||
service::{
|
||||
master_handler::{
|
||||
MasterHandler, MessageResult,
|
||||
handlers::{OnCommandHandler, OnConfigUpdateHandler},
|
||||
},
|
||||
nginx_handler::{command_handler::CommandHandler, fs_handler::FsHandler},
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG_PATH: &str = "nginx.conf";
|
||||
const DEFAULT_NGINX_CONFIG_CONTENT: &str = r#"
|
||||
events {}
|
||||
"#;
|
||||
|
||||
pub trait NginxMasterMessageHandler: Send + Sync + 'static
|
||||
//
|
||||
+ OnConfigUpdateHandler
|
||||
+ OnCommandHandler
|
||||
{}
|
||||
|
||||
pub struct NginxMasterMessageHandlerImpl {
|
||||
settings: Arc<NginxSettings>,
|
||||
command_handler: Arc<dyn CommandHandler>,
|
||||
fs_handler: Arc<dyn FsHandler>,
|
||||
master_handler: Arc<dyn MasterHandler>,
|
||||
//
|
||||
// dash_map for for storing the on-going config updates, with the key as deployment_id, and the value as a tuple of (version_id, timestamp). On-going update must lock the deployment_id, and the new update with newer timestamp will wait until the lock is released. This is to ensure the config updates are applied in order.
|
||||
// When the current timestamp is older than the timestamp in the map, the current update must be rejected, and the master should be informed to resend the update with the latest timestamp.
|
||||
ongoing_updates: DashMap<String, (String, i64)>,
|
||||
}
|
||||
|
||||
impl NginxMasterMessageHandlerImpl {
|
||||
pub fn new(
|
||||
settings: Arc<NginxSettings>,
|
||||
command_handler: Arc<dyn CommandHandler>,
|
||||
fs_handler: Arc<dyn FsHandler>,
|
||||
master_handler: Arc<dyn MasterHandler>,
|
||||
) -> Self {
|
||||
Self {
|
||||
settings,
|
||||
command_handler,
|
||||
fs_handler,
|
||||
master_handler,
|
||||
ongoing_updates: DashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NginxMasterMessageHandler for NginxMasterMessageHandlerImpl {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl OnConfigUpdateHandler for NginxMasterMessageHandlerImpl {
|
||||
async fn on_config_update(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
timestamp: i64,
|
||||
message_id: &str,
|
||||
config_info: ConfigUpdate,
|
||||
) -> MessageResult<()> {
|
||||
// TODO: handle concurrency, expect only the latest version with latest timestamp is applied
|
||||
// when a newer config update comes in, and the older config update is still being processed. The new config will wait until the old config is applied.
|
||||
let deployment_id = format!("{}-{}", config_info.config_id, config_info.version);
|
||||
// write the configs
|
||||
let root_config_path = match config_info.root_config {
|
||||
Some(config_content) => {
|
||||
self.fs_handler
|
||||
.write_config(
|
||||
&deployment_id,
|
||||
&config_content.content,
|
||||
&config_content.path,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
// If the config content is not provided, write a default config to ensure the deployment folder is created and can be used for later updates.
|
||||
warn!(
|
||||
"Config content is not provided for config update, writing a default minimal config for deployment_id: {}",
|
||||
deployment_id
|
||||
);
|
||||
self.fs_handler
|
||||
.write_config(
|
||||
&deployment_id,
|
||||
DEFAULT_NGINX_CONFIG_CONTENT,
|
||||
DEFAULT_CONFIG_PATH,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
//
|
||||
for config in config_info.configs {
|
||||
self.fs_handler
|
||||
.write_config(&deployment_id, &config.content, &config.path)
|
||||
.await?;
|
||||
}
|
||||
// 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?;
|
||||
info!("Persisted last deployment path: {}", root_config_path);
|
||||
// Reply the master to confirm the config update is successful
|
||||
self.master_handler
|
||||
.send_message_to_master(nxmesh_proto::AgentMessage {
|
||||
agent_id: agent_id.to_string(),
|
||||
timestamp,
|
||||
message_id: message_id.to_string(),
|
||||
payload: Some(ConfigUpdateResultPayload(ConfigUpdateResult {
|
||||
success: true,
|
||||
error_message: None,
|
||||
config_id: config_info.config_id,
|
||||
version: config_info.version,
|
||||
})),
|
||||
})
|
||||
.await?;
|
||||
//
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl OnCommandHandler for NginxMasterMessageHandlerImpl {
|
||||
async fn on_command(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
timestamp: i64,
|
||||
message_id: &str,
|
||||
command: Command,
|
||||
) -> MessageResult<()> {
|
||||
// execute the command
|
||||
let mut agent_message = nxmesh_proto::AgentMessage {
|
||||
agent_id: agent_id.to_string(),
|
||||
timestamp,
|
||||
message_id: message_id.to_string(),
|
||||
payload: None,
|
||||
};
|
||||
// load the last known deployment path for use with Reload/Test commands
|
||||
let last_config_path = self.fs_handler.load_last_deployment().await?;
|
||||
|
||||
let result: command_result::Result = match command {
|
||||
Command::Reload(_) => {
|
||||
let result = self
|
||||
.command_handler
|
||||
.reload(last_config_path.as_deref())
|
||||
.await;
|
||||
command_result::Result::ReloadResult(nxmesh_proto::ReloadResult {
|
||||
success: result.is_ok(),
|
||||
error_message: result.err().map(|e| e.to_string()).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
Command::Test(_) => {
|
||||
let result = self
|
||||
.command_handler
|
||||
.validate(last_config_path.as_deref())
|
||||
.await;
|
||||
command_result::Result::TestResult(nxmesh_proto::TestResult {
|
||||
success: result.is_ok(),
|
||||
error_message: result.err().map(|e| e.to_string()).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
};
|
||||
// Reply the master to confirm the command execution is successful, and return the command output
|
||||
agent_message.payload = Some(nxmesh_proto::agent_message::Payload::CommandResult(
|
||||
nxmesh_proto::CommandResult {
|
||||
result: Some(result),
|
||||
},
|
||||
));
|
||||
|
||||
self.master_handler
|
||||
.send_message_to_master(agent_message)
|
||||
.await?;
|
||||
//
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
161
apps/nxmesh-agent/src/service/nginx_handler/mod.rs
Normal file
161
apps/nxmesh-agent/src/service/nginx_handler/mod.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nxmesh_proto::{ConfigUpdate, command::Command};
|
||||
|
||||
use crate::{
|
||||
config::settings::NginxSettings,
|
||||
service::{
|
||||
master_handler::{
|
||||
MasterHandler, MessageResult,
|
||||
handlers::{OnCommandHandler, OnConfigUpdateHandler},
|
||||
},
|
||||
nginx_handler::{
|
||||
command_handler::{CommandHandler, CommandHandlerImpl, CommandHandlerResult},
|
||||
fs_handler::{FsHandler, FsHandlerImpl, FsResult},
|
||||
message_handler::{NginxMasterMessageHandler, NginxMasterMessageHandlerImpl},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mod command_handler;
|
||||
mod fs_handler;
|
||||
mod message_handler;
|
||||
|
||||
#[cfg(test)]
|
||||
use mockall::predicate::*;
|
||||
// TODO: custom error type
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
pub trait NginxHandler: Send + Sync + 'static {
|
||||
// Reload nginx to apply new config. The config_path is an optional parameter that specifies the path to the nginx config file to be used for this reload operation. If not provided, the default config path will be used.
|
||||
async fn reload(&self, config_path: Option<&str>) -> CommandHandlerResult<()>;
|
||||
async fn stop(&self) -> CommandHandlerResult<()>;
|
||||
async fn validate(&self, config_path: Option<&str>) -> CommandHandlerResult<()>;
|
||||
async fn get_version(&self) -> CommandHandlerResult<String>;
|
||||
async fn get_status(&self) -> CommandHandlerResult<String>;
|
||||
// Write a new config file for nginx.
|
||||
// The output_path is a relative path to the nginx config directory of the deployment folder. The actual path to the config should not be assumed by the caller, as it can be different in different environments, but will be promised to be relative to the deployment folder for each the corresponding deployment_id. Path traversal is not allowed.
|
||||
async fn write_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> FsResult<String>;
|
||||
// Append a new config content to an existing config file for nginx. This is useful for some use cases where we want to keep the existing config and just add some new config content to it. The output_path is a relative path to the nginx config directory of the deployment folder, which should be the same as the one used in write_config function. Path traversal is not allowed.
|
||||
async fn append_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> FsResult<String>;
|
||||
|
||||
// clean up old config files that are applied to nginx
|
||||
// keep only latest n deployments.
|
||||
async fn cleanup_config(&self, n: usize) -> FsResult<()>;
|
||||
}
|
||||
|
||||
pub struct NginxHandlerImpl {
|
||||
settings: Arc<NginxSettings>,
|
||||
command_handler: Arc<dyn CommandHandler>,
|
||||
fs_handler: Arc<dyn FsHandler>,
|
||||
nginx_master_message_handler: Arc<dyn NginxMasterMessageHandler>,
|
||||
}
|
||||
|
||||
impl NginxHandlerImpl {
|
||||
pub fn new(settings: Arc<NginxSettings>, master_handler: Arc<dyn MasterHandler>) -> Self {
|
||||
let command_handler: Arc<dyn CommandHandler> =
|
||||
Arc::new(CommandHandlerImpl::new(settings.clone()));
|
||||
let fs_handler: Arc<dyn FsHandler> = Arc::new(FsHandlerImpl::new(settings.clone()));
|
||||
Self {
|
||||
settings: settings.clone(),
|
||||
command_handler: command_handler.clone(),
|
||||
fs_handler: fs_handler.clone(),
|
||||
nginx_master_message_handler: Arc::new(NginxMasterMessageHandlerImpl::new(
|
||||
settings.clone(),
|
||||
command_handler.clone(),
|
||||
fs_handler.clone(),
|
||||
master_handler,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NginxHandler for NginxHandlerImpl {
|
||||
async fn reload(&self, config_path: Option<&str>) -> CommandHandlerResult<()> {
|
||||
self.command_handler.reload(config_path).await
|
||||
}
|
||||
|
||||
async fn stop(&self) -> CommandHandlerResult<()> {
|
||||
self.command_handler.stop().await
|
||||
}
|
||||
|
||||
async fn validate(&self, config_path: Option<&str>) -> CommandHandlerResult<()> {
|
||||
self.command_handler.validate(config_path).await
|
||||
}
|
||||
|
||||
async fn get_version(&self) -> CommandHandlerResult<String> {
|
||||
self.command_handler.get_version().await
|
||||
}
|
||||
|
||||
async fn get_status(&self) -> CommandHandlerResult<String> {
|
||||
self.command_handler.get_status().await
|
||||
}
|
||||
|
||||
async fn write_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> FsResult<String> {
|
||||
self.fs_handler
|
||||
.write_config(deployment_id, config_content, output_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn append_config(
|
||||
&self,
|
||||
deployment_id: &str,
|
||||
config_content: &str,
|
||||
output_path: &str,
|
||||
) -> FsResult<String> {
|
||||
self.fs_handler
|
||||
.append_config(deployment_id, config_content, output_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn cleanup_config(&self, n: usize) -> FsResult<()> {
|
||||
self.fs_handler.cleanup_config(n).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl OnConfigUpdateHandler for NginxHandlerImpl {
|
||||
async fn on_config_update(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
timestamp: i64,
|
||||
message_id: &str,
|
||||
config_info: ConfigUpdate,
|
||||
) -> MessageResult<()> {
|
||||
self.nginx_master_message_handler
|
||||
.on_config_update(agent_id, timestamp, message_id, config_info)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl OnCommandHandler for NginxHandlerImpl {
|
||||
async fn on_command(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
timestamp: i64,
|
||||
message_id: &str,
|
||||
command: Command,
|
||||
) -> MessageResult<()> {
|
||||
self.nginx_master_message_handler
|
||||
.on_command(agent_id, timestamp, message_id, command)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,586 +0,0 @@
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use rcgen::string::Ia5String;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::{net::IpAddr, str::FromStr};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
type ValidationError = String;
|
||||
|
||||
trait Validate {
|
||||
fn validate(&self) -> Result<(), ValidationError>;
|
||||
}
|
||||
|
||||
/// Master server settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub server: ServerSettings,
|
||||
pub database: DatabaseSettings,
|
||||
pub grpc: GrpcSettings,
|
||||
pub auth: AuthSettings,
|
||||
#[serde(default)]
|
||||
pub log: LogSettings,
|
||||
}
|
||||
|
||||
/// HTTP server settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerSettings {
|
||||
#[serde(default = "default_server_bind_address")]
|
||||
pub bind_address: String,
|
||||
#[serde(default = "default_server_port")]
|
||||
pub port: u16,
|
||||
#[serde(default)]
|
||||
pub certificate: CertificateSettings,
|
||||
#[serde(default)]
|
||||
pub cors: Option<CorsSettings>,
|
||||
}
|
||||
|
||||
/// Database connection settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DatabaseSettings {
|
||||
pub url: String,
|
||||
pub max_connections: Option<u32>,
|
||||
}
|
||||
|
||||
/// gRPC server settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GrpcSettings {
|
||||
#[serde(default = "default_grpc_bind_address")]
|
||||
pub bind_address: String,
|
||||
#[serde(default = "default_grpc_port")]
|
||||
pub port: u16,
|
||||
#[serde(default)]
|
||||
pub certificate: CertificateSettings,
|
||||
#[serde(default)]
|
||||
pub cors: Option<CorsSettings>,
|
||||
}
|
||||
|
||||
/// Authentication settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthSettings {
|
||||
pub jwt_secret: String,
|
||||
#[serde(default = "default_jwt_expiration_hours")]
|
||||
pub jwt_expiration_hours: u64,
|
||||
}
|
||||
|
||||
/// TLS certificate settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CertificateSettings {
|
||||
#[serde(default = "default_cert_folder")]
|
||||
pub cert_dir: String,
|
||||
#[serde(
|
||||
default,
|
||||
serialize_with = "serialize_ia5string_vec",
|
||||
deserialize_with = "deserialize_ia5string_vec"
|
||||
)]
|
||||
pub san_dns: Vec<Ia5String>,
|
||||
#[serde(default)]
|
||||
pub san_ip: Vec<IpAddr>,
|
||||
#[serde(default)]
|
||||
cert_path: Option<String>,
|
||||
#[serde(default)]
|
||||
key_path: Option<String>,
|
||||
}
|
||||
|
||||
impl CertificateSettings {
|
||||
pub fn cert_path(&self) -> Option<String> {
|
||||
self.cert_path
|
||||
.as_ref()
|
||||
.map(|p| format!("{}/{}", self.cert_dir, p))
|
||||
}
|
||||
|
||||
pub fn key_path(&self) -> Option<String> {
|
||||
self.key_path
|
||||
.as_ref()
|
||||
.map(|p| format!("{}/{}", self.cert_dir, p))
|
||||
}
|
||||
}
|
||||
|
||||
/// CORS settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CorsSettings {
|
||||
#[serde(default)]
|
||||
pub allowed_origins: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_methods: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_headers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_credentials: bool,
|
||||
}
|
||||
|
||||
/// Logging settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogSettings {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
pub level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Default for LogSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: default_log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for Settings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
self.server.validate()?;
|
||||
self.grpc.validate()?;
|
||||
self.database.validate()?;
|
||||
self.auth.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Load settings from config files and environment
|
||||
pub fn load() -> Result<Self, ConfigError> {
|
||||
let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
|
||||
|
||||
let settings = Config::builder()
|
||||
.add_source(File::with_name("config/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
|
||||
.add_source(File::with_name("config/master/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/master/{}", run_mode)).required(false))
|
||||
.add_source(Environment::with_prefix("NXMESH").separator("__"))
|
||||
.build()?;
|
||||
|
||||
let settings: Self = settings.try_deserialize()?;
|
||||
|
||||
settings.validate().map_err(ConfigError::Message)?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for ServerSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.bind_address.is_empty() {
|
||||
return Err("Server bind address cannot be empty".into());
|
||||
}
|
||||
if self.port == 0 {
|
||||
return Err("Server port must be greater than 0".into());
|
||||
}
|
||||
self.certificate.validate()?;
|
||||
if let Some(cors) = &self.cors {
|
||||
cors.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for GrpcSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.bind_address.is_empty() {
|
||||
return Err("gRPC bind address cannot be empty".into());
|
||||
}
|
||||
if self.port == 0 {
|
||||
return Err("gRPC port must be greater than 0".into());
|
||||
}
|
||||
self.certificate.validate()?;
|
||||
if let Some(cors) = &self.cors {
|
||||
cors.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for DatabaseSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.url.is_empty() {
|
||||
return Err("Database URL cannot be empty".into());
|
||||
}
|
||||
if let Some(max_connections) = self.max_connections
|
||||
&& max_connections == 0
|
||||
{
|
||||
return Err("Max database connections must be greater than 0".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for AuthSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.jwt_secret.is_empty() {
|
||||
return Err("JWT secret cannot be empty".into());
|
||||
}
|
||||
if self.jwt_expiration_hours == 0 {
|
||||
return Err("JWT expiration hours must be greater than 0".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for CertificateSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
let base_path = std::path::Path::new(&self.cert_dir);
|
||||
if !base_path.exists() {
|
||||
// create the cert directory if it doesn't exist
|
||||
std::fs::create_dir_all(base_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to create certificate directory {:?}: {}",
|
||||
base_path, e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let cert_path = self.cert_path.as_ref().map(|p| base_path.join(p));
|
||||
let key_path = self.key_path.as_ref().map(|p| base_path.join(p));
|
||||
if (cert_path.is_some() && key_path.is_none())
|
||||
|| (cert_path.is_none() && key_path.is_some())
|
||||
{
|
||||
return Err("Both certificate and key paths must be provided for TLS".into());
|
||||
}
|
||||
if let (Some(cert_path), Some(key_path)) = (&cert_path, &key_path) {
|
||||
if !std::path::Path::new(cert_path).exists() {
|
||||
return Err(format!("Certificate file not found: {:?}", cert_path));
|
||||
}
|
||||
if !std::path::Path::new(key_path).exists() {
|
||||
return Err(format!("Key file not found: {:?}", key_path));
|
||||
}
|
||||
}
|
||||
|
||||
// validate for SAN entries - must be valid DNS names or IP addresses
|
||||
for dns in &self.san_dns {
|
||||
if dns.to_string().is_empty() {
|
||||
return Err("SAN DNS entries cannot be empty".into());
|
||||
}
|
||||
}
|
||||
for ip in &self.san_ip {
|
||||
if ip.is_unspecified() {
|
||||
return Err("SAN IP entries cannot be unspecified".into());
|
||||
}
|
||||
}
|
||||
// require at least one SAN entry for the generated certificate
|
||||
if self.san_dns.is_empty() && self.san_ip.is_empty() {
|
||||
return Err(
|
||||
"At least one SAN entry (DNS or IP) must be provided for the certificate".into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for CorsSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_jwt_expiration_hours() -> u64 {
|
||||
24
|
||||
}
|
||||
|
||||
fn default_server_bind_address() -> String {
|
||||
"0.0.0.0".into()
|
||||
}
|
||||
|
||||
fn default_server_port() -> u16 {
|
||||
8080
|
||||
}
|
||||
|
||||
fn default_grpc_bind_address() -> String {
|
||||
"0.0.0.0".into()
|
||||
}
|
||||
|
||||
fn default_grpc_port() -> u16 {
|
||||
50051
|
||||
}
|
||||
|
||||
fn default_log_level() -> LevelFilter {
|
||||
LevelFilter::INFO
|
||||
}
|
||||
|
||||
fn default_cert_folder() -> String {
|
||||
"./certs".into()
|
||||
}
|
||||
|
||||
fn deserialize_level_filter<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
LevelFilter::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn serialize_level_filter<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&level.to_string())
|
||||
}
|
||||
|
||||
fn deserialize_ia5string_vec<'de, D>(deserializer: D) -> Result<Vec<Ia5String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec = Vec::<String>::deserialize(deserializer)?;
|
||||
vec.into_iter()
|
||||
.map(|s| Ia5String::try_from(s).map_err(serde::de::Error::custom))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn serialize_ia5string_vec<S>(vec: &Vec<Ia5String>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let string_vec: Vec<String> = vec.iter().map(|ia5| ia5.to_string()).collect();
|
||||
string_vec.serialize(serializer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Settings>();
|
||||
assert_send_sync::<ServerSettings>();
|
||||
assert_send_sync::<DatabaseSettings>();
|
||||
assert_send_sync::<GrpcSettings>();
|
||||
assert_send_sync::<AuthSettings>();
|
||||
assert_send_sync::<CertificateSettings>();
|
||||
assert_send_sync::<CorsSettings>();
|
||||
assert_send_sync::<LogSettings>();
|
||||
}
|
||||
|
||||
fn make_temp_dir(prefix: &str) -> PathBuf {
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH);
|
||||
assert!(ts.is_ok());
|
||||
let ts = ts.unwrap_or_default();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"{}_{}_{}",
|
||||
prefix,
|
||||
std::process::id(),
|
||||
ts.as_nanos()
|
||||
));
|
||||
let created = fs::create_dir_all(&path);
|
||||
assert!(created.is_ok());
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_paths_include_cert_dir() {
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: "./certs".to_string(),
|
||||
san_dns: Vec::new(),
|
||||
san_ip: Vec::new(),
|
||||
cert_path: Some("server.crt".to_string()),
|
||||
key_path: Some("server.key".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(cert.cert_path(), Some("./certs/server.crt".to_string()));
|
||||
assert_eq!(cert.key_path(), Some("./certs/server.key".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_creates_directory_when_missing() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-create").join("nested");
|
||||
let san = Ia5String::try_from("localhost".to_string());
|
||||
assert!(san.is_ok());
|
||||
let san = san.unwrap_or_else(|_| unreachable!());
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: vec![san],
|
||||
san_ip: Vec::new(),
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_ok());
|
||||
assert!(cert_dir.exists());
|
||||
|
||||
let _ = fs::remove_dir_all(cert_dir.parent().unwrap_or(&cert_dir));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_fails_when_only_cert_path_is_set() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-partial");
|
||||
let san = Ia5String::try_from("localhost".to_string());
|
||||
assert!(san.is_ok());
|
||||
let san = san.unwrap_or_else(|_| unreachable!());
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: vec![san],
|
||||
san_ip: Vec::new(),
|
||||
cert_path: Some("server.crt".to_string()),
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("Both certificate and key paths must be provided"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_fails_with_unspecified_ip() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-unspecified-ip");
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: Vec::new(),
|
||||
san_ip: vec![IpAddr::V4(Ipv4Addr::UNSPECIFIED)],
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("SAN IP entries cannot be unspecified"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_fails_without_any_san_entries() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-no-san");
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: Vec::new(),
|
||||
san_ip: Vec::new(),
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("At least one SAN entry"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_validate_fails_for_zero_max_connections() {
|
||||
let db = DatabaseSettings {
|
||||
url: "postgres://localhost/db".to_string(),
|
||||
max_connections: Some(0),
|
||||
};
|
||||
|
||||
let result = db.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("Max database connections must be greater than 0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_validate_fails_for_empty_secret() {
|
||||
let auth = AuthSettings {
|
||||
jwt_secret: "".to_string(),
|
||||
jwt_expiration_hours: 24,
|
||||
};
|
||||
|
||||
let result = auth.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("JWT secret cannot be empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_validate_fails_for_zero_port() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-server-validate");
|
||||
let san = Ia5String::try_from("localhost".to_string());
|
||||
assert!(san.is_ok());
|
||||
let san = san.unwrap_or_else(|_| unreachable!());
|
||||
let server = ServerSettings {
|
||||
bind_address: "0.0.0.0".to_string(),
|
||||
port: 0,
|
||||
certificate: CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: vec![san],
|
||||
san_ip: Vec::new(),
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
},
|
||||
cors: None,
|
||||
};
|
||||
|
||||
let result = server.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("Server port must be greater than 0"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_filter_round_trip_serialization() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Wrapper {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
let data = Wrapper {
|
||||
level: LevelFilter::DEBUG,
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_string(&data);
|
||||
assert!(encoded.is_ok());
|
||||
let encoded = encoded.unwrap_or_default();
|
||||
assert!(encoded.to_lowercase().contains("debug"));
|
||||
|
||||
let decoded: Result<Wrapper, _> = serde_json::from_str(&encoded);
|
||||
assert!(decoded.is_ok());
|
||||
let decoded = decoded.unwrap_or(Wrapper {
|
||||
level: LevelFilter::ERROR,
|
||||
});
|
||||
assert_eq!(decoded.level, LevelFilter::DEBUG);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ia5string_vec_round_trip_serialization() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Wrapper {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_ia5string_vec",
|
||||
serialize_with = "serialize_ia5string_vec"
|
||||
)]
|
||||
san_dns: Vec<Ia5String>,
|
||||
}
|
||||
|
||||
let first = Ia5String::try_from("localhost".to_string());
|
||||
assert!(first.is_ok());
|
||||
let second = Ia5String::try_from("example.com".to_string());
|
||||
assert!(second.is_ok());
|
||||
|
||||
let first = first.unwrap_or_else(|_| unreachable!());
|
||||
let second = second.unwrap_or_else(|_| unreachable!());
|
||||
|
||||
let data = Wrapper {
|
||||
san_dns: vec![first, second],
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_string(&data);
|
||||
assert!(encoded.is_ok());
|
||||
let encoded = encoded.unwrap_or_default();
|
||||
assert!(encoded.contains("localhost"));
|
||||
assert!(encoded.contains("example.com"));
|
||||
|
||||
let decoded: Result<Wrapper, _> = serde_json::from_str(&encoded);
|
||||
assert!(decoded.is_ok());
|
||||
let decoded = decoded.unwrap_or(Wrapper {
|
||||
san_dns: Vec::new(),
|
||||
});
|
||||
assert_eq!(decoded.san_dns.len(), 2);
|
||||
}
|
||||
}
|
||||
58
apps/nxmesh-master/src/config/settings/auth.rs
Normal file
58
apps/nxmesh-master/src/config/settings/auth.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{Validate, ValidationError};
|
||||
|
||||
/// Authentication settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthSettings {
|
||||
pub jwt_secret: String,
|
||||
#[serde(default = "default_jwt_expiration_hours")]
|
||||
pub jwt_expiration_hours: u64,
|
||||
}
|
||||
|
||||
impl Validate for AuthSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.jwt_secret.is_empty() {
|
||||
return Err("JWT secret cannot be empty".into());
|
||||
}
|
||||
if self.jwt_expiration_hours == 0 {
|
||||
return Err("JWT expiration hours must be greater than 0".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_jwt_expiration_hours() -> u64 {
|
||||
24
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<AuthSettings>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_validate_fails_for_empty_secret() {
|
||||
let auth = AuthSettings {
|
||||
jwt_secret: "".to_string(),
|
||||
jwt_expiration_hours: 24,
|
||||
};
|
||||
|
||||
let result = auth.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("JWT secret cannot be empty"));
|
||||
}
|
||||
}
|
||||
276
apps/nxmesh-master/src/config/settings/cert.rs
Normal file
276
apps/nxmesh-master/src/config/settings/cert.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use rcgen::string::Ia5String;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::config::settings::{Validate, ValidationError};
|
||||
|
||||
/// TLS certificate settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CertificateSettings {
|
||||
#[serde(default = "default_cert_folder")]
|
||||
pub cert_dir: String,
|
||||
#[serde(
|
||||
default,
|
||||
serialize_with = "serialize_ia5string_vec",
|
||||
deserialize_with = "deserialize_ia5string_vec"
|
||||
)]
|
||||
pub san_dns: Vec<Ia5String>,
|
||||
#[serde(default)]
|
||||
pub san_ip: Vec<IpAddr>,
|
||||
#[serde(default)]
|
||||
pub cert_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub key_path: Option<String>,
|
||||
}
|
||||
|
||||
impl CertificateSettings {
|
||||
pub fn cert_path(&self) -> Option<String> {
|
||||
self.cert_path
|
||||
.as_ref()
|
||||
.map(|p| format!("{}/{}", self.cert_dir, p))
|
||||
}
|
||||
|
||||
pub fn key_path(&self) -> Option<String> {
|
||||
self.key_path
|
||||
.as_ref()
|
||||
.map(|p| format!("{}/{}", self.cert_dir, p))
|
||||
}
|
||||
}
|
||||
|
||||
impl Validate for CertificateSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
let base_path = std::path::Path::new(&self.cert_dir);
|
||||
if !base_path.exists() {
|
||||
// create the cert directory if it doesn't exist
|
||||
std::fs::create_dir_all(base_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to create certificate directory {:?}: {}",
|
||||
base_path, e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let cert_path = self.cert_path.as_ref().map(|p| base_path.join(p));
|
||||
let key_path = self.key_path.as_ref().map(|p| base_path.join(p));
|
||||
if (cert_path.is_some() && key_path.is_none())
|
||||
|| (cert_path.is_none() && key_path.is_some())
|
||||
{
|
||||
return Err("Both certificate and key paths must be provided for TLS".into());
|
||||
}
|
||||
if let (Some(cert_path), Some(key_path)) = (&cert_path, &key_path) {
|
||||
if !std::path::Path::new(cert_path).exists() {
|
||||
return Err(format!("Certificate file not found: {:?}", cert_path));
|
||||
}
|
||||
if !std::path::Path::new(key_path).exists() {
|
||||
return Err(format!("Key file not found: {:?}", key_path));
|
||||
}
|
||||
}
|
||||
|
||||
// validate for SAN entries - must be valid DNS names or IP addresses
|
||||
for dns in &self.san_dns {
|
||||
if dns.to_string().is_empty() {
|
||||
return Err("SAN DNS entries cannot be empty".into());
|
||||
}
|
||||
}
|
||||
for ip in &self.san_ip {
|
||||
if ip.is_unspecified() {
|
||||
return Err("SAN IP entries cannot be unspecified".into());
|
||||
}
|
||||
}
|
||||
// require at least one SAN entry for the generated certificate
|
||||
if self.san_dns.is_empty() && self.san_ip.is_empty() {
|
||||
return Err(
|
||||
"At least one SAN entry (DNS or IP) must be provided for the certificate".into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_cert_folder() -> String {
|
||||
"./certs".into()
|
||||
}
|
||||
|
||||
fn deserialize_ia5string_vec<'de, D>(deserializer: D) -> Result<Vec<Ia5String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec = Vec::<String>::deserialize(deserializer)?;
|
||||
vec.into_iter()
|
||||
.map(|s| Ia5String::try_from(s).map_err(serde::de::Error::custom))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn serialize_ia5string_vec<S>(vec: &Vec<Ia5String>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let string_vec: Vec<String> = vec.iter().map(|ia5| ia5.to_string()).collect();
|
||||
string_vec.serialize(serializer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<CertificateSettings>();
|
||||
}
|
||||
|
||||
fn make_temp_dir(prefix: &str) -> PathBuf {
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH);
|
||||
assert!(ts.is_ok());
|
||||
let ts = ts.unwrap_or_default();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"{}_{}_{}",
|
||||
prefix,
|
||||
std::process::id(),
|
||||
ts.as_nanos()
|
||||
));
|
||||
let created = fs::create_dir_all(&path);
|
||||
assert!(created.is_ok());
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_paths_include_cert_dir() {
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: "./certs".to_string(),
|
||||
san_dns: Vec::new(),
|
||||
san_ip: Vec::new(),
|
||||
cert_path: Some("server.crt".to_string()),
|
||||
key_path: Some("server.key".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(cert.cert_path(), Some("./certs/server.crt".to_string()));
|
||||
assert_eq!(cert.key_path(), Some("./certs/server.key".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_creates_directory_when_missing() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-create").join("nested");
|
||||
let san = Ia5String::try_from("localhost".to_string());
|
||||
assert!(san.is_ok());
|
||||
let san = san.unwrap_or_else(|_| unreachable!());
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: vec![san],
|
||||
san_ip: Vec::new(),
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_ok());
|
||||
assert!(cert_dir.exists());
|
||||
|
||||
let _ = fs::remove_dir_all(cert_dir.parent().unwrap_or(&cert_dir));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_fails_when_only_cert_path_is_set() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-partial");
|
||||
let san = Ia5String::try_from("localhost".to_string());
|
||||
assert!(san.is_ok());
|
||||
let san = san.unwrap_or_else(|_| unreachable!());
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: vec![san],
|
||||
san_ip: Vec::new(),
|
||||
cert_path: Some("server.crt".to_string()),
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("Both certificate and key paths must be provided"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_fails_with_unspecified_ip() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-unspecified-ip");
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: Vec::new(),
|
||||
san_ip: vec![IpAddr::V4(Ipv4Addr::UNSPECIFIED)],
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("SAN IP entries cannot be unspecified"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certificate_validate_fails_without_any_san_entries() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-cert-no-san");
|
||||
let cert = CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: Vec::new(),
|
||||
san_ip: Vec::new(),
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
};
|
||||
|
||||
let result = cert.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("At least one SAN entry"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ia5string_vec_round_trip_serialization() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Wrapper {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_ia5string_vec",
|
||||
serialize_with = "serialize_ia5string_vec"
|
||||
)]
|
||||
san_dns: Vec<Ia5String>,
|
||||
}
|
||||
|
||||
let first = Ia5String::try_from("localhost".to_string());
|
||||
assert!(first.is_ok());
|
||||
let second = Ia5String::try_from("example.com".to_string());
|
||||
assert!(second.is_ok());
|
||||
|
||||
let first = first.unwrap_or_else(|_| unreachable!());
|
||||
let second = second.unwrap_or_else(|_| unreachable!());
|
||||
|
||||
let data = Wrapper {
|
||||
san_dns: vec![first, second],
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_string(&data);
|
||||
assert!(encoded.is_ok());
|
||||
let encoded = encoded.unwrap_or_default();
|
||||
assert!(encoded.contains("localhost"));
|
||||
assert!(encoded.contains("example.com"));
|
||||
|
||||
let decoded: Result<Wrapper, _> = serde_json::from_str(&encoded);
|
||||
assert!(decoded.is_ok());
|
||||
let decoded = decoded.unwrap_or(Wrapper {
|
||||
san_dns: Vec::new(),
|
||||
});
|
||||
assert_eq!(decoded.san_dns.len(), 2);
|
||||
}
|
||||
}
|
||||
34
apps/nxmesh-master/src/config/settings/cors.rs
Normal file
34
apps/nxmesh-master/src/config/settings/cors.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{Validate, ValidationError};
|
||||
|
||||
/// CORS settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CorsSettings {
|
||||
#[serde(default)]
|
||||
pub allowed_origins: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_methods: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed_headers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_credentials: bool,
|
||||
}
|
||||
|
||||
impl Validate for CorsSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<CorsSettings>();
|
||||
}
|
||||
}
|
||||
48
apps/nxmesh-master/src/config/settings/database.rs
Normal file
48
apps/nxmesh-master/src/config/settings/database.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{Validate, ValidationError};
|
||||
|
||||
/// Database connection settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DatabaseSettings {
|
||||
pub url: String,
|
||||
pub max_connections: Option<u32>,
|
||||
}
|
||||
|
||||
impl Validate for DatabaseSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.url.is_empty() {
|
||||
return Err("Database URL cannot be empty".into());
|
||||
}
|
||||
if let Some(max_connections) = self.max_connections
|
||||
&& max_connections == 0
|
||||
{
|
||||
return Err("Max database connections must be greater than 0".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<DatabaseSettings>();
|
||||
}
|
||||
#[test]
|
||||
fn database_validate_fails_for_zero_max_connections() {
|
||||
let db = DatabaseSettings {
|
||||
url: "postgres://localhost/db".to_string(),
|
||||
max_connections: Some(0),
|
||||
};
|
||||
|
||||
let result = db.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("Max database connections must be greater than 0"));
|
||||
}
|
||||
}
|
||||
53
apps/nxmesh-master/src/config/settings/grpc.rs
Normal file
53
apps/nxmesh-master/src/config/settings/grpc.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{
|
||||
Validate, ValidationError, cert::CertificateSettings, cors::CorsSettings,
|
||||
};
|
||||
|
||||
/// gRPC server settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GrpcSettings {
|
||||
#[serde(default = "default_grpc_bind_address")]
|
||||
pub bind_address: String,
|
||||
#[serde(default = "default_grpc_port")]
|
||||
pub port: u16,
|
||||
#[serde(default)]
|
||||
pub certificate: CertificateSettings,
|
||||
#[serde(default)]
|
||||
pub cors: Option<CorsSettings>,
|
||||
}
|
||||
|
||||
impl Validate for GrpcSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.bind_address.is_empty() {
|
||||
return Err("gRPC bind address cannot be empty".into());
|
||||
}
|
||||
if self.port == 0 {
|
||||
return Err("gRPC port must be greater than 0".into());
|
||||
}
|
||||
self.certificate.validate()?;
|
||||
if let Some(cors) = &self.cors {
|
||||
cors.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_grpc_bind_address() -> String {
|
||||
"0.0.0.0".into()
|
||||
}
|
||||
|
||||
fn default_grpc_port() -> u16 {
|
||||
50051
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<GrpcSettings>();
|
||||
}
|
||||
}
|
||||
81
apps/nxmesh-master/src/config/settings/log.rs
Normal file
81
apps/nxmesh-master/src/config/settings/log.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
/// Logging settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogSettings {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
pub level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Default for LogSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: default_log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_log_level() -> LevelFilter {
|
||||
LevelFilter::INFO
|
||||
}
|
||||
|
||||
fn deserialize_level_filter<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
LevelFilter::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn serialize_level_filter<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&level.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<LogSettings>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_filter_round_trip_serialization() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Wrapper {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_level_filter",
|
||||
serialize_with = "serialize_level_filter"
|
||||
)]
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
let data = Wrapper {
|
||||
level: LevelFilter::DEBUG,
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_string(&data);
|
||||
assert!(encoded.is_ok());
|
||||
let encoded = encoded.unwrap_or_default();
|
||||
assert!(encoded.to_lowercase().contains("debug"));
|
||||
|
||||
let decoded: Result<Wrapper, _> = serde_json::from_str(&encoded);
|
||||
assert!(decoded.is_ok());
|
||||
let decoded = decoded.unwrap_or(Wrapper {
|
||||
level: LevelFilter::ERROR,
|
||||
});
|
||||
assert_eq!(decoded.level, LevelFilter::DEBUG);
|
||||
}
|
||||
}
|
||||
75
apps/nxmesh-master/src/config/settings/mod.rs
Normal file
75
apps/nxmesh-master/src/config/settings/mod.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type ValidationError = String;
|
||||
|
||||
pub mod auth;
|
||||
pub mod cert;
|
||||
pub mod cors;
|
||||
pub mod database;
|
||||
pub mod grpc;
|
||||
pub mod log;
|
||||
pub mod server;
|
||||
|
||||
use auth::AuthSettings;
|
||||
use database::DatabaseSettings;
|
||||
use grpc::GrpcSettings;
|
||||
use log::LogSettings;
|
||||
use server::ServerSettings;
|
||||
|
||||
pub trait Validate {
|
||||
fn validate(&self) -> Result<(), ValidationError>;
|
||||
}
|
||||
|
||||
/// Master server settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub server: ServerSettings,
|
||||
pub database: DatabaseSettings,
|
||||
pub grpc: GrpcSettings,
|
||||
pub auth: AuthSettings,
|
||||
#[serde(default)]
|
||||
pub log: LogSettings,
|
||||
}
|
||||
|
||||
impl Validate for Settings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
self.server.validate()?;
|
||||
self.grpc.validate()?;
|
||||
self.database.validate()?;
|
||||
self.auth.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Load settings from config files and environment
|
||||
pub fn load() -> Result<Self, ConfigError> {
|
||||
let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
|
||||
|
||||
let settings = Config::builder()
|
||||
.add_source(File::with_name("config/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
|
||||
.add_source(File::with_name("config/master/default").required(false))
|
||||
.add_source(File::with_name(&format!("config/master/{}", run_mode)).required(false))
|
||||
.add_source(Environment::with_prefix("NXMESH").separator("__"))
|
||||
.build()?;
|
||||
|
||||
let settings: Self = settings.try_deserialize()?;
|
||||
|
||||
settings.validate().map_err(ConfigError::Message)?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Settings>();
|
||||
}
|
||||
}
|
||||
103
apps/nxmesh-master/src/config/settings/server.rs
Normal file
103
apps/nxmesh-master/src/config/settings/server.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::settings::{
|
||||
Validate, ValidationError, cert::CertificateSettings, cors::CorsSettings,
|
||||
};
|
||||
|
||||
/// HTTP server settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerSettings {
|
||||
#[serde(default = "default_server_bind_address")]
|
||||
pub bind_address: String,
|
||||
#[serde(default = "default_server_port")]
|
||||
pub port: u16,
|
||||
#[serde(default)]
|
||||
pub certificate: CertificateSettings,
|
||||
#[serde(default)]
|
||||
pub cors: Option<CorsSettings>,
|
||||
}
|
||||
|
||||
impl Validate for ServerSettings {
|
||||
fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.bind_address.is_empty() {
|
||||
return Err("Server bind address cannot be empty".into());
|
||||
}
|
||||
if self.port == 0 {
|
||||
return Err("Server port must be greater than 0".into());
|
||||
}
|
||||
self.certificate.validate()?;
|
||||
if let Some(cors) = &self.cors {
|
||||
cors.validate()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_server_bind_address() -> String {
|
||||
"0.0.0.0".into()
|
||||
}
|
||||
|
||||
fn default_server_port() -> u16 {
|
||||
8080
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use rcgen::string::Ia5String;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_esnure_send_and_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<ServerSettings>();
|
||||
}
|
||||
|
||||
fn make_temp_dir(prefix: &str) -> PathBuf {
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH);
|
||||
assert!(ts.is_ok());
|
||||
let ts = ts.unwrap_or_default();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"{}_{}_{}",
|
||||
prefix,
|
||||
std::process::id(),
|
||||
ts.as_nanos()
|
||||
));
|
||||
let created = fs::create_dir_all(&path);
|
||||
assert!(created.is_ok());
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_validate_fails_for_zero_port() {
|
||||
let cert_dir = make_temp_dir("nxmesh-master-server-validate");
|
||||
let san = Ia5String::try_from("localhost".to_string());
|
||||
assert!(san.is_ok());
|
||||
let san = san.unwrap_or_else(|_| unreachable!());
|
||||
let server = ServerSettings {
|
||||
bind_address: "0.0.0.0".to_string(),
|
||||
port: 0,
|
||||
certificate: CertificateSettings {
|
||||
cert_dir: cert_dir.to_string_lossy().to_string(),
|
||||
san_dns: vec![san],
|
||||
san_ip: Vec::new(),
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
},
|
||||
cors: None,
|
||||
};
|
||||
|
||||
let result = server.validate();
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap_or_default();
|
||||
assert!(msg.contains("Server port must be greater than 0"));
|
||||
|
||||
let _ = fs::remove_dir_all(&cert_dir);
|
||||
}
|
||||
}
|
||||
61
apps/nxmesh-master/src/db/entities/access_rule.rs
Normal file
61
apps/nxmesh-master/src/db/entities/access_rule.rs
Normal file
@@ -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<Uuid>,
|
||||
pub location_id: Option<Uuid>,
|
||||
pub r#type: String,
|
||||
pub ip_cidr: String,
|
||||
pub description: Option<String>,
|
||||
pub priority: i32,
|
||||
pub is_deleted: bool,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::location_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LocationBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
64
apps/nxmesh-master/src/db/entities/agent_config_binding.rs
Normal file
64
apps/nxmesh-master/src/db/entities/agent_config_binding.rs
Normal file
@@ -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<Uuid>,
|
||||
pub group_id: Option<Uuid>,
|
||||
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<super::agent_group::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AgentGroup.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::agents::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Agents.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::proxy_config::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ProxyConfig.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
35
apps/nxmesh-master/src/db/entities/agent_group.rs
Normal file
35
apps/nxmesh-master/src/db/entities/agent_group.rs
Normal file
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[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<super::agent_config_binding::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AgentConfigBinding.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::agents::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Agents.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -19,9 +19,33 @@ pub struct Model {
|
||||
pub labels: Option<Json>,
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
pub updated_at: DateTimeWithTimeZone,
|
||||
pub group_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::agent_config_binding::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AgentConfigBinding.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::agent_group::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AgentGroup.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
37
apps/nxmesh-master/src/db/entities/cache_zone.rs
Normal file
37
apps/nxmesh-master/src/db/entities/cache_zone.rs
Normal file
@@ -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<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::proxy_setting::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ProxySetting.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
37
apps/nxmesh-master/src/db/entities/config_inheritance.rs
Normal file
37
apps/nxmesh-master/src/db/entities/config_inheritance.rs
Normal file
@@ -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<i32>,
|
||||
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 {}
|
||||
59
apps/nxmesh-master/src/db/entities/limit_rule.rs
Normal file
59
apps/nxmesh-master/src/db/entities/limit_rule.rs
Normal file
@@ -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<i32>,
|
||||
pub nodelay: Option<bool>,
|
||||
pub is_deleted: bool,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::limit_zone::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LimitZone.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::location_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LocationBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
37
apps/nxmesh-master/src/db/entities/limit_zone.rs
Normal file
37
apps/nxmesh-master/src/db/entities/limit_zone.rs
Normal file
@@ -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<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::limit_rule::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LimitRule.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
91
apps/nxmesh-master/src/db/entities/location_block.rs
Normal file
91
apps/nxmesh-master/src/db/entities/location_block.rs
Normal file
@@ -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<Uuid>,
|
||||
#[sea_orm(column_type = "JsonBinary", nullable)]
|
||||
pub metadata: Option<Json>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::access_rule::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AccessRule.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::limit_rule::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LimitRule.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::proxy_setting::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ProxySetting.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::rewrite_rule::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::RewriteRule.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::upstream::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Upstream.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
44
apps/nxmesh-master/src/db/entities/log_setting.rs
Normal file
44
apps/nxmesh-master/src/db/entities/log_setting.rs
Normal file
@@ -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<String>,
|
||||
pub error_log_path: Option<String>,
|
||||
pub log_level: Option<String>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::server_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
46
apps/nxmesh-master/src/db/entities/proxy_config.rs
Normal file
46
apps/nxmesh-master/src/db/entities/proxy_config.rs
Normal file
@@ -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<String>,
|
||||
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<super::agent_config_binding::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AgentConfigBinding.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::upstream::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Upstream.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
60
apps/nxmesh-master/src/db/entities/proxy_setting.rs
Normal file
60
apps/nxmesh-master/src/db/entities/proxy_setting.rs
Normal file
@@ -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<i32>,
|
||||
pub connect_timeout: Option<i32>,
|
||||
pub buffer_size: Option<i32>,
|
||||
pub cache_enabled: Option<bool>,
|
||||
pub cache_zone: Option<Uuid>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::cache_zone::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::CacheZone.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::location_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LocationBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
46
apps/nxmesh-master/src/db/entities/rewrite_rule.rs
Normal file
46
apps/nxmesh-master/src/db/entities/rewrite_rule.rs
Normal file
@@ -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<String>,
|
||||
pub priority: i32,
|
||||
pub is_deleted: bool,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::location_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LocationBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
83
apps/nxmesh-master/src/db/entities/server_block.rs
Normal file
83
apps/nxmesh-master/src/db/entities/server_block.rs
Normal file
@@ -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<Vec<String>>,
|
||||
pub listen_port: i32,
|
||||
pub ssl_enabled: Option<bool>,
|
||||
pub ssl_cert_id: Option<Uuid>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::access_rule::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AccessRule.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::location_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LocationBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::log_setting::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LogSetting.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::proxy_config::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ProxyConfig.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::ssl_certificate::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SslCertificate.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
29
apps/nxmesh-master/src/db/entities/ssl_certificate.rs
Normal file
29
apps/nxmesh-master/src/db/entities/ssl_certificate.rs
Normal file
@@ -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<super::server_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
54
apps/nxmesh-master/src/db/entities/upstream.rs
Normal file
54
apps/nxmesh-master/src/db/entities/upstream.rs
Normal file
@@ -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<Json>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[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<super::location_block::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LocationBlock.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::proxy_config::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ProxyConfig.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
41
apps/nxmesh-master/src/routes/api/agents/add_agent.rs
Normal file
41
apps/nxmesh-master/src/routes/api/agents/add_agent.rs
Normal file
@@ -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<String>,
|
||||
}
|
||||
|
||||
pub async fn add_agent_handler(
|
||||
State(agent_service): State<Arc<dyn AgentService>>,
|
||||
Json(body): Json<CreateAgentRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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)})),
|
||||
))
|
||||
}
|
||||
27
apps/nxmesh-master/src/routes/api/agents/delete_agent.rs
Normal file
27
apps/nxmesh-master/src/routes/api/agents/delete_agent.rs
Normal file
@@ -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<Arc<dyn AgentService>>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
39
apps/nxmesh-master/src/routes/api/agents/dto.rs
Normal file
39
apps/nxmesh-master/src/routes/api/agents/dto.rs
Normal file
@@ -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<String>,
|
||||
pub ip_address: Option<String>,
|
||||
pub last_seen_at: Option<String>,
|
||||
pub labels: Option<serde_json::Value>,
|
||||
//
|
||||
pub number_of_routes: usize,
|
||||
//
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub is_disabled: bool,
|
||||
}
|
||||
|
||||
impl From<AgentRecord> 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/nxmesh-master/src/routes/api/agents/get_agent.rs
Normal file
52
apps/nxmesh-master/src/routes/api/agents/get_agent.rs
Normal file
@@ -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<AgentInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct GetAgentResponse {
|
||||
agent: AgentInfo,
|
||||
}
|
||||
|
||||
pub async fn get_agents_handler(
|
||||
State(agent_service): State<Arc<dyn AgentService>>,
|
||||
) -> Result<Json<GetAgentsResponse>, 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<Arc<dyn AgentService>>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<GetAgentResponse>, 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),
|
||||
}
|
||||
}
|
||||
223
apps/nxmesh-master/src/routes/api/agents/mod.rs
Normal file
223
apps/nxmesh-master/src/routes/api/agents/mod.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
62
apps/nxmesh-master/src/routes/api/agents/update_agent.rs
Normal file
62
apps/nxmesh-master/src/routes/api/agents/update_agent.rs
Normal file
@@ -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<String>,
|
||||
#[serde(default)]
|
||||
pub ip_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub state: Option<AgentState>,
|
||||
#[serde(default)]
|
||||
pub deployment_mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub labels: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub async fn update_agent_handler(
|
||||
State(agent_service): State<Arc<dyn AgentService>>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
Json(body): Json<UpdateAgentRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
if let Some(ref name) = body.name {
|
||||
if 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),
|
||||
}
|
||||
}
|
||||
55
apps/nxmesh-master/src/routes/api/error.rs
Normal file
55
apps/nxmesh-master/src/routes/api/error.rs
Normal file
@@ -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<ProxyServiceError> 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",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
148
apps/nxmesh-master/src/routes/api/mod.rs
Normal file
148
apps/nxmesh-master/src/routes/api/mod.rs
Normal file
@@ -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<dyn AgentService>,
|
||||
pub proxy_service: Arc<dyn ProxyServiceTrait>,
|
||||
pub server_block_service: Arc<dyn ServerBlockService>,
|
||||
pub upstream_service: Arc<dyn UpstreamService>,
|
||||
pub location_block_service: Arc<dyn LocationBlockService>,
|
||||
pub access_rule_service: Arc<dyn AccessRuleService>,
|
||||
pub cache_zone_service: Arc<dyn CacheZoneService>,
|
||||
pub limit_rule_service: Arc<dyn LimitRuleService>,
|
||||
pub limit_zone_service: Arc<dyn LimitZoneService>,
|
||||
pub log_setting_service: Arc<dyn LogSettingService>,
|
||||
pub proxy_setting_service: Arc<dyn ProxySettingService>,
|
||||
pub rewrite_rule_service: Arc<dyn RewriteRuleService>,
|
||||
pub ssl_certificate_service: Arc<dyn SslCertificateService>,
|
||||
pub config_inheritance_service: Arc<dyn ConfigInheritanceService>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalApiState(pub Arc<ApiState>);
|
||||
|
||||
impl From<ApiState> for LocalApiState {
|
||||
fn from(api_state: ApiState) -> Self {
|
||||
LocalApiState(Arc::new(api_state))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<ApiState>> for LocalApiState {
|
||||
fn from(api_state: Arc<ApiState>) -> Self {
|
||||
LocalApiState(api_state)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ApiRouter = Router<LocalApiState>;
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn AgentService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn AgentService> {
|
||||
api_state.0.agent_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn ProxyServiceTrait> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn ProxyServiceTrait> {
|
||||
api_state.0.proxy_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn ServerBlockService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn ServerBlockService> {
|
||||
api_state.0.server_block_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn UpstreamService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn UpstreamService> {
|
||||
api_state.0.upstream_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn LocationBlockService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn LocationBlockService> {
|
||||
api_state.0.location_block_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn AccessRuleService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn AccessRuleService> {
|
||||
api_state.0.access_rule_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn CacheZoneService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn CacheZoneService> {
|
||||
api_state.0.cache_zone_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn LimitRuleService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn LimitRuleService> {
|
||||
api_state.0.limit_rule_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn LimitZoneService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn LimitZoneService> {
|
||||
api_state.0.limit_zone_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn LogSettingService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn LogSettingService> {
|
||||
api_state.0.log_setting_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn ProxySettingService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn ProxySettingService> {
|
||||
api_state.0.proxy_setting_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn RewriteRuleService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn RewriteRuleService> {
|
||||
api_state.0.rewrite_rule_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn SslCertificateService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn SslCertificateService> {
|
||||
api_state.0.ssl_certificate_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<LocalApiState> for Arc<dyn ConfigInheritanceService> {
|
||||
fn from_ref(api_state: &LocalApiState) -> Arc<dyn ConfigInheritanceService> {
|
||||
api_state.0.config_inheritance_service.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_router(state: impl Into<LocalApiState>) -> Router {
|
||||
ApiRouter::new()
|
||||
.nest("/agents", agents::get_router().await)
|
||||
.nest("/proxy", proxy::get_router().await)
|
||||
.with_state(state.into())
|
||||
}
|
||||
164
apps/nxmesh-master/src/routes/api/proxy/access_rules.rs
Normal file
164
apps/nxmesh-master/src/routes/api/proxy/access_rules.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
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::{AppError, ApiRouter};
|
||||
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<String>,
|
||||
pub priority: i32,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<AccessRuleConfig> 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<Uuid>,
|
||||
pub location_id: Option<Uuid>,
|
||||
pub r#type: String,
|
||||
pub ip_cidr: String,
|
||||
pub description: Option<String>,
|
||||
pub priority: i32,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateAccessRuleRequest> 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<Option<Uuid>>,
|
||||
pub location_id: Option<Option<Uuid>>,
|
||||
pub r#type: Option<String>,
|
||||
pub ip_cidr: Option<String>,
|
||||
pub description: Option<Option<String>>,
|
||||
pub priority: Option<i32>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateAccessRuleRequest> 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<Arc<dyn AccessRuleService>>,
|
||||
Path(server_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<AccessRuleResponse>>, 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<Arc<dyn AccessRuleService>>,
|
||||
Path(location_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<AccessRuleResponse>>, 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<Arc<dyn AccessRuleService>>,
|
||||
Json(body): Json<CreateAccessRuleRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let rule = svc.create(body.into()).await?;
|
||||
Ok((StatusCode::CREATED, Json(AccessRuleResponse::from(rule))))
|
||||
}
|
||||
|
||||
async fn get_access_rule(
|
||||
State(svc): State<Arc<dyn AccessRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<AccessRuleResponse>, AppError> {
|
||||
let rule = svc.get(id).await?;
|
||||
Ok(Json(rule.into()))
|
||||
}
|
||||
|
||||
async fn update_access_rule(
|
||||
State(svc): State<Arc<dyn AccessRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateAccessRuleRequest>,
|
||||
) -> Result<Json<AccessRuleResponse>, AppError> {
|
||||
let rule = svc.update(id, body.into()).await?;
|
||||
Ok(Json(rule.into()))
|
||||
}
|
||||
|
||||
async fn delete_access_rule(
|
||||
State(svc): State<Arc<dyn AccessRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
82
apps/nxmesh-master/src/routes/api/proxy/agents.rs
Normal file
82
apps/nxmesh-master/src/routes/api/proxy/agents.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
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::{AppError, ApiRouter};
|
||||
use crate::service::proxy::{ProxyServiceTrait, types::AgentConfigBinding};
|
||||
|
||||
use super::configs::ProxyConfigResponse;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct AgentConfigResponse {
|
||||
pub id: Uuid,
|
||||
pub agent_id: Option<Uuid>,
|
||||
pub group_id: Option<Uuid>,
|
||||
pub config_id: Uuid,
|
||||
pub is_active: bool,
|
||||
pub applied_at: String,
|
||||
}
|
||||
|
||||
impl From<AgentConfigBinding> 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<Arc<dyn ProxyServiceTrait>>,
|
||||
Path(agent_id): Path<Uuid>,
|
||||
) -> Result<Json<Option<ProxyConfigResponse>>, AppError> {
|
||||
let config = svc.get_active_agent_config(agent_id).await?;
|
||||
Ok(Json(config.map(Into::into)))
|
||||
}
|
||||
|
||||
async fn bind_agent(
|
||||
State(svc): State<Arc<dyn ProxyServiceTrait>>,
|
||||
Path(agent_id): Path<Uuid>,
|
||||
Json(body): Json<BindAgentRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn ProxyServiceTrait>>,
|
||||
Path(agent_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
133
apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs
Normal file
133
apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CacheZoneConfig> 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<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateCacheZoneRequest> 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<String>,
|
||||
pub path: Option<String>,
|
||||
pub size_limit: Option<String>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateCacheZoneRequest> 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<Arc<dyn CacheZoneService>>,
|
||||
) -> Result<Json<Vec<CacheZoneResponse>>, AppError> {
|
||||
let zones = svc.list().await?;
|
||||
Ok(Json(zones.into_iter().map(Into::into).collect()))
|
||||
}
|
||||
|
||||
async fn create_cache_zone(
|
||||
State(svc): State<Arc<dyn CacheZoneService>>,
|
||||
Json(body): Json<CreateCacheZoneRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let zone = svc.create(body.into()).await?;
|
||||
Ok((StatusCode::CREATED, Json(CacheZoneResponse::from(zone))))
|
||||
}
|
||||
|
||||
async fn get_cache_zone(
|
||||
State(svc): State<Arc<dyn CacheZoneService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<CacheZoneResponse>, AppError> {
|
||||
let zone = svc.get(id).await?;
|
||||
Ok(Json(zone.into()))
|
||||
}
|
||||
|
||||
async fn update_cache_zone(
|
||||
State(svc): State<Arc<dyn CacheZoneService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateCacheZoneRequest>,
|
||||
) -> Result<Json<CacheZoneResponse>, AppError> {
|
||||
let zone = svc.update(id, body.into()).await?;
|
||||
Ok(Json(zone.into()))
|
||||
}
|
||||
|
||||
async fn delete_cache_zone(
|
||||
State(svc): State<Arc<dyn CacheZoneService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
107
apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs
Normal file
107
apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
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::{AppError, ApiRouter};
|
||||
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<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct InheritanceRecordResponse {
|
||||
pub id: Uuid,
|
||||
pub child_config_id: Uuid,
|
||||
pub parent_config_id: Uuid,
|
||||
pub priority: Option<i32>,
|
||||
pub applied_at: String,
|
||||
}
|
||||
|
||||
impl From<ConfigInheritanceRecord> 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<Arc<dyn ConfigInheritanceService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let records = svc.list_parents(id).await?;
|
||||
let response: Vec<InheritanceRecordResponse> = records.into_iter().map(Into::into).collect();
|
||||
Ok(Json(serde_json::json!(response)))
|
||||
}
|
||||
|
||||
async fn add_parent(
|
||||
State(svc): State<Arc<dyn ConfigInheritanceService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<AddParentRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn ConfigInheritanceService>>,
|
||||
Path((id, parent_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn ConfigInheritanceService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let records = svc.list_children(id).await?;
|
||||
let response: Vec<InheritanceRecordResponse> = 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),
|
||||
)
|
||||
}
|
||||
147
apps/nxmesh-master/src/routes/api/proxy/configs.rs
Normal file
147
apps/nxmesh-master/src/routes/api/proxy/configs.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
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::{AppError, ApiRouter};
|
||||
use crate::service::proxy::{
|
||||
ProxyServiceTrait,
|
||||
types::{CreateProxyConfigParams, ProxyConfigSummary, UpdateProxyConfigParams},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct ProxyConfigResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub is_template: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<ProxyConfigSummary> 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<String>,
|
||||
#[serde(default)]
|
||||
pub is_template: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct UpdateConfigRequest {
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub is_template: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct ListConfigsResponse {
|
||||
pub configs: Vec<ProxyConfigResponse>,
|
||||
}
|
||||
|
||||
async fn list_configs(
|
||||
State(svc): State<Arc<dyn ProxyServiceTrait>>,
|
||||
) -> Result<Json<ListConfigsResponse>, 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<Arc<dyn ProxyServiceTrait>>,
|
||||
Json(body): Json<CreateConfigRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn ProxyServiceTrait>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, 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<Arc<dyn ProxyServiceTrait>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateConfigRequest>,
|
||||
) -> Result<Json<ProxyConfigResponse>, 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<Arc<dyn ProxyServiceTrait>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let deleted = svc.delete_config(id).await?;
|
||||
if deleted {
|
||||
Ok((StatusCode::NO_CONTENT,))
|
||||
} else {
|
||||
Err(AppError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
async fn render_config(
|
||||
State(svc): State<Arc<dyn ProxyServiceTrait>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<String, AppError> {
|
||||
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))
|
||||
}
|
||||
144
apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs
Normal file
144
apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<i32>,
|
||||
pub nodelay: Option<bool>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<LimitRuleConfig> 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<i32>,
|
||||
pub nodelay: Option<bool>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateLimitRuleRequest> 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<Uuid>,
|
||||
pub zone_id: Option<Uuid>,
|
||||
pub burst: Option<Option<i32>>,
|
||||
pub nodelay: Option<Option<bool>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateLimitRuleRequest> 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<Arc<dyn LimitRuleService>>,
|
||||
Path(location_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<LimitRuleResponse>>, 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<Arc<dyn LimitRuleService>>,
|
||||
Json(body): Json<CreateLimitRuleRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let rule = svc.create(body.into()).await?;
|
||||
Ok((StatusCode::CREATED, Json(LimitRuleResponse::from(rule))))
|
||||
}
|
||||
|
||||
async fn get_limit_rule(
|
||||
State(svc): State<Arc<dyn LimitRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<LimitRuleResponse>, AppError> {
|
||||
let rule = svc.get(id).await?;
|
||||
Ok(Json(rule.into()))
|
||||
}
|
||||
|
||||
async fn update_limit_rule(
|
||||
State(svc): State<Arc<dyn LimitRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateLimitRuleRequest>,
|
||||
) -> Result<Json<LimitRuleResponse>, AppError> {
|
||||
let rule = svc.update(id, body.into()).await?;
|
||||
Ok(Json(rule.into()))
|
||||
}
|
||||
|
||||
async fn delete_limit_rule(
|
||||
State(svc): State<Arc<dyn LimitRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
133
apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs
Normal file
133
apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<Uuid>,
|
||||
}
|
||||
|
||||
impl From<LimitZoneConfig> 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<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateLimitZoneRequest> 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<String>,
|
||||
pub key: Option<String>,
|
||||
pub rate: Option<String>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateLimitZoneRequest> 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<Arc<dyn LimitZoneService>>,
|
||||
) -> Result<Json<Vec<LimitZoneResponse>>, AppError> {
|
||||
let zones = svc.list().await?;
|
||||
Ok(Json(zones.into_iter().map(Into::into).collect()))
|
||||
}
|
||||
|
||||
async fn create_limit_zone(
|
||||
State(svc): State<Arc<dyn LimitZoneService>>,
|
||||
Json(body): Json<CreateLimitZoneRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let zone = svc.create(body.into()).await?;
|
||||
Ok((StatusCode::CREATED, Json(LimitZoneResponse::from(zone))))
|
||||
}
|
||||
|
||||
async fn get_limit_zone(
|
||||
State(svc): State<Arc<dyn LimitZoneService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<LimitZoneResponse>, AppError> {
|
||||
let zone = svc.get(id).await?;
|
||||
Ok(Json(zone.into()))
|
||||
}
|
||||
|
||||
async fn update_limit_zone(
|
||||
State(svc): State<Arc<dyn LimitZoneService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateLimitZoneRequest>,
|
||||
) -> Result<Json<LimitZoneResponse>, AppError> {
|
||||
let zone = svc.update(id, body.into()).await?;
|
||||
Ok(Json(zone.into()))
|
||||
}
|
||||
|
||||
async fn delete_limit_zone(
|
||||
State(svc): State<Arc<dyn LimitZoneService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
142
apps/nxmesh-master/src/routes/api/proxy/locations.rs
Normal file
142
apps/nxmesh-master/src/routes/api/proxy/locations.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<Uuid>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<LocationBlockConfig> 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<Uuid>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateLocationBlockRequest> 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<Uuid>,
|
||||
pub path_pattern: Option<String>,
|
||||
pub proxy_pass_upstream_id: Option<Option<Uuid>>,
|
||||
pub metadata: Option<Option<serde_json::Value>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateLocationBlockRequest> 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<Arc<dyn LocationBlockService>>,
|
||||
Path(server_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<LocationBlockResponse>>, 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<Arc<dyn LocationBlockService>>,
|
||||
Path(server_id): Path<Uuid>,
|
||||
Json(body): Json<CreateLocationBlockRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn LocationBlockService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<LocationBlockResponse>, AppError> {
|
||||
let location = svc.get(id).await?;
|
||||
Ok(Json(location.into()))
|
||||
}
|
||||
|
||||
async fn update_location(
|
||||
State(svc): State<Arc<dyn LocationBlockService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateLocationBlockRequest>,
|
||||
) -> Result<Json<LocationBlockResponse>, AppError> {
|
||||
let location = svc.update(id, body.into()).await?;
|
||||
Ok(Json(location.into()))
|
||||
}
|
||||
|
||||
async fn delete_location(
|
||||
State(svc): State<Arc<dyn LocationBlockService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
140
apps/nxmesh-master/src/routes/api/proxy/log_settings.rs
Normal file
140
apps/nxmesh-master/src/routes/api/proxy/log_settings.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<String>,
|
||||
pub error_log_path: Option<String>,
|
||||
pub log_level: Option<String>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<LogSettingConfig> 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<String>,
|
||||
pub error_log_path: Option<String>,
|
||||
pub log_level: Option<String>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateLogSettingRequest> 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<Uuid>,
|
||||
pub access_log_path: Option<Option<String>>,
|
||||
pub error_log_path: Option<Option<String>>,
|
||||
pub log_level: Option<Option<String>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateLogSettingRequest> 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<Arc<dyn LogSettingService>>,
|
||||
Path(server_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<LogSettingResponse>>, 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<Arc<dyn LogSettingService>>,
|
||||
Path(server_id): Path<Uuid>,
|
||||
Json(body): Json<CreateLogSettingRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn LogSettingService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<LogSettingResponse>, AppError> {
|
||||
let setting = svc.get(id).await?;
|
||||
Ok(Json(setting.into()))
|
||||
}
|
||||
|
||||
async fn update_log_setting(
|
||||
State(svc): State<Arc<dyn LogSettingService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateLogSettingRequest>,
|
||||
) -> Result<Json<LogSettingResponse>, AppError> {
|
||||
let setting = svc.update(id, body.into()).await?;
|
||||
Ok(Json(setting.into()))
|
||||
}
|
||||
|
||||
async fn delete_log_setting(
|
||||
State(svc): State<Arc<dyn LogSettingService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
37
apps/nxmesh-master/src/routes/api/proxy/mod.rs
Normal file
37
apps/nxmesh-master/src/routes/api/proxy/mod.rs
Normal file
@@ -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())
|
||||
}
|
||||
154
apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs
Normal file
154
apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
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::{AppError, ApiRouter};
|
||||
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<i32>,
|
||||
pub connect_timeout: Option<i32>,
|
||||
pub buffer_size: Option<i32>,
|
||||
pub cache_enabled: Option<bool>,
|
||||
pub cache_zone: Option<Uuid>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<ProxySettingConfig> 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<i32>,
|
||||
pub connect_timeout: Option<i32>,
|
||||
pub buffer_size: Option<i32>,
|
||||
pub cache_enabled: Option<bool>,
|
||||
pub cache_zone: Option<Uuid>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateProxySettingRequest> 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<Uuid>,
|
||||
pub read_timeout: Option<Option<i32>>,
|
||||
pub connect_timeout: Option<Option<i32>>,
|
||||
pub buffer_size: Option<Option<i32>>,
|
||||
pub cache_enabled: Option<Option<bool>>,
|
||||
pub cache_zone: Option<Option<Uuid>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateProxySettingRequest> 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<Arc<dyn ProxySettingService>>,
|
||||
Path(location_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<ProxySettingResponse>>, 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<Arc<dyn ProxySettingService>>,
|
||||
Path(location_id): Path<Uuid>,
|
||||
Json(body): Json<CreateProxySettingRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn ProxySettingService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<ProxySettingResponse>, AppError> {
|
||||
let setting = svc.get(id).await?;
|
||||
Ok(Json(setting.into()))
|
||||
}
|
||||
|
||||
async fn update_proxy_setting(
|
||||
State(svc): State<Arc<dyn ProxySettingService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateProxySettingRequest>,
|
||||
) -> Result<Json<ProxySettingResponse>, AppError> {
|
||||
let setting = svc.update(id, body.into()).await?;
|
||||
Ok(Json(setting.into()))
|
||||
}
|
||||
|
||||
async fn delete_proxy_setting(
|
||||
State(svc): State<Arc<dyn ProxySettingService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
148
apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs
Normal file
148
apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<String>,
|
||||
pub priority: i32,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<RewriteRuleConfig> 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<String>,
|
||||
pub priority: i32,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateRewriteRuleRequest> 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<Uuid>,
|
||||
pub pattern: Option<String>,
|
||||
pub replacement: Option<String>,
|
||||
pub flag: Option<Option<String>>,
|
||||
pub priority: Option<i32>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateRewriteRuleRequest> 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<Arc<dyn RewriteRuleService>>,
|
||||
Path(location_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<RewriteRuleResponse>>, 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<Arc<dyn RewriteRuleService>>,
|
||||
Path(location_id): Path<Uuid>,
|
||||
Json(body): Json<CreateRewriteRuleRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn RewriteRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<RewriteRuleResponse>, AppError> {
|
||||
let rule = svc.get(id).await?;
|
||||
Ok(Json(rule.into()))
|
||||
}
|
||||
|
||||
async fn update_rewrite_rule(
|
||||
State(svc): State<Arc<dyn RewriteRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateRewriteRuleRequest>,
|
||||
) -> Result<Json<RewriteRuleResponse>, AppError> {
|
||||
let rule = svc.update(id, body.into()).await?;
|
||||
Ok(Json(rule.into()))
|
||||
}
|
||||
|
||||
async fn delete_rewrite_rule(
|
||||
State(svc): State<Arc<dyn RewriteRuleService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
142
apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs
Normal file
142
apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<Vec<String>>,
|
||||
pub listen_port: i32,
|
||||
pub ssl_enabled: Option<bool>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<ServerBlockConfig> 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<Vec<String>>,
|
||||
pub listen_port: i32,
|
||||
pub ssl_enabled: Option<bool>,
|
||||
pub ssl_cert_id: Option<Uuid>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateServerBlockRequest> 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<Option<Vec<String>>>,
|
||||
pub listen_port: Option<i32>,
|
||||
pub ssl_enabled: Option<Option<bool>>,
|
||||
pub ssl_cert_id: Option<Option<Uuid>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateServerBlockRequest> 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<Arc<dyn ServerBlockService>>,
|
||||
Path(config_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<ServerBlockResponse>>, 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<Arc<dyn ServerBlockService>>,
|
||||
Path(config_id): Path<Uuid>,
|
||||
Json(body): Json<CreateServerBlockRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn ServerBlockService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<ServerBlockResponse>, AppError> {
|
||||
let block = svc.get(id).await?;
|
||||
Ok(Json(block.into()))
|
||||
}
|
||||
|
||||
async fn update_server_block(
|
||||
State(svc): State<Arc<dyn ServerBlockService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateServerBlockRequest>,
|
||||
) -> Result<Json<ServerBlockResponse>, AppError> {
|
||||
let block = svc.update(id, body.into()).await?;
|
||||
Ok(Json(block.into()))
|
||||
}
|
||||
|
||||
async fn delete_server_block(
|
||||
State(svc): State<Arc<dyn ServerBlockService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
133
apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs
Normal file
133
apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
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<SslCertificateConfig> 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<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<CreateSslCertificateRequest> 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<String>,
|
||||
pub cert_path: Option<String>,
|
||||
pub key_path: Option<String>,
|
||||
pub expiry_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
impl From<UpdateSslCertificateRequest> 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<Arc<dyn SslCertificateService>>,
|
||||
) -> Result<Json<Vec<SslCertificateResponse>>, AppError> {
|
||||
let certs = svc.list().await?;
|
||||
Ok(Json(certs.into_iter().map(Into::into).collect()))
|
||||
}
|
||||
|
||||
async fn create_ssl_certificate(
|
||||
State(svc): State<Arc<dyn SslCertificateService>>,
|
||||
Json(body): Json<CreateSslCertificateRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let cert = svc.create(body.into()).await?;
|
||||
Ok((StatusCode::CREATED, Json(SslCertificateResponse::from(cert))))
|
||||
}
|
||||
|
||||
async fn get_ssl_certificate(
|
||||
State(svc): State<Arc<dyn SslCertificateService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<SslCertificateResponse>, AppError> {
|
||||
let cert = svc.get(id).await?;
|
||||
Ok(Json(cert.into()))
|
||||
}
|
||||
|
||||
async fn update_ssl_certificate(
|
||||
State(svc): State<Arc<dyn SslCertificateService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateSslCertificateRequest>,
|
||||
) -> Result<Json<SslCertificateResponse>, AppError> {
|
||||
let cert = svc.update(id, body.into()).await?;
|
||||
Ok(Json(cert.into()))
|
||||
}
|
||||
|
||||
async fn delete_ssl_certificate(
|
||||
State(svc): State<Arc<dyn SslCertificateService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
174
apps/nxmesh-master/src/routes/api/proxy/test_builder.rs
Normal file
174
apps/nxmesh-master/src/routes/api/proxy/test_builder.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
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<MockProxyServiceTrait>,
|
||||
server_block_service: Option<MockServerBlockService>,
|
||||
upstream_service: Option<MockUpstreamService>,
|
||||
location_block_service: Option<MockLocationBlockService>,
|
||||
access_rule_service: Option<MockAccessRuleService>,
|
||||
cache_zone_service: Option<MockCacheZoneService>,
|
||||
limit_rule_service: Option<MockLimitRuleService>,
|
||||
limit_zone_service: Option<MockLimitZoneService>,
|
||||
log_setting_service: Option<MockLogSettingService>,
|
||||
proxy_setting_service: Option<MockProxySettingService>,
|
||||
rewrite_rule_service: Option<MockRewriteRuleService>,
|
||||
ssl_certificate_service: Option<MockSslCertificateService>,
|
||||
config_inheritance_service: Option<MockConfigInheritanceService>,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
144
apps/nxmesh-master/src/routes/api/proxy/upstreams.rs
Normal file
144
apps/nxmesh-master/src/routes/api/proxy/upstreams.rs
Normal file
@@ -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::{AppError, ApiRouter};
|
||||
use crate::service::proxy::types::UpstreamConfig;
|
||||
use crate::service::proxy::upstream::{
|
||||
CreateUpstreamParams, UpstreamService, UpdateUpstreamParams,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct UpstreamResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub target_host: String,
|
||||
pub target_port: i32,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<UpstreamConfig> 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<serde_json::Value>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<CreateUpstreamRequest> 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<String>,
|
||||
pub target_host: Option<String>,
|
||||
pub target_port: Option<i32>,
|
||||
pub metadata: Option<Option<serde_json::Value>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
impl From<UpdateUpstreamRequest> 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<Arc<dyn UpstreamService>>,
|
||||
Path(config_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<UpstreamResponse>>, 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<Arc<dyn UpstreamService>>,
|
||||
Path(config_id): Path<Uuid>,
|
||||
Json(body): Json<CreateUpstreamRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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<Arc<dyn UpstreamService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<UpstreamResponse>, AppError> {
|
||||
let upstream = svc.get(id).await?;
|
||||
Ok(Json(upstream.into()))
|
||||
}
|
||||
|
||||
async fn update_upstream(
|
||||
State(svc): State<Arc<dyn UpstreamService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateUpstreamRequest>,
|
||||
) -> Result<Json<UpstreamResponse>, AppError> {
|
||||
let upstream = svc.update(id, body.into()).await?;
|
||||
Ok(Json(upstream.into()))
|
||||
}
|
||||
|
||||
async fn delete_upstream(
|
||||
State(svc): State<Arc<dyn UpstreamService>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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),
|
||||
)
|
||||
}
|
||||
@@ -37,7 +37,11 @@ pub async fn get_fallback_handler() -> Result<axum::response::Html<Vec<u8>>, axu
|
||||
}
|
||||
|
||||
fn get_index_html() -> Option<Vec<u8>> {
|
||||
FrontendAssets::get(INDEX_HTML).map(|asset| asset.data.as_ref().to_owned())
|
||||
// Try root index.html first, then fall back to client/index.html when assets
|
||||
// are packaged under the `client/` subfolder.
|
||||
FrontendAssets::get(INDEX_HTML)
|
||||
.or_else(|| FrontendAssets::get(&format!("client/{}", INDEX_HTML)))
|
||||
.map(|asset| asset.data.as_ref().to_owned())
|
||||
}
|
||||
|
||||
async fn get_file_handler(
|
||||
@@ -49,7 +53,10 @@ async fn get_file_handler(
|
||||
path
|
||||
};
|
||||
|
||||
match FrontendAssets::get(&file_path) {
|
||||
// Try direct lookup first, then fallback to the `client/` subfolder.
|
||||
match FrontendAssets::get(&file_path)
|
||||
.or_else(|| FrontendAssets::get(&format!("client/{}", file_path)))
|
||||
{
|
||||
Some(asset) => {
|
||||
let content_type = mime_guess::from_path(&file_path).first_or_octet_stream();
|
||||
let response = axum::response::Response::builder()
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
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<Arc<api::ApiState>>) -> 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 +46,24 @@ 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);
|
||||
|
||||
@@ -1,54 +1,129 @@
|
||||
use chrono::Utc;
|
||||
use nxmesh_proto::{
|
||||
Ack, AgentMessage, HealthReport, MasterMessage, MetricsBatch,
|
||||
agent_service_server::AgentService,
|
||||
AgentMessage, MasterMessage, agent_service_server::AgentService as GrpcAgentService,
|
||||
};
|
||||
use tracing::warn;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
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<Vec<AgentRecord>, RepoError>;
|
||||
async fn get(&self, id: Uuid) -> Result<Option<AgentRecord>, RepoError>;
|
||||
async fn create(&self, rec: &CreateAgentRecord) -> Result<AgentRecord, RepoError>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
rec: &UpdateAgentRecord,
|
||||
) -> Result<Option<AgentRecord>, RepoError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<bool, RepoError>;
|
||||
}
|
||||
|
||||
pub struct AgentServiceImpl {
|
||||
repo: Box<dyn AgentRepo>,
|
||||
}
|
||||
|
||||
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<Vec<AgentRecord>, RepoError> {
|
||||
self.repo.list().await
|
||||
}
|
||||
|
||||
async fn get(&self, id: Uuid) -> Result<Option<AgentRecord>, RepoError> {
|
||||
self.repo.get(id).await
|
||||
}
|
||||
|
||||
async fn create(&self, rec: &CreateAgentRecord) -> Result<AgentRecord, RepoError> {
|
||||
self.repo.create(rec).await
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
rec: &UpdateAgentRecord,
|
||||
) -> Result<Option<AgentRecord>, RepoError> {
|
||||
self.repo.update(id, rec).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<bool, RepoError> {
|
||||
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<MasterMessage>;
|
||||
impl GrpcAgentService for AgentServerService {
|
||||
type StreamStream =
|
||||
tokio_stream::wrappers::ReceiverStream<std::result::Result<MasterMessage, tonic::Status>>;
|
||||
|
||||
#[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<tonic::Streaming<AgentMessage>>,
|
||||
) -> Result<tonic::Response<Self::StreamStream>, tonic::Status> {
|
||||
todo!()
|
||||
let mut inbound = request.into_inner();
|
||||
|
||||
let (tx, rx) = mpsc::channel::<std::result::Result<MasterMessage, tonic::Status>>(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))
|
||||
}
|
||||
|
||||
#[doc = " ReportHealth sends a health report to the master"]
|
||||
#[allow(
|
||||
mismatched_lifetime_syntaxes,
|
||||
clippy::type_complexity,
|
||||
clippy::type_repetition_in_bounds
|
||||
)]
|
||||
async fn report_health(
|
||||
async fn connection_test(
|
||||
&self,
|
||||
request: tonic::Request<HealthReport>,
|
||||
) -> Result<tonic::Response<Ack>, tonic::Status> {
|
||||
warn!("Received health report: {:?}", request.get_ref());
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[doc = " ReportMetrics sends metrics batch to the master"]
|
||||
#[allow(
|
||||
mismatched_lifetime_syntaxes,
|
||||
clippy::type_complexity,
|
||||
clippy::type_repetition_in_bounds
|
||||
)]
|
||||
async fn report_metrics(
|
||||
&self,
|
||||
request: tonic::Request<MetricsBatch>,
|
||||
) -> Result<tonic::Response<Ack>, tonic::Status> {
|
||||
todo!()
|
||||
_request: tonic::Request<nxmesh_proto::TestRequest>,
|
||||
) -> Result<tonic::Response<nxmesh_proto::TestResponse>, tonic::Status> {
|
||||
Ok(tonic::Response::new(nxmesh_proto::TestResponse {
|
||||
success: true,
|
||||
error_message: String::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
146
apps/nxmesh-master/src/service/agent/repo.rs
Normal file
146
apps/nxmesh-master/src/service/agent/repo.rs
Normal file
@@ -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<Vec<AgentRecord>, RepoError>;
|
||||
async fn get(&self, id: Uuid) -> Result<Option<AgentRecord>, RepoError>;
|
||||
async fn create(&self, rec: &CreateAgentRecord) -> Result<AgentRecord, RepoError>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
rec: &UpdateAgentRecord,
|
||||
) -> Result<Option<AgentRecord>, RepoError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<bool, RepoError>;
|
||||
}
|
||||
|
||||
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<Vec<AgentRecord>, 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<Option<AgentRecord>, 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<AgentRecord, RepoError> {
|
||||
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<Option<AgentRecord>, 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<bool, RepoError> {
|
||||
let result = Agent::delete_by_id(id).exec(&self.db).await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
60
apps/nxmesh-master/src/service/agent/types.rs
Normal file
60
apps/nxmesh-master/src/service/agent/types.rs
Normal file
@@ -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<State> 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<String> 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<String>,
|
||||
pub state: State,
|
||||
pub deployment_mode: Option<String>,
|
||||
pub last_seen_at: Option<String>,
|
||||
pub labels: Option<serde_json::Value>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub struct CreateAgentRecord {
|
||||
pub name: String,
|
||||
pub ip_address: Option<String>,
|
||||
}
|
||||
|
||||
pub struct UpdateAgentRecord {
|
||||
pub name: Option<String>,
|
||||
pub ip_address: Option<String>,
|
||||
pub state: Option<State>,
|
||||
pub deployment_mode: Option<String>,
|
||||
pub labels: Option<serde_json::Value>,
|
||||
}
|
||||
13
apps/nxmesh-master/src/service/error.rs
Normal file
13
apps/nxmesh-master/src/service/error.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum RepoError {
|
||||
#[error("internal error: {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for RepoError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
match err {
|
||||
other => RepoError::InternalError(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,68 @@ 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)
|
||||
|
||||
156
apps/nxmesh-master/src/service/proxy/access_rule/mod.rs
Normal file
156
apps/nxmesh-master/src/service/proxy/access_rule/mod.rs
Normal file
@@ -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<Uuid>,
|
||||
pub location_id: Option<Uuid>,
|
||||
pub r#type: String,
|
||||
pub ip_cidr: String,
|
||||
pub description: Option<String>,
|
||||
pub priority: i32,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub struct UpdateAccessRuleParams {
|
||||
pub server_id: Option<Option<Uuid>>,
|
||||
pub location_id: Option<Option<Uuid>>,
|
||||
pub r#type: Option<String>,
|
||||
pub ip_cidr: Option<String>,
|
||||
pub description: Option<Option<String>>,
|
||||
pub priority: Option<i32>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait AccessRuleService: Send + Sync + 'static {
|
||||
async fn get(&self, id: Uuid) -> ProxyServiceResult<AccessRuleConfig>;
|
||||
async fn list_by_server(&self, server_id: Uuid) -> ProxyServiceResult<Vec<AccessRuleConfig>>;
|
||||
async fn list_by_location(
|
||||
&self,
|
||||
location_id: Uuid,
|
||||
) -> ProxyServiceResult<Vec<AccessRuleConfig>>;
|
||||
async fn create(&self, params: CreateAccessRuleParams) -> ProxyServiceResult<AccessRuleConfig>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
params: UpdateAccessRuleParams,
|
||||
) -> ProxyServiceResult<AccessRuleConfig>;
|
||||
async fn delete(&self, id: Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
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<AccessRuleConfig> {
|
||||
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<Vec<AccessRuleConfig>> {
|
||||
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<Vec<AccessRuleConfig>> {
|
||||
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<AccessRuleConfig> {
|
||||
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<AccessRuleConfig> {
|
||||
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<bool> {
|
||||
let result = crate::db::entities::access_rule::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
116
apps/nxmesh-master/src/service/proxy/cache_zone/mod.rs
Normal file
116
apps/nxmesh-master/src/service/proxy/cache_zone/mod.rs
Normal file
@@ -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<Uuid>,
|
||||
}
|
||||
|
||||
pub struct UpdateCacheZoneParams {
|
||||
pub name: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub size_limit: Option<String>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait CacheZoneService: Send + Sync + 'static {
|
||||
async fn get(&self, id: Uuid) -> ProxyServiceResult<CacheZoneConfig>;
|
||||
async fn list(&self) -> ProxyServiceResult<Vec<CacheZoneConfig>>;
|
||||
async fn create(&self, params: CreateCacheZoneParams) -> ProxyServiceResult<CacheZoneConfig>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
params: UpdateCacheZoneParams,
|
||||
) -> ProxyServiceResult<CacheZoneConfig>;
|
||||
async fn delete(&self, id: Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
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<CacheZoneConfig> {
|
||||
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<Vec<CacheZoneConfig>> {
|
||||
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<CacheZoneConfig> {
|
||||
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<CacheZoneConfig> {
|
||||
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<bool> {
|
||||
let result = crate::db::entities::cache_zone::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
144
apps/nxmesh-master/src/service/proxy/config_inheritance/mod.rs
Normal file
144
apps/nxmesh-master/src/service/proxy/config_inheritance/mod.rs
Normal file
@@ -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<i32>,
|
||||
}
|
||||
|
||||
pub struct ConfigInheritanceRecord {
|
||||
pub id: Uuid,
|
||||
pub child_config_id: Uuid,
|
||||
pub parent_config_id: Uuid,
|
||||
pub priority: Option<i32>,
|
||||
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<ConfigInheritanceRecord>;
|
||||
async fn remove(
|
||||
&self,
|
||||
child_config_id: Uuid,
|
||||
parent_config_id: Uuid,
|
||||
) -> ProxyServiceResult<bool>;
|
||||
async fn list_parents(
|
||||
&self,
|
||||
child_config_id: Uuid,
|
||||
) -> ProxyServiceResult<Vec<ConfigInheritanceRecord>>;
|
||||
async fn list_children(
|
||||
&self,
|
||||
parent_config_id: Uuid,
|
||||
) -> ProxyServiceResult<Vec<ConfigInheritanceRecord>>;
|
||||
}
|
||||
|
||||
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<ConfigInheritanceRecord> {
|
||||
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<bool> {
|
||||
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<Vec<ConfigInheritanceRecord>> {
|
||||
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<Vec<ConfigInheritanceRecord>> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
130
apps/nxmesh-master/src/service/proxy/limit_rule/mod.rs
Normal file
130
apps/nxmesh-master/src/service/proxy/limit_rule/mod.rs
Normal file
@@ -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<i32>,
|
||||
pub nodelay: Option<bool>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub struct UpdateLimitRuleParams {
|
||||
pub location_id: Option<Uuid>,
|
||||
pub zone_id: Option<Uuid>,
|
||||
pub burst: Option<Option<i32>>,
|
||||
pub nodelay: Option<Option<bool>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait LimitRuleService: Send + Sync + 'static {
|
||||
async fn get(&self, id: Uuid) -> ProxyServiceResult<LimitRuleConfig>;
|
||||
async fn list_by_location(&self, location_id: Uuid)
|
||||
-> ProxyServiceResult<Vec<LimitRuleConfig>>;
|
||||
async fn create(&self, params: CreateLimitRuleParams) -> ProxyServiceResult<LimitRuleConfig>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
params: UpdateLimitRuleParams,
|
||||
) -> ProxyServiceResult<LimitRuleConfig>;
|
||||
async fn delete(&self, id: Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
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<LimitRuleConfig> {
|
||||
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<Vec<LimitRuleConfig>> {
|
||||
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<LimitRuleConfig> {
|
||||
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<LimitRuleConfig> {
|
||||
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<bool> {
|
||||
let result = crate::db::entities::limit_rule::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
116
apps/nxmesh-master/src/service/proxy/limit_zone/mod.rs
Normal file
116
apps/nxmesh-master/src/service/proxy/limit_zone/mod.rs
Normal file
@@ -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<Uuid>,
|
||||
}
|
||||
|
||||
pub struct UpdateLimitZoneParams {
|
||||
pub name: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub rate: Option<String>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait LimitZoneService: Send + Sync + 'static {
|
||||
async fn get(&self, id: Uuid) -> ProxyServiceResult<LimitZoneConfig>;
|
||||
async fn list(&self) -> ProxyServiceResult<Vec<LimitZoneConfig>>;
|
||||
async fn create(&self, params: CreateLimitZoneParams) -> ProxyServiceResult<LimitZoneConfig>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
params: UpdateLimitZoneParams,
|
||||
) -> ProxyServiceResult<LimitZoneConfig>;
|
||||
async fn delete(&self, id: Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
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<LimitZoneConfig> {
|
||||
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<Vec<LimitZoneConfig>> {
|
||||
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<LimitZoneConfig> {
|
||||
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<LimitZoneConfig> {
|
||||
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<bool> {
|
||||
let result = crate::db::entities::limit_zone::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
206
apps/nxmesh-master/src/service/proxy/location_block/mod.rs
Normal file
206
apps/nxmesh-master/src/service/proxy/location_block/mod.rs
Normal file
@@ -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<Uuid>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub struct UpdateLocationBlockParams {
|
||||
pub server_id: Option<Uuid>,
|
||||
pub path_pattern: Option<String>,
|
||||
pub proxy_pass_upstream_id: Option<Option<Uuid>>,
|
||||
pub metadata: Option<Option<serde_json::Value>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait LocationBlockService: Send + Sync + 'static {
|
||||
async fn get(&self, id: Uuid) -> ProxyServiceResult<LocationBlockConfig>;
|
||||
async fn list_by_server(&self, server_id: Uuid)
|
||||
-> ProxyServiceResult<Vec<LocationBlockConfig>>;
|
||||
async fn create(
|
||||
&self,
|
||||
params: CreateLocationBlockParams,
|
||||
) -> ProxyServiceResult<LocationBlockConfig>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
params: UpdateLocationBlockParams,
|
||||
) -> ProxyServiceResult<LocationBlockConfig>;
|
||||
async fn delete(&self, id: Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
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<LocationBlockConfig> {
|
||||
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<LocationBlockConfig> {
|
||||
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<Vec<LocationBlockConfig>> {
|
||||
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<LocationBlockConfig> {
|
||||
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<LocationBlockConfig> {
|
||||
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<bool> {
|
||||
let result = crate::db::entities::location_block::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
125
apps/nxmesh-master/src/service/proxy/log_setting/mod.rs
Normal file
125
apps/nxmesh-master/src/service/proxy/log_setting/mod.rs
Normal file
@@ -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<String>,
|
||||
pub error_log_path: Option<String>,
|
||||
pub log_level: Option<String>,
|
||||
pub override_of_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub struct UpdateLogSettingParams {
|
||||
pub server_id: Option<Uuid>,
|
||||
pub access_log_path: Option<Option<String>>,
|
||||
pub error_log_path: Option<Option<String>>,
|
||||
pub log_level: Option<Option<String>>,
|
||||
pub override_of_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait LogSettingService: Send + Sync + 'static {
|
||||
async fn get(&self, id: Uuid) -> ProxyServiceResult<LogSettingConfig>;
|
||||
async fn list_by_server(&self, server_id: Uuid) -> ProxyServiceResult<Vec<LogSettingConfig>>;
|
||||
async fn create(&self, params: CreateLogSettingParams) -> ProxyServiceResult<LogSettingConfig>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
params: UpdateLogSettingParams,
|
||||
) -> ProxyServiceResult<LogSettingConfig>;
|
||||
async fn delete(&self, id: Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
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<LogSettingConfig> {
|
||||
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<Vec<LogSettingConfig>> {
|
||||
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<LogSettingConfig> {
|
||||
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<LogSettingConfig> {
|
||||
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<bool> {
|
||||
let result = crate::db::entities::log_setting::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
60
apps/nxmesh-master/src/service/proxy/mod.rs
Normal file
60
apps/nxmesh-master/src/service/proxy/mod.rs
Normal file
@@ -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<ProxyConfig>;
|
||||
|
||||
// CRUD
|
||||
async fn list_configs(&self) -> ProxyServiceResult<Vec<ProxyConfigSummary>>;
|
||||
async fn create_config(
|
||||
&self,
|
||||
params: CreateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary>;
|
||||
async fn update_config(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
params: UpdateProxyConfigParams,
|
||||
) -> ProxyServiceResult<ProxyConfigSummary>;
|
||||
async fn delete_config(&self, id: uuid::Uuid) -> ProxyServiceResult<bool>;
|
||||
|
||||
// Render
|
||||
async fn render_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult<String>;
|
||||
|
||||
// Binding
|
||||
async fn get_active_agent_config(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<Option<ProxyConfigSummary>>;
|
||||
async fn bind_agent(
|
||||
&self,
|
||||
agent_id: uuid::Uuid,
|
||||
config_id: uuid::Uuid,
|
||||
) -> ProxyServiceResult<AgentConfigBinding>;
|
||||
async fn unbind_agent(&self, agent_id: uuid::Uuid) -> ProxyServiceResult<bool>;
|
||||
}
|
||||
|
||||
pub trait ProxyConfigRenderer: Send + Sync + 'static {
|
||||
fn render(&self, config: &ProxyConfig) -> String;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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, ";")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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, " }}")
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
11
apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs
Normal file
11
apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs
Normal file
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user