Compare commits
37 Commits
feature/ag
...
b43f9fcb00
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b43f9fcb00 | ||
|
|
1b251fd89f | ||
|
|
dd79cbe0bb | ||
|
|
37f4163870 | ||
|
|
481abd7f26 | ||
|
|
7a557d6e00 | ||
|
|
0cbc223b4e | ||
|
|
fdfd1c98e0 | ||
|
|
abeea4fad7 | ||
|
|
fea205cba8 | ||
|
|
3cc6b40e61 | ||
|
|
41023939db | ||
|
|
6eb0d9060b | ||
|
|
7d83838da3 | ||
|
|
921165476c | ||
|
|
a0621e2407 | ||
|
|
f8b13be650 | ||
|
|
238c3db92b | ||
|
|
814f76291c | ||
|
|
7ac3368715 | ||
|
|
35fadb46f6 | ||
|
|
91d0e1cd7c | ||
| c33e3aa0ca | |||
|
|
6f5596dc69 | ||
|
|
96e7f36731 | ||
|
|
410328a2af | ||
|
|
9f122566d0 | ||
|
|
c65dc3af47 | ||
|
|
6f395ed1ae | ||
|
|
e6e85564e7 | ||
|
|
c7a090f78a | ||
|
|
8cc2775fe4 | ||
|
|
0b64538754 | ||
|
|
8009ca20ff | ||
|
|
7a5b9471e0 | ||
|
|
bb55e37b49 | ||
| 9a264a61ac |
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@@ -12,11 +12,8 @@ on:
|
|||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# setup is now handled by a composite action used by downstream jobs to keep
|
|
||||||
# the workflow DRY. The composite action performs checkout, cache restore and
|
|
||||||
# toolchain setup.
|
|
||||||
|
|
||||||
test:
|
test-crates:
|
||||||
needs: frontend-build
|
needs: frontend-build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -39,7 +36,7 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --all-features
|
run: cargo test --all-features
|
||||||
|
|
||||||
lint:
|
lint-crates:
|
||||||
needs: frontend-build
|
needs: frontend-build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -83,13 +80,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: apps/frontend/pnpm-lock.yaml
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
cd apps/frontend
|
cd apps/frontend
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
- name: Run frontend linter
|
- name: Run frontend linter
|
||||||
run: |
|
run: |
|
||||||
cd apps/frontend
|
cd apps/frontend
|
||||||
@@ -114,7 +111,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: apps/frontend/pnpm-lock.yaml
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -142,12 +139,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: apps/frontend/pnpm-lock.yaml
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
cd apps/frontend
|
cd apps/frontend
|
||||||
pnpm install
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
79
.github/workflows/verify.yml
vendored
79
.github/workflows/verify.yml
vendored
@@ -11,18 +11,15 @@ on:
|
|||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# setup is now handled by a composite action used by downstream jobs to keep
|
|
||||||
# the workflow DRY. The composite action performs checkout, cache restore and
|
|
||||||
# toolchain setup.
|
|
||||||
|
|
||||||
verify-generated-code:
|
verify-generated-database-code:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Rust, checkout and restore caches
|
- name: Setup Rust, checkout and restore caches
|
||||||
uses: ./.github/actions/setup-rust
|
uses: ./.github/actions/setup-rust
|
||||||
|
|
||||||
@@ -32,14 +29,62 @@ jobs:
|
|||||||
cargo run -- db:migrate_and_generate --output-path ../../public/database/src/generated/entities
|
cargo run -- db:migrate_and_generate --output-path ../../public/database/src/generated/entities
|
||||||
- name: Check for uncommitted changes in /generated/
|
- name: Check for uncommitted changes in /generated/
|
||||||
run: |
|
run: |
|
||||||
if [[ -n $(git status --porcelain | grep '^ M .*\/generated\/') ]]; then
|
if [[ -n $(git status --porcelain --untracked-files=all | grep '/generated/') ]]; then
|
||||||
echo "Generated code is not up to date. Please run the code generation locally and commit the changes."
|
echo "Generated code is not up to date. Please run the code generation locally and commit the changes."
|
||||||
git status --porcelain | grep '^ M .*\/generated\/'
|
git status --porcelain --untracked-files=all | grep '/generated/'
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "Generated code is up to date."
|
echo "Generated code is up to date."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
verify-generated-agent-code:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: Setup Rust, checkout and restore caches
|
||||||
|
uses: ./.github/actions/setup-rust
|
||||||
|
|
||||||
|
- name: Setup PNPM
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install root dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: generate agent client code
|
||||||
|
run: |
|
||||||
|
pnpm just generate-agent-client
|
||||||
|
|
||||||
|
|
||||||
|
- name: Check for uncommitted changes in agent client code
|
||||||
|
run: |
|
||||||
|
if [[ -n $(git status --porcelain --untracked-files=all | grep 'public/agent-client/') ]]; then
|
||||||
|
echo "Agent client code is not up to date. Please run the agent client code generation locally and commit the changes."
|
||||||
|
git status --porcelain --untracked-files=all | grep 'public/agent-client/'
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Agent client code is up to date."
|
||||||
|
fi
|
||||||
|
|
||||||
verify-openapi-spec:
|
verify-openapi-spec:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -83,7 +128,7 @@ jobs:
|
|||||||
- name: Setup Rust, checkout and restore caches
|
- name: Setup Rust, checkout and restore caches
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
if: steps.check_changes.outputs.changed == 'true'
|
||||||
uses: ./.github/actions/setup-rust
|
uses: ./.github/actions/setup-rust
|
||||||
|
|
||||||
- name: Generate dummy frontend build (to satisfy dependencies)
|
- name: Generate dummy frontend build (to satisfy dependencies)
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
if: steps.check_changes.outputs.changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -95,13 +140,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd apps/api
|
cd apps/api
|
||||||
cargo run -- generate:openapi --output-path ./swagger.json
|
cargo run -- generate:openapi --output-path ./swagger.json
|
||||||
|
|
||||||
- name: Check for uncommitted changes in swagger.json
|
- name: Check for uncommitted changes in swagger.json
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
if: steps.check_changes.outputs.changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
if [[ -n $(git status --porcelain | grep '^ M apps/api/swagger.json') ]]; then
|
if [[ -n $(git status --porcelain --untracked-files=all | grep 'apps/api/swagger.json') ]]; then
|
||||||
echo "OpenAPI spec is not up to date. Please run the OpenAPI generation locally and commit the changes."
|
echo "OpenAPI spec is not up to date. Please run the OpenAPI generation locally and commit the changes."
|
||||||
git status --porcelain | grep '^ M apps/api/swagger.json'
|
git status --porcelain --untracked-files=all | grep 'apps/api/swagger.json'
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "OpenAPI spec is up to date."
|
echo "OpenAPI spec is up to date."
|
||||||
@@ -165,14 +210,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: apps/frontend/pnpm-lock.yaml
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
if: steps.check_swagger_changes.outputs.changed == 'true'
|
if: steps.check_swagger_changes.outputs.changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd apps/frontend
|
cd apps/frontend
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
- name: Generate frontend API client
|
- name: Generate frontend API client
|
||||||
if: steps.check_swagger_changes.outputs.changed == 'true'
|
if: steps.check_swagger_changes.outputs.changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -182,14 +227,14 @@ jobs:
|
|||||||
- name: Check for uncommitted changes in frontend API client
|
- name: Check for uncommitted changes in frontend API client
|
||||||
if: steps.check_swagger_changes.outputs.changed == 'true'
|
if: steps.check_swagger_changes.outputs.changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
if [[ -n $(git status --porcelain | grep '^ M apps/frontend/app/generated/api-client') ]]; then
|
if [[ -n $(git status --porcelain --untracked-files=all | grep 'apps/frontend/app/generated/api-client') ]]; then
|
||||||
echo "Frontend API client is not up to date. Please run the API client generation locally and commit the changes."
|
echo "Frontend API client is not up to date. Please run the API client generation locally and commit the changes."
|
||||||
git status --porcelain | grep '^ M apps/frontend/app/generated/api-client'
|
git status --porcelain --untracked-files=all | grep 'apps/frontend/app/generated/api-client'
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "Frontend API client is up to date."
|
echo "Frontend API client is up to date."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Skip frontend API client generation (no relevant changes)
|
- name: Skip frontend API client generation (no relevant changes)
|
||||||
if: steps.check_swagger_changes.outputs.changed == 'false'
|
if: steps.check_swagger_changes.outputs.changed == 'false'
|
||||||
run: echo "No changes in apps/api/swagger.json nor apps/frontend/app/generated/api-client, skipping frontend API client generation verification."
|
run: echo "No changes in apps/api/swagger.json nor apps/frontend/app/generated/api-client, skipping frontend API client generation verification."
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,3 +27,5 @@ target
|
|||||||
.env.generated
|
.env.generated
|
||||||
|
|
||||||
generated-config.yaml
|
generated-config.yaml
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|||||||
665
Cargo.lock
generated
665
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ members = [
|
|||||||
"apps/container",
|
"apps/container",
|
||||||
"apps/cli",
|
"apps/cli",
|
||||||
"apps/agent",
|
"apps/agent",
|
||||||
|
"public/agent-client",
|
||||||
"public/shared",
|
"public/shared",
|
||||||
"public/database",
|
"public/database",
|
||||||
"public/migration"
|
"public/migration"
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ serde = { version = "1.0.228", features = ["std", "derive"] }
|
|||||||
tokio-cron-scheduler = { version = "0.15.1", features = ["signal"] }
|
tokio-cron-scheduler = { version = "0.15.1", features = ["signal"] }
|
||||||
clap = { version = "4", features = ["derive", "env"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
nix = { version = "0.30.1", features = ["user", "fs"] }
|
nix = { version = "0.30.1", features = ["user", "fs"] }
|
||||||
|
utoipa = { version = "5.4.0", features = ["macros", "axum_extras", "chrono", "decimal", "uuid", "time", "openapi_extensions"] }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod openapi;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
@@ -13,6 +14,7 @@ use tokio::net::UnixListener;
|
|||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::commands::NginxService;
|
use crate::commands::NginxService;
|
||||||
|
use crate::openapi::{GenerateOpenapiArgs, generate_openapi_doc};
|
||||||
use crate::routes::{status, validate, validate_and_reload, write_config};
|
use crate::routes::{status, validate, validate_and_reload, write_config};
|
||||||
|
|
||||||
const SOCK_ENV: &str = "YANPM_AGENT_SOCK";
|
const SOCK_ENV: &str = "YANPM_AGENT_SOCK";
|
||||||
@@ -43,6 +45,19 @@ struct Args {
|
|||||||
/// GID to set on the unix socket, default: current user's primary group
|
/// GID to set on the unix socket, default: current user's primary group
|
||||||
#[arg(long, default_value_t = String::from(SOCK_GID_DEFAULT), env = SOCK_GID_ENV)]
|
#[arg(long, default_value_t = String::from(SOCK_GID_DEFAULT), env = SOCK_GID_ENV)]
|
||||||
sock_gid: String,
|
sock_gid: String,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<SubCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug)]
|
||||||
|
pub enum SubCommand {
|
||||||
|
/// Generate OpenAPI spec to file or stdout
|
||||||
|
GenerateOpenapi {
|
||||||
|
/// Output file path.
|
||||||
|
#[arg(short = 'o', long)]
|
||||||
|
output: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -59,6 +74,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if let Some(cmd) = &args.command {
|
||||||
|
match cmd {
|
||||||
|
SubCommand::GenerateOpenapi { output } => {
|
||||||
|
generate_openapi_doc(&GenerateOpenapiArgs {
|
||||||
|
output: output.clone(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (sock, nginx_config_dir, sock_perm, sock_gid) = get_args(&args).await?;
|
let (sock, nginx_config_dir, sock_perm, sock_gid) = get_args(&args).await?;
|
||||||
|
|
||||||
let path = PathBuf::from(&sock);
|
let path = PathBuf::from(&sock);
|
||||||
|
|||||||
45
apps/agent/src/openapi.rs
Normal file
45
apps/agent/src/openapi.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use tracing::info;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
pub mod tag {
|
||||||
|
/// nginx
|
||||||
|
pub const NGINX_TAG: &str = "Nginx Agent";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(utoipa::OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::routes::status,
|
||||||
|
crate::routes::validate,
|
||||||
|
crate::routes::validate_and_reload,
|
||||||
|
crate::routes::write_config,
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(crate::routes::StatusResp),
|
||||||
|
schemas(crate::routes::ValidateAndReloadResp),
|
||||||
|
schemas(crate::routes::ValidateBody),
|
||||||
|
schemas(crate::routes::WriteConfigBody),
|
||||||
|
schemas(crate::routes::ValidateAndReloadBody),
|
||||||
|
),
|
||||||
|
tags(
|
||||||
|
(name = tag::NGINX_TAG, description = "Nginx Agent API"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
struct ApiDoc;
|
||||||
|
|
||||||
|
pub struct GenerateOpenapiArgs {
|
||||||
|
pub output: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_openapi_doc(
|
||||||
|
args: &GenerateOpenapiArgs,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
info!("Generating OpenAPI documentation...");
|
||||||
|
let doc = ApiDoc::openapi();
|
||||||
|
let json = doc
|
||||||
|
.to_pretty_json()
|
||||||
|
.expect("Failed to serialize OpenAPI doc to JSON");
|
||||||
|
std::fs::write(&args.output, json).expect("Failed to write OpenAPI doc to file");
|
||||||
|
info!("OpenAPI documentation generated at {}", args.output);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -9,28 +9,46 @@ use tracing::warn;
|
|||||||
|
|
||||||
use crate::commands::NginxService;
|
use crate::commands::NginxService;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
pub struct StatusResp {
|
pub struct StatusResp {
|
||||||
pub ok: bool,
|
pub ok: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Health check endpoint
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/status",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Status response", body = StatusResp)
|
||||||
|
),
|
||||||
|
tag = crate::openapi::tag::NGINX_TAG
|
||||||
|
)]
|
||||||
pub async fn status() -> impl IntoResponse {
|
pub async fn status() -> impl IntoResponse {
|
||||||
let resp = StatusResp { ok: true };
|
let resp = StatusResp { ok: true };
|
||||||
(axum::http::StatusCode::OK, axum::Json(resp))
|
(axum::http::StatusCode::OK, axum::Json(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
pub struct ValidateAndReloadResp {
|
pub struct ValidateAndReloadResp {
|
||||||
pub rc: i32,
|
pub rc: i32,
|
||||||
pub ro: String,
|
pub ro: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
pub struct ValidateBody {
|
pub struct ValidateBody {
|
||||||
config_name: String,
|
config_name: String,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/validate",
|
||||||
|
request_body = ValidateBody,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Validation response", body = serde_json::Value)
|
||||||
|
),
|
||||||
|
tag = crate::openapi::tag::NGINX_TAG
|
||||||
|
)]
|
||||||
pub async fn validate(
|
pub async fn validate(
|
||||||
State(nginx_controller): State<Arc<NginxService>>,
|
State(nginx_controller): State<Arc<NginxService>>,
|
||||||
Json(payload): Json<Value>,
|
Json(payload): Json<Value>,
|
||||||
@@ -57,12 +75,21 @@ pub async fn validate(
|
|||||||
(axum::http::StatusCode::OK, axum::Json(resp)).into_response()
|
(axum::http::StatusCode::OK, axum::Json(resp)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
pub struct ValidateAndReloadBody {
|
pub struct ValidateAndReloadBody {
|
||||||
config_name: String,
|
config_name: String,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/validate_and_reload",
|
||||||
|
request_body = ValidateAndReloadBody,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Validate and reload response", body = ValidateAndReloadResp)
|
||||||
|
),
|
||||||
|
tag = crate::openapi::tag::NGINX_TAG
|
||||||
|
)]
|
||||||
pub async fn validate_and_reload(
|
pub async fn validate_and_reload(
|
||||||
State(nginx_controller): State<Arc<NginxService>>,
|
State(nginx_controller): State<Arc<NginxService>>,
|
||||||
Json(payload): Json<Value>,
|
Json(payload): Json<Value>,
|
||||||
@@ -96,13 +123,23 @@ pub async fn validate_and_reload(
|
|||||||
(axum::http::StatusCode::OK, axum::Json(resp)).into_response()
|
(axum::http::StatusCode::OK, axum::Json(resp)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
pub struct WriteConfigBody {
|
pub struct WriteConfigBody {
|
||||||
config_name: String,
|
config_name: String,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
content: String,
|
content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/write_config",
|
||||||
|
request_body = WriteConfigBody,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Write config response"),
|
||||||
|
(status = 500, description = "Internal server error", body = serde_json::Value)
|
||||||
|
),
|
||||||
|
tag = crate::openapi::tag::NGINX_TAG
|
||||||
|
)]
|
||||||
pub async fn write_config(
|
pub async fn write_config(
|
||||||
State(nginx_controller): State<Arc<NginxService>>,
|
State(nginx_controller): State<Arc<NginxService>>,
|
||||||
Json(payload): Json<Value>,
|
Json(payload): Json<Value>,
|
||||||
|
|||||||
215
apps/agent/swagger.json
Normal file
215
apps/agent/swagger.json
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"info": {
|
||||||
|
"title": "yanpm-agent",
|
||||||
|
"description": "",
|
||||||
|
"license": {
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
|
"version": "0.1.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/status": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx Agent"
|
||||||
|
],
|
||||||
|
"summary": "Health check endpoint",
|
||||||
|
"operationId": "status",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Status response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StatusResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/validate": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx Agent"
|
||||||
|
],
|
||||||
|
"operationId": "validate",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidateBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Validation response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/validate_and_reload": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx Agent"
|
||||||
|
],
|
||||||
|
"operationId": "validate_and_reload",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidateAndReloadBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Validate and reload response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidateAndReloadResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/write_config": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx Agent"
|
||||||
|
],
|
||||||
|
"operationId": "write_config",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WriteConfigBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Write config response"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"StatusResp": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"ok"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"ok": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ValidateAndReloadBody": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"config_name",
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"config_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ValidateAndReloadResp": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"rc",
|
||||||
|
"ro"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"rc": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"ro": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ValidateBody": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"config_name",
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"config_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WriteConfigBody": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"config_name",
|
||||||
|
"timestamp",
|
||||||
|
"content"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"config_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Nginx Agent",
|
||||||
|
"description": "Nginx Agent API"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
database = { path = "../../public/database" }
|
database = { path = "../../public/database" }
|
||||||
migration = { path = "../../public/migration" }
|
migration = { path = "../../public/migration" }
|
||||||
|
agent_client = { path = "../../public/agent-client" }
|
||||||
|
|
||||||
axum = { version = "0.8.7", features = ["form", "http1", "http2", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing", "macros"] }
|
axum = { version = "0.8.7", features = ["form", "http1", "http2", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing", "macros"] }
|
||||||
axum-extra = { version = "0.12.2", features = ["cookie"] }
|
axum-extra = { version = "0.12.2", features = ["cookie"] }
|
||||||
@@ -28,4 +29,13 @@ argon2 = { version = "0.5.3", features = ["std"] }
|
|||||||
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
|
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
|
||||||
uuid = { version = "1.19.0", features = ["v4", "serde", "fast-rng"] }
|
uuid = { version = "1.19.0", features = ["v4", "serde", "fast-rng"] }
|
||||||
tower-http = { version = "0.6.8", features = ["cors"] }
|
tower-http = { version = "0.6.8", features = ["cors"] }
|
||||||
|
reqwest = { version = "^0.12", features = ["json", "multipart", "stream"] }
|
||||||
|
serde_urlencoded = { version = "0.7.1" }
|
||||||
|
optfield = { version = "0.4.0" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
axum-test = "18.4.1"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
unwrap_used = "deny"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ fn action(
|
|||||||
_matches: &clap::ArgMatches,
|
_matches: &clap::ArgMatches,
|
||||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
|
||||||
let output_path = _matches.get_one::<String>("output_path");
|
let output_path = _matches.get_one::<String>("output_path");
|
||||||
let output_path = output_path.unwrap().to_string();
|
let output_path = output_path.expect("output_path is required").to_string();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
tracing::subscriber::with_default(log::make_temporary_subscriber(), || {
|
tracing::subscriber::with_default(log::make_temporary_subscriber(), || {
|
||||||
|
|||||||
@@ -11,15 +11,8 @@ use crate::{
|
|||||||
cmd::CliCommand,
|
cmd::CliCommand,
|
||||||
configs::{ProgramSettings, get_program_settings, logging::LoggingSettings},
|
configs::{ProgramSettings, get_program_settings, logging::LoggingSettings},
|
||||||
log,
|
log,
|
||||||
routes::{self, AppService, AppState},
|
routes::{self, AppState},
|
||||||
services::{
|
services::get_app_service,
|
||||||
auth::{
|
|
||||||
authentication::{AuthenticationServiceImpl, strategies::password::PasswordStrategy},
|
|
||||||
user::UserServiceImpl,
|
|
||||||
},
|
|
||||||
server_state::ServerStateService,
|
|
||||||
settings::SettingsService,
|
|
||||||
},
|
|
||||||
tasks,
|
tasks,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,19 +141,7 @@ fn get_app_state(
|
|||||||
AppState {
|
AppState {
|
||||||
database_connection: db_connection.clone(),
|
database_connection: db_connection.clone(),
|
||||||
config: Arc::new(settings.clone()),
|
config: Arc::new(settings.clone()),
|
||||||
service: Arc::new(AppService {
|
service: Arc::new(get_app_service(db_connection, settings)),
|
||||||
server_state: Arc::new(ServerStateService::new(db_connection.clone())),
|
|
||||||
settings: Arc::new(SettingsService::new(db_connection.clone())),
|
|
||||||
auth_state: routes::AuthState {
|
|
||||||
strategy: routes::AuthStrategy {
|
|
||||||
password: Arc::new(PasswordStrategy::new(db_connection.clone())),
|
|
||||||
},
|
|
||||||
authentication: Arc::new(AuthenticationServiceImpl::new(
|
|
||||||
settings.auth.jwt_secret.clone(),
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
user: Arc::new(UserServiceImpl::new(db_connection.clone())),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod agent;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
@@ -21,6 +22,7 @@ pub struct ProgramSettings {
|
|||||||
pub database: database::DatabaseSettings,
|
pub database: database::DatabaseSettings,
|
||||||
pub server: server::ServerSettings,
|
pub server: server::ServerSettings,
|
||||||
pub auth: auth::AuthSettings,
|
pub auth: auth::AuthSettings,
|
||||||
|
pub agent: agent::AgentSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromConfig for ProgramSettings {
|
impl FromConfig for ProgramSettings {
|
||||||
@@ -30,6 +32,7 @@ impl FromConfig for ProgramSettings {
|
|||||||
database: database::DatabaseSettings::from_config(_config)?,
|
database: database::DatabaseSettings::from_config(_config)?,
|
||||||
server: server::ServerSettings::from_config(_config)?,
|
server: server::ServerSettings::from_config(_config)?,
|
||||||
auth: auth::AuthSettings::from_config(_config)?,
|
auth: auth::AuthSettings::from_config(_config)?,
|
||||||
|
agent: agent::AgentSettings::from_config(_config)?,
|
||||||
};
|
};
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@@ -50,6 +53,7 @@ impl FromConfig for ProgramSettings {
|
|||||||
database: database::DatabaseSettings::mock(),
|
database: database::DatabaseSettings::mock(),
|
||||||
server: server::ServerSettings::mock(),
|
server: server::ServerSettings::mock(),
|
||||||
auth: auth::AuthSettings::mock(),
|
auth: auth::AuthSettings::mock(),
|
||||||
|
agent: agent::AgentSettings::mock(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
apps/api/src/configs/agent.rs
Normal file
58
apps/api/src/configs/agent.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use config::Config;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::configs::key::AGENT_SOCK_PATH_KEY;
|
||||||
|
|
||||||
|
use super::FromConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AgentSettings {
|
||||||
|
pub socket_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromConfig for AgentSettings {
|
||||||
|
fn from_config(_config: &Config) -> Result<Self, String> {
|
||||||
|
Ok(AgentSettings {
|
||||||
|
socket_path: _config.get_string(AGENT_SOCK_PATH_KEY).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Failed to get {} from configuration. Err: {}",
|
||||||
|
AGENT_SOCK_PATH_KEY, err
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> Result<(), String> {
|
||||||
|
// ensure socket_path exists and is readable and writable
|
||||||
|
if !std::path::Path::new(&self.socket_path).exists() {
|
||||||
|
let msg = format!("Agent socket path '{}' does not exist", self.socket_path);
|
||||||
|
error!("{}", msg);
|
||||||
|
return Err(msg);
|
||||||
|
}
|
||||||
|
if std::path::Path::new(&self.socket_path)
|
||||||
|
.metadata()
|
||||||
|
.map(|meta| {
|
||||||
|
let permissions = meta.permissions();
|
||||||
|
// Check read and write permissions for the owner
|
||||||
|
!permissions.readonly()
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let msg = format!(
|
||||||
|
"Agent socket path '{}' is not readable/writable",
|
||||||
|
self.socket_path
|
||||||
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
Err(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn mock() -> Self {
|
||||||
|
AgentSettings {
|
||||||
|
socket_path: "/tmp/agent.sock".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,5 @@ pub(crate) const DATABASE_MIGRATE_ON_STARTUP_KEY: &str = "DATABASE.MIGRATION.MIG
|
|||||||
pub(crate) const AUTH_JWT_SECRET_KEY: &str = "AUTH.JWT_SECRET";
|
pub(crate) const AUTH_JWT_SECRET_KEY: &str = "AUTH.JWT_SECRET";
|
||||||
pub(crate) const AUTH_DEFAULT_ADMIN_USERNAME_KEY: &str = "AUTH.DEFAULT_ADMIN_USERNAME";
|
pub(crate) const AUTH_DEFAULT_ADMIN_USERNAME_KEY: &str = "AUTH.DEFAULT_ADMIN_USERNAME";
|
||||||
pub(crate) const AUTH_DEFAULT_ADMIN_PASSWORD_KEY: &str = "AUTH.DEFAULT_ADMIN_PASSWORD";
|
pub(crate) const AUTH_DEFAULT_ADMIN_PASSWORD_KEY: &str = "AUTH.DEFAULT_ADMIN_PASSWORD";
|
||||||
|
//
|
||||||
|
pub(crate) const AGENT_SOCK_PATH_KEY: &str = "AGENT.SOCK.PATH";
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ impl FromConfig for ServerSettings {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn mock() -> Self {
|
fn mock() -> Self {
|
||||||
ServerSettings {
|
ServerSettings {
|
||||||
address: "0.0.0.0".parse().unwrap(),
|
address: "0.0.0.0".parse().expect("Failed to parse mock IP address"),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
serve_openapi: false,
|
serve_openapi: false,
|
||||||
cors: CORSSettings {
|
cors: CORSSettings {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
pub mod api_error;
|
||||||
pub mod service_error;
|
pub mod service_error;
|
||||||
|
|||||||
21
apps/api/src/errors/api_error.rs
Normal file
21
apps/api/src/errors/api_error.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
|
use crate::errors::service_error::ServiceError;
|
||||||
|
|
||||||
|
pub enum ApiError {
|
||||||
|
ServiceError(ServiceError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ServiceError> for ApiError {
|
||||||
|
fn from(err: ServiceError) -> Self {
|
||||||
|
ApiError::ServiceError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
ApiError::ServiceError(service_error) => service_error.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use axum::response::IntoResponse;
|
||||||
use sea_orm::DbErr;
|
use sea_orm::DbErr;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -37,3 +38,23 @@ impl From<DbErr> for ServiceError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ServiceError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
let (status, message) = match &self {
|
||||||
|
ServiceError::NotFound(msg) => (axum::http::StatusCode::NOT_FOUND, msg.clone()),
|
||||||
|
ServiceError::DatabaseError(msg) => {
|
||||||
|
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
|
||||||
|
}
|
||||||
|
ServiceError::Unauthorized(msg) => (axum::http::StatusCode::UNAUTHORIZED, msg.clone()),
|
||||||
|
ServiceError::InternalError(msg) => {
|
||||||
|
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
|
||||||
|
}
|
||||||
|
ServiceError::BadRequest(msg) => (axum::http::StatusCode::BAD_REQUEST, msg.clone()),
|
||||||
|
};
|
||||||
|
let body = axum::Json(serde_json::json!({
|
||||||
|
"error": message,
|
||||||
|
}));
|
||||||
|
(status, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
|
pub mod macros;
|
||||||
|
|||||||
@@ -11,3 +11,20 @@ macro_rules! with_conn {
|
|||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Filters {
|
||||||
|
pub pagination: Option<PaginationFilter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PaginationFilter {
|
||||||
|
pub page: u64,
|
||||||
|
pub per_page: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaginationFilter {
|
||||||
|
pub fn get_offset_limit(&self) -> (u64, u64) {
|
||||||
|
let offset = (self.page - 1) * self.per_page;
|
||||||
|
let limit = self.per_page;
|
||||||
|
(offset, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
9
apps/api/src/helpers/macros.rs
Normal file
9
apps/api/src/helpers/macros.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! set_if_some {
|
||||||
|
($field:expr) => {
|
||||||
|
match $field {
|
||||||
|
Some(value) => sea_orm::ActiveValue::Set(value),
|
||||||
|
None => sea_orm::ActiveValue::NotSet,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,34 @@
|
|||||||
|
use axum::{extract::FromRequestParts, http::StatusCode};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RequestInfo {
|
pub struct RequestInfo {
|
||||||
pub user_id: Option<Uuid>,
|
pub user_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AuthenticatedRequestInfo {
|
||||||
|
pub user_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> FromRequestParts<S> for AuthenticatedRequestInfo
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = StatusCode;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
parts: &mut axum::http::request::Parts,
|
||||||
|
_state: &S,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let request_info = parts
|
||||||
|
.extensions
|
||||||
|
.get::<RequestInfo>()
|
||||||
|
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||||
|
|
||||||
|
if let Some(user_id) = request_info.user_id {
|
||||||
|
Ok(AuthenticatedRequestInfo { user_id })
|
||||||
|
} else {
|
||||||
|
Err(StatusCode::UNAUTHORIZED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,3 +68,42 @@ async fn handle_unauthenticated() -> Result<Response, StatusCode> {
|
|||||||
// TODO: log unauthenticated access attempts
|
// TODO: log unauthenticated access attempts
|
||||||
Err(StatusCode::UNAUTHORIZED)
|
Err(StatusCode::UNAUTHORIZED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod mock {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub const REQUEST_AUTH_USER_ID_HEADER: &str = "x-mock-authenticated-user-id";
|
||||||
|
pub const REQUEST_AUTH_USER_INVALID_HEADER: &str = "x-mock-authenticated-invalid";
|
||||||
|
|
||||||
|
pub async fn mock_require_auth(
|
||||||
|
req: Request<axum::body::Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
let mut req = req;
|
||||||
|
let invalid_present = req
|
||||||
|
.headers()
|
||||||
|
.get(REQUEST_AUTH_USER_INVALID_HEADER)
|
||||||
|
.is_some();
|
||||||
|
let user_id_header = req.headers().get(REQUEST_AUTH_USER_ID_HEADER).cloned();
|
||||||
|
|
||||||
|
if invalid_present {
|
||||||
|
return handle_unauthenticated().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = req
|
||||||
|
.extensions_mut()
|
||||||
|
.get_or_insert_with(|| RequestInfo { user_id: None });
|
||||||
|
user.user_id = Some(if let Some(user_id_header) = user_id_header {
|
||||||
|
let user_id_str = user_id_header
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
||||||
|
Uuid::parse_str(user_id_str).map_err(|_| StatusCode::UNAUTHORIZED)?
|
||||||
|
} else {
|
||||||
|
Uuid::new_v4()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,12 +12,8 @@ use crate::{
|
|||||||
configs::{ProgramSettings, server::CORSSettings},
|
configs::{ProgramSettings, server::CORSSettings},
|
||||||
middlewares,
|
middlewares,
|
||||||
services::{
|
services::{
|
||||||
auth::{
|
AppService, ServiceState,
|
||||||
authentication::{AuthenticationService, strategies::password::PasswordStrategy},
|
auth::authentication::{AuthenticationService, strategies::password::PasswordStrategy},
|
||||||
user::UserService,
|
|
||||||
},
|
|
||||||
server_state::ServerStateStore,
|
|
||||||
settings::SettingsStore,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,8 +24,6 @@ pub struct AppState {
|
|||||||
pub config: Arc<ProgramSettings>,
|
pub config: Arc<ProgramSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ServiceState<T> = Arc<T>;
|
|
||||||
|
|
||||||
pub struct AuthStrategy {
|
pub struct AuthStrategy {
|
||||||
pub password: ServiceState<PasswordStrategy>,
|
pub password: ServiceState<PasswordStrategy>,
|
||||||
}
|
}
|
||||||
@@ -39,13 +33,6 @@ pub struct AuthState {
|
|||||||
pub authentication: ServiceState<dyn AuthenticationService>,
|
pub authentication: ServiceState<dyn AuthenticationService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppService {
|
|
||||||
pub settings: ServiceState<dyn SettingsStore>,
|
|
||||||
pub auth_state: AuthState,
|
|
||||||
pub user: ServiceState<dyn UserService>,
|
|
||||||
pub server_state: ServiceState<dyn ServerStateStore>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_root_router(
|
pub fn get_root_router(
|
||||||
state: impl Into<Arc<AppState>>,
|
state: impl Into<Arc<AppState>>,
|
||||||
cors_settings: Arc<CORSSettings>,
|
cors_settings: Arc<CORSSettings>,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod auth;
|
mod auth;
|
||||||
mod health;
|
mod health;
|
||||||
|
mod helper;
|
||||||
mod openapi;
|
mod openapi;
|
||||||
mod restricted;
|
mod restricted;
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ pub async fn get_health_info(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::configs::FromConfig;
|
use crate::configs::FromConfig;
|
||||||
|
use crate::services::agent_client::AgentService;
|
||||||
|
use crate::services::get_app_service;
|
||||||
use crate::{
|
use crate::{
|
||||||
routes::{AppState, api::health::state::HealthState},
|
routes::{AppState, api::health::state::HealthState},
|
||||||
services::{
|
services::{
|
||||||
@@ -94,6 +96,7 @@ mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use agent_client::apis::configuration::Configuration;
|
||||||
use axum::body::to_bytes;
|
use axum::body::to_bytes;
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
@@ -114,17 +117,10 @@ mod test {
|
|||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
database_connection: db.clone(),
|
database_connection: db.clone(),
|
||||||
config: Arc::new(crate::configs::ProgramSettings::mock()),
|
config: Arc::new(crate::configs::ProgramSettings::mock()),
|
||||||
service: Arc::new(crate::routes::AppService {
|
service: Arc::new(get_app_service(
|
||||||
settings: Arc::new(SettingsService::new(db.clone())),
|
&db.clone(),
|
||||||
auth_state: crate::routes::AuthState {
|
&crate::configs::ProgramSettings::mock(),
|
||||||
strategy: crate::routes::AuthStrategy {
|
)),
|
||||||
password: Arc::new(PasswordStrategy::new(db.clone())),
|
|
||||||
},
|
|
||||||
authentication: Arc::new(AuthenticationServiceImpl::new(None)),
|
|
||||||
},
|
|
||||||
user: Arc::new(UserServiceImpl::new(db.clone())),
|
|
||||||
server_state: Arc::new(ServerStateService::new(db.clone())),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -135,13 +131,21 @@ mod test {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
.oneshot(Request::builder().uri("/info").body(Body::empty()).unwrap())
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/info")
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("Failed to build request"),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
let body = to_bytes(response.into_body(), 1024 * 1024).await.unwrap(); // Set limit to 1 MB
|
let body = to_bytes(response.into_body(), 1024 * 1024)
|
||||||
let health_info: HealthInfo = serde_json::from_slice(&body).unwrap();
|
.await
|
||||||
|
.expect("Failed to read response body"); // Set limit to 1 MB
|
||||||
|
let health_info: HealthInfo =
|
||||||
|
serde_json::from_slice(&body).expect("Failed to deserialize response body");
|
||||||
assert_eq!(health_info.status, STATUS_HEALTHY);
|
assert_eq!(health_info.status, STATUS_HEALTHY);
|
||||||
assert_eq!(health_info.version, env!("CARGO_PKG_VERSION"));
|
assert_eq!(health_info.version, env!("CARGO_PKG_VERSION"));
|
||||||
assert!(health_info.errors.is_none());
|
assert!(health_info.errors.is_none());
|
||||||
|
|||||||
1
apps/api/src/routes/api/helper.rs
Normal file
1
apps/api/src/routes/api/helper.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod pagination;
|
||||||
76
apps/api/src/routes/api/helper/pagination.rs
Normal file
76
apps/api/src/routes/api/helper/pagination.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::{StatusCode, request::Parts},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::helpers::database::PaginationFilter;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema, Clone)]
|
||||||
|
/// Pagination parameters for API requests
|
||||||
|
pub struct Pagination {
|
||||||
|
/// Page number (1-based)
|
||||||
|
pub page: u32,
|
||||||
|
/// Items per page
|
||||||
|
pub per_page: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Pagination {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Pagination> for PaginationFilter {
|
||||||
|
fn from(pagination: Pagination) -> Self {
|
||||||
|
Self {
|
||||||
|
page: pagination.page as u64,
|
||||||
|
per_page: pagination.per_page as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
/// Pagination information included in API responses
|
||||||
|
pub struct PaginationInfo {
|
||||||
|
/// Total number of items
|
||||||
|
pub total_items: u64,
|
||||||
|
/// Total number of pages
|
||||||
|
pub total_pages: u32,
|
||||||
|
/// Current page number
|
||||||
|
pub current_page: u32,
|
||||||
|
/// Items per page
|
||||||
|
pub per_page: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extractor for pagination parameters from query string
|
||||||
|
pub struct ExtractPagination(pub Pagination);
|
||||||
|
|
||||||
|
impl<S> FromRequestParts<S> for ExtractPagination
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = (StatusCode, &'static str);
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let query = parts.uri.query().unwrap_or("");
|
||||||
|
let pagination: Pagination = serde_urlencoded::from_str(query).unwrap_or_default();
|
||||||
|
|
||||||
|
// validation
|
||||||
|
if pagination.page == 0 {
|
||||||
|
return Err((StatusCode::BAD_REQUEST, "page must be greater than 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pagination.per_page < 1 || pagination.per_page > 100 {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"per_page must be between 1 and 100",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExtractPagination(pagination))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod nginx;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -9,6 +10,7 @@ use crate::{middlewares::require_auth::require_auth, routes::AppState};
|
|||||||
pub fn get_restricted_router(state: Arc<AppState>) -> Router {
|
pub fn get_restricted_router(state: Arc<AppState>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/user", user::get_user_router(state.clone()))
|
.nest("/user", user::get_user_router(state.clone()))
|
||||||
|
.nest("/nginx", nginx::get_nginx_router(state.clone()))
|
||||||
.layer(axum::middleware::from_fn_with_state(
|
.layer(axum::middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_auth,
|
require_auth,
|
||||||
|
|||||||
11
apps/api/src/routes/api/restricted/nginx.rs
Normal file
11
apps/api/src/routes/api/restricted/nginx.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod upstream;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::routes::AppState;
|
||||||
|
|
||||||
|
pub fn get_nginx_router(state: Arc<AppState>) -> Router {
|
||||||
|
Router::new().merge(upstream::get_upstream_router(state.clone()))
|
||||||
|
}
|
||||||
32
apps/api/src/routes/api/restricted/nginx/upstream.rs
Normal file
32
apps/api/src/routes/api/restricted/nginx/upstream.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
pub mod create_upstream;
|
||||||
|
pub mod create_upstream_target;
|
||||||
|
pub mod get_upstream;
|
||||||
|
pub mod get_upstream_target;
|
||||||
|
pub mod info;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::routes::AppState;
|
||||||
|
|
||||||
|
pub fn get_upstream_router(state: Arc<AppState>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/upstreams",
|
||||||
|
get(get_upstream::get_upstream_list).post(create_upstream::create_upstream),
|
||||||
|
)
|
||||||
|
.route("/upstreams/{upstream_id}", get(get_upstream::get_upstream))
|
||||||
|
.route(
|
||||||
|
"/upstreams/{upstream_id}/targets",
|
||||||
|
post(create_upstream_target::add_upstream_target),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/upstream_targets/{upstream_target_id}",
|
||||||
|
get(get_upstream_target::get_upstream_target),
|
||||||
|
)
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{Json, extract::State, response::Result as AxumResult};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{AppState, api::restricted::nginx::upstream::info::response::UpstreamInfoResponse},
|
||||||
|
services::nginx::info::upstream::UpstreamCreateInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||||
|
pub struct UpstreamTargetInfo {
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
pub weight: Option<i64>,
|
||||||
|
pub is_backup: Option<bool>,
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConcreteUpstreamTargetInfo {
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
pub weight: i64,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpstreamTargetInfo> for ConcreteUpstreamTargetInfo {
|
||||||
|
fn from(info: UpstreamTargetInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
host: info.host,
|
||||||
|
port: info.port,
|
||||||
|
weight: info.weight.unwrap_or(1),
|
||||||
|
is_backup: info.is_backup.unwrap_or(false),
|
||||||
|
enabled: info.enabled.unwrap_or(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||||
|
pub struct CreateUpstreamRequestBody {
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: Option<String>,
|
||||||
|
pub sticky_session: Option<bool>,
|
||||||
|
pub upstream_targets: Vec<UpstreamTargetInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConcreteCreateUpstreamRequestBody {
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub sticky_session: bool,
|
||||||
|
pub upstream_targets: Vec<ConcreteUpstreamTargetInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateUpstreamRequestBody> for ConcreteCreateUpstreamRequestBody {
|
||||||
|
fn from(payload: CreateUpstreamRequestBody) -> Self {
|
||||||
|
Self {
|
||||||
|
name: payload.name,
|
||||||
|
protocol: payload.protocol,
|
||||||
|
algorithm: payload
|
||||||
|
.algorithm
|
||||||
|
.unwrap_or_else(|| "round_robin".to_string()),
|
||||||
|
sticky_session: payload.sticky_session.unwrap_or(false),
|
||||||
|
upstream_targets: payload
|
||||||
|
.upstream_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|target| target.into())
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
pub async fn create_upstream(
|
||||||
|
request_info: AuthenticatedRequestInfo,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<CreateUpstreamRequestBody>,
|
||||||
|
) -> AxumResult<Json<UpstreamInfoResponse>, ApiError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
let concrete_payload: ConcreteCreateUpstreamRequestBody = payload.into();
|
||||||
|
|
||||||
|
let create_info = UpstreamCreateInfo {
|
||||||
|
name: concrete_payload.name,
|
||||||
|
protocol: concrete_payload.protocol,
|
||||||
|
algorithm: concrete_payload.algorithm,
|
||||||
|
sticky_session: concrete_payload.sticky_session,
|
||||||
|
created_by: Some(request_info.user_id),
|
||||||
|
upstream_targets: concrete_payload
|
||||||
|
.upstream_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|target| crate::services::nginx::info::upstream_target::UpstreamTargetCreateInfo {
|
||||||
|
target_host: target.host,
|
||||||
|
target_port: target.port,
|
||||||
|
weight: target.weight,
|
||||||
|
is_backup: target.is_backup,
|
||||||
|
enabled: target.enabled,
|
||||||
|
upstream_id: uuid::Uuid::nil(), // Placeholder, will be set in service
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let upstream_info = upstream_service.create_upstream(create_info, None).await?;
|
||||||
|
|
||||||
|
Ok(Json(upstream_info.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
|
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||||
|
routes::api::restricted::nginx::upstream::{
|
||||||
|
create_upstream::{CreateUpstreamRequestBody, UpstreamTargetInfo as ReqTarget},
|
||||||
|
get_upstream_router,
|
||||||
|
},
|
||||||
|
services::get_app_service,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_create_upstream_succeeds_returns_created() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "new_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "round_robin".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: Some(uuid::Uuid::new_v4()),
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
let target_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: up_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// service will likely perform an insert and then query to return created models
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateUpstreamRequestBody {
|
||||||
|
name: "new_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: vec![ReqTarget {
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
weight: None,
|
||||||
|
is_backup: None,
|
||||||
|
enabled: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server.post("/upstreams").json(&payload).await;
|
||||||
|
|
||||||
|
res.assert_status_ok();
|
||||||
|
let text = res.text();
|
||||||
|
let body: crate::routes::api::restricted::nginx::upstream::info::response::UpstreamInfoResponse =
|
||||||
|
serde_json::from_str(&text).expect("failed to parse json");
|
||||||
|
|
||||||
|
assert_eq!(body.id, up_id);
|
||||||
|
assert_eq!(body.name, "new_upstream");
|
||||||
|
assert_eq!(body.protocol, "http");
|
||||||
|
assert_eq!(body.upstream_targets.len(), 1);
|
||||||
|
assert_eq!(body.upstream_targets[0].id, target_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_create_upstream_invalid_payload_returns_bad_request() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
// missing required fields -> send empty object
|
||||||
|
let res = server.post("/upstreams").json(&serde_json::json!({})).await;
|
||||||
|
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_create_upstream_unauthenticated_returns_unauthorized() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateUpstreamRequestBody {
|
||||||
|
name: "new_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: vec![ReqTarget {
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
weight: None,
|
||||||
|
is_backup: None,
|
||||||
|
enabled: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.post("/upstreams")
|
||||||
|
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{Json, extract::State, response::Result as AxumResult};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{
|
||||||
|
AppState, api::restricted::nginx::upstream::info::response::UpstreamTargetInfoResponse,
|
||||||
|
},
|
||||||
|
services::nginx::info::upstream_target::UpstreamTargetCreateInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||||
|
pub struct CreateUpstreamTargetInfo {
|
||||||
|
pub upstream_id: uuid::Uuid,
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
pub weight: Option<i64>,
|
||||||
|
pub is_backup: Option<bool>,
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConcreteCreateUpstreamTargetInfo {
|
||||||
|
pub upstream_id: uuid::Uuid,
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
pub weight: i64,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateUpstreamTargetInfo> for ConcreteCreateUpstreamTargetInfo {
|
||||||
|
fn from(info: CreateUpstreamTargetInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
upstream_id: info.upstream_id,
|
||||||
|
host: info.host,
|
||||||
|
port: info.port,
|
||||||
|
weight: info.weight.unwrap_or(1),
|
||||||
|
is_backup: info.is_backup.unwrap_or(false),
|
||||||
|
enabled: info.enabled.unwrap_or(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
pub async fn add_upstream_target(
|
||||||
|
_request_info: AuthenticatedRequestInfo,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<CreateUpstreamTargetInfo>,
|
||||||
|
) -> AxumResult<Json<UpstreamTargetInfoResponse>, ApiError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
let concrete_payload: ConcreteCreateUpstreamTargetInfo = payload.into();
|
||||||
|
|
||||||
|
let create_info = UpstreamTargetCreateInfo {
|
||||||
|
weight: concrete_payload.weight,
|
||||||
|
is_backup: concrete_payload.is_backup,
|
||||||
|
enabled: concrete_payload.enabled,
|
||||||
|
target_host: concrete_payload.host,
|
||||||
|
target_port: concrete_payload.port,
|
||||||
|
upstream_id: concrete_payload.upstream_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let upstream_info = upstream_service
|
||||||
|
.create_upstream_target(create_info, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(upstream_info.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::upstream_target;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
|
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||||
|
routes::api::restricted::nginx::upstream::{
|
||||||
|
create_upstream_target::CreateUpstreamTargetInfo, get_upstream_router,
|
||||||
|
},
|
||||||
|
services::get_app_service,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_add_upstream_target_succeeds_returns_created() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
let target_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: up_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateUpstreamTargetInfo {
|
||||||
|
upstream_id: up_id,
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
weight: None,
|
||||||
|
is_backup: None,
|
||||||
|
enabled: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.post(&format!("/upstreams/{}/targets", up_id))
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status_ok();
|
||||||
|
let text = res.text();
|
||||||
|
let body: crate::routes::api::restricted::nginx::upstream::info::response::UpstreamTargetInfoResponse =
|
||||||
|
serde_json::from_str(&text).expect("failed to parse json");
|
||||||
|
|
||||||
|
assert_eq!(body.id, target_id);
|
||||||
|
assert_eq!(body.host, "127.0.0.1");
|
||||||
|
assert_eq!(body.port, 8080);
|
||||||
|
assert_eq!(body.upstream_id, up_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_add_upstream_target_invalid_payload_returns_bad_request() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.post(&format!("/upstreams/{}/targets", uuid::Uuid::new_v4()))
|
||||||
|
.json(&serde_json::json!({}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_add_upstream_target_unauthenticated_returns_unauthorized() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateUpstreamTargetInfo {
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
weight: None,
|
||||||
|
is_backup: None,
|
||||||
|
enabled: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.post(&format!("/upstreams/{}/targets", payload.upstream_id))
|
||||||
|
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{api_error::ApiError, service_error::ServiceError},
|
||||||
|
routes::{
|
||||||
|
AppState,
|
||||||
|
api::{
|
||||||
|
helper::pagination::{ExtractPagination, PaginationInfo},
|
||||||
|
restricted::nginx::upstream::info::response::{
|
||||||
|
UpstreamInfoResponse, UpstreamListResponse,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services::nginx::upstream::GetUpstreamOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct GetUpstreamParams {
|
||||||
|
pub include_targets: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConcreteGetUpstreamParams {
|
||||||
|
pub include_targets: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetUpstreamParams> for ConcreteGetUpstreamParams {
|
||||||
|
fn from(params: GetUpstreamParams) -> Self {
|
||||||
|
Self {
|
||||||
|
include_targets: params.include_targets.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_upstream_list(
|
||||||
|
ExtractPagination(pagination): ExtractPagination,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> AxumResult<Json<UpstreamListResponse>, ServiceError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
let upstreams = upstream_service
|
||||||
|
.get_upstreams(Some(pagination.clone().into()), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
//
|
||||||
|
Ok(Json(UpstreamListResponse {
|
||||||
|
items: upstreams.into_iter().map(|u| u.into()).collect(),
|
||||||
|
pagination: PaginationInfo {
|
||||||
|
total_items: 0,
|
||||||
|
total_pages: 0,
|
||||||
|
current_page: pagination.page,
|
||||||
|
per_page: pagination.per_page,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_upstream(
|
||||||
|
Path(upstream_id): Path<Uuid>,
|
||||||
|
Query(params): Query<GetUpstreamParams>,
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
) -> AxumResult<Json<UpstreamInfoResponse>, ApiError> {
|
||||||
|
let concrete_params: ConcreteGetUpstreamParams = params.into();
|
||||||
|
let upstream_service = &_state.service.nginx.get_upstream_service();
|
||||||
|
let upstream_info = if concrete_params.include_targets {
|
||||||
|
upstream_service
|
||||||
|
.get_upstream(
|
||||||
|
upstream_id,
|
||||||
|
Some(GetUpstreamOptions {
|
||||||
|
include_targets: true,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
upstream_service
|
||||||
|
.get_upstream(upstream_id, None, None)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
Ok(Json(upstream_info.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
use crate::configs::{FromConfig, ProgramSettings};
|
||||||
|
|
||||||
|
use crate::routes::api::restricted::nginx::upstream::get_upstream_router;
|
||||||
|
use crate::routes::api::restricted::nginx::upstream::info::response::UpstreamInfoResponse;
|
||||||
|
use crate::services::get_app_service;
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
get_upstream_router(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_list_returns_list() {
|
||||||
|
let u1 = upstream::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "u1".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
let u2 = upstream::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "u2".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![u1.clone(), u2.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let res = server.get("/upstreams").await;
|
||||||
|
res.assert_status_ok();
|
||||||
|
let body = res.json::<UpstreamListResponse>();
|
||||||
|
assert_eq!(body.items.len(), 2);
|
||||||
|
assert_eq!(body.pagination.current_page, 1u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_with_targets_returns_targets() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "with_targets".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "least_conn".to_string(),
|
||||||
|
sticky_session: true,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_model = upstream_target::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
upstream_id: up_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
// find_by_id -> returns upstream model
|
||||||
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
|
// find targets -> returns the target(s)
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let url = format!("/upstreams/{}?include_targets=true", up_id);
|
||||||
|
let res = server.get(&url).await;
|
||||||
|
res.assert_status_ok();
|
||||||
|
let body = res.json::<UpstreamInfoResponse>();
|
||||||
|
assert_eq!(body.id, up_id);
|
||||||
|
assert_eq!(body.upstream_targets.len(), 1);
|
||||||
|
assert_eq!(body.upstream_targets[0].target_host, "127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn extractor_pagination_validation_rejects_bad_values() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
// page = 0 should be rejected
|
||||||
|
let res = server.get("/upstreams?page=0&per_page=10").await;
|
||||||
|
res.assert_status(StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
// per_page out of range should be rejected
|
||||||
|
let res = server.get("/upstreams?page=1&per_page=0").await;
|
||||||
|
res.assert_status(StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
// valid values accepted
|
||||||
|
let res = server.get("/upstreams?page=2&per_page=5").await;
|
||||||
|
res.assert_status_ok();
|
||||||
|
let body = res.json::<UpstreamListResponse>();
|
||||||
|
assert_eq!(body.pagination.current_page, 2u32);
|
||||||
|
assert_eq!(body.pagination.per_page, 5u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_not_found_returns_service_error() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let url = format!("/upstreams/{}?include_targets=false", up_id);
|
||||||
|
let res = server.get(&url).await;
|
||||||
|
res.assert_status(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_without_targets_returns_info() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "simple_up".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
// find_by_id -> returns upstream model
|
||||||
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
// include_targets omitted -> should not include targets
|
||||||
|
let url = format!("/upstreams/{}", up_id);
|
||||||
|
let res = server.get(&url).await;
|
||||||
|
res.assert_status_ok();
|
||||||
|
let body = res.json::<UpstreamInfoResponse>();
|
||||||
|
assert_eq!(body.id, up_id);
|
||||||
|
assert!(body.upstream_targets.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_list_empty_returns_empty_items() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let res = server.get("/upstreams?page=3&per_page=10").await;
|
||||||
|
res.assert_status_ok();
|
||||||
|
let body = res.json::<UpstreamListResponse>();
|
||||||
|
assert_eq!(body.items.len(), 0);
|
||||||
|
assert_eq!(body.pagination.current_page, 3u32);
|
||||||
|
assert_eq!(body.pagination.per_page, 10u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
routes::{AppState, api::restricted::nginx::upstream::info::response::UpstreamTargetInfo},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct GetUpstreamTargetsParams {
|
||||||
|
pub include_upstream: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConcreteGetUpstreamTargetsParams {
|
||||||
|
pub include_upstream: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetUpstreamTargetsParams> for ConcreteGetUpstreamTargetsParams {
|
||||||
|
fn from(params: GetUpstreamTargetsParams) -> Self {
|
||||||
|
Self {
|
||||||
|
include_upstream: params.include_upstream.unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_upstream_target(
|
||||||
|
Path(upstream_target_id): Path<Uuid>,
|
||||||
|
Query(params): Query<GetUpstreamTargetsParams>,
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
) -> AxumResult<Json<UpstreamTargetInfo>, ApiError> {
|
||||||
|
let concrete_params: ConcreteGetUpstreamTargetsParams = params.into();
|
||||||
|
let upstream_service = &_state.service.nginx.get_upstream_service();
|
||||||
|
let upstream_target_info = upstream_service
|
||||||
|
.get_upstream_target(
|
||||||
|
upstream_target_id,
|
||||||
|
if concrete_params.include_upstream {
|
||||||
|
Some(crate::services::nginx::upstream::GetUpstreamTargetOptions {
|
||||||
|
include_upstream: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(upstream_target_info.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
use crate::configs::{FromConfig, ProgramSettings};
|
||||||
|
|
||||||
|
use crate::routes::api::restricted::nginx::upstream::get_upstream_router;
|
||||||
|
use crate::services::get_app_service;
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
get_upstream_router(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_target_with_upstream_returns_upstream() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "with_targets".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "least_conn".to_string(),
|
||||||
|
sticky_session: true,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
let target_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: up_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
// query returns joined (upstream_target, upstream)
|
||||||
|
.append_query_results(vec![vec![(target_model.clone(), Some(up_model.clone()))]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let url = format!("/upstream_targets/{}?include_upstream=true", target_id);
|
||||||
|
let res = server.get(&url).await;
|
||||||
|
res.assert_status_ok();
|
||||||
|
let text = res.text();
|
||||||
|
let body: UpstreamTargetInfo = serde_json::from_str(&text).expect("failed to parse json");
|
||||||
|
assert_eq!(body.upstream_id, up_id);
|
||||||
|
assert!(body.upstream.is_some());
|
||||||
|
let upstream = body.upstream.expect("upstream to be present");
|
||||||
|
assert_eq!(upstream.id, up_id);
|
||||||
|
assert_eq!(upstream.name, "with_targets");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_target_without_upstream_returns_info() {
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let target_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
target_host: "10.0.0.1".to_string(),
|
||||||
|
target_port: 9090,
|
||||||
|
weight: 5,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let url = format!("/upstream_targets/{}", target_id);
|
||||||
|
let res = server.get(&url).await;
|
||||||
|
res.assert_status_ok();
|
||||||
|
let text = res.text();
|
||||||
|
let body: UpstreamTargetInfo = serde_json::from_str(&text).expect("failed to parse json");
|
||||||
|
assert_eq!(body.id, target_id);
|
||||||
|
assert!(body.upstream.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_get_upstream_target_not_found_returns_service_error() {
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let url = format!("/upstream_targets/{}?include_upstream=false", target_id);
|
||||||
|
let res = server.get(&url).await;
|
||||||
|
res.assert_status(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod response;
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::routes::api::helper::pagination::PaginationInfo;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpstreamTargetInfo {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub target_host: String,
|
||||||
|
pub target_port: i64,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub weight: i32,
|
||||||
|
//
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
//
|
||||||
|
pub upstream_id: Uuid,
|
||||||
|
pub upstream: Option<UpstreamBasicInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpstreamBasicInfo {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
//
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::services::nginx::info::upstream_target::UpstreamTargetInfo>
|
||||||
|
for UpstreamTargetInfo
|
||||||
|
{
|
||||||
|
fn from(info: crate::services::nginx::info::upstream_target::UpstreamTargetInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
id: info.id,
|
||||||
|
target_host: info.target_host,
|
||||||
|
target_port: info.target_port,
|
||||||
|
enabled: info.enabled,
|
||||||
|
is_backup: info.is_backup,
|
||||||
|
weight: info.weight as i32,
|
||||||
|
//
|
||||||
|
created_at: info.created_at,
|
||||||
|
updated_at: info.updated_at,
|
||||||
|
//
|
||||||
|
upstream_id: info.upstream_id,
|
||||||
|
upstream: info.upstream.map(|u| UpstreamBasicInfo {
|
||||||
|
id: u.id,
|
||||||
|
name: u.name,
|
||||||
|
protocol: u.protocol,
|
||||||
|
created_at: u.created_at,
|
||||||
|
updated_at: u.updated_at,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpstreamTargetBasicInfo {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub target_host: String,
|
||||||
|
pub target_port: i64,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub weight: i32,
|
||||||
|
//
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::services::nginx::info::upstream_target::UpstreamTargetInfo>
|
||||||
|
for UpstreamTargetBasicInfo
|
||||||
|
{
|
||||||
|
fn from(info: crate::services::nginx::info::upstream_target::UpstreamTargetInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
id: info.id,
|
||||||
|
target_host: info.target_host,
|
||||||
|
target_port: info.target_port,
|
||||||
|
enabled: info.enabled,
|
||||||
|
is_backup: info.is_backup,
|
||||||
|
weight: info.weight as i32,
|
||||||
|
//
|
||||||
|
created_at: info.created_at,
|
||||||
|
updated_at: info.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpstreamInfoResponse {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub sticky_session: bool,
|
||||||
|
pub created_by: Option<uuid::Uuid>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
//
|
||||||
|
pub upstream_targets: Vec<UpstreamTargetBasicInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::services::nginx::info::upstream::UpstreamInfo> for UpstreamInfoResponse {
|
||||||
|
fn from(info: crate::services::nginx::info::upstream::UpstreamInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
id: info.id,
|
||||||
|
name: info.name,
|
||||||
|
protocol: info.protocol,
|
||||||
|
algorithm: info.algorithm,
|
||||||
|
sticky_session: info.sticky_session,
|
||||||
|
created_by: info.created_by,
|
||||||
|
created_at: info.created_at,
|
||||||
|
updated_at: info.updated_at,
|
||||||
|
upstream_targets: info
|
||||||
|
.upstream_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.into())
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpstreamListResponse {
|
||||||
|
pub items: Vec<UpstreamInfoResponse>,
|
||||||
|
pub pagination: PaginationInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpstreamTargetInfoResponse {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub weight: i32,
|
||||||
|
//
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
//
|
||||||
|
pub upstream_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::services::nginx::info::upstream_target::UpstreamTargetInfo>
|
||||||
|
for UpstreamTargetInfoResponse
|
||||||
|
{
|
||||||
|
fn from(info: crate::services::nginx::info::upstream_target::UpstreamTargetInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
id: info.id,
|
||||||
|
host: info.target_host,
|
||||||
|
port: info.target_port,
|
||||||
|
enabled: info.enabled,
|
||||||
|
is_backup: info.is_backup,
|
||||||
|
weight: info.weight as i32,
|
||||||
|
//
|
||||||
|
created_at: info.created_at,
|
||||||
|
updated_at: info.updated_at,
|
||||||
|
//
|
||||||
|
upstream_id: info.upstream_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,59 @@
|
|||||||
|
pub mod agent_client;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod nginx;
|
||||||
pub mod server_state;
|
pub mod server_state;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ::agent_client::apis::configuration::Configuration;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
configs::ProgramSettings,
|
||||||
|
routes::{self, AuthState},
|
||||||
|
services::{
|
||||||
|
auth::{
|
||||||
|
authentication::{AuthenticationServiceImpl, strategies::password::PasswordStrategy},
|
||||||
|
user::{UserService, UserServiceImpl},
|
||||||
|
},
|
||||||
|
nginx::NginxService,
|
||||||
|
server_state::{ServerStateService, ServerStateStore},
|
||||||
|
settings::{SettingsService, SettingsStore},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type ServiceState<T> = Arc<T>;
|
||||||
|
|
||||||
|
pub struct AppService {
|
||||||
|
pub settings: ServiceState<dyn SettingsStore>,
|
||||||
|
pub auth_state: AuthState,
|
||||||
|
pub user: ServiceState<dyn UserService>,
|
||||||
|
pub server_state: ServiceState<dyn ServerStateStore>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub nginx: ServiceState<NginxService>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub agent_client: ServiceState<agent_client::AgentService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_app_service(
|
||||||
|
db_connection: &Arc<sea_orm::DatabaseConnection>,
|
||||||
|
settings: &ProgramSettings,
|
||||||
|
) -> AppService {
|
||||||
|
AppService {
|
||||||
|
server_state: Arc::new(ServerStateService::new(db_connection.clone())),
|
||||||
|
settings: Arc::new(SettingsService::new(db_connection.clone())),
|
||||||
|
auth_state: routes::AuthState {
|
||||||
|
strategy: routes::AuthStrategy {
|
||||||
|
password: Arc::new(PasswordStrategy::new(db_connection.clone())),
|
||||||
|
},
|
||||||
|
authentication: Arc::new(AuthenticationServiceImpl::new(
|
||||||
|
settings.auth.jwt_secret.clone(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
user: Arc::new(UserServiceImpl::new(db_connection.clone())),
|
||||||
|
nginx: Arc::new(NginxService::new(db_connection.clone())),
|
||||||
|
agent_client: Arc::new(agent_client::AgentService::new(Configuration::from(
|
||||||
|
settings.agent.clone(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
114
apps/api/src/services/agent_client.rs
Normal file
114
apps/api/src/services/agent_client.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agent_client::apis::{ApiClient, configuration::Configuration};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::configs::agent::AgentSettings;
|
||||||
|
|
||||||
|
pub struct AgentService {
|
||||||
|
client: Arc<ApiClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AgentSettings> for Configuration {
|
||||||
|
fn from(settings: AgentSettings) -> Self {
|
||||||
|
let mut config = Configuration::default();
|
||||||
|
let mut builder = reqwest::Client::builder();
|
||||||
|
|
||||||
|
let url = settings.socket_path;
|
||||||
|
if url.starts_with("unix://") {
|
||||||
|
builder = builder.unix_socket(url.to_string());
|
||||||
|
config.client = builder.build().expect("Failed to build reqwest client");
|
||||||
|
} else {
|
||||||
|
warn!("AgentSettings contains a non-unix socket path: {}", url);
|
||||||
|
config.base_path = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentService {
|
||||||
|
pub fn new(config: impl Into<Arc<Configuration>>) -> Self {
|
||||||
|
let client = ApiClient::new(config.into());
|
||||||
|
AgentService {
|
||||||
|
client: Arc::new(client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_client(&self) -> Arc<ApiClient> {
|
||||||
|
Arc::clone(&self.client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use agent_client::{
|
||||||
|
apis::{Api, nginx_agent_api::StatusSuccess},
|
||||||
|
models::StatusResp,
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, response::Json};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_agent_service_creation() {
|
||||||
|
let config = Configuration::default();
|
||||||
|
let service = AgentService::new(config);
|
||||||
|
let client = service.get_client();
|
||||||
|
assert!(Arc::ptr_eq(&client, &service.client));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_agent_socket_support() {
|
||||||
|
// create temporary socket path
|
||||||
|
let dir = tempdir().expect("Failed to create temp dir");
|
||||||
|
let socket_path = dir.path().join("agent.sock");
|
||||||
|
|
||||||
|
// create axum app with a simple /status route
|
||||||
|
let app = axum::Router::new().route(
|
||||||
|
"/status",
|
||||||
|
axum::routing::get(|| async {
|
||||||
|
let result: (StatusCode, StatusResp) = (StatusCode::OK, StatusResp { ok: true });
|
||||||
|
|
||||||
|
(result.0, Json(result.1))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// bind tokio unix listener and serve in background
|
||||||
|
let listener =
|
||||||
|
tokio::net::UnixListener::bind(&socket_path).expect("Failed to bind to unix socket");
|
||||||
|
let server_fut = axum::serve::serve(listener, app);
|
||||||
|
let _srv = tokio::spawn(async move {
|
||||||
|
let _ = server_fut.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
// give server a moment to start
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
let client: ApiClient = ApiClient::new(Arc::new(Configuration {
|
||||||
|
base_path: "http://localhost".to_string(),
|
||||||
|
client: reqwest::Client::builder()
|
||||||
|
.unix_socket(socket_path.clone())
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build reqwest client"),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.nginx_agent_api()
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get status");
|
||||||
|
let body = res.entity.expect("Response entity is missing");
|
||||||
|
|
||||||
|
assert!(res.status.is_success());
|
||||||
|
if let StatusSuccess::Status200(body) = body {
|
||||||
|
assert!(body.ok);
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected response body");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -197,14 +197,17 @@ mod tests {
|
|||||||
let (token, _) = service
|
let (token, _) = service
|
||||||
.generate_jwt(user_id, 60)
|
.generate_jwt(user_id, 60)
|
||||||
.await
|
.await
|
||||||
.expect("generate jwt");
|
.expect("Failed to generate jwt");
|
||||||
|
|
||||||
let valid = service
|
let valid = service
|
||||||
.is_valid_jwt(&token, None)
|
.is_valid_jwt(&token, None)
|
||||||
.await
|
.await
|
||||||
.expect("validate jwt");
|
.expect("Failed to validate jwt");
|
||||||
assert!(valid.is_some(), "Generated token should be valid");
|
assert!(valid.is_some(), "Generated token should be valid");
|
||||||
let claims = service.parse_jwt(&token).await.expect("parse jwt");
|
let claims = service
|
||||||
|
.parse_jwt(&token)
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse jwt");
|
||||||
assert_eq!(claims.sub, user_id.to_string());
|
assert_eq!(claims.sub, user_id.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,10 +216,16 @@ mod tests {
|
|||||||
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let (token, _) = service.generate_jwt(user_id, 60).await.unwrap();
|
let (token, _) = service
|
||||||
|
.generate_jwt(user_id, 60)
|
||||||
|
.await
|
||||||
|
.expect("Failed to generate jwt");
|
||||||
|
|
||||||
let other_sub = Uuid::new_v4().to_string();
|
let other_sub = Uuid::new_v4().to_string();
|
||||||
let valid = service.is_valid_jwt(&token, Some(other_sub)).await.unwrap();
|
let valid = service
|
||||||
|
.is_valid_jwt(&token, Some(other_sub))
|
||||||
|
.await
|
||||||
|
.expect("jwt is not valid");
|
||||||
assert!(
|
assert!(
|
||||||
valid.is_none(),
|
valid.is_none(),
|
||||||
"Token should be invalid for a different subject"
|
"Token should be invalid for a different subject"
|
||||||
@@ -236,10 +245,19 @@ mod tests {
|
|||||||
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let (token, _) = service.generate_jwt(user_id, 60).await.unwrap();
|
let (token, _) = service
|
||||||
let new_token = service.refresh_jwt(&token, 120).await.unwrap();
|
.generate_jwt(user_id, 60)
|
||||||
|
.await
|
||||||
|
.expect("Failed to generate jwt");
|
||||||
|
let new_token = service
|
||||||
|
.refresh_jwt(&token, 120)
|
||||||
|
.await
|
||||||
|
.expect("Failed to refresh jwt");
|
||||||
|
|
||||||
let claims = service.parse_jwt(&new_token).await.unwrap();
|
let claims = service
|
||||||
|
.parse_jwt(&new_token)
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse refreshed jwt");
|
||||||
assert_eq!(claims.sub, user_id.to_string());
|
assert_eq!(claims.sub, user_id.to_string());
|
||||||
assert_eq!(claims.exp - claims.iat, 120);
|
assert_eq!(claims.exp - claims.iat, 120);
|
||||||
}
|
}
|
||||||
@@ -249,10 +267,16 @@ mod tests {
|
|||||||
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let (token, claims) = service.generate_jwt(user_id, 1).await.unwrap();
|
let (token, claims) = service
|
||||||
|
.generate_jwt(user_id, 1)
|
||||||
|
.await
|
||||||
|
.expect("Failed to generate jwt");
|
||||||
sleep(Duration::from_secs(2)).await;
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
let valid = service.is_valid_jwt(&token, None).await.unwrap();
|
let valid = service
|
||||||
|
.is_valid_jwt(&token, None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to validate jwt");
|
||||||
assert!(
|
assert!(
|
||||||
valid.is_none(),
|
valid.is_none(),
|
||||||
"Token should be expired and thus invalid. Current time: {:?}. Diff: {}",
|
"Token should be expired and thus invalid. Current time: {:?}. Diff: {}",
|
||||||
@@ -266,9 +290,15 @@ mod tests {
|
|||||||
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
let service = AuthenticationServiceImpl::new(Some("secret".to_string()));
|
||||||
|
|
||||||
let user_id = Uuid::new_v4();
|
let user_id = Uuid::new_v4();
|
||||||
let (token, _) = service.generate_jwt(user_id, 1).await.unwrap();
|
let (token, _) = service
|
||||||
|
.generate_jwt(user_id, 1)
|
||||||
|
.await
|
||||||
|
.expect("Failed to generate jwt");
|
||||||
|
|
||||||
service.invalidate_jwt(&token).await.unwrap();
|
service
|
||||||
|
.invalidate_jwt(&token)
|
||||||
|
.await
|
||||||
|
.expect("Failed to invalidate jwt");
|
||||||
|
|
||||||
// ensure entry is present
|
// ensure entry is present
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ mod test {
|
|||||||
"CorrectPassword".as_bytes(),
|
"CorrectPassword".as_bytes(),
|
||||||
&SaltString::generate(&mut OsRng),
|
&SaltString::generate(&mut OsRng),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.expect("Failed to hash password")
|
||||||
.to_string();
|
.to_string();
|
||||||
let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![user::Model {
|
.append_query_results(vec![vec![user::Model {
|
||||||
@@ -281,7 +281,7 @@ mod test {
|
|||||||
"CorrectPassword".as_bytes(),
|
"CorrectPassword".as_bytes(),
|
||||||
&SaltString::generate(&mut OsRng),
|
&SaltString::generate(&mut OsRng),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.expect("Failed to hash password")
|
||||||
.to_string();
|
.to_string();
|
||||||
let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![user::Model {
|
.append_query_results(vec![vec![user::Model {
|
||||||
|
|||||||
31
apps/api/src/services/nginx.rs
Normal file
31
apps/api/src/services/nginx.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
pub mod builder;
|
||||||
|
pub mod info;
|
||||||
|
pub mod traits;
|
||||||
|
|
||||||
|
pub mod upstream;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
|
use upstream::UpstreamService;
|
||||||
|
|
||||||
|
pub struct NginxService {
|
||||||
|
connection: Arc<DatabaseConnection>,
|
||||||
|
//
|
||||||
|
upstream_service: Arc<UpstreamService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxService {
|
||||||
|
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||||
|
Self {
|
||||||
|
connection: connection.clone(),
|
||||||
|
//
|
||||||
|
upstream_service: Arc::new(UpstreamService::new(connection.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_upstream_service(&self) -> Arc<UpstreamService> {
|
||||||
|
self.upstream_service.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
47
apps/api/src/services/nginx/builder.rs
Normal file
47
apps/api/src/services/nginx/builder.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::services::nginx::info::upstream::UpstreamInfo;
|
||||||
|
|
||||||
|
pub const INDENT_SIZE: usize = 2;
|
||||||
|
|
||||||
|
pub trait NginxConfigProvider {
|
||||||
|
fn to_nginx_config(&self, indent: Option<usize>) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NginxConfigBuilder {
|
||||||
|
upstreams: Vec<UpstreamInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxConfigBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
upstreams: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_upstream(&mut self, upstream: UpstreamInfo) {
|
||||||
|
self.upstreams.push(upstream);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_upstreams(&mut self, upstreams: Vec<UpstreamInfo>) {
|
||||||
|
for upstream in upstreams {
|
||||||
|
self.add_upstream(upstream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxConfigProvider for NginxConfigBuilder {
|
||||||
|
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
||||||
|
let mut config = format!(
|
||||||
|
"# Nginx Config Generated by YANPM at {}",
|
||||||
|
chrono::Utc::now()
|
||||||
|
);
|
||||||
|
|
||||||
|
for upstream in &self.upstreams {
|
||||||
|
config.push('\n');
|
||||||
|
config.push_str(&upstream.to_nginx_config(indent));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add other sections like servers, locations, etc.
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
2
apps/api/src/services/nginx/info.rs
Normal file
2
apps/api/src/services/nginx/info.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod upstream;
|
||||||
|
pub mod upstream_target;
|
||||||
159
apps/api/src/services/nginx/info/upstream.rs
Normal file
159
apps/api/src/services/nginx/info/upstream.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use optfield::optfield;
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
services::nginx::{
|
||||||
|
builder::{INDENT_SIZE, NginxConfigProvider},
|
||||||
|
info::upstream_target as upstream_target_info,
|
||||||
|
traits::indentable::Indentable,
|
||||||
|
},
|
||||||
|
set_if_some,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[optfield(pub UpdateUpstreamInfo)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UpstreamInfo {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub sticky_session: bool,
|
||||||
|
pub created_by: Option<uuid::Uuid>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
//
|
||||||
|
pub upstream_targets: Vec<upstream_target_info::UpstreamTargetInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpstreamCreateInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub sticky_session: bool,
|
||||||
|
pub created_by: Option<uuid::Uuid>,
|
||||||
|
//
|
||||||
|
pub upstream_targets: Vec<upstream_target_info::UpstreamTargetCreateInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxConfigProvider for UpstreamInfo {
|
||||||
|
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
||||||
|
let targets_config: Vec<String> = self
|
||||||
|
.upstream_targets
|
||||||
|
.iter()
|
||||||
|
.map(|target| target.to_nginx_config(Some(indent.unwrap_or(0) + INDENT_SIZE)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"upstream {} {{\n{}\n}}",
|
||||||
|
self.name,
|
||||||
|
targets_config.join("\n".indent(indent.unwrap_or(0) + INDENT_SIZE).as_str())
|
||||||
|
)
|
||||||
|
.indent(indent.unwrap_or(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpstreamCreateInfo> for (upstream::ActiveModel, Vec<upstream_target::ActiveModel>) {
|
||||||
|
fn from(val: UpstreamCreateInfo) -> (upstream::ActiveModel, Vec<upstream_target::ActiveModel>) {
|
||||||
|
let upstream_uuid = Uuid::new_v4();
|
||||||
|
let upstream = upstream::ActiveModel {
|
||||||
|
id: sea_orm::ActiveValue::Set(upstream_uuid),
|
||||||
|
name: sea_orm::ActiveValue::Set(val.name),
|
||||||
|
protocol: sea_orm::ActiveValue::Set(val.protocol),
|
||||||
|
algorithm: sea_orm::ActiveValue::Set(val.algorithm),
|
||||||
|
sticky_session: sea_orm::ActiveValue::Set(val.sticky_session),
|
||||||
|
created_by: sea_orm::ActiveValue::Set(val.created_by),
|
||||||
|
created_at: sea_orm::ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
updated_at: sea_orm::ActiveValue::Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
let upstream_targets = val
|
||||||
|
.upstream_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|target| {
|
||||||
|
let mut active_model: upstream_target::ActiveModel = target.into();
|
||||||
|
active_model.upstream_id = sea_orm::ActiveValue::Set(upstream_uuid);
|
||||||
|
active_model
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(upstream, upstream_targets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<upstream::Model> for UpstreamInfo {
|
||||||
|
fn from(model: upstream::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name,
|
||||||
|
protocol: model.protocol,
|
||||||
|
algorithm: model.algorithm,
|
||||||
|
sticky_session: model.sticky_session,
|
||||||
|
created_by: model.created_by,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
upstream_targets: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(upstream::Model, Vec<upstream_target::Model>)> for UpstreamInfo {
|
||||||
|
fn from(data: (upstream::Model, Vec<upstream_target::Model>)) -> Self {
|
||||||
|
let (upstream_model, upstream_target_models) = data;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: upstream_model.id,
|
||||||
|
name: upstream_model.name,
|
||||||
|
protocol: upstream_model.protocol,
|
||||||
|
algorithm: upstream_model.algorithm,
|
||||||
|
sticky_session: upstream_model.sticky_session,
|
||||||
|
created_by: upstream_model.created_by,
|
||||||
|
created_at: upstream_model.created_at,
|
||||||
|
updated_at: upstream_model.updated_at,
|
||||||
|
upstream_targets: upstream_target_models
|
||||||
|
.into_iter()
|
||||||
|
.map(upstream_target_info::UpstreamTargetInfo::from)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpstreamInfo> for (upstream::ActiveModel, Vec<upstream_target::ActiveModel>) {
|
||||||
|
fn from(val: UpstreamInfo) -> Self {
|
||||||
|
(
|
||||||
|
upstream::ActiveModel {
|
||||||
|
id: sea_orm::ActiveValue::Set(val.id),
|
||||||
|
name: sea_orm::ActiveValue::Set(val.name),
|
||||||
|
protocol: sea_orm::ActiveValue::Set(val.protocol),
|
||||||
|
algorithm: sea_orm::ActiveValue::Set(val.algorithm),
|
||||||
|
sticky_session: sea_orm::ActiveValue::Set(val.sticky_session),
|
||||||
|
created_by: sea_orm::ActiveValue::Set(val.created_by),
|
||||||
|
created_at: sea_orm::ActiveValue::Set(val.created_at),
|
||||||
|
updated_at: sea_orm::ActiveValue::Set(val.updated_at),
|
||||||
|
},
|
||||||
|
val.upstream_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|target| target.into())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateUpstreamInfo {
|
||||||
|
pub fn apply_to_model(self, current_model: upstream::Model) -> upstream::ActiveModel {
|
||||||
|
upstream::ActiveModel {
|
||||||
|
id: sea_orm::ActiveValue::Unchanged(current_model.id),
|
||||||
|
name: set_if_some!(self.name),
|
||||||
|
protocol: set_if_some!(self.protocol),
|
||||||
|
algorithm: set_if_some!(self.algorithm),
|
||||||
|
sticky_session: set_if_some!(self.sticky_session),
|
||||||
|
created_by: set_if_some!(if self.created_by.is_some() {
|
||||||
|
Some(self.created_by)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
created_at: set_if_some!(self.created_at),
|
||||||
|
updated_at: set_if_some!(self.updated_at),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
apps/api/src/services/nginx/info/upstream_target.rs
Normal file
154
apps/api/src/services/nginx/info/upstream_target.rs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use optfield::optfield;
|
||||||
|
|
||||||
|
use sea_orm::ActiveValue::{Set, Unchanged};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
services::nginx::{builder::NginxConfigProvider, traits::indentable::Indentable},
|
||||||
|
set_if_some,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[optfield(pub UpdateUpstreamTargetInfo)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UpstreamTargetInfo {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub target_host: String,
|
||||||
|
pub target_port: i64,
|
||||||
|
pub weight: i64,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
//
|
||||||
|
pub upstream_id: uuid::Uuid,
|
||||||
|
pub upstream: Option<UpstreamBasicInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UpstreamBasicInfo {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
//
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpstreamTargetCreateInfo {
|
||||||
|
pub target_host: String,
|
||||||
|
pub target_port: i64,
|
||||||
|
pub weight: i64,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub enabled: bool,
|
||||||
|
//
|
||||||
|
pub upstream_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<upstream_target::Model> for UpstreamTargetInfo {
|
||||||
|
fn from(model: upstream_target::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: model.id,
|
||||||
|
target_host: model.target_host,
|
||||||
|
target_port: model.target_port,
|
||||||
|
weight: model.weight,
|
||||||
|
is_backup: model.is_backup,
|
||||||
|
enabled: model.enabled,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
upstream_id: model.upstream_id,
|
||||||
|
upstream: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(upstream_target::Model, upstream::Model)> for UpstreamTargetInfo {
|
||||||
|
fn from(data: (upstream_target::Model, upstream::Model)) -> Self {
|
||||||
|
let (target_model, up_model) = data;
|
||||||
|
Self {
|
||||||
|
id: target_model.id,
|
||||||
|
target_host: target_model.target_host,
|
||||||
|
target_port: target_model.target_port,
|
||||||
|
weight: target_model.weight,
|
||||||
|
is_backup: target_model.is_backup,
|
||||||
|
enabled: target_model.enabled,
|
||||||
|
created_at: target_model.created_at,
|
||||||
|
updated_at: target_model.updated_at,
|
||||||
|
upstream_id: target_model.upstream_id,
|
||||||
|
upstream: Some(UpstreamBasicInfo {
|
||||||
|
id: up_model.id,
|
||||||
|
name: up_model.name,
|
||||||
|
protocol: up_model.protocol,
|
||||||
|
created_at: up_model.created_at,
|
||||||
|
updated_at: up_model.updated_at,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpstreamTargetInfo> for upstream_target::ActiveModel {
|
||||||
|
fn from(val: UpstreamTargetInfo) -> Self {
|
||||||
|
upstream_target::ActiveModel {
|
||||||
|
id: Set(val.id),
|
||||||
|
target_host: Set(val.target_host),
|
||||||
|
target_port: Set(val.target_port),
|
||||||
|
weight: Set(val.weight),
|
||||||
|
is_backup: Set(val.is_backup),
|
||||||
|
enabled: Set(val.enabled),
|
||||||
|
created_at: Set(val.created_at),
|
||||||
|
updated_at: Set(val.updated_at),
|
||||||
|
upstream_id: Set(val.upstream_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpstreamTargetCreateInfo> for upstream_target::ActiveModel {
|
||||||
|
fn from(val: UpstreamTargetCreateInfo) -> Self {
|
||||||
|
upstream_target::ActiveModel {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
target_host: Set(val.target_host),
|
||||||
|
target_port: Set(val.target_port),
|
||||||
|
weight: Set(val.weight),
|
||||||
|
is_backup: Set(val.is_backup),
|
||||||
|
enabled: Set(val.enabled),
|
||||||
|
created_at: Set(chrono::Utc::now()),
|
||||||
|
updated_at: Set(chrono::Utc::now()),
|
||||||
|
upstream_id: Set(val.upstream_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxConfigProvider for UpstreamTargetInfo {
|
||||||
|
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
||||||
|
format!(
|
||||||
|
"{}:{} weight={}{}{}",
|
||||||
|
self.target_host,
|
||||||
|
self.target_port,
|
||||||
|
self.weight,
|
||||||
|
if self.is_backup { " backup" } else { "" },
|
||||||
|
if !self.enabled { " down" } else { "" },
|
||||||
|
)
|
||||||
|
.indent(indent.unwrap_or(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateUpstreamTargetInfo {
|
||||||
|
pub fn apply_to_model(
|
||||||
|
self,
|
||||||
|
current_model: upstream_target::Model,
|
||||||
|
) -> upstream_target::ActiveModel {
|
||||||
|
upstream_target::ActiveModel {
|
||||||
|
id: Unchanged(current_model.id),
|
||||||
|
target_host: set_if_some!(self.target_host),
|
||||||
|
target_port: set_if_some!(self.target_port),
|
||||||
|
weight: set_if_some!(self.weight),
|
||||||
|
is_backup: set_if_some!(self.is_backup),
|
||||||
|
enabled: set_if_some!(self.enabled),
|
||||||
|
created_at: set_if_some!(self.created_at),
|
||||||
|
updated_at: set_if_some!(self.updated_at),
|
||||||
|
upstream_id: set_if_some!(self.upstream_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/api/src/services/nginx/traits.rs
Normal file
1
apps/api/src/services/nginx/traits.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod indentable;
|
||||||
31
apps/api/src/services/nginx/traits/indentable.rs
Normal file
31
apps/api/src/services/nginx/traits/indentable.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
pub trait Indentable<T> {
|
||||||
|
fn indent(&self, spaces: T) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indentable<usize> for &str {
|
||||||
|
fn indent(&self, spaces: usize) -> String {
|
||||||
|
let indent_str = " ".repeat(spaces);
|
||||||
|
self.lines()
|
||||||
|
.map(|line| format!("{}{}", indent_str, line))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indentable<Option<usize>> for String {
|
||||||
|
fn indent(&self, spaces: Option<usize>) -> String {
|
||||||
|
self.as_str().indent(spaces.unwrap_or(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indentable<usize> for String {
|
||||||
|
fn indent(&self, spaces: usize) -> String {
|
||||||
|
self.as_str().indent(spaces)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indentable<Option<usize>> for &str {
|
||||||
|
fn indent(&self, spaces: Option<usize>) -> String {
|
||||||
|
self.indent(spaces.unwrap_or(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
697
apps/api/src/services/nginx/upstream.rs
Normal file
697
apps/api/src/services/nginx/upstream.rs
Normal file
@@ -0,0 +1,697 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
|
||||||
|
ModelTrait, QueryFilter, QuerySelect, TransactionTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
|
helpers::database::PaginationFilter,
|
||||||
|
services::nginx::info::{
|
||||||
|
upstream::{UpdateUpstreamInfo, UpstreamCreateInfo, UpstreamInfo},
|
||||||
|
upstream_target::{UpdateUpstreamTargetInfo, UpstreamTargetCreateInfo, UpstreamTargetInfo},
|
||||||
|
},
|
||||||
|
with_conn,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct UpstreamService {
|
||||||
|
connection: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct GetUpstreamOptions {
|
||||||
|
pub include_targets: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct GetUpstreamTargetOptions {
|
||||||
|
pub include_upstream: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpstreamService {
|
||||||
|
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||||
|
Self { connection }
|
||||||
|
}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
pub async fn create_upstream(
|
||||||
|
&self,
|
||||||
|
create_info: UpstreamCreateInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamInfo, ServiceError> {
|
||||||
|
let (upstream_model, upstream_target_models): (
|
||||||
|
upstream::ActiveModel,
|
||||||
|
Vec<upstream_target::ActiveModel>,
|
||||||
|
) = create_info.into();
|
||||||
|
|
||||||
|
// If a transaction was provided use it, otherwise create and own one here.
|
||||||
|
let mut maybe_owned_tx: Option<DatabaseTransaction> = None;
|
||||||
|
let tx_ref: Option<&mut DatabaseTransaction> = if let Some(tx) = tx {
|
||||||
|
Some(tx)
|
||||||
|
} else {
|
||||||
|
maybe_owned_tx = Some(self.connection.begin().await?);
|
||||||
|
maybe_owned_tx.as_mut()
|
||||||
|
};
|
||||||
|
|
||||||
|
let r = with_conn!(&*self.connection, tx_ref, conn, {
|
||||||
|
let created_upstream = upstream_model.insert(*conn).await?;
|
||||||
|
let created_targets = upstream_target::Entity::insert_many(
|
||||||
|
upstream_target_models
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut model| {
|
||||||
|
model.upstream_id = sea_orm::ActiveValue::Set(created_upstream.id);
|
||||||
|
model
|
||||||
|
})
|
||||||
|
.collect::<Vec<upstream_target::ActiveModel>>(),
|
||||||
|
)
|
||||||
|
.exec_with_returning(*conn)
|
||||||
|
.await?;
|
||||||
|
(created_upstream, created_targets)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Commit only if we created the transaction here (we own it).
|
||||||
|
if let Some(t) = maybe_owned_tx.take() {
|
||||||
|
t.commit().await?;
|
||||||
|
}
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_upstream(
|
||||||
|
&self,
|
||||||
|
upstream_id: uuid::Uuid,
|
||||||
|
options: Option<GetUpstreamOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamInfo, ServiceError> {
|
||||||
|
let concrete_options = options.unwrap_or_default();
|
||||||
|
let info: UpstreamInfo = if concrete_options.include_targets {
|
||||||
|
let (up_model, targets) = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
let up = upstream::Entity::find_by_id(upstream_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream with id {} not found",
|
||||||
|
upstream_id
|
||||||
|
)))?;
|
||||||
|
let targets = upstream_target::Entity::find()
|
||||||
|
.filter(upstream_target::Column::UpstreamId.eq(upstream_id))
|
||||||
|
.all(*conn)
|
||||||
|
.await?;
|
||||||
|
(up, targets)
|
||||||
|
});
|
||||||
|
(up_model, targets).into()
|
||||||
|
} else {
|
||||||
|
with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream::Entity::find_by_id(upstream_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream with id {} not found",
|
||||||
|
upstream_id
|
||||||
|
)))?
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_upstreams(
|
||||||
|
&self,
|
||||||
|
pagination: Option<PaginationFilter>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<UpstreamInfo>, ServiceError> {
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
let find_query = upstream::Entity::find();
|
||||||
|
let find_query = if let Some(pagination) = pagination {
|
||||||
|
let (offset, limit) = pagination.get_offset_limit();
|
||||||
|
find_query.offset(offset).limit(limit)
|
||||||
|
} else {
|
||||||
|
find_query
|
||||||
|
};
|
||||||
|
find_query.all(*conn).await?
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(r.into_iter().map(|m| m.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_upstream(
|
||||||
|
&self,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
upstream: UpdateUpstreamInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamInfo, ServiceError> {
|
||||||
|
let current_model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream::Entity::find_by_id(id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream with id {} not found",
|
||||||
|
id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
let active_model = upstream.apply_to_model(current_model);
|
||||||
|
|
||||||
|
let r = active_model.update(&*self.connection).await?;
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_upstream(
|
||||||
|
&self,
|
||||||
|
upstream_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
let model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream::Entity::find_by_id(upstream_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream with id {} not found",
|
||||||
|
upstream_id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
with_conn!(&*self.connection, tx, conn, {
|
||||||
|
model.delete(*conn).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
pub async fn create_upstream_target(
|
||||||
|
&self,
|
||||||
|
create_info: UpstreamTargetCreateInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamTargetInfo, ServiceError> {
|
||||||
|
let model: upstream_target::ActiveModel = create_info.into();
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, { model.insert(*conn).await? });
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_upstream_target(
|
||||||
|
&self,
|
||||||
|
target_id: uuid::Uuid,
|
||||||
|
options: Option<GetUpstreamTargetOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamTargetInfo, ServiceError> {
|
||||||
|
let concrete_options = options.unwrap_or_default();
|
||||||
|
let info: UpstreamTargetInfo = if concrete_options.include_upstream {
|
||||||
|
match with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream_target::Entity::find_by_id(target_id)
|
||||||
|
.find_also_related(upstream::Entity)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
}) {
|
||||||
|
Some((target_model, Some(upstream_model))) => (target_model, upstream_model).into(),
|
||||||
|
Some((_target_model, None)) => {
|
||||||
|
return Err(ServiceError::InternalError(format!(
|
||||||
|
"Inconsistent data: Upstream target with id {} has no associated upstream",
|
||||||
|
target_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(ServiceError::NotFound(format!(
|
||||||
|
"Upstream target with id {} not found",
|
||||||
|
target_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream_target::Entity::find_by_id(target_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream target with id {} not found",
|
||||||
|
target_id
|
||||||
|
)))?
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_upstream_targets_by_upstream(
|
||||||
|
&self,
|
||||||
|
upstream_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<UpstreamTargetInfo>, ServiceError> {
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream_target::Entity::find()
|
||||||
|
.filter(upstream_target::Column::UpstreamId.eq(upstream_id))
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
});
|
||||||
|
Ok(r.into_iter().map(|m| m.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_upstream_target(
|
||||||
|
&self,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
target: UpdateUpstreamTargetInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamTargetInfo, ServiceError> {
|
||||||
|
let current_model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream_target::Entity::find_by_id(id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream target with id {} not found",
|
||||||
|
id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
let active_model = target.apply_to_model(current_model);
|
||||||
|
|
||||||
|
let r = active_model.update(&*self.connection).await?;
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_upstream_target(
|
||||||
|
&self,
|
||||||
|
target_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
let model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream_target::Entity::find_by_id(target_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream target with id {} not found",
|
||||||
|
target_id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
with_conn!(&*self.connection, tx, conn, {
|
||||||
|
model.delete(*conn).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::MockExecResult;
|
||||||
|
use sea_orm::{DatabaseBackend, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_upstream_returns_info() {
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "test_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "round_robin".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let create_info = crate::services::nginx::info::upstream::UpstreamCreateInfo {
|
||||||
|
name: "test_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "round_robin".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
upstream_targets: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = svc.create_upstream(create_info, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let info = res.expect("Failed to create upstream");
|
||||||
|
assert_eq!(info.name, "test_upstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_upstream_with_targets_returns_targets() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "with_targets".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "least_conn".to_string(),
|
||||||
|
sticky_session: true,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_model = upstream_target::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
upstream_id: up_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
// find_by_id -> returns upstream model
|
||||||
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
|
// find targets -> returns the target(s)
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc
|
||||||
|
.get_upstream(
|
||||||
|
up_id,
|
||||||
|
Some(GetUpstreamOptions {
|
||||||
|
include_targets: true,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let info = res.expect("Failed to get upstream with targets");
|
||||||
|
assert_eq!(info.id, up_id);
|
||||||
|
assert_eq!(info.upstream_targets.len(), 1);
|
||||||
|
assert_eq!(info.upstream_targets[0].target_host, "127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_upstream_not_found_returns_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.get_upstream(uuid::Uuid::new_v4(), None, None).await;
|
||||||
|
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_upstreams_returns_list() {
|
||||||
|
let u1 = upstream::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "u1".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
let u2 = upstream::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: "u2".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![u1.clone(), u2.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.get_upstreams(None, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let list = res.expect("Failed to get upstreams");
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_upstream_targets_by_upstream_returns_targets() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let t = upstream_target::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
upstream_id: up_id,
|
||||||
|
target_host: "10.0.0.1".to_string(),
|
||||||
|
target_port: 80,
|
||||||
|
weight: 10,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![t.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.get_upstream_targets_by_upstream(up_id, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let targets = res.expect("Failed to get upstream targets");
|
||||||
|
assert_eq!(targets.len(), 1);
|
||||||
|
assert_eq!(targets[0].target_host, "10.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_upstream_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let existing = upstream::Model {
|
||||||
|
id,
|
||||||
|
name: "old".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
let updated = upstream::Model {
|
||||||
|
id,
|
||||||
|
name: "new".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![existing.clone()]]) // find_by_id
|
||||||
|
.append_query_results(vec![vec![updated.clone()]]) // update result
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let update_info = crate::services::nginx::info::upstream::UpdateUpstreamInfo {
|
||||||
|
id: None,
|
||||||
|
name: None,
|
||||||
|
protocol: None,
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
created_by: None,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
upstream_targets: None,
|
||||||
|
};
|
||||||
|
let res = svc.update_upstream(id, update_info, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let got = res.expect("Failed to update upstream");
|
||||||
|
assert_eq!(got.name, "new");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_upstream_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc
|
||||||
|
.update_upstream(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
crate::services::nginx::info::upstream::UpdateUpstreamInfo {
|
||||||
|
id: None,
|
||||||
|
name: None,
|
||||||
|
protocol: None,
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
created_by: None,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
upstream_targets: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_upstream_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let existing = upstream::Model {
|
||||||
|
id,
|
||||||
|
name: "todelete".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "rr".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: None,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![existing.clone()]])
|
||||||
|
.append_exec_results(vec![MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
}])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.delete_upstream(id, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_upstream_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.delete_upstream(uuid::Uuid::new_v4(), None).await;
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_upstream_target_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let upstream_id = uuid::Uuid::new_v4();
|
||||||
|
let created = upstream_target::Model {
|
||||||
|
id,
|
||||||
|
upstream_id,
|
||||||
|
target_host: "1.2.3.4".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 5,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![created.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let create_info = crate::services::nginx::info::upstream_target::UpstreamTargetCreateInfo {
|
||||||
|
target_host: "1.2.3.4".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 5,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
upstream_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = svc.create_upstream_target(create_info, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let t = res.expect("Failed to create target");
|
||||||
|
assert_eq!(t.target_host, "1.2.3.4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_upstream_target_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let existing = upstream_target::Model {
|
||||||
|
id,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
target_host: "old".to_string(),
|
||||||
|
target_port: 80,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
let updated = upstream_target::Model {
|
||||||
|
id,
|
||||||
|
upstream_id: existing.upstream_id,
|
||||||
|
target_host: "new".to_string(),
|
||||||
|
target_port: 80,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![existing.clone()]])
|
||||||
|
.append_query_results(vec![vec![updated.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
|
||||||
|
let update_info = crate::services::nginx::info::upstream_target::UpdateUpstreamTargetInfo {
|
||||||
|
id: None,
|
||||||
|
target_host: None,
|
||||||
|
target_port: None,
|
||||||
|
weight: None,
|
||||||
|
is_backup: None,
|
||||||
|
enabled: None,
|
||||||
|
created_at: None,
|
||||||
|
updated_at: None,
|
||||||
|
upstream_id: None,
|
||||||
|
upstream: None,
|
||||||
|
};
|
||||||
|
let res = svc.update_upstream_target(id, update_info, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let got = res.expect("Failed to update target");
|
||||||
|
assert_eq!(got.target_host, "new");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_upstream_target_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let existing = upstream_target::Model {
|
||||||
|
id,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
target_host: "del".to_string(),
|
||||||
|
target_port: 80,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![existing.clone()]])
|
||||||
|
.append_exec_results(vec![MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
}])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = UpstreamService::new(Arc::new(db));
|
||||||
|
let res = svc.delete_upstream_target(id, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
24
justfile
24
justfile
@@ -50,9 +50,31 @@ generate-openapi:
|
|||||||
# Generate API client for frontend
|
# Generate API client for frontend
|
||||||
cd apps/frontend && \
|
cd apps/frontend && \
|
||||||
pnpm generate:openapi
|
pnpm generate:openapi
|
||||||
|
# Generate API client for agent
|
||||||
|
cd apps/agent && \
|
||||||
|
cargo run -- generate-openapi --output swagger.json
|
||||||
|
|
||||||
|
generate-agent-client:
|
||||||
|
# Generate API client for agent
|
||||||
|
pnpm openapi-generator-cli generate \
|
||||||
|
-g rust --skip-validate-spec \
|
||||||
|
-o ./public/agent-client -i ./apps/agent/swagger.json \
|
||||||
|
--additional-properties=library=reqwest-trait \
|
||||||
|
--additional-properties=mockall=true \
|
||||||
|
--additional-properties=packageName=agent_client \
|
||||||
|
--additional-properties=packageVersion=0.1.0 \
|
||||||
|
--additional-properties=supportAsync=true \
|
||||||
|
--additional-properties=supportMultipleResponses=true \
|
||||||
|
--additional-properties=topLevelApiClient=true \
|
||||||
|
--additional-properties=useSingleRequestParameter=true
|
||||||
|
# format generated code
|
||||||
|
cd public/agent-client && \
|
||||||
|
cargo fmt
|
||||||
|
# append lint allows/forbids to the end of Cargo.toml to disable warnings in generated code and forbid unsafe code
|
||||||
|
cd public/agent-client && \
|
||||||
|
echo '\n[lints.clippy]\nall = "allow"\n[lints.rust]\nunsafe_code = "forbid"\n' >> Cargo.toml
|
||||||
|
|
||||||
generate-all: generate-entity generate-openapi
|
generate-all: generate-entity generate-openapi generate-agent-client
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
# build frontend assets
|
# build frontend assets
|
||||||
|
|||||||
7
openapitools.json
Normal file
7
openapitools.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||||
|
"spaces": 2,
|
||||||
|
"generator-cli": {
|
||||||
|
"version": "7.18.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@openapitools/openapi-generator-cli": "^2.26.0",
|
||||||
|
"rust-just": "^1.44.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3418
apps/frontend/pnpm-lock.yaml → pnpm-lock.yaml
generated
3418
apps/frontend/pnpm-lock.yaml → pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
pnpm-workspace.yaml
Normal file
7
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
packages:
|
||||||
|
- apps/frontend
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@nestjs/core'
|
||||||
|
- '@openapitools/openapi-generator-cli'
|
||||||
|
- esbuild
|
||||||
3
public/agent-client/.gitignore
vendored
Normal file
3
public/agent-client/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
23
public/agent-client/.openapi-generator-ignore
Normal file
23
public/agent-client/.openapi-generator-ignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# OpenAPI Generator Ignore
|
||||||
|
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||||
|
|
||||||
|
# Use this file to prevent files from being overwritten by the generator.
|
||||||
|
# The patterns follow closely to .gitignore or .dockerignore.
|
||||||
|
|
||||||
|
# As an example, the C# client generator defines ApiClient.cs.
|
||||||
|
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||||
|
#ApiClient.cs
|
||||||
|
|
||||||
|
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||||
|
#foo/*/qux
|
||||||
|
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||||
|
#foo/**/qux
|
||||||
|
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can also negate patterns with an exclamation (!).
|
||||||
|
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||||
|
#docs/*.md
|
||||||
|
# Then explicitly reverse the ignore rule for a single file:
|
||||||
|
#!docs/README.md
|
||||||
21
public/agent-client/.openapi-generator/FILES
Normal file
21
public/agent-client/.openapi-generator/FILES
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.gitignore
|
||||||
|
.travis.yml
|
||||||
|
Cargo.toml
|
||||||
|
README.md
|
||||||
|
docs/NginxAgentApi.md
|
||||||
|
docs/StatusResp.md
|
||||||
|
docs/ValidateAndReloadBody.md
|
||||||
|
docs/ValidateAndReloadResp.md
|
||||||
|
docs/ValidateBody.md
|
||||||
|
docs/WriteConfigBody.md
|
||||||
|
git_push.sh
|
||||||
|
src/apis/configuration.rs
|
||||||
|
src/apis/mod.rs
|
||||||
|
src/apis/nginx_agent_api.rs
|
||||||
|
src/lib.rs
|
||||||
|
src/models/mod.rs
|
||||||
|
src/models/status_resp.rs
|
||||||
|
src/models/validate_and_reload_body.rs
|
||||||
|
src/models/validate_and_reload_resp.rs
|
||||||
|
src/models/validate_body.rs
|
||||||
|
src/models/write_config_body.rs
|
||||||
1
public/agent-client/.openapi-generator/VERSION
Normal file
1
public/agent-client/.openapi-generator/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
7.18.0
|
||||||
1
public/agent-client/.travis.yml
Normal file
1
public/agent-client/.travis.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
language: rust
|
||||||
27
public/agent-client/Cargo.toml
Normal file
27
public/agent-client/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "agent_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["OpenAPI Generator team and contributors"]
|
||||||
|
description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)"
|
||||||
|
license = ""
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
|
serde_json = "^1.0"
|
||||||
|
serde_repr = "^0.1"
|
||||||
|
url = "^2.5"
|
||||||
|
async-trait = "^0.1"
|
||||||
|
reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "stream"] }
|
||||||
|
mockall = { version = "^0.13", optional = true}
|
||||||
|
[features]
|
||||||
|
default = ["native-tls"]
|
||||||
|
native-tls = ["reqwest/native-tls"]
|
||||||
|
rustls-tls = ["reqwest/rustls-tls"]
|
||||||
|
mockall = ["dep:mockall"]
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
all = "allow"
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
53
public/agent-client/README.md
Normal file
53
public/agent-client/README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Rust API client for agent_client
|
||||||
|
|
||||||
|
No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client.
|
||||||
|
|
||||||
|
- API version: 0.1.0
|
||||||
|
- Package version: 0.1.0
|
||||||
|
- Generator version: 7.18.0
|
||||||
|
- Build package: `org.openapitools.codegen.languages.RustClientCodegen`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Put the package under your project folder in a directory named `agent_client` and add the following to `Cargo.toml` under `[dependencies]`:
|
||||||
|
|
||||||
|
```
|
||||||
|
agent_client = { path = "./agent_client" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation for API Endpoints
|
||||||
|
|
||||||
|
All URIs are relative to *http://localhost*
|
||||||
|
|
||||||
|
Class | Method | HTTP request | Description
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
*NginxAgentApi* | [**status**](docs/NginxAgentApi.md#status) | **GET** /status | Health check endpoint
|
||||||
|
*NginxAgentApi* | [**validate**](docs/NginxAgentApi.md#validate) | **POST** /validate |
|
||||||
|
*NginxAgentApi* | [**validate_and_reload**](docs/NginxAgentApi.md#validate_and_reload) | **POST** /validate_and_reload |
|
||||||
|
*NginxAgentApi* | [**write_config**](docs/NginxAgentApi.md#write_config) | **POST** /write_config |
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation For Models
|
||||||
|
|
||||||
|
- [StatusResp](docs/StatusResp.md)
|
||||||
|
- [ValidateAndReloadBody](docs/ValidateAndReloadBody.md)
|
||||||
|
- [ValidateAndReloadResp](docs/ValidateAndReloadResp.md)
|
||||||
|
- [ValidateBody](docs/ValidateBody.md)
|
||||||
|
- [WriteConfigBody](docs/WriteConfigBody.md)
|
||||||
|
|
||||||
|
|
||||||
|
To get access to the crate's generated documentation, use:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo doc --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
121
public/agent-client/docs/NginxAgentApi.md
Normal file
121
public/agent-client/docs/NginxAgentApi.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# \NginxAgentApi
|
||||||
|
|
||||||
|
All URIs are relative to *http://localhost*
|
||||||
|
|
||||||
|
Method | HTTP request | Description
|
||||||
|
------------- | ------------- | -------------
|
||||||
|
[**status**](NginxAgentApi.md#status) | **GET** /status | Health check endpoint
|
||||||
|
[**validate**](NginxAgentApi.md#validate) | **POST** /validate |
|
||||||
|
[**validate_and_reload**](NginxAgentApi.md#validate_and_reload) | **POST** /validate_and_reload |
|
||||||
|
[**write_config**](NginxAgentApi.md#write_config) | **POST** /write_config |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## status
|
||||||
|
|
||||||
|
> models::StatusResp status()
|
||||||
|
Health check endpoint
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**models::StatusResp**](StatusResp.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
|
## validate
|
||||||
|
|
||||||
|
> serde_json::Value validate(validate_body)
|
||||||
|
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
|
||||||
|
Name | Type | Description | Required | Notes
|
||||||
|
------------- | ------------- | ------------- | ------------- | -------------
|
||||||
|
**validate_body** | [**ValidateBody**](ValidateBody.md) | | [required] |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**serde_json::Value**](serde_json::Value.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
|
## validate_and_reload
|
||||||
|
|
||||||
|
> models::ValidateAndReloadResp validate_and_reload(validate_and_reload_body)
|
||||||
|
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
|
||||||
|
Name | Type | Description | Required | Notes
|
||||||
|
------------- | ------------- | ------------- | ------------- | -------------
|
||||||
|
**validate_and_reload_body** | [**ValidateAndReloadBody**](ValidateAndReloadBody.md) | | [required] |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**models::ValidateAndReloadResp**](ValidateAndReloadResp.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
|
## write_config
|
||||||
|
|
||||||
|
> write_config(write_config_body)
|
||||||
|
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
|
||||||
|
Name | Type | Description | Required | Notes
|
||||||
|
------------- | ------------- | ------------- | ------------- | -------------
|
||||||
|
**write_config_body** | [**WriteConfigBody**](WriteConfigBody.md) | | [required] |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
(empty response body)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
11
public/agent-client/docs/StatusResp.md
Normal file
11
public/agent-client/docs/StatusResp.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# StatusResp
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**ok** | **bool** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
121
public/agent-client/docs/TagNginxtagApi.md
Normal file
121
public/agent-client/docs/TagNginxtagApi.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# \TagNginxtagApi
|
||||||
|
|
||||||
|
All URIs are relative to *http://localhost*
|
||||||
|
|
||||||
|
Method | HTTP request | Description
|
||||||
|
------------- | ------------- | -------------
|
||||||
|
[**status**](TagNginxtagApi.md#status) | **GET** /status | Health check endpoint
|
||||||
|
[**validate**](TagNginxtagApi.md#validate) | **POST** /validate |
|
||||||
|
[**validate_and_reload**](TagNginxtagApi.md#validate_and_reload) | **POST** /validate_and_reload |
|
||||||
|
[**write_config**](TagNginxtagApi.md#write_config) | **POST** /write_config |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## status
|
||||||
|
|
||||||
|
> models::StatusResp status()
|
||||||
|
Health check endpoint
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**models::StatusResp**](StatusResp.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
|
## validate
|
||||||
|
|
||||||
|
> serde_json::Value validate(validate_body)
|
||||||
|
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
|
||||||
|
Name | Type | Description | Required | Notes
|
||||||
|
------------- | ------------- | ------------- | ------------- | -------------
|
||||||
|
**validate_body** | [**ValidateBody**](ValidateBody.md) | | [required] |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**serde_json::Value**](serde_json::Value.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
|
## validate_and_reload
|
||||||
|
|
||||||
|
> models::ValidateAndReloadResp validate_and_reload(validate_and_reload_body)
|
||||||
|
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
|
||||||
|
Name | Type | Description | Required | Notes
|
||||||
|
------------- | ------------- | ------------- | ------------- | -------------
|
||||||
|
**validate_and_reload_body** | [**ValidateAndReloadBody**](ValidateAndReloadBody.md) | | [required] |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**models::ValidateAndReloadResp**](ValidateAndReloadResp.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
|
## write_config
|
||||||
|
|
||||||
|
> write_config(write_config_body)
|
||||||
|
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
|
||||||
|
Name | Type | Description | Required | Notes
|
||||||
|
------------- | ------------- | ------------- | ------------- | -------------
|
||||||
|
**write_config_body** | [**WriteConfigBody**](WriteConfigBody.md) | | [required] |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
(empty response body)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: application/json
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
12
public/agent-client/docs/ValidateAndReloadBody.md
Normal file
12
public/agent-client/docs/ValidateAndReloadBody.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ValidateAndReloadBody
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**config_name** | **String** | |
|
||||||
|
**timestamp** | **i64** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
12
public/agent-client/docs/ValidateAndReloadResp.md
Normal file
12
public/agent-client/docs/ValidateAndReloadResp.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ValidateAndReloadResp
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**rc** | **i32** | |
|
||||||
|
**ro** | **String** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
12
public/agent-client/docs/ValidateBody.md
Normal file
12
public/agent-client/docs/ValidateBody.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# ValidateBody
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**config_name** | **String** | |
|
||||||
|
**timestamp** | **i64** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
13
public/agent-client/docs/WriteConfigBody.md
Normal file
13
public/agent-client/docs/WriteConfigBody.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# WriteConfigBody
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**config_name** | **String** | |
|
||||||
|
**content** | **String** | |
|
||||||
|
**timestamp** | **i64** | |
|
||||||
|
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
57
public/agent-client/git_push.sh
Normal file
57
public/agent-client/git_push.sh
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||||
|
#
|
||||||
|
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
|
||||||
|
|
||||||
|
git_user_id=$1
|
||||||
|
git_repo_id=$2
|
||||||
|
release_note=$3
|
||||||
|
git_host=$4
|
||||||
|
|
||||||
|
if [ "$git_host" = "" ]; then
|
||||||
|
git_host="github.com"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_user_id" = "" ]; then
|
||||||
|
git_user_id="GIT_USER_ID"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_repo_id" = "" ]; then
|
||||||
|
git_repo_id="GIT_REPO_ID"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$release_note" = "" ]; then
|
||||||
|
release_note="Minor update"
|
||||||
|
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize the local directory as a Git repository
|
||||||
|
git init
|
||||||
|
|
||||||
|
# Adds the files in the local repository and stages them for commit.
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||||
|
git commit -m "$release_note"
|
||||||
|
|
||||||
|
# Sets the new remote
|
||||||
|
git_remote=$(git remote)
|
||||||
|
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||||
|
|
||||||
|
if [ "$GIT_TOKEN" = "" ]; then
|
||||||
|
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||||
|
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
|
||||||
|
else
|
||||||
|
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||||
|
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
|
||||||
|
git push origin master 2>&1 | grep -v 'To https'
|
||||||
48
public/agent-client/src/apis/configuration.rs
Normal file
48
public/agent-client/src/apis/configuration.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Configuration {
|
||||||
|
pub base_path: String,
|
||||||
|
pub user_agent: Option<String>,
|
||||||
|
pub client: reqwest::Client,
|
||||||
|
pub basic_auth: Option<BasicAuth>,
|
||||||
|
pub oauth_access_token: Option<String>,
|
||||||
|
pub bearer_access_token: Option<String>,
|
||||||
|
pub api_key: Option<ApiKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type BasicAuth = (String, Option<String>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ApiKey {
|
||||||
|
pub prefix: Option<String>,
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
pub fn new() -> Configuration {
|
||||||
|
Configuration::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Configuration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Configuration {
|
||||||
|
base_path: "http://localhost".to_owned(),
|
||||||
|
user_agent: Some("OpenAPI-Generator/0.1.0/rust".to_owned()),
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
basic_auth: None,
|
||||||
|
oauth_access_token: None,
|
||||||
|
bearer_access_token: None,
|
||||||
|
api_key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
165
public/agent-client/src/apis/mod.rs
Normal file
165
public/agent-client/src/apis/mod.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ResponseContent<T> {
|
||||||
|
pub status: reqwest::StatusCode,
|
||||||
|
pub content: String,
|
||||||
|
pub entity: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error<T> {
|
||||||
|
Reqwest(reqwest::Error),
|
||||||
|
Serde(serde_json::Error),
|
||||||
|
Io(std::io::Error),
|
||||||
|
ResponseError(ResponseContent<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Display for Error<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let (module, e) = match self {
|
||||||
|
Error::Reqwest(e) => ("reqwest", e.to_string()),
|
||||||
|
Error::Serde(e) => ("serde", e.to_string()),
|
||||||
|
Error::Io(e) => ("IO", e.to_string()),
|
||||||
|
Error::ResponseError(e) => ("response", format!("status code {}", e.status)),
|
||||||
|
};
|
||||||
|
write!(f, "error in {}: {}", module, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: fmt::Debug> error::Error for Error<T> {
|
||||||
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
|
Some(match self {
|
||||||
|
Error::Reqwest(e) => e,
|
||||||
|
Error::Serde(e) => e,
|
||||||
|
Error::Io(e) => e,
|
||||||
|
Error::ResponseError(_) => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<reqwest::Error> for Error<T> {
|
||||||
|
fn from(e: reqwest::Error) -> Self {
|
||||||
|
Error::Reqwest(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<serde_json::Error> for Error<T> {
|
||||||
|
fn from(e: serde_json::Error) -> Self {
|
||||||
|
Error::Serde(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<std::io::Error> for Error<T> {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
Error::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn urlencode<T: AsRef<str>>(s: T) -> String {
|
||||||
|
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> {
|
||||||
|
if let serde_json::Value::Object(object) = value {
|
||||||
|
let mut params = vec![];
|
||||||
|
|
||||||
|
for (key, value) in object {
|
||||||
|
match value {
|
||||||
|
serde_json::Value::Object(_) => params.append(&mut parse_deep_object(
|
||||||
|
&format!("{}[{}]", prefix, key),
|
||||||
|
value,
|
||||||
|
)),
|
||||||
|
serde_json::Value::Array(array) => {
|
||||||
|
for (i, value) in array.iter().enumerate() {
|
||||||
|
params.append(&mut parse_deep_object(
|
||||||
|
&format!("{}[{}][{}]", prefix, key, i),
|
||||||
|
value,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::Value::String(s) => {
|
||||||
|
params.push((format!("{}[{}]", prefix, key), s.clone()))
|
||||||
|
}
|
||||||
|
_ => params.push((format!("{}[{}]", prefix, key), value.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
unimplemented!("Only objects are supported with style=deepObject")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal use only
|
||||||
|
/// A content type supported by this client.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum ContentType {
|
||||||
|
Json,
|
||||||
|
Text,
|
||||||
|
Unsupported(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for ContentType {
|
||||||
|
fn from(content_type: &str) -> Self {
|
||||||
|
if content_type.starts_with("application") && content_type.contains("json") {
|
||||||
|
return Self::Json;
|
||||||
|
} else if content_type.starts_with("text/plain") {
|
||||||
|
return Self::Text;
|
||||||
|
} else {
|
||||||
|
return Self::Unsupported(content_type.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod nginx_agent_api;
|
||||||
|
|
||||||
|
pub mod configuration;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub trait Api {
|
||||||
|
fn nginx_agent_api(&self) -> &dyn nginx_agent_api::NginxAgentApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiClient {
|
||||||
|
nginx_agent_api: Box<dyn nginx_agent_api::NginxAgentApi>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiClient {
|
||||||
|
pub fn new(configuration: Arc<configuration::Configuration>) -> Self {
|
||||||
|
Self {
|
||||||
|
nginx_agent_api: Box::new(nginx_agent_api::NginxAgentApiClient::new(
|
||||||
|
configuration.clone(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Api for ApiClient {
|
||||||
|
fn nginx_agent_api(&self) -> &dyn nginx_agent_api::NginxAgentApi {
|
||||||
|
self.nginx_agent_api.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mockall")]
|
||||||
|
pub struct MockApiClient {
|
||||||
|
pub nginx_agent_api_mock: nginx_agent_api::MockNginxAgentApi,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mockall")]
|
||||||
|
impl MockApiClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
nginx_agent_api_mock: nginx_agent_api::MockNginxAgentApi::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mockall")]
|
||||||
|
impl Api for MockApiClient {
|
||||||
|
fn nginx_agent_api(&self) -> &dyn nginx_agent_api::NginxAgentApi {
|
||||||
|
&self.nginx_agent_api_mock
|
||||||
|
}
|
||||||
|
}
|
||||||
329
public/agent-client/src/apis/nginx_agent_api.rs
Normal file
329
public/agent-client/src/apis/nginx_agent_api.rs
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::{configuration, Error};
|
||||||
|
use crate::apis::ContentType;
|
||||||
|
use crate::{apis::ResponseContent, models};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
#[cfg(feature = "mockall")]
|
||||||
|
use mockall::automock;
|
||||||
|
use reqwest;
|
||||||
|
use serde::{de::Error as _, Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "mockall", automock)]
|
||||||
|
#[async_trait]
|
||||||
|
pub trait NginxAgentApi: Send + Sync {
|
||||||
|
/// GET /status
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn status(&self) -> Result<ResponseContent<StatusSuccess>, Error<StatusError>>;
|
||||||
|
|
||||||
|
/// POST /validate
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn validate(
|
||||||
|
&self,
|
||||||
|
params: ValidateParams,
|
||||||
|
) -> Result<ResponseContent<ValidateSuccess>, Error<ValidateError>>;
|
||||||
|
|
||||||
|
/// POST /validate_and_reload
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn validate_and_reload(
|
||||||
|
&self,
|
||||||
|
params: ValidateAndReloadParams,
|
||||||
|
) -> Result<ResponseContent<ValidateAndReloadSuccess>, Error<ValidateAndReloadError>>;
|
||||||
|
|
||||||
|
/// POST /write_config
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn write_config(
|
||||||
|
&self,
|
||||||
|
params: WriteConfigParams,
|
||||||
|
) -> Result<ResponseContent<WriteConfigSuccess>, Error<WriteConfigError>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NginxAgentApiClient {
|
||||||
|
configuration: Arc<configuration::Configuration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxAgentApiClient {
|
||||||
|
pub fn new(configuration: Arc<configuration::Configuration>) -> Self {
|
||||||
|
Self { configuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for passing parameters to the method [`NginxAgentApi::validate`]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ValidateParams {
|
||||||
|
pub validate_body: models::ValidateBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for passing parameters to the method [`NginxAgentApi::validate_and_reload`]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ValidateAndReloadParams {
|
||||||
|
pub validate_and_reload_body: models::ValidateAndReloadBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for passing parameters to the method [`NginxAgentApi::write_config`]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WriteConfigParams {
|
||||||
|
pub write_config_body: models::WriteConfigBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl NginxAgentApi for NginxAgentApiClient {
|
||||||
|
async fn status(&self) -> Result<ResponseContent<StatusSuccess>, Error<StatusError>> {
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str = format!("{}/status", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder =
|
||||||
|
local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder
|
||||||
|
.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<StatusSuccess> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<StatusError> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate(
|
||||||
|
&self,
|
||||||
|
params: ValidateParams,
|
||||||
|
) -> Result<ResponseContent<ValidateSuccess>, Error<ValidateError>> {
|
||||||
|
let ValidateParams { validate_body } = params;
|
||||||
|
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str = format!("{}/validate", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder =
|
||||||
|
local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder
|
||||||
|
.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
local_var_req_builder = local_var_req_builder.json(&validate_body);
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<ValidateSuccess> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<ValidateError> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_and_reload(
|
||||||
|
&self,
|
||||||
|
params: ValidateAndReloadParams,
|
||||||
|
) -> Result<ResponseContent<ValidateAndReloadSuccess>, Error<ValidateAndReloadError>> {
|
||||||
|
let ValidateAndReloadParams {
|
||||||
|
validate_and_reload_body,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str =
|
||||||
|
format!("{}/validate_and_reload", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder =
|
||||||
|
local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder
|
||||||
|
.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
local_var_req_builder = local_var_req_builder.json(&validate_and_reload_body);
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<ValidateAndReloadSuccess> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<ValidateAndReloadError> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_config(
|
||||||
|
&self,
|
||||||
|
params: WriteConfigParams,
|
||||||
|
) -> Result<ResponseContent<WriteConfigSuccess>, Error<WriteConfigError>> {
|
||||||
|
let WriteConfigParams { write_config_body } = params;
|
||||||
|
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str = format!("{}/write_config", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder =
|
||||||
|
local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder
|
||||||
|
.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
local_var_req_builder = local_var_req_builder.json(&write_config_body);
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<WriteConfigSuccess> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<WriteConfigError> =
|
||||||
|
serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent {
|
||||||
|
status: local_var_status,
|
||||||
|
content: local_var_content,
|
||||||
|
entity: local_var_entity,
|
||||||
|
};
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`NginxAgentApi::status`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum StatusSuccess {
|
||||||
|
Status200(models::StatusResp),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`NginxAgentApi::validate`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateSuccess {
|
||||||
|
Status200(serde_json::Value),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`NginxAgentApi::validate_and_reload`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateAndReloadSuccess {
|
||||||
|
Status200(models::ValidateAndReloadResp),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`NginxAgentApi::write_config`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum WriteConfigSuccess {
|
||||||
|
Status200(),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`NginxAgentApi::status`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum StatusError {
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`NginxAgentApi::validate`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateError {
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`NginxAgentApi::validate_and_reload`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateAndReloadError {
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`NginxAgentApi::write_config`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum WriteConfigError {
|
||||||
|
Status500(serde_json::Value),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
280
public/agent-client/src/apis/tag_nginxtag_api.rs
Normal file
280
public/agent-client/src/apis/tag_nginxtag_api.rs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
#[cfg(feature = "mockall")]
|
||||||
|
use mockall::automock;
|
||||||
|
use reqwest;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use serde::{Deserialize, Serialize, de::Error as _};
|
||||||
|
use crate::{apis::ResponseContent, models};
|
||||||
|
use super::{Error, configuration};
|
||||||
|
use crate::apis::ContentType;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "mockall", automock)]
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TagNginxtagApi: Send + Sync {
|
||||||
|
|
||||||
|
/// GET /status
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn status(&self, ) -> Result<ResponseContent<StatusSuccess>, Error<StatusError>>;
|
||||||
|
|
||||||
|
/// POST /validate
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn validate(&self, params: ValidateParams ) -> Result<ResponseContent<ValidateSuccess>, Error<ValidateError>>;
|
||||||
|
|
||||||
|
/// POST /validate_and_reload
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn validate_and_reload(&self, params: ValidateAndReloadParams ) -> Result<ResponseContent<ValidateAndReloadSuccess>, Error<ValidateAndReloadError>>;
|
||||||
|
|
||||||
|
/// POST /write_config
|
||||||
|
///
|
||||||
|
///
|
||||||
|
async fn write_config(&self, params: WriteConfigParams ) -> Result<ResponseContent<WriteConfigSuccess>, Error<WriteConfigError>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TagNginxtagApiClient {
|
||||||
|
configuration: Arc<configuration::Configuration>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagNginxtagApiClient {
|
||||||
|
pub fn new(configuration: Arc<configuration::Configuration>) -> Self {
|
||||||
|
Self { configuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// struct for passing parameters to the method [`TagNginxtagApi::validate`]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ValidateParams {
|
||||||
|
pub validate_body: models::ValidateBody
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for passing parameters to the method [`TagNginxtagApi::validate_and_reload`]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ValidateAndReloadParams {
|
||||||
|
pub validate_and_reload_body: models::ValidateAndReloadBody
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for passing parameters to the method [`TagNginxtagApi::write_config`]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WriteConfigParams {
|
||||||
|
pub write_config_body: models::WriteConfigBody
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TagNginxtagApi for TagNginxtagApiClient {
|
||||||
|
async fn status(&self, ) -> Result<ResponseContent<StatusSuccess>, Error<StatusError>> {
|
||||||
|
|
||||||
|
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str = format!("{}/status", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder = local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<StatusSuccess> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<StatusError> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate(&self, params: ValidateParams ) -> Result<ResponseContent<ValidateSuccess>, Error<ValidateError>> {
|
||||||
|
|
||||||
|
let ValidateParams {
|
||||||
|
validate_body,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str = format!("{}/validate", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
local_var_req_builder = local_var_req_builder.json(&validate_body);
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<ValidateSuccess> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<ValidateError> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_and_reload(&self, params: ValidateAndReloadParams ) -> Result<ResponseContent<ValidateAndReloadSuccess>, Error<ValidateAndReloadError>> {
|
||||||
|
|
||||||
|
let ValidateAndReloadParams {
|
||||||
|
validate_and_reload_body,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str = format!("{}/validate_and_reload", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
local_var_req_builder = local_var_req_builder.json(&validate_and_reload_body);
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<ValidateAndReloadSuccess> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<ValidateAndReloadError> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_config(&self, params: WriteConfigParams ) -> Result<ResponseContent<WriteConfigSuccess>, Error<WriteConfigError>> {
|
||||||
|
|
||||||
|
let WriteConfigParams {
|
||||||
|
write_config_body,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
|
||||||
|
let local_var_configuration = &self.configuration;
|
||||||
|
|
||||||
|
let local_var_client = &local_var_configuration.client;
|
||||||
|
|
||||||
|
let local_var_uri_str = format!("{}/write_config", local_var_configuration.base_path);
|
||||||
|
let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str());
|
||||||
|
|
||||||
|
if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
|
||||||
|
local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
|
||||||
|
}
|
||||||
|
local_var_req_builder = local_var_req_builder.json(&write_config_body);
|
||||||
|
|
||||||
|
let local_var_req = local_var_req_builder.build()?;
|
||||||
|
let local_var_resp = local_var_client.execute(local_var_req).await?;
|
||||||
|
|
||||||
|
let local_var_status = local_var_resp.status();
|
||||||
|
let local_var_content = local_var_resp.text().await?;
|
||||||
|
|
||||||
|
if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
|
||||||
|
let local_var_entity: Option<WriteConfigSuccess> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_result = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Ok(local_var_result)
|
||||||
|
} else {
|
||||||
|
let local_var_entity: Option<WriteConfigError> = serde_json::from_str(&local_var_content).ok();
|
||||||
|
let local_var_error = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity };
|
||||||
|
Err(Error::ResponseError(local_var_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`TagNginxtagApi::status`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum StatusSuccess {
|
||||||
|
Status200(models::StatusResp),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`TagNginxtagApi::validate`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateSuccess {
|
||||||
|
Status200(serde_json::Value),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`TagNginxtagApi::validate_and_reload`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateAndReloadSuccess {
|
||||||
|
Status200(models::ValidateAndReloadResp),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed successes of method [`TagNginxtagApi::write_config`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum WriteConfigSuccess {
|
||||||
|
Status200(),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`TagNginxtagApi::status`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum StatusError {
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`TagNginxtagApi::validate`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateError {
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`TagNginxtagApi::validate_and_reload`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ValidateAndReloadError {
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// struct for typed errors of method [`TagNginxtagApi::write_config`]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum WriteConfigError {
|
||||||
|
Status500(serde_json::Value),
|
||||||
|
UnknownValue(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
10
public/agent-client/src/lib.rs
Normal file
10
public/agent-client/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#![allow(unused_imports)]
|
||||||
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
|
extern crate serde;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate serde_repr;
|
||||||
|
extern crate url;
|
||||||
|
|
||||||
|
pub mod apis;
|
||||||
|
pub mod models;
|
||||||
10
public/agent-client/src/models/mod.rs
Normal file
10
public/agent-client/src/models/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
pub mod status_resp;
|
||||||
|
pub use self::status_resp::StatusResp;
|
||||||
|
pub mod validate_and_reload_body;
|
||||||
|
pub use self::validate_and_reload_body::ValidateAndReloadBody;
|
||||||
|
pub mod validate_and_reload_resp;
|
||||||
|
pub use self::validate_and_reload_resp::ValidateAndReloadResp;
|
||||||
|
pub mod validate_body;
|
||||||
|
pub use self::validate_body::ValidateBody;
|
||||||
|
pub mod write_config_body;
|
||||||
|
pub use self::write_config_body::WriteConfigBody;
|
||||||
24
public/agent-client/src/models/status_resp.rs
Normal file
24
public/agent-client/src/models/status_resp.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::models;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct StatusResp {
|
||||||
|
#[serde(rename = "ok")]
|
||||||
|
pub ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusResp {
|
||||||
|
pub fn new(ok: bool) -> StatusResp {
|
||||||
|
StatusResp { ok }
|
||||||
|
}
|
||||||
|
}
|
||||||
29
public/agent-client/src/models/validate_and_reload_body.rs
Normal file
29
public/agent-client/src/models/validate_and_reload_body.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::models;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ValidateAndReloadBody {
|
||||||
|
#[serde(rename = "config_name")]
|
||||||
|
pub config_name: String,
|
||||||
|
#[serde(rename = "timestamp")]
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidateAndReloadBody {
|
||||||
|
pub fn new(config_name: String, timestamp: i64) -> ValidateAndReloadBody {
|
||||||
|
ValidateAndReloadBody {
|
||||||
|
config_name,
|
||||||
|
timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
public/agent-client/src/models/validate_and_reload_resp.rs
Normal file
26
public/agent-client/src/models/validate_and_reload_resp.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::models;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ValidateAndReloadResp {
|
||||||
|
#[serde(rename = "rc")]
|
||||||
|
pub rc: i32,
|
||||||
|
#[serde(rename = "ro")]
|
||||||
|
pub ro: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidateAndReloadResp {
|
||||||
|
pub fn new(rc: i32, ro: String) -> ValidateAndReloadResp {
|
||||||
|
ValidateAndReloadResp { rc, ro }
|
||||||
|
}
|
||||||
|
}
|
||||||
29
public/agent-client/src/models/validate_body.rs
Normal file
29
public/agent-client/src/models/validate_body.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::models;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ValidateBody {
|
||||||
|
#[serde(rename = "config_name")]
|
||||||
|
pub config_name: String,
|
||||||
|
#[serde(rename = "timestamp")]
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidateBody {
|
||||||
|
pub fn new(config_name: String, timestamp: i64) -> ValidateBody {
|
||||||
|
ValidateBody {
|
||||||
|
config_name,
|
||||||
|
timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
public/agent-client/src/models/write_config_body.rs
Normal file
32
public/agent-client/src/models/write_config_body.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* yanpm-agent
|
||||||
|
*
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 0.1.0
|
||||||
|
*
|
||||||
|
* Generated by: https://openapi-generator.tech
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::models;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct WriteConfigBody {
|
||||||
|
#[serde(rename = "config_name")]
|
||||||
|
pub config_name: String,
|
||||||
|
#[serde(rename = "content")]
|
||||||
|
pub content: String,
|
||||||
|
#[serde(rename = "timestamp")]
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteConfigBody {
|
||||||
|
pub fn new(config_name: String, content: String, timestamp: i64) -> WriteConfigBody {
|
||||||
|
WriteConfigBody {
|
||||||
|
config_name,
|
||||||
|
content,
|
||||||
|
timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
public/database/src/generated/entities/edit_operation.rs
Normal file
30
public/database/src/generated/entities/edit_operation.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[sea_orm::model]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "edit_operation")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub session_id: Uuid,
|
||||||
|
pub resource_type: String,
|
||||||
|
pub resource_id: Option<Uuid>,
|
||||||
|
pub operation_type: String,
|
||||||
|
#[sea_orm(column_type = "JsonBinary")]
|
||||||
|
pub payload: Json,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub applied_at: Option<DateTimeUtc>,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to,
|
||||||
|
from = "session_id",
|
||||||
|
to = "id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
pub editing_session: HasOne<super::editing_session::Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
23
public/database/src/generated/entities/editing_session.rs
Normal file
23
public/database/src/generated/entities/editing_session.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[sea_orm::model]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "editing_session")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub applied_at: Option<DateTimeUtc>,
|
||||||
|
pub applied_by: Option<Uuid>,
|
||||||
|
pub expires_at: Option<DateTimeUtc>,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
|
pub edit_operations: HasMany<super::edit_operation::Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -3,5 +3,9 @@
|
|||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod edit_operation;
|
||||||
|
pub mod editing_session;
|
||||||
|
pub mod upstream;
|
||||||
|
pub mod upstream_target;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_identity;
|
pub mod user_identity;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
||||||
|
|
||||||
pub use super::config::Entity as Config;
|
pub use super::config::Entity as Config;
|
||||||
|
pub use super::edit_operation::Entity as EditOperation;
|
||||||
|
pub use super::editing_session::Entity as EditingSession;
|
||||||
|
pub use super::upstream::Entity as Upstream;
|
||||||
|
pub use super::upstream_target::Entity as UpstreamTarget;
|
||||||
pub use super::user::Entity as User;
|
pub use super::user::Entity as User;
|
||||||
pub use super::user_identity::Entity as UserIdentity;
|
pub use super::user_identity::Entity as UserIdentity;
|
||||||
|
|||||||
23
public/database/src/generated/entities/upstream.rs
Normal file
23
public/database/src/generated/entities/upstream.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[sea_orm::model]
|
||||||
|
#[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 name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub sticky_session: bool,
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
|
pub upstream_targets: HasMany<super::upstream_target::Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -5,25 +5,26 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
#[sea_orm::model]
|
#[sea_orm::model]
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "session")]
|
#[sea_orm(table_name = "upstream_target")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub user_id: Uuid,
|
pub upstream_id: Uuid,
|
||||||
#[sea_orm(unique)]
|
pub target_host: String,
|
||||||
pub refresh_token_hash: Option<String>,
|
pub target_port: i64,
|
||||||
pub expires_at: DateTimeUtc,
|
pub weight: i64,
|
||||||
pub revoked_at: Option<DateTimeUtc>,
|
pub is_backup: bool,
|
||||||
|
pub enabled: bool,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to,
|
belongs_to,
|
||||||
from = "user_id",
|
from = "upstream_id",
|
||||||
to = "id",
|
to = "id",
|
||||||
on_update = "Cascade",
|
on_update = "Cascade",
|
||||||
on_delete = "Cascade"
|
on_delete = "Cascade"
|
||||||
)]
|
)]
|
||||||
pub user: HasOne<super::user::Entity>,
|
pub upstream: HasOne<super::upstream::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
208
public/migration/doc/nginx-tables.md
Normal file
208
public/migration/doc/nginx-tables.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# Migration Tables → nginx mapping
|
||||||
|
|
||||||
|
This document explains the purpose of each migration table added under `public/migration/src/migrations` and how the rows map to generated nginx configuration (HTTP `http {}` and `stream {}` contexts).
|
||||||
|
|
||||||
|
Summary of tables covered:
|
||||||
|
|
||||||
|
- `upstream`
|
||||||
|
- `upstream_target`
|
||||||
|
- `proxy_host`
|
||||||
|
- `location`
|
||||||
|
- `stream_service`
|
||||||
|
- `access_list`
|
||||||
|
- `access_list_entry`
|
||||||
|
- `audit_log`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `upstream`
|
||||||
|
|
||||||
|
Purpose: A named backend pool of servers. Shared by HTTP and stream services.
|
||||||
|
|
||||||
|
Key fields:
|
||||||
|
|
||||||
|
- `id`: UUID primary key
|
||||||
|
- `name`: identifier used when generating nginx `upstream <name> {}`
|
||||||
|
- `protocol`: `http` | `tcp` | `udp` — determines how nginx will use the pool
|
||||||
|
- `algorithm`: load balancing strategy (`round_robin`, `least_conn`, `ip_hash`)
|
||||||
|
- `sticky_session`: whether to enable sticky behavior when supported
|
||||||
|
- `health_check`: optional JSON describing health probes
|
||||||
|
|
||||||
|
nginx mapping (HTTP):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
upstream <name> {
|
||||||
|
server 10.0.0.5:8080 weight=2;
|
||||||
|
server 10.0.0.6:8080 backup;
|
||||||
|
# optional LB settings generated from `algorithm` and `sticky_session`
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://<name>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
nginx mapping (stream):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
stream {
|
||||||
|
upstream <name> {
|
||||||
|
server 10.0.0.5:3306;
|
||||||
|
server 10.0.0.6:3306 backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 3306;
|
||||||
|
proxy_pass <name>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes: `upstream.protocol` selects which block and directive forms to generate;
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `upstream_target`
|
||||||
|
|
||||||
|
Purpose: One row per backend server in an `upstream` pool.
|
||||||
|
|
||||||
|
Key fields:
|
||||||
|
|
||||||
|
- `upstream_id`: FK to `upstream`
|
||||||
|
- `target_host`, `target_port`
|
||||||
|
- `weight`, `is_backup`, `enabled`
|
||||||
|
|
||||||
|
nginx mapping: each row becomes a `server` line in the generated `upstream` block (weights and backup flags applied). Disabled targets are omitted.
|
||||||
|
|
||||||
|
Example generated line:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server 10.0.0.5:8080 weight=3;
|
||||||
|
server 10.0.0.6:8080 backup;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `proxy_host`
|
||||||
|
|
||||||
|
Purpose: Represents an HTTP(S) host (a top-level `server` block in nginx `http` context).
|
||||||
|
|
||||||
|
Key fields:
|
||||||
|
|
||||||
|
- `domain`: `server_name` value (may be a wildcard)
|
||||||
|
- `listen_port`: port to listen on (80/443)
|
||||||
|
- `scheme`: http|https (informs UI; TLS handled elsewhere)
|
||||||
|
- `forward_host/forward_port` or `default_upstream_id`: host-level forwarding fallback
|
||||||
|
- `preserve_host_header`: whether to forward original `Host` header
|
||||||
|
- `enable_websocket`: toggles websocket header handling
|
||||||
|
- `meta`: JSON for optional host-level settings (timeouts, client_max_body_size, custom snippets)
|
||||||
|
|
||||||
|
nginx mapping (host-level default):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen <listen_port>;
|
||||||
|
server_name <domain>;
|
||||||
|
|
||||||
|
# host-level fallback if no matching location
|
||||||
|
location / {
|
||||||
|
proxy_pass http://<default_upstream_name>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `forward_host`/`forward_port` is set instead of `default_upstream_id`, generate `proxy_pass http://forward_host:forward_port;`.
|
||||||
|
|
||||||
|
`meta` entries are injected into the `server` block (careful: snippets can break reloads).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `location`
|
||||||
|
|
||||||
|
Purpose: Path-level routing (`location` blocks inside a `server`). More specific than `proxy_host` default.
|
||||||
|
|
||||||
|
Key fields:
|
||||||
|
|
||||||
|
- `host_id`: FK to `proxy_host`
|
||||||
|
- `path`: `location` match (e.g., `/api`, `~^/assets/`)
|
||||||
|
- `match_type`: `prefix` | `exact` | `regex`
|
||||||
|
- `upstream_id` or `proxy_pass_host`/`proxy_pass_port`
|
||||||
|
- `allowed_methods`: optional method whitelist
|
||||||
|
- `custom_config`: raw nginx snippet inserted inside the `location`
|
||||||
|
|
||||||
|
nginx mapping:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://api_upstream;
|
||||||
|
# optional custom_config injected here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ordering and match type produce correct nginx `location` selection semantics; `order` field can break ties for equal specificity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `stream_service`
|
||||||
|
|
||||||
|
Purpose: A TCP/UDP service in nginx `stream` context — corresponds to a `server` block inside `stream {}`.
|
||||||
|
|
||||||
|
Key fields:
|
||||||
|
|
||||||
|
- `listen_host`, `listen_port`
|
||||||
|
- `protocol`: `tcp` | `udp`
|
||||||
|
- `mode`: `direct` | `upstream` (direct forwards to `forward_host:forward_port`, `upstream` uses `upstream_id` pool)
|
||||||
|
- `preserved_client_ip`: whether to enable proxy_protocol or other client-ip forwarding
|
||||||
|
- `meta`: JSON for advanced stream options (ssl_preread, proxy_timeout, buffer sizes)
|
||||||
|
|
||||||
|
nginx mapping (stream):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
stream {
|
||||||
|
upstream <name> {
|
||||||
|
server 10.0.0.5:3306;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 3306;
|
||||||
|
proxy_pass <name>; # or proxy_pass 10.0.0.10:3306 for direct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes: Stream services bypass HTTP processing. Use `meta` for `proxy_protocol` and `ssl_preread` toggles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `access_list` and `access_list_entry`
|
||||||
|
|
||||||
|
Purpose: Nameable allow/deny lists for IP/CIDR or other entry types that can be applied to hosts/locations/stream services.
|
||||||
|
|
||||||
|
Key fields (access_list): `id`, `name`, `description`.
|
||||||
|
Key fields (entry): `access_list_id`, `entry_type` (e.g., `allow`, `deny`, `note`), `value` (IP or CIDR), `comment`.
|
||||||
|
|
||||||
|
nginx mapping (HTTP example):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location /admin {
|
||||||
|
allow 10.0.0.0/24;
|
||||||
|
deny all;
|
||||||
|
proxy_pass http://admin_upstream;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
nginx mapping (stream example):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 3306;
|
||||||
|
allow 10.0.0.0/24;
|
||||||
|
deny all;
|
||||||
|
proxy_pass backend_pool;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -13,6 +13,9 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20251011_000001_create_config_table::Migration),
|
Box::new(m20251011_000001_create_config_table::Migration),
|
||||||
Box::new(m20251011_000002_create_user_table::Migration),
|
Box::new(m20251011_000002_create_user_table::Migration),
|
||||||
Box::new(m20251011_000003_create_user_identity_table::Migration),
|
Box::new(m20251011_000003_create_user_identity_table::Migration),
|
||||||
|
Box::new(m20251223_000004_create_upstream_table::Migration),
|
||||||
|
Box::new(m20251223_000005_create_upstream_target_table::Migration),
|
||||||
|
Box::new(m20251230_000006_create_editing_session_table::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
pub mod m20251011_000001_create_config_table;
|
pub mod m20251011_000001_create_config_table;
|
||||||
pub mod m20251011_000002_create_user_table;
|
pub mod m20251011_000002_create_user_table;
|
||||||
pub mod m20251011_000003_create_user_identity_table;
|
pub mod m20251011_000003_create_user_identity_table;
|
||||||
|
pub mod m20251223_000004_create_upstream_table;
|
||||||
|
pub mod m20251223_000005_create_upstream_target_table;
|
||||||
|
pub mod m20251230_000006_create_editing_session_table;
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[forbid(dead_code)]
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum Upstream {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
Protocol,
|
||||||
|
Algorithm,
|
||||||
|
StickySession,
|
||||||
|
CreatedBy,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Upstream::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_uuid(Upstream::Id))
|
||||||
|
.col(ColumnDef::new(Upstream::Name).string().not_null())
|
||||||
|
.col(ColumnDef::new(Upstream::Protocol).string().not_null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Upstream::Algorithm)
|
||||||
|
.string()
|
||||||
|
.default("round_robin")
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Upstream::StickySession)
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Upstream::CreatedBy).uuid().null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Upstream::CreatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Upstream::UpdatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Upstream::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[forbid(dead_code)]
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum UpstreamTarget {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
UpstreamId,
|
||||||
|
TargetHost,
|
||||||
|
TargetPort,
|
||||||
|
Weight,
|
||||||
|
IsBackup,
|
||||||
|
Enabled,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(UpstreamTarget::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_uuid(UpstreamTarget::Id))
|
||||||
|
.col(ColumnDef::new(UpstreamTarget::UpstreamId).uuid().not_null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk-upstream-target-upstream-id")
|
||||||
|
.from(UpstreamTarget::Table, UpstreamTarget::UpstreamId)
|
||||||
|
.to(
|
||||||
|
super::m20251223_000004_create_upstream_table::Upstream::Table,
|
||||||
|
super::m20251223_000004_create_upstream_table::Upstream::Id,
|
||||||
|
)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(UpstreamTarget::TargetHost)
|
||||||
|
.string()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(UpstreamTarget::TargetPort)
|
||||||
|
.integer()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(UpstreamTarget::Weight)
|
||||||
|
.integer()
|
||||||
|
.default(1)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(UpstreamTarget::IsBackup)
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(UpstreamTarget::Enabled)
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(UpstreamTarget::CreatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(UpstreamTarget::UpdatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(UpstreamTarget::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[forbid(dead_code)]
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum EditingSession {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
CreatedBy,
|
||||||
|
Status,
|
||||||
|
CreatedAt,
|
||||||
|
AppliedAt,
|
||||||
|
AppliedBy,
|
||||||
|
ExpiresAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[forbid(dead_code)]
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum EditOperation {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
SessionId,
|
||||||
|
ResourceType,
|
||||||
|
ResourceId,
|
||||||
|
OperationType,
|
||||||
|
Payload,
|
||||||
|
CreatedAt,
|
||||||
|
AppliedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(EditingSession::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_uuid(EditingSession::Id))
|
||||||
|
.col(ColumnDef::new(EditingSession::Name).string().null())
|
||||||
|
.col(ColumnDef::new(EditingSession::CreatedBy).uuid().null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(EditingSession::Status)
|
||||||
|
.string()
|
||||||
|
.default("pending")
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(EditingSession::CreatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(EditingSession::AppliedAt).timestamp().null())
|
||||||
|
.col(ColumnDef::new(EditingSession::AppliedBy).uuid().null())
|
||||||
|
.col(ColumnDef::new(EditingSession::ExpiresAt).timestamp().null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(EditOperation::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_uuid(EditOperation::Id))
|
||||||
|
.col(ColumnDef::new(EditOperation::SessionId).uuid().not_null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(EditOperation::ResourceType)
|
||||||
|
.string()
|
||||||
|
.not_null(),
|
||||||
|
) // e.g. "upstream", "location"
|
||||||
|
.col(ColumnDef::new(EditOperation::ResourceId).uuid().null()) // null for create
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(EditOperation::OperationType)
|
||||||
|
.string()
|
||||||
|
.not_null(),
|
||||||
|
) // "create"|"update"|"delete"
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(EditOperation::Payload)
|
||||||
|
.json_binary()
|
||||||
|
.not_null(),
|
||||||
|
) // patch or full object
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(EditOperation::CreatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(EditOperation::AppliedAt).timestamp().null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk-edit-op-session")
|
||||||
|
.from(EditOperation::Table, EditOperation::SessionId)
|
||||||
|
.to(EditingSession::Table, EditingSession::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(EditOperation::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(EditingSession::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user