Compare commits
10 Commits
b43f9fcb00
...
d184261027
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d184261027 | ||
|
|
6a30a03e59 | ||
|
|
9c3f775a67 | ||
|
|
331b4e1e96 | ||
|
|
4f85d88380 | ||
|
|
d81e5fe48d | ||
|
|
dff560019f | ||
|
|
b2a322ed79 | ||
|
|
f05544267c | ||
|
|
f4db47daf2 |
43
Cargo.lock
generated
43
Cargo.lock
generated
@@ -7,7 +7,7 @@ name = "agent_client"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"mockall",
|
"mockall 0.13.1",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2247,7 +2247,21 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"downcast",
|
"downcast",
|
||||||
"fragile",
|
"fragile",
|
||||||
"mockall_derive",
|
"mockall_derive 0.13.1",
|
||||||
|
"predicates",
|
||||||
|
"predicates-tree",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"downcast",
|
||||||
|
"fragile",
|
||||||
|
"mockall_derive 0.14.0",
|
||||||
"predicates",
|
"predicates",
|
||||||
"predicates-tree",
|
"predicates-tree",
|
||||||
]
|
]
|
||||||
@@ -2264,6 +2278,18 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"syn 2.0.111",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall_derive"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.111",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -2465,17 +2491,6 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "optfield"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.111",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-float"
|
name = "ordered-float"
|
||||||
version = "4.6.0"
|
version = "4.6.0"
|
||||||
@@ -5599,8 +5614,8 @@ dependencies = [
|
|||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"migration",
|
"migration",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"mockall 0.14.0",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"optfield",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ 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"] }
|
reqwest = { version = "^0.12", features = ["json", "multipart", "stream"] }
|
||||||
serde_urlencoded = { version = "0.7.1" }
|
serde_urlencoded = { version = "0.7.1" }
|
||||||
optfield = { version = "0.4.0" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
axum-test = "18.4.1"
|
axum-test = "18.4.1"
|
||||||
|
agent_client = { path = "../../public/agent-client", features = ["mockall"] }
|
||||||
|
mockall = { version = "0.14.0", features = [] }
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
unwrap_used = "deny"
|
unwrap_used = "deny"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
use sea_orm::DbErr;
|
||||||
|
|
||||||
use crate::errors::service_error::ServiceError;
|
use crate::errors::service_error::ServiceError;
|
||||||
|
|
||||||
@@ -12,6 +13,12 @@ impl From<ServiceError> for ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<DbErr> for ApiError {
|
||||||
|
fn from(err: DbErr) -> Self {
|
||||||
|
ServiceError::from(err).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoResponse for ApiError {
|
impl IntoResponse for ApiError {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ macro_rules! with_conn {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Filters {
|
|
||||||
pub pagination: Option<PaginationFilter>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PaginationFilter {
|
pub struct PaginationFilter {
|
||||||
pub page: u64,
|
pub page: u64,
|
||||||
pub per_page: u64,
|
pub per_page: u64,
|
||||||
|
|||||||
@@ -78,25 +78,12 @@ pub async fn get_health_info(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::configs::FromConfig;
|
|
||||||
use crate::services::agent_client::AgentService;
|
|
||||||
use crate::services::get_app_service;
|
|
||||||
use crate::{
|
|
||||||
routes::{AppState, api::health::state::HealthState},
|
|
||||||
services::{
|
|
||||||
auth::{
|
|
||||||
authentication::{
|
|
||||||
AuthenticationServiceImpl, strategies::password::PasswordStrategy,
|
|
||||||
},
|
|
||||||
user::UserServiceImpl,
|
|
||||||
},
|
|
||||||
server_state::ServerStateService,
|
|
||||||
settings::SettingsService,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use agent_client::apis::configuration::Configuration;
|
|
||||||
|
use crate::configs::FromConfig;
|
||||||
|
use crate::routes::{AppState, api::health::state::HealthState};
|
||||||
|
use crate::services::get_app_service;
|
||||||
|
|
||||||
use axum::body::to_bytes;
|
use axum::body::to_bytes;
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod tag {
|
|||||||
pub const HEALTH_TAG: &str = "Health";
|
pub const HEALTH_TAG: &str = "Health";
|
||||||
pub const AUTH_TAG: &str = "Authentication";
|
pub const AUTH_TAG: &str = "Authentication";
|
||||||
pub const USER_TAG: &str = "User";
|
pub const USER_TAG: &str = "User";
|
||||||
|
pub const NGINX_TAG: &str = "Nginx";
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(utoipa::OpenApi)]
|
#[derive(utoipa::OpenApi)]
|
||||||
@@ -14,6 +15,16 @@ pub mod tag {
|
|||||||
crate::routes::api::auth::init_admin::init_admin,
|
crate::routes::api::auth::init_admin::init_admin,
|
||||||
// User management paths
|
// User management paths
|
||||||
crate::routes::api::restricted::user::me::get_user_info,
|
crate::routes::api::restricted::user::me::get_user_info,
|
||||||
|
// Nginx upstream management
|
||||||
|
crate::routes::api::restricted::nginx::upstream::create_upstream::create_upstream,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::create_upstream_target::add_upstream_target,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::get_upstream::get_upstream_list,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::get_upstream::get_upstream,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::get_upstream_target::get_upstream_target,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::update_upstream::update_upstream,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::update_upstream_target::update_upstream_target,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::remove_upstream::remove_upstream,
|
||||||
|
crate::routes::api::restricted::nginx::upstream::remove_upstream_target::remove_upstream_target,
|
||||||
),
|
),
|
||||||
components(
|
components(
|
||||||
schemas(crate::routes::api::health::info::HealthInfo),
|
schemas(crate::routes::api::health::info::HealthInfo),
|
||||||
@@ -22,11 +33,25 @@ pub mod tag {
|
|||||||
schemas(crate::routes::api::auth::init_admin::AdminInitRequest),
|
schemas(crate::routes::api::auth::init_admin::AdminInitRequest),
|
||||||
// User management schemas
|
// User management schemas
|
||||||
schemas(crate::routes::api::restricted::user::me::UserInfo),
|
schemas(crate::routes::api::restricted::user::me::UserInfo),
|
||||||
|
// Nginx upstream schemas
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::create_upstream::CreateUpstreamRequestBody),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::create_upstream_target::CreateUpstreamTargetInfo),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::get_upstream::GetUpstreamParams),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::get_upstream_target::GetUpstreamTargetsParams),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpstreamTargetInfo),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpstreamInfoResponse),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpstreamListResponse),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpstreamTargetInfoResponse),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::update_upstream::UpdateUpstreamRequestBody),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::update_upstream_target::UpdateUpstreamTargetRequestBody),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamInfoResponse),
|
||||||
|
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamTargetInfoResponse),
|
||||||
),
|
),
|
||||||
tags(
|
tags(
|
||||||
(name = tag::HEALTH_TAG, description = "Health information API"),
|
(name = tag::HEALTH_TAG, description = "Health information API"),
|
||||||
(name = tag::AUTH_TAG, description = "Authentication API"),
|
(name = tag::AUTH_TAG, description = "Authentication API"),
|
||||||
(name = tag::USER_TAG, description = "User management API")
|
(name = tag::USER_TAG, description = "User management API"),
|
||||||
|
(name = tag::NGINX_TAG, description = "Nginx management API")
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub struct ApiDoc;
|
pub struct ApiDoc;
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ pub mod create_upstream_target;
|
|||||||
pub mod get_upstream;
|
pub mod get_upstream;
|
||||||
pub mod get_upstream_target;
|
pub mod get_upstream_target;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
|
pub mod remove_upstream;
|
||||||
|
pub mod remove_upstream_target;
|
||||||
|
pub mod update_upstream;
|
||||||
|
pub mod update_upstream_target;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -19,14 +23,21 @@ pub fn get_upstream_router(state: Arc<AppState>) -> Router {
|
|||||||
"/upstreams",
|
"/upstreams",
|
||||||
get(get_upstream::get_upstream_list).post(create_upstream::create_upstream),
|
get(get_upstream::get_upstream_list).post(create_upstream::create_upstream),
|
||||||
)
|
)
|
||||||
.route("/upstreams/{upstream_id}", get(get_upstream::get_upstream))
|
.route(
|
||||||
|
"/upstreams/{upstream_id}",
|
||||||
|
get(get_upstream::get_upstream)
|
||||||
|
.patch(update_upstream::update_upstream)
|
||||||
|
.delete(remove_upstream::remove_upstream),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/upstreams/{upstream_id}/targets",
|
"/upstreams/{upstream_id}/targets",
|
||||||
post(create_upstream_target::add_upstream_target),
|
post(create_upstream_target::add_upstream_target),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/upstream_targets/{upstream_target_id}",
|
"/upstream_targets/{upstream_target_id}",
|
||||||
get(get_upstream_target::get_upstream_target),
|
get(get_upstream_target::get_upstream_target)
|
||||||
|
.patch(update_upstream_target::update_upstream_target)
|
||||||
|
.delete(remove_upstream_target::remove_upstream_target),
|
||||||
)
|
)
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{Json, extract::State, response::Result as AxumResult};
|
use axum::{Json, extract::State, response::Result as AxumResult};
|
||||||
|
use sea_orm::TransactionTrait;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::api_error::ApiError,
|
errors::api_error::ApiError,
|
||||||
middlewares::request_info::AuthenticatedRequestInfo,
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
routes::{AppState, api::restricted::nginx::upstream::info::response::UpstreamInfoResponse},
|
routes::{
|
||||||
|
AppState,
|
||||||
|
api::{
|
||||||
|
openapi::tag::NGINX_TAG,
|
||||||
|
restricted::nginx::upstream::info::response::UpstreamInfoResponse,
|
||||||
|
},
|
||||||
|
},
|
||||||
services::nginx::info::upstream::UpstreamCreateInfo,
|
services::nginx::info::upstream::UpstreamCreateInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,6 +81,18 @@ impl From<CreateUpstreamRequestBody> for ConcreteCreateUpstreamRequestBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/upstreams",
|
||||||
|
request_body = CreateUpstreamRequestBody,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Upstream created successfully", body = UpstreamInfoResponse),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 422, description = "Invalid request"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
pub async fn create_upstream(
|
pub async fn create_upstream(
|
||||||
request_info: AuthenticatedRequestInfo,
|
request_info: AuthenticatedRequestInfo,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
@@ -104,7 +123,18 @@ pub async fn create_upstream(
|
|||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let upstream_info = upstream_service.create_upstream(create_info, None).await?;
|
let mut tx = state.database_connection.begin().await?;
|
||||||
|
let upstream_info = upstream_service
|
||||||
|
.create_upstream(create_info, Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.service
|
||||||
|
.nginx
|
||||||
|
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(Json(upstream_info.into()))
|
Ok(Json(upstream_info.into()))
|
||||||
}
|
}
|
||||||
@@ -126,12 +156,17 @@ mod tests {
|
|||||||
create_upstream::{CreateUpstreamRequestBody, UpstreamTargetInfo as ReqTarget},
|
create_upstream::{CreateUpstreamRequestBody, UpstreamTargetInfo as ReqTarget},
|
||||||
get_upstream_router,
|
get_upstream_router,
|
||||||
},
|
},
|
||||||
services::get_app_service,
|
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
let program_settings = ProgramSettings::mock();
|
let program_settings = ProgramSettings::mock();
|
||||||
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
let state = Arc::new(crate::routes::AppState {
|
let state = Arc::new(crate::routes::AppState {
|
||||||
database_connection: Arc::new(db),
|
database_connection: Arc::new(db),
|
||||||
service: Arc::new(app_service),
|
service: Arc::new(app_service),
|
||||||
@@ -174,6 +209,10 @@ mod tests {
|
|||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![up_model.clone()]])
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
.append_query_results(vec![vec![target_model.clone()]])
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
|
// `find_with_related` returns rows of `(upstream, Option<target>)` which
|
||||||
|
// the mock DB expects as `(Model, Option<Model>)` per row.
|
||||||
|
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
@@ -218,6 +257,85 @@ mod tests {
|
|||||||
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
|
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_create_upstream_agent_error_returns_internal() {
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// configure mock agent to error on apply
|
||||||
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| {
|
||||||
|
Err(
|
||||||
|
crate::services::agent_client::AgentError::ApplicationFailed(
|
||||||
|
"internal".to_string(),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
));
|
||||||
|
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(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn handler_create_upstream_unauthenticated_returns_unauthorized() {
|
async fn handler_create_upstream_unauthenticated_returns_unauthorized() {
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{Json, extract::State, response::Result as AxumResult};
|
use axum::{Json, extract::State, response::Result as AxumResult};
|
||||||
|
use sea_orm::TransactionTrait;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::api_error::ApiError,
|
errors::api_error::ApiError,
|
||||||
middlewares::request_info::AuthenticatedRequestInfo,
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
routes::{
|
routes::{
|
||||||
AppState, api::restricted::nginx::upstream::info::response::UpstreamTargetInfoResponse,
|
AppState,
|
||||||
|
api::{
|
||||||
|
openapi::tag::NGINX_TAG,
|
||||||
|
restricted::nginx::upstream::info::response::UpstreamTargetInfoResponse,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
services::nginx::info::upstream_target::UpstreamTargetCreateInfo,
|
services::nginx::info::upstream_target::UpstreamTargetCreateInfo,
|
||||||
};
|
};
|
||||||
@@ -44,6 +49,18 @@ impl From<CreateUpstreamTargetInfo> for ConcreteCreateUpstreamTargetInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/upstreams/{upstream_id}/targets",
|
||||||
|
request_body = CreateUpstreamTargetInfo,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Upstream target created successfully", body = UpstreamTargetInfoResponse),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 422, description = "Invalid request"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
pub async fn add_upstream_target(
|
pub async fn add_upstream_target(
|
||||||
_request_info: AuthenticatedRequestInfo,
|
_request_info: AuthenticatedRequestInfo,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
@@ -61,10 +78,19 @@ pub async fn add_upstream_target(
|
|||||||
upstream_id: concrete_payload.upstream_id,
|
upstream_id: concrete_payload.upstream_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut tx = state.database_connection.begin().await?;
|
||||||
let upstream_info = upstream_service
|
let upstream_info = upstream_service
|
||||||
.create_upstream_target(create_info, None)
|
.create_upstream_target(create_info, Some(&mut tx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.service
|
||||||
|
.nginx
|
||||||
|
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(Json(upstream_info.into()))
|
Ok(Json(upstream_info.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +102,7 @@ mod tests {
|
|||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||||
|
|
||||||
use database::generated::entities::upstream_target;
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::{FromConfig, ProgramSettings},
|
configs::{FromConfig, ProgramSettings},
|
||||||
@@ -84,12 +110,17 @@ mod tests {
|
|||||||
routes::api::restricted::nginx::upstream::{
|
routes::api::restricted::nginx::upstream::{
|
||||||
create_upstream_target::CreateUpstreamTargetInfo, get_upstream_router,
|
create_upstream_target::CreateUpstreamTargetInfo, get_upstream_router,
|
||||||
},
|
},
|
||||||
services::get_app_service,
|
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
let program_settings = ProgramSettings::mock();
|
let program_settings = ProgramSettings::mock();
|
||||||
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
let state = Arc::new(crate::routes::AppState {
|
let state = Arc::new(crate::routes::AppState {
|
||||||
database_connection: Arc::new(db),
|
database_connection: Arc::new(db),
|
||||||
service: Arc::new(app_service),
|
service: Arc::new(app_service),
|
||||||
@@ -100,6 +131,83 @@ mod tests {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_add_upstream_target_agent_error_returns_internal() {
|
||||||
|
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 up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// configure mock agent to return an error on apply
|
||||||
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| {
|
||||||
|
Err(
|
||||||
|
crate::services::agent_client::AgentError::ApplicationFailed(
|
||||||
|
"internal".to_string(),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
));
|
||||||
|
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(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn handler_add_upstream_target_succeeds_returns_created() {
|
async fn handler_add_upstream_target_succeeds_returns_created() {
|
||||||
let up_id = uuid::Uuid::new_v4();
|
let up_id = uuid::Uuid::new_v4();
|
||||||
@@ -117,8 +225,21 @@ mod tests {
|
|||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
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)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![target_model.clone()]])
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
|
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use crate::{
|
|||||||
AppState,
|
AppState,
|
||||||
api::{
|
api::{
|
||||||
helper::pagination::{ExtractPagination, PaginationInfo},
|
helper::pagination::{ExtractPagination, PaginationInfo},
|
||||||
|
openapi::tag::NGINX_TAG,
|
||||||
restricted::nginx::upstream::info::response::{
|
restricted::nginx::upstream::info::response::{
|
||||||
UpstreamInfoResponse, UpstreamListResponse,
|
UpstreamInfoResponse, UpstreamListResponse,
|
||||||
},
|
},
|
||||||
@@ -39,6 +40,15 @@ impl From<GetUpstreamParams> for ConcreteGetUpstreamParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/upstreams",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List upstreams", body = UpstreamListResponse),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
pub async fn get_upstream_list(
|
pub async fn get_upstream_list(
|
||||||
ExtractPagination(pagination): ExtractPagination,
|
ExtractPagination(pagination): ExtractPagination,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
@@ -60,6 +70,16 @@ pub async fn get_upstream_list(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/upstreams/{upstream_id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get upstream info", body = UpstreamInfoResponse),
|
||||||
|
(status = 404, description = "Not found"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
pub async fn get_upstream(
|
pub async fn get_upstream(
|
||||||
Path(upstream_id): Path<Uuid>,
|
Path(upstream_id): Path<Uuid>,
|
||||||
Query(params): Query<GetUpstreamParams>,
|
Query(params): Query<GetUpstreamParams>,
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::api_error::ApiError,
|
errors::api_error::ApiError,
|
||||||
routes::{AppState, api::restricted::nginx::upstream::info::response::UpstreamTargetInfo},
|
routes::{
|
||||||
|
AppState,
|
||||||
|
api::{
|
||||||
|
openapi::tag::NGINX_TAG,
|
||||||
|
restricted::nginx::upstream::info::response::UpstreamTargetInfo,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
@@ -30,6 +36,16 @@ impl From<GetUpstreamTargetsParams> for ConcreteGetUpstreamTargetsParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/upstream_targets/{upstream_target_id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get upstream target info", body = UpstreamTargetInfo),
|
||||||
|
(status = 404, description = "Not found"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
pub async fn get_upstream_target(
|
pub async fn get_upstream_target(
|
||||||
Path(upstream_target_id): Path<Uuid>,
|
Path(upstream_target_id): Path<Uuid>,
|
||||||
Query(params): Query<GetUpstreamTargetsParams>,
|
Query(params): Query<GetUpstreamTargetsParams>,
|
||||||
|
|||||||
@@ -162,3 +162,71 @@ impl From<crate::services::nginx::info::upstream_target::UpstreamTargetInfo>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpdateUpstreamInfoResponse {
|
||||||
|
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 UpdateUpstreamInfoResponse {
|
||||||
|
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 UpdateUpstreamTargetInfoResponse {
|
||||||
|
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 UpdateUpstreamTargetInfoResponse
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use sea_orm::TransactionTrait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{AppState, api::openapi::tag::NGINX_TAG},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/upstreams/{upstream_id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Upstream removed successfully", body = ()),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 404, description = "Not found"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
|
pub async fn remove_upstream(
|
||||||
|
_request_info: AuthenticatedRequestInfo,
|
||||||
|
Path(upstream_id): Path<Uuid>,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> AxumResult<Json<()>, ApiError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
|
||||||
|
let mut tx = state.database_connection.begin().await?;
|
||||||
|
upstream_service
|
||||||
|
.delete_upstream(upstream_id, Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.service
|
||||||
|
.nginx
|
||||||
|
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult};
|
||||||
|
|
||||||
|
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::get_upstream_router,
|
||||||
|
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
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_remove_upstream_succeeds_returns_ok() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let existing = upstream::Model {
|
||||||
|
id: up_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 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)
|
||||||
|
.append_query_results(vec![vec![existing.clone()]])
|
||||||
|
.append_exec_results(vec![
|
||||||
|
MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
},
|
||||||
|
MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
|
.append_query_results(vec![vec![(existing.clone(), Some(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 res = server.delete(&format!("/upstreams/{}", up_id)).await;
|
||||||
|
|
||||||
|
res.assert_status_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_remove_upstream_agent_error_returns_internal() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let existing = upstream::Model {
|
||||||
|
id: up_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 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 mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| {
|
||||||
|
Err(
|
||||||
|
crate::services::agent_client::AgentError::ApplicationFailed(
|
||||||
|
"internal".to_string(),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.append_query_results(vec![vec![(existing.clone(), Some(target_model.clone()))]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
));
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let res = server.delete(&format!("/upstreams/{}", up_id)).await;
|
||||||
|
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_remove_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 res = server
|
||||||
|
.delete(&format!("/upstreams/{}", uuid::Uuid::new_v4()))
|
||||||
|
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_remove_upstream_not_found_returns_not_found() {
|
||||||
|
let empty_results: Vec<Vec<upstream::Model>> = vec![Vec::<upstream::Model>::new()];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(empty_results)
|
||||||
|
.into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.delete(&format!("/upstreams/{}", uuid::Uuid::new_v4()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use sea_orm::TransactionTrait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{AppState, api::openapi::tag::NGINX_TAG},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/upstream_targets/{upstream_target_id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Upstream target removed successfully", body = ()),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 404, description = "Not found"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
|
pub async fn remove_upstream_target(
|
||||||
|
_request_info: AuthenticatedRequestInfo,
|
||||||
|
Path(upstream_target_id): Path<Uuid>,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> AxumResult<Json<()>, ApiError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
|
||||||
|
let mut tx = state.database_connection.begin().await?;
|
||||||
|
upstream_service
|
||||||
|
.delete_upstream_target(upstream_target_id, Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.service
|
||||||
|
.nginx
|
||||||
|
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult};
|
||||||
|
|
||||||
|
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::get_upstream_router,
|
||||||
|
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
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_remove_upstream_target_succeeds_returns_ok() {
|
||||||
|
let ut_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let current_model = upstream_target::Model {
|
||||||
|
id: ut_id,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// first find_by_id, then delete (delete typically doesn't return models)
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: current_model.upstream_id,
|
||||||
|
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 first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(first)
|
||||||
|
.append_exec_results(vec![MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
}])
|
||||||
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
|
.append_query_results(vec![vec![(up_model.clone(), Some(current_model.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.delete(&format!("/upstream_targets/{}", ut_id)).await;
|
||||||
|
|
||||||
|
res.assert_status_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_remove_upstream_target_agent_error_returns_internal() {
|
||||||
|
let ut_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let current_model = upstream_target::Model {
|
||||||
|
id: ut_id,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
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 up_model = upstream::Model {
|
||||||
|
id: current_model.upstream_id,
|
||||||
|
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 mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| {
|
||||||
|
Err(
|
||||||
|
crate::services::agent_client::AgentError::ApplicationFailed(
|
||||||
|
"internal".to_string(),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
|
||||||
|
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(first)
|
||||||
|
.append_exec_results(vec![MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
}])
|
||||||
|
.append_query_results(vec![vec![(up_model.clone(), Some(current_model.clone()))]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
));
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let res = server.delete(&format!("/upstream_targets/{}", ut_id)).await;
|
||||||
|
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_remove_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 res = server
|
||||||
|
.delete(&format!("/upstream_targets/{}", uuid::Uuid::new_v4()))
|
||||||
|
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_remove_upstream_target_not_found_returns_not_found() {
|
||||||
|
let empty_results: Vec<Vec<upstream_target::Model>> =
|
||||||
|
vec![Vec::<upstream_target::Model>::new()];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(empty_results)
|
||||||
|
.into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.delete(&format!("/upstream_targets/{}", uuid::Uuid::new_v4()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use sea_orm::TransactionTrait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{
|
||||||
|
AppState, api::openapi::tag::NGINX_TAG,
|
||||||
|
api::restricted::nginx::upstream::info::response::UpdateUpstreamInfoResponse,
|
||||||
|
},
|
||||||
|
services::nginx::info::upstream::UpdateUpstreamInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema, Serialize)]
|
||||||
|
pub struct UpstreamTargetBasicUpdateInfo {
|
||||||
|
pub id: i64,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema, Serialize)]
|
||||||
|
pub struct UpdateUpstreamRequestBody {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub protocol: Option<String>,
|
||||||
|
pub algorithm: Option<String>,
|
||||||
|
pub sticky_session: Option<bool>,
|
||||||
|
// only updates upstream targets' enabled status for now
|
||||||
|
pub upstream_targets: Option<Vec<UpstreamTargetBasicUpdateInfo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpdateUpstreamRequestBody> for UpdateUpstreamInfo {
|
||||||
|
fn from(val: UpdateUpstreamRequestBody) -> Self {
|
||||||
|
Self {
|
||||||
|
name: val.name,
|
||||||
|
protocol: val.protocol,
|
||||||
|
algorithm: val.algorithm,
|
||||||
|
sticky_session: val.sticky_session,
|
||||||
|
//
|
||||||
|
upstream_targets: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = "/api/upstreams/{upstream_id}",
|
||||||
|
request_body = UpdateUpstreamRequestBody,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Upstream updated successfully", body = UpdateUpstreamInfoResponse),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 404, description = "Not found"),
|
||||||
|
(status = 422, description = "Invalid request"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
|
pub async fn update_upstream(
|
||||||
|
_request_info: AuthenticatedRequestInfo,
|
||||||
|
Path(upstream_id): Path<Uuid>,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<UpdateUpstreamRequestBody>,
|
||||||
|
) -> AxumResult<Json<UpdateUpstreamInfoResponse>, ApiError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
let update_info: UpdateUpstreamInfo = payload.into();
|
||||||
|
|
||||||
|
let mut tx = state.database_connection.begin().await?;
|
||||||
|
let r = upstream_service
|
||||||
|
.update_upstream(upstream_id, update_info, Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.service
|
||||||
|
.nginx
|
||||||
|
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(r.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 super::UpdateUpstreamRequestBody;
|
||||||
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
|
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||||
|
routes::api::restricted::nginx::upstream::get_upstream_router,
|
||||||
|
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
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_update_upstream_succeeds_returns_ok() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let current_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "old_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 updated_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "updated_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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// first find_by_id, then update returns updated model
|
||||||
|
let up_model = current_model.clone();
|
||||||
|
let first: Vec<Vec<upstream::Model>> = vec![vec![current_model.clone()]];
|
||||||
|
let second: Vec<Vec<upstream::Model>> = vec![vec![updated_model.clone()]];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(first)
|
||||||
|
.append_query_results(second)
|
||||||
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
|
.append_query_results(vec![vec![(
|
||||||
|
up_model.clone(),
|
||||||
|
Option::<upstream_target::Model>::None,
|
||||||
|
)]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = UpdateUpstreamRequestBody {
|
||||||
|
name: Some("updated_upstream".to_string()),
|
||||||
|
protocol: None,
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstreams/{}", up_id))
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status_ok();
|
||||||
|
let text = res.text();
|
||||||
|
let body: crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamInfoResponse =
|
||||||
|
serde_json::from_str(&text).expect("failed to parse json");
|
||||||
|
|
||||||
|
assert_eq!(body.id, up_id);
|
||||||
|
assert_eq!(body.name, "updated_upstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_update_upstream_agent_error_returns_internal() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let current_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "old_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 updated_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "updated_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 up_model = current_model.clone();
|
||||||
|
|
||||||
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| {
|
||||||
|
Err(
|
||||||
|
crate::services::agent_client::AgentError::ApplicationFailed(
|
||||||
|
"internal".to_string(),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
|
||||||
|
let first: Vec<Vec<upstream::Model>> = vec![vec![current_model.clone()]];
|
||||||
|
let second: Vec<Vec<upstream::Model>> = vec![vec![updated_model.clone()]];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(first)
|
||||||
|
.append_query_results(second)
|
||||||
|
.append_query_results(vec![vec![(
|
||||||
|
up_model.clone(),
|
||||||
|
Option::<upstream_target::Model>::None,
|
||||||
|
)]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
));
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = UpdateUpstreamRequestBody {
|
||||||
|
name: Some("updated_upstream".to_string()),
|
||||||
|
protocol: None,
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstreams/{}", up_id))
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_update_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 = UpdateUpstreamRequestBody {
|
||||||
|
name: Some("updated_upstream".to_string()),
|
||||||
|
protocol: None,
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstreams/{}", uuid::Uuid::new_v4()))
|
||||||
|
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_update_upstream_not_found_returns_not_found() {
|
||||||
|
let empty_results: Vec<Vec<upstream::Model>> = vec![Vec::<upstream::Model>::new()];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(empty_results)
|
||||||
|
.into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = UpdateUpstreamRequestBody {
|
||||||
|
name: Some("updated_upstream".to_string()),
|
||||||
|
protocol: None,
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstreams/{}", uuid::Uuid::new_v4()))
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use sea_orm::TransactionTrait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{
|
||||||
|
AppState, api::openapi::tag::NGINX_TAG,
|
||||||
|
api::restricted::nginx::upstream::info::response::UpdateUpstreamTargetInfoResponse,
|
||||||
|
},
|
||||||
|
services::nginx::info::upstream_target::UpdateUpstreamTargetInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema, Serialize)]
|
||||||
|
pub struct UpdateUpstreamTargetRequestBody {
|
||||||
|
pub host: Option<String>,
|
||||||
|
pub port: Option<i64>,
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
pub is_backup: Option<bool>,
|
||||||
|
pub weight: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpdateUpstreamTargetRequestBody> for UpdateUpstreamTargetInfo {
|
||||||
|
fn from(val: UpdateUpstreamTargetRequestBody) -> Self {
|
||||||
|
Self {
|
||||||
|
target_host: val.host,
|
||||||
|
target_port: val.port,
|
||||||
|
enabled: val.enabled,
|
||||||
|
is_backup: val.is_backup,
|
||||||
|
weight: val.weight.map(|w| w as i64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = "/api/upstream_targets/{upstream_target_id}",
|
||||||
|
request_body = UpdateUpstreamTargetRequestBody,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Upstream target updated successfully", body = UpdateUpstreamTargetInfoResponse),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 404, description = "Not found"),
|
||||||
|
(status = 422, description = "Invalid request"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
tag = NGINX_TAG,
|
||||||
|
)]
|
||||||
|
pub async fn update_upstream_target(
|
||||||
|
_request_info: AuthenticatedRequestInfo,
|
||||||
|
Path(upstream_target_id): Path<Uuid>,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<UpdateUpstreamTargetRequestBody>,
|
||||||
|
) -> AxumResult<Json<UpdateUpstreamTargetInfoResponse>, ApiError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
let update_info: UpdateUpstreamTargetInfo = payload.into();
|
||||||
|
|
||||||
|
let mut tx = state.database_connection.begin().await?;
|
||||||
|
let r = upstream_service
|
||||||
|
.update_upstream_target(upstream_target_id, update_info, Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.service
|
||||||
|
.nginx
|
||||||
|
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(r.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::routing::patch;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
use super::UpdateUpstreamTargetRequestBody;
|
||||||
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
|
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||||
|
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
axum::Router::new()
|
||||||
|
.route(
|
||||||
|
"/upstream_targets/{upstream_target_id}",
|
||||||
|
patch(crate::routes::api::restricted::nginx::upstream::update_upstream_target::update_upstream_target),
|
||||||
|
)
|
||||||
|
.with_state(state)
|
||||||
|
.layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_update_upstream_target_succeeds_returns_ok() {
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let current_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
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 updated_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: current_model.upstream_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8081,
|
||||||
|
weight: 2,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: false,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: current_model.upstream_id,
|
||||||
|
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 first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
|
||||||
|
let second: Vec<Vec<upstream_target::Model>> = vec![vec![updated_model.clone()]];
|
||||||
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
|
let third: Vec<Vec<(upstream::Model, Option<upstream_target::Model>)>> =
|
||||||
|
vec![vec![(up_model.clone(), Some(updated_model.clone()))]];
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(first)
|
||||||
|
.append_query_results(second)
|
||||||
|
.append_query_results(third)
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = UpdateUpstreamTargetRequestBody {
|
||||||
|
host: None,
|
||||||
|
port: Some(8081),
|
||||||
|
enabled: Some(false),
|
||||||
|
is_backup: None,
|
||||||
|
weight: Some(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstream_targets/{}", target_id))
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status_ok();
|
||||||
|
let text = res.text();
|
||||||
|
let body: crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamTargetInfoResponse =
|
||||||
|
serde_json::from_str(&text).expect("failed to parse json");
|
||||||
|
|
||||||
|
assert_eq!(body.id, target_id);
|
||||||
|
assert_eq!(body.port, 8081);
|
||||||
|
assert!(!body.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_update_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 = UpdateUpstreamTargetRequestBody {
|
||||||
|
host: None,
|
||||||
|
port: Some(8081),
|
||||||
|
enabled: Some(false),
|
||||||
|
is_backup: None,
|
||||||
|
weight: Some(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstream_targets/{}", uuid::Uuid::new_v4()))
|
||||||
|
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_update_upstream_target_not_found_returns_not_found() {
|
||||||
|
let empty_results: Vec<Vec<upstream_target::Model>> =
|
||||||
|
vec![Vec::<upstream_target::Model>::new()];
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(empty_results)
|
||||||
|
.into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = UpdateUpstreamTargetRequestBody {
|
||||||
|
host: None,
|
||||||
|
port: Some(8081),
|
||||||
|
enabled: Some(false),
|
||||||
|
is_backup: None,
|
||||||
|
weight: Some(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstream_targets/{}", uuid::Uuid::new_v4()))
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_update_upstream_target_agent_error_returns_internal() {
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let current_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
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 updated_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: current_model.upstream_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8081,
|
||||||
|
weight: 2,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: false,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: current_model.upstream_id,
|
||||||
|
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 mut mock = MockAgentService::new();
|
||||||
|
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||||
|
mock.expect_apply().returning(|_cfg| {
|
||||||
|
Err(
|
||||||
|
crate::services::agent_client::AgentError::ApplicationFailed(
|
||||||
|
"internal".to_string(),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mock_agent = Arc::new(mock);
|
||||||
|
|
||||||
|
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
|
||||||
|
let second: Vec<Vec<upstream_target::Model>> = vec![vec![updated_model.clone()]];
|
||||||
|
let third: Vec<Vec<(upstream::Model, Option<upstream_target::Model>)>> =
|
||||||
|
vec![vec![(up_model.clone(), Some(updated_model.clone()))]];
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(first)
|
||||||
|
.append_query_results(second)
|
||||||
|
.append_query_results(third)
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service =
|
||||||
|
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router = axum::Router::new()
|
||||||
|
.route(
|
||||||
|
"/upstream_targets/{upstream_target_id}",
|
||||||
|
axum::routing::patch(crate::routes::api::restricted::nginx::upstream::update_upstream_target::update_upstream_target),
|
||||||
|
)
|
||||||
|
.with_state(state)
|
||||||
|
.layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
));
|
||||||
|
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = UpdateUpstreamTargetRequestBody {
|
||||||
|
host: None,
|
||||||
|
port: Some(8081),
|
||||||
|
enabled: Some(false),
|
||||||
|
is_backup: None,
|
||||||
|
weight: Some(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.patch(&format!("/upstream_targets/{}", target_id))
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,13 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ::agent_client::apis::configuration::Configuration;
|
use ::agent_client::apis::configuration::Configuration;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::services::agent_client::MockAgentService;
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::ProgramSettings,
|
configs::ProgramSettings,
|
||||||
routes::{self, AuthState},
|
routes::{self, AuthState},
|
||||||
services::{
|
services::{
|
||||||
|
agent_client::{AgentService, AgentServiceImpl},
|
||||||
auth::{
|
auth::{
|
||||||
authentication::{AuthenticationServiceImpl, strategies::password::PasswordStrategy},
|
authentication::{AuthenticationServiceImpl, strategies::password::PasswordStrategy},
|
||||||
user::{UserService, UserServiceImpl},
|
user::{UserService, UserServiceImpl},
|
||||||
@@ -32,7 +35,7 @@ pub struct AppService {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub nginx: ServiceState<NginxService>,
|
pub nginx: ServiceState<NginxService>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub agent_client: ServiceState<agent_client::AgentService>,
|
pub agent_client: ServiceState<dyn AgentService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_app_service(
|
pub fn get_app_service(
|
||||||
@@ -52,8 +55,31 @@ pub fn get_app_service(
|
|||||||
},
|
},
|
||||||
user: Arc::new(UserServiceImpl::new(db_connection.clone())),
|
user: Arc::new(UserServiceImpl::new(db_connection.clone())),
|
||||||
nginx: Arc::new(NginxService::new(db_connection.clone())),
|
nginx: Arc::new(NginxService::new(db_connection.clone())),
|
||||||
agent_client: Arc::new(agent_client::AgentService::new(Configuration::from(
|
agent_client: Arc::new(AgentServiceImpl::new(Configuration::from(
|
||||||
settings.agent.clone(),
|
settings.agent.clone(),
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn get_mock_app_service(
|
||||||
|
db_connection: &Arc<sea_orm::DatabaseConnection>,
|
||||||
|
settings: &ProgramSettings,
|
||||||
|
mock_agent: Arc<MockAgentService>,
|
||||||
|
) -> 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: mock_agent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,113 @@
|
|||||||
use std::sync::Arc;
|
use std::{os::unix::fs::FileTypeExt, sync::Arc};
|
||||||
|
|
||||||
use agent_client::apis::{ApiClient, configuration::Configuration};
|
use agent_client::{
|
||||||
|
apis::{
|
||||||
|
Api, ApiClient, Error as ApiError, ResponseContent,
|
||||||
|
configuration::Configuration,
|
||||||
|
nginx_agent_api::{ValidateAndReloadParams, ValidateParams, WriteConfigParams},
|
||||||
|
},
|
||||||
|
models::{ValidateAndReloadBody, ValidateBody, WriteConfigBody},
|
||||||
|
};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::configs::agent::AgentSettings;
|
use crate::{configs::agent::AgentSettings, errors::service_error::ServiceError};
|
||||||
|
|
||||||
pub struct AgentService {
|
#[derive(Debug)]
|
||||||
|
pub enum AgentError {
|
||||||
|
// (internal messages, user-facing messages)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
ValidationFailed(String, String),
|
||||||
|
// (internal messages, user-facing messages)
|
||||||
|
ApplicationFailed(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AgentError> for ServiceError {
|
||||||
|
fn from(err: AgentError) -> Self {
|
||||||
|
match err {
|
||||||
|
AgentError::ValidationFailed(_internal, user) => ServiceError::InternalError(user),
|
||||||
|
AgentError::ApplicationFailed(_internal, user) => ServiceError::InternalError(user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::fmt::Debug> From<ResponseContent<T>> for AgentError {
|
||||||
|
fn from(err: ResponseContent<T>) -> Self {
|
||||||
|
let ResponseContent {
|
||||||
|
status,
|
||||||
|
content,
|
||||||
|
entity,
|
||||||
|
} = err;
|
||||||
|
{
|
||||||
|
let entity_str = entity
|
||||||
|
.map(|e| format!("{:?}", e))
|
||||||
|
.unwrap_or_else(|| "<empty>".to_string());
|
||||||
|
AgentError::ApplicationFailed(
|
||||||
|
format!(
|
||||||
|
"Agent responded with error status {}: {}, entity: {}",
|
||||||
|
status, content, entity_str
|
||||||
|
),
|
||||||
|
"Agent reported an error during operation.".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::fmt::Debug> From<ApiError<T>> for AgentError {
|
||||||
|
fn from(err: ApiError<T>) -> Self {
|
||||||
|
match err {
|
||||||
|
ApiError::ResponseError(resp) => AgentError::from(resp),
|
||||||
|
ApiError::Io(err) => AgentError::ApplicationFailed(
|
||||||
|
format!("IO error during agent communication: {}", err),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
ApiError::Reqwest(err) => AgentError::ApplicationFailed(
|
||||||
|
format!("Reqwest error during agent communication: {}", err),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
ApiError::Serde(err) => AgentError::ApplicationFailed(
|
||||||
|
format!("Serialization error during agent communication: {}", err),
|
||||||
|
"Failed to communicate with the agent.".to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, mockall::automock)]
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait AgentService: Send + Sync {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn get_client(&self) -> Arc<ApiClient>;
|
||||||
|
|
||||||
|
// TODO: improve error handling and reporting, error reasons
|
||||||
|
// validate configurations that has been created/updated before the given timestamp
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn validate(&self, config: &str) -> Result<(), AgentError>;
|
||||||
|
// validate and apply configurations that has been created/updated before the given timestamp
|
||||||
|
async fn apply(&self, config: &str) -> Result<(), AgentError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AgentServiceImpl {
|
||||||
client: Arc<ApiClient>,
|
client: Arc<ApiClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AgentServiceImpl {
|
||||||
|
pub fn new(config: impl Into<Arc<Configuration>>) -> Self {
|
||||||
|
let client = ApiClient::new(config.into());
|
||||||
|
AgentServiceImpl {
|
||||||
|
client: Arc::new(client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<AgentSettings> for Configuration {
|
impl From<AgentSettings> for Configuration {
|
||||||
fn from(settings: AgentSettings) -> Self {
|
fn from(settings: AgentSettings) -> Self {
|
||||||
let mut config = Configuration::default();
|
let mut config = Configuration::default();
|
||||||
let mut builder = reqwest::Client::builder();
|
let mut builder = reqwest::Client::builder();
|
||||||
|
|
||||||
let url = settings.socket_path;
|
let url = settings.socket_path;
|
||||||
if url.starts_with("unix://") {
|
// check if the url is a unix socket path
|
||||||
|
let is_socket = std::fs::metadata(&url).is_ok_and(|m| m.file_type().is_socket());
|
||||||
|
if is_socket || url.starts_with("unix://") {
|
||||||
builder = builder.unix_socket(url.to_string());
|
builder = builder.unix_socket(url.to_string());
|
||||||
config.client = builder.build().expect("Failed to build reqwest client");
|
config.client = builder.build().expect("Failed to build reqwest client");
|
||||||
} else {
|
} else {
|
||||||
@@ -27,17 +119,73 @@ impl From<AgentSettings> for Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentService {
|
#[async_trait::async_trait]
|
||||||
pub fn new(config: impl Into<Arc<Configuration>>) -> Self {
|
impl AgentService for AgentServiceImpl {
|
||||||
let client = ApiClient::new(config.into());
|
fn get_client(&self) -> Arc<ApiClient> {
|
||||||
AgentService {
|
Arc::clone(&self.client)
|
||||||
client: Arc::new(client),
|
}
|
||||||
|
|
||||||
|
async fn validate(&self, config: &str) -> Result<(), AgentError> {
|
||||||
|
let timestamp = chrono::Utc::now().timestamp_millis();
|
||||||
|
let name = Self::get_config_name(true);
|
||||||
|
self._validate(&name, timestamp, config).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply(&self, config: &str) -> Result<(), AgentError> {
|
||||||
|
let timestamp = chrono::Utc::now().timestamp_millis();
|
||||||
|
let name = Self::get_config_name(false);
|
||||||
|
self._validate(&name, timestamp, config).await?;
|
||||||
|
self._apply(&name, timestamp).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
impl AgentServiceImpl {
|
||||||
pub fn get_client(&self) -> Arc<ApiClient> {
|
fn get_config_name(is_validate_only: bool) -> String {
|
||||||
Arc::clone(&self.client)
|
format!(
|
||||||
|
"nginx_config_{}{}",
|
||||||
|
if is_validate_only {
|
||||||
|
"validation_"
|
||||||
|
} else {
|
||||||
|
"application_"
|
||||||
|
},
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _validate(&self, name: &str, timestamp: i64, config: &str) -> Result<(), AgentError> {
|
||||||
|
let api = self.client.nginx_agent_api();
|
||||||
|
|
||||||
|
api.write_config(WriteConfigParams {
|
||||||
|
write_config_body: WriteConfigBody {
|
||||||
|
config_name: name.to_string(),
|
||||||
|
content: config.to_string(),
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
api.validate(ValidateParams {
|
||||||
|
validate_body: ValidateBody {
|
||||||
|
config_name: name.to_string(),
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _apply(&self, name: &str, timestamp: i64) -> Result<(), AgentError> {
|
||||||
|
let api = self.client.nginx_agent_api();
|
||||||
|
api.validate_and_reload(ValidateAndReloadParams {
|
||||||
|
validate_and_reload_body: ValidateAndReloadBody {
|
||||||
|
config_name: name.to_string(),
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +204,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_agent_service_creation() {
|
fn test_agent_service_creation() {
|
||||||
let config = Configuration::default();
|
let config = Configuration::default();
|
||||||
let service = AgentService::new(config);
|
let service = AgentServiceImpl::new(config);
|
||||||
let client = service.get_client();
|
let client = service.get_client();
|
||||||
assert!(Arc::ptr_eq(&client, &service.client));
|
assert!(Arc::ptr_eq(&client, &service.client));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,24 @@ pub mod upstream;
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::{DatabaseConnection, DatabaseTransaction};
|
||||||
|
|
||||||
use upstream::UpstreamService;
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
|
services::{
|
||||||
|
agent_client::AgentService,
|
||||||
|
nginx::{
|
||||||
|
builder::{NginxConfigBuilder, NginxConfigProvider},
|
||||||
|
upstream::{UpstreamService, UpstreamServiceImpl},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct NginxService {
|
pub struct NginxService {
|
||||||
|
#[allow(dead_code)]
|
||||||
connection: Arc<DatabaseConnection>,
|
connection: Arc<DatabaseConnection>,
|
||||||
//
|
//
|
||||||
upstream_service: Arc<UpstreamService>,
|
upstream_service: Arc<dyn UpstreamService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NginxService {
|
impl NginxService {
|
||||||
@@ -21,11 +31,56 @@ impl NginxService {
|
|||||||
Self {
|
Self {
|
||||||
connection: connection.clone(),
|
connection: connection.clone(),
|
||||||
//
|
//
|
||||||
upstream_service: Arc::new(UpstreamService::new(connection.clone())),
|
upstream_service: Arc::new(UpstreamServiceImpl::new(connection.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_upstream_service(&self) -> Arc<UpstreamService> {
|
pub fn get_upstream_service(&self) -> Arc<dyn UpstreamService> {
|
||||||
self.upstream_service.clone()
|
self.upstream_service.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn validate_config(
|
||||||
|
&self,
|
||||||
|
agent: Arc<dyn AgentService>,
|
||||||
|
config: &str,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
agent.validate(config).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn apply_changes(
|
||||||
|
&self,
|
||||||
|
agent: Arc<dyn AgentService>,
|
||||||
|
config: &str,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
agent.apply(config).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_config(
|
||||||
|
&self,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<String, ServiceError> {
|
||||||
|
let mut builder = NginxConfigBuilder::default();
|
||||||
|
self.upstream_service
|
||||||
|
.generate_config(&mut builder, tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(builder.to_nginx_config(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn regenerate_and_apply_config(
|
||||||
|
&self,
|
||||||
|
agent: Arc<dyn AgentService>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
let config = self.generate_config(tx).await?;
|
||||||
|
|
||||||
|
self.apply_changes(agent, &config).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,12 @@ pub trait NginxConfigProvider {
|
|||||||
fn to_nginx_config(&self, indent: Option<usize>) -> String;
|
fn to_nginx_config(&self, indent: Option<usize>) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct NginxConfigBuilder {
|
pub struct NginxConfigBuilder {
|
||||||
upstreams: Vec<UpstreamInfo>,
|
upstreams: Vec<UpstreamInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NginxConfigBuilder {
|
impl NginxConfigBuilder {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
upstreams: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_upstream(&mut self, upstream: UpstreamInfo) {
|
pub fn add_upstream(&mut self, upstream: UpstreamInfo) {
|
||||||
self.upstreams.push(upstream);
|
self.upstreams.push(upstream);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use optfield::optfield;
|
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
use sea_orm::ActiveValue::{Set, Unchanged};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -13,15 +13,14 @@ use crate::{
|
|||||||
set_if_some,
|
set_if_some,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[optfield(pub UpdateUpstreamInfo)]
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UpstreamInfo {
|
pub struct UpstreamInfo {
|
||||||
pub id: uuid::Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub protocol: String,
|
pub protocol: String,
|
||||||
pub algorithm: String,
|
pub algorithm: String,
|
||||||
pub sticky_session: bool,
|
pub sticky_session: bool,
|
||||||
pub created_by: Option<uuid::Uuid>,
|
pub created_by: Option<Uuid>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
//
|
//
|
||||||
@@ -33,11 +32,21 @@ pub struct UpstreamCreateInfo {
|
|||||||
pub protocol: String,
|
pub protocol: String,
|
||||||
pub algorithm: String,
|
pub algorithm: String,
|
||||||
pub sticky_session: bool,
|
pub sticky_session: bool,
|
||||||
pub created_by: Option<uuid::Uuid>,
|
pub created_by: Option<Uuid>,
|
||||||
//
|
//
|
||||||
pub upstream_targets: Vec<upstream_target_info::UpstreamTargetCreateInfo>,
|
pub upstream_targets: Vec<upstream_target_info::UpstreamTargetCreateInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UpdateUpstreamInfo {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub protocol: Option<String>,
|
||||||
|
pub algorithm: Option<String>,
|
||||||
|
pub sticky_session: Option<bool>,
|
||||||
|
//
|
||||||
|
pub upstream_targets: Option<Vec<(Uuid, bool)>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl NginxConfigProvider for UpstreamInfo {
|
impl NginxConfigProvider for UpstreamInfo {
|
||||||
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
||||||
let targets_config: Vec<String> = self
|
let targets_config: Vec<String> = self
|
||||||
@@ -142,18 +151,14 @@ impl From<UpstreamInfo> for (upstream::ActiveModel, Vec<upstream_target::ActiveM
|
|||||||
impl UpdateUpstreamInfo {
|
impl UpdateUpstreamInfo {
|
||||||
pub fn apply_to_model(self, current_model: upstream::Model) -> upstream::ActiveModel {
|
pub fn apply_to_model(self, current_model: upstream::Model) -> upstream::ActiveModel {
|
||||||
upstream::ActiveModel {
|
upstream::ActiveModel {
|
||||||
id: sea_orm::ActiveValue::Unchanged(current_model.id),
|
id: Unchanged(current_model.id),
|
||||||
name: set_if_some!(self.name),
|
name: set_if_some!(self.name),
|
||||||
protocol: set_if_some!(self.protocol),
|
protocol: set_if_some!(self.protocol),
|
||||||
algorithm: set_if_some!(self.algorithm),
|
algorithm: set_if_some!(self.algorithm),
|
||||||
sticky_session: set_if_some!(self.sticky_session),
|
sticky_session: set_if_some!(self.sticky_session),
|
||||||
created_by: set_if_some!(if self.created_by.is_some() {
|
created_by: Unchanged(current_model.created_by),
|
||||||
Some(self.created_by)
|
created_at: Unchanged(current_model.created_at),
|
||||||
} else {
|
updated_at: Set(chrono::Utc::now()),
|
||||||
None
|
|
||||||
}),
|
|
||||||
created_at: set_if_some!(self.created_at),
|
|
||||||
updated_at: set_if_some!(self.updated_at),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use optfield::optfield;
|
|
||||||
|
|
||||||
use sea_orm::ActiveValue::{Set, Unchanged};
|
use sea_orm::ActiveValue::{Set, Unchanged};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -11,7 +10,6 @@ use crate::{
|
|||||||
set_if_some,
|
set_if_some,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[optfield(pub UpdateUpstreamTargetInfo)]
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UpstreamTargetInfo {
|
pub struct UpstreamTargetInfo {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
@@ -27,6 +25,15 @@ pub struct UpstreamTargetInfo {
|
|||||||
pub upstream: Option<UpstreamBasicInfo>,
|
pub upstream: Option<UpstreamBasicInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UpdateUpstreamTargetInfo {
|
||||||
|
pub target_host: Option<String>,
|
||||||
|
pub target_port: Option<i64>,
|
||||||
|
pub weight: Option<i64>,
|
||||||
|
pub is_backup: Option<bool>,
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UpstreamBasicInfo {
|
pub struct UpstreamBasicInfo {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
@@ -146,9 +153,9 @@ impl UpdateUpstreamTargetInfo {
|
|||||||
weight: set_if_some!(self.weight),
|
weight: set_if_some!(self.weight),
|
||||||
is_backup: set_if_some!(self.is_backup),
|
is_backup: set_if_some!(self.is_backup),
|
||||||
enabled: set_if_some!(self.enabled),
|
enabled: set_if_some!(self.enabled),
|
||||||
created_at: set_if_some!(self.created_at),
|
created_at: Unchanged(current_model.created_at),
|
||||||
updated_at: set_if_some!(self.updated_at),
|
updated_at: Set(chrono::Utc::now()),
|
||||||
upstream_id: set_if_some!(self.upstream_id),
|
upstream_id: Unchanged(current_model.upstream_id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,83 @@ use database::generated::entities::{upstream, upstream_target};
|
|||||||
use crate::{
|
use crate::{
|
||||||
errors::service_error::ServiceError,
|
errors::service_error::ServiceError,
|
||||||
helpers::database::PaginationFilter,
|
helpers::database::PaginationFilter,
|
||||||
services::nginx::info::{
|
services::nginx::{
|
||||||
|
builder::NginxConfigBuilder,
|
||||||
|
info::{
|
||||||
upstream::{UpdateUpstreamInfo, UpstreamCreateInfo, UpstreamInfo},
|
upstream::{UpdateUpstreamInfo, UpstreamCreateInfo, UpstreamInfo},
|
||||||
upstream_target::{UpdateUpstreamTargetInfo, UpstreamTargetCreateInfo, UpstreamTargetInfo},
|
upstream_target::{
|
||||||
|
UpdateUpstreamTargetInfo, UpstreamTargetCreateInfo, UpstreamTargetInfo,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
with_conn,
|
with_conn,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct UpstreamService {
|
#[async_trait::async_trait]
|
||||||
|
pub trait UpstreamService: Send + Sync {
|
||||||
|
async fn create_upstream(
|
||||||
|
&self,
|
||||||
|
create_info: UpstreamCreateInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamInfo, ServiceError>;
|
||||||
|
async fn get_upstream(
|
||||||
|
&self,
|
||||||
|
upstream_id: uuid::Uuid,
|
||||||
|
options: Option<GetUpstreamOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamInfo, ServiceError>;
|
||||||
|
async fn get_upstreams(
|
||||||
|
&self,
|
||||||
|
pagination: Option<PaginationFilter>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<UpstreamInfo>, ServiceError>;
|
||||||
|
async fn update_upstream(
|
||||||
|
&self,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
upstream: UpdateUpstreamInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamInfo, ServiceError>;
|
||||||
|
async fn delete_upstream(
|
||||||
|
&self,
|
||||||
|
upstream_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError>;
|
||||||
|
async fn create_upstream_target(
|
||||||
|
&self,
|
||||||
|
create_info: UpstreamTargetCreateInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamTargetInfo, ServiceError>;
|
||||||
|
async fn get_upstream_target(
|
||||||
|
&self,
|
||||||
|
target_id: uuid::Uuid,
|
||||||
|
options: Option<GetUpstreamTargetOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamTargetInfo, ServiceError>;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn get_upstream_targets_by_upstream(
|
||||||
|
&self,
|
||||||
|
upstream_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<UpstreamTargetInfo>, ServiceError>;
|
||||||
|
async fn update_upstream_target(
|
||||||
|
&self,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
target: UpdateUpstreamTargetInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<UpstreamTargetInfo, ServiceError>;
|
||||||
|
async fn delete_upstream_target(
|
||||||
|
&self,
|
||||||
|
target_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError>;
|
||||||
|
async fn generate_config(
|
||||||
|
&self,
|
||||||
|
builder: &mut NginxConfigBuilder,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpstreamServiceImpl {
|
||||||
connection: Arc<DatabaseConnection>,
|
connection: Arc<DatabaseConnection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +100,15 @@ pub struct GetUpstreamTargetOptions {
|
|||||||
pub include_upstream: bool,
|
pub include_upstream: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpstreamService {
|
impl UpstreamServiceImpl {
|
||||||
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||||
Self { connection }
|
Self { connection }
|
||||||
}
|
}
|
||||||
//
|
}
|
||||||
//
|
|
||||||
pub async fn create_upstream(
|
#[async_trait::async_trait]
|
||||||
|
impl UpstreamService for UpstreamServiceImpl {
|
||||||
|
async fn create_upstream(
|
||||||
&self,
|
&self,
|
||||||
create_info: UpstreamCreateInfo,
|
create_info: UpstreamCreateInfo,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
@@ -79,7 +150,7 @@ impl UpstreamService {
|
|||||||
Ok(r.into())
|
Ok(r.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_upstream(
|
async fn get_upstream(
|
||||||
&self,
|
&self,
|
||||||
upstream_id: uuid::Uuid,
|
upstream_id: uuid::Uuid,
|
||||||
options: Option<GetUpstreamOptions>,
|
options: Option<GetUpstreamOptions>,
|
||||||
@@ -117,7 +188,7 @@ impl UpstreamService {
|
|||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_upstreams(
|
async fn get_upstreams(
|
||||||
&self,
|
&self,
|
||||||
pagination: Option<PaginationFilter>,
|
pagination: Option<PaginationFilter>,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
@@ -136,13 +207,22 @@ impl UpstreamService {
|
|||||||
Ok(r.into_iter().map(|m| m.into()).collect())
|
Ok(r.into_iter().map(|m| m.into()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_upstream(
|
async fn update_upstream(
|
||||||
&self,
|
&self,
|
||||||
id: uuid::Uuid,
|
id: uuid::Uuid,
|
||||||
upstream: UpdateUpstreamInfo,
|
upstream: UpdateUpstreamInfo,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<UpstreamInfo, ServiceError> {
|
) -> Result<UpstreamInfo, ServiceError> {
|
||||||
let current_model = with_conn!(&*self.connection, tx, conn, {
|
// 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 current_model = with_conn!(&*self.connection, tx_ref, conn, {
|
||||||
upstream::Entity::find_by_id(id)
|
upstream::Entity::find_by_id(id)
|
||||||
.one(*conn)
|
.one(*conn)
|
||||||
.await?
|
.await?
|
||||||
@@ -151,13 +231,40 @@ impl UpstreamService {
|
|||||||
id
|
id
|
||||||
)))?
|
)))?
|
||||||
});
|
});
|
||||||
let active_model = upstream.apply_to_model(current_model);
|
let upstream_active_model = upstream.clone().apply_to_model(current_model);
|
||||||
|
|
||||||
let r = active_model.update(&*self.connection).await?;
|
let r = with_conn!(&*self.connection, tx_ref, conn, {
|
||||||
|
let updated_upstream_model = upstream_active_model.update(*conn).await?;
|
||||||
|
|
||||||
|
// update upstream targets if any
|
||||||
|
if let Some(targets) = upstream.upstream_targets {
|
||||||
|
for (target_id, enabled) in targets.into_iter() {
|
||||||
|
let target_model = upstream_target::Entity::find_by_id(target_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Upstream target with id {} not found",
|
||||||
|
target_id
|
||||||
|
)))?;
|
||||||
|
let mut target_active_model: upstream_target::ActiveModel = target_model.into();
|
||||||
|
target_active_model.enabled = sea_orm::ActiveValue::Set(enabled);
|
||||||
|
|
||||||
|
target_active_model.update(*conn).await?;
|
||||||
|
Ok::<(), ServiceError>(())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated_upstream_model
|
||||||
|
});
|
||||||
|
|
||||||
|
// Commit
|
||||||
|
if let Some(t) = maybe_owned_tx.take() {
|
||||||
|
t.commit().await?;
|
||||||
|
}
|
||||||
Ok(r.into())
|
Ok(r.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_upstream(
|
async fn delete_upstream(
|
||||||
&self,
|
&self,
|
||||||
upstream_id: uuid::Uuid,
|
upstream_id: uuid::Uuid,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
@@ -172,6 +279,11 @@ impl UpstreamService {
|
|||||||
)))?
|
)))?
|
||||||
});
|
});
|
||||||
with_conn!(&*self.connection, tx, conn, {
|
with_conn!(&*self.connection, tx, conn, {
|
||||||
|
// delete all targets belonging to the upstream
|
||||||
|
upstream_target::Entity::delete_many()
|
||||||
|
.filter(upstream_target::Column::UpstreamId.eq(upstream_id))
|
||||||
|
.exec(*conn)
|
||||||
|
.await?;
|
||||||
model.delete(*conn).await?;
|
model.delete(*conn).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -179,7 +291,7 @@ impl UpstreamService {
|
|||||||
|
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
pub async fn create_upstream_target(
|
async fn create_upstream_target(
|
||||||
&self,
|
&self,
|
||||||
create_info: UpstreamTargetCreateInfo,
|
create_info: UpstreamTargetCreateInfo,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
@@ -189,7 +301,7 @@ impl UpstreamService {
|
|||||||
Ok(r.into())
|
Ok(r.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_upstream_target(
|
async fn get_upstream_target(
|
||||||
&self,
|
&self,
|
||||||
target_id: uuid::Uuid,
|
target_id: uuid::Uuid,
|
||||||
options: Option<GetUpstreamTargetOptions>,
|
options: Option<GetUpstreamTargetOptions>,
|
||||||
@@ -232,7 +344,7 @@ impl UpstreamService {
|
|||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_upstream_targets_by_upstream(
|
async fn get_upstream_targets_by_upstream(
|
||||||
&self,
|
&self,
|
||||||
upstream_id: uuid::Uuid,
|
upstream_id: uuid::Uuid,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
@@ -246,7 +358,7 @@ impl UpstreamService {
|
|||||||
Ok(r.into_iter().map(|m| m.into()).collect())
|
Ok(r.into_iter().map(|m| m.into()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_upstream_target(
|
async fn update_upstream_target(
|
||||||
&self,
|
&self,
|
||||||
id: uuid::Uuid,
|
id: uuid::Uuid,
|
||||||
target: UpdateUpstreamTargetInfo,
|
target: UpdateUpstreamTargetInfo,
|
||||||
@@ -267,7 +379,7 @@ impl UpstreamService {
|
|||||||
Ok(r.into())
|
Ok(r.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_upstream_target(
|
async fn delete_upstream_target(
|
||||||
&self,
|
&self,
|
||||||
target_id: uuid::Uuid,
|
target_id: uuid::Uuid,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
@@ -286,6 +398,26 @@ impl UpstreamService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn generate_config(
|
||||||
|
&self,
|
||||||
|
builder: &mut NginxConfigBuilder,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
// get all upstreams and their targets
|
||||||
|
let upstreams = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream::Entity::find()
|
||||||
|
.find_with_related(upstream_target::Entity)
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
});
|
||||||
|
let upstreams_info = upstreams
|
||||||
|
.into_iter()
|
||||||
|
.map(|(up_model, target_models)| (up_model, target_models).into())
|
||||||
|
.collect::<Vec<UpstreamInfo>>();
|
||||||
|
builder.add_upstreams(upstreams_info);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -315,7 +447,7 @@ mod tests {
|
|||||||
.append_query_results(vec![vec![up_model.clone()]])
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let create_info = crate::services::nginx::info::upstream::UpstreamCreateInfo {
|
let create_info = crate::services::nginx::info::upstream::UpstreamCreateInfo {
|
||||||
name: "test_upstream".to_string(),
|
name: "test_upstream".to_string(),
|
||||||
@@ -366,7 +498,7 @@ mod tests {
|
|||||||
.append_query_results(vec![vec![target_model.clone()]])
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let res = svc
|
let res = svc
|
||||||
.get_upstream(
|
.get_upstream(
|
||||||
@@ -391,7 +523,7 @@ mod tests {
|
|||||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let res = svc.get_upstream(uuid::Uuid::new_v4(), None, None).await;
|
let res = svc.get_upstream(uuid::Uuid::new_v4(), None, None).await;
|
||||||
|
|
||||||
@@ -425,7 +557,7 @@ mod tests {
|
|||||||
.append_query_results(vec![vec![u1.clone(), u2.clone()]])
|
.append_query_results(vec![vec![u1.clone(), u2.clone()]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let res = svc.get_upstreams(None, None).await;
|
let res = svc.get_upstreams(None, None).await;
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
@@ -453,7 +585,7 @@ mod tests {
|
|||||||
.append_query_results(vec![vec![t.clone()]])
|
.append_query_results(vec![vec![t.clone()]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let res = svc.get_upstream_targets_by_upstream(up_id, None).await;
|
let res = svc.get_upstream_targets_by_upstream(up_id, None).await;
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
@@ -491,17 +623,13 @@ mod tests {
|
|||||||
.append_query_results(vec![vec![updated.clone()]]) // update result
|
.append_query_results(vec![vec![updated.clone()]]) // update result
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let update_info = crate::services::nginx::info::upstream::UpdateUpstreamInfo {
|
let update_info = crate::services::nginx::info::upstream::UpdateUpstreamInfo {
|
||||||
id: None,
|
|
||||||
name: None,
|
name: None,
|
||||||
protocol: None,
|
protocol: None,
|
||||||
algorithm: None,
|
algorithm: None,
|
||||||
sticky_session: None,
|
sticky_session: None,
|
||||||
created_by: None,
|
|
||||||
created_at: None,
|
|
||||||
updated_at: None,
|
|
||||||
upstream_targets: None,
|
upstream_targets: None,
|
||||||
};
|
};
|
||||||
let res = svc.update_upstream(id, update_info, None).await;
|
let res = svc.update_upstream(id, update_info, None).await;
|
||||||
@@ -516,20 +644,17 @@ mod tests {
|
|||||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let res = svc
|
let res = svc
|
||||||
.update_upstream(
|
.update_upstream(
|
||||||
uuid::Uuid::new_v4(),
|
uuid::Uuid::new_v4(),
|
||||||
crate::services::nginx::info::upstream::UpdateUpstreamInfo {
|
crate::services::nginx::info::upstream::UpdateUpstreamInfo {
|
||||||
id: None,
|
|
||||||
name: None,
|
name: None,
|
||||||
protocol: None,
|
protocol: None,
|
||||||
algorithm: None,
|
algorithm: None,
|
||||||
sticky_session: None,
|
sticky_session: None,
|
||||||
created_by: None,
|
|
||||||
created_at: None,
|
|
||||||
updated_at: None,
|
|
||||||
upstream_targets: None,
|
upstream_targets: None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
@@ -555,13 +680,19 @@ mod tests {
|
|||||||
|
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![existing.clone()]])
|
.append_query_results(vec![vec![existing.clone()]])
|
||||||
.append_exec_results(vec![MockExecResult {
|
.append_exec_results(vec![
|
||||||
|
MockExecResult {
|
||||||
rows_affected: 1,
|
rows_affected: 1,
|
||||||
last_insert_id: 0,
|
last_insert_id: 0,
|
||||||
}])
|
},
|
||||||
|
MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let res = svc.delete_upstream(id, None).await;
|
let res = svc.delete_upstream(id, None).await;
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
@@ -573,7 +704,7 @@ mod tests {
|
|||||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let res = svc.delete_upstream(uuid::Uuid::new_v4(), None).await;
|
let res = svc.delete_upstream(uuid::Uuid::new_v4(), None).await;
|
||||||
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
@@ -599,7 +730,7 @@ mod tests {
|
|||||||
.append_query_results(vec![vec![created.clone()]])
|
.append_query_results(vec![vec![created.clone()]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let create_info = crate::services::nginx::info::upstream_target::UpstreamTargetCreateInfo {
|
let create_info = crate::services::nginx::info::upstream_target::UpstreamTargetCreateInfo {
|
||||||
target_host: "1.2.3.4".to_string(),
|
target_host: "1.2.3.4".to_string(),
|
||||||
@@ -647,19 +778,14 @@ mod tests {
|
|||||||
.append_query_results(vec![vec![updated.clone()]])
|
.append_query_results(vec![vec![updated.clone()]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
let update_info = crate::services::nginx::info::upstream_target::UpdateUpstreamTargetInfo {
|
let update_info = crate::services::nginx::info::upstream_target::UpdateUpstreamTargetInfo {
|
||||||
id: None,
|
|
||||||
target_host: None,
|
target_host: None,
|
||||||
target_port: None,
|
target_port: None,
|
||||||
weight: None,
|
weight: None,
|
||||||
is_backup: None,
|
is_backup: None,
|
||||||
enabled: 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;
|
let res = svc.update_upstream_target(id, update_info, None).await;
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
@@ -690,7 +816,7 @@ mod tests {
|
|||||||
}])
|
}])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamServiceImpl::new(Arc::new(db));
|
||||||
let res = svc.delete_upstream_target(id, None).await;
|
let res = svc.delete_upstream_target(id, None).await;
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,357 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/upstream_targets/{upstream_target_id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "get_upstream_target",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "upstream_target_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Get upstream target info",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamTargetInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "remove_upstream_target",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "upstream_target_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upstream target removed successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "update_upstream_target",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "upstream_target_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateUpstreamTargetRequestBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upstream target updated successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateUpstreamTargetInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid request"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/upstreams": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "get_upstream_list",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List upstreams",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "create_upstream",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateUpstreamRequestBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upstream created successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid request"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/upstreams/{upstream_id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "get_upstream",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "upstream_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Get upstream info",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "remove_upstream",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "upstream_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upstream removed successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "update_upstream",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "upstream_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateUpstreamRequestBody"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upstream updated successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateUpstreamInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid request"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/upstreams/{upstream_id}/targets": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Nginx"
|
||||||
|
],
|
||||||
|
"operationId": "add_upstream_target",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateUpstreamTargetInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upstream target created successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamTargetInfoResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid request"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/user/me": {
|
"/api/user/me": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -157,6 +508,102 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreateUpstreamRequestBody": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"protocol",
|
||||||
|
"upstream_targets"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"algorithm": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sticky_session": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstream_targets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamTargetInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CreateUpstreamTargetInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"upstream_id",
|
||||||
|
"host",
|
||||||
|
"port"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_backup": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"upstream_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GetUpstreamParams": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"include_targets": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GetUpstreamTargetsParams": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"include_upstream": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"HealthInfo": {
|
"HealthInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "System health information",
|
"description": "System health information",
|
||||||
@@ -212,6 +659,486 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"PaginationInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Pagination information included in API responses",
|
||||||
|
"required": [
|
||||||
|
"total_items",
|
||||||
|
"total_pages",
|
||||||
|
"current_page",
|
||||||
|
"per_page"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"current_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "Current page number",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"per_page": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "Items per page",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"total_items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Total number of items",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"total_pages": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "Total number of pages",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateUpstreamInfoResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"protocol",
|
||||||
|
"algorithm",
|
||||||
|
"sticky_session",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"upstream_targets"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"algorithm": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"created_by": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sticky_session": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"upstream_targets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamTargetBasicInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateUpstreamRequestBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"algorithm": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sticky_session": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstream_targets": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamTargetBasicUpdateInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateUpstreamTargetInfoResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"host",
|
||||||
|
"port",
|
||||||
|
"enabled",
|
||||||
|
"is_backup",
|
||||||
|
"weight",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"upstream_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"is_backup": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"upstream_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateUpstreamTargetRequestBody": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is_backup": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamBasicInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"protocol",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamInfoResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"protocol",
|
||||||
|
"algorithm",
|
||||||
|
"sticky_session",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"upstream_targets"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"algorithm": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"created_by": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sticky_session": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"upstream_targets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamTargetBasicInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"items",
|
||||||
|
"pagination"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UpstreamInfoResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"$ref": "#/components/schemas/PaginationInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamTargetBasicInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"target_host",
|
||||||
|
"target_port",
|
||||||
|
"enabled",
|
||||||
|
"is_backup",
|
||||||
|
"weight",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"is_backup": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"target_host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_port": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamTargetBasicUpdateInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamTargetInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"target_host",
|
||||||
|
"target_port",
|
||||||
|
"enabled",
|
||||||
|
"is_backup",
|
||||||
|
"weight",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"upstream_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"is_backup": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"target_host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_port": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"upstream": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UpstreamBasicInfo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstream_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpstreamTargetInfoResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"host",
|
||||||
|
"port",
|
||||||
|
"enabled",
|
||||||
|
"is_backup",
|
||||||
|
"weight",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"upstream_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"is_backup": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"upstream_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"UserInfo": {
|
"UserInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "System health information",
|
"description": "System health information",
|
||||||
@@ -245,6 +1172,10 @@
|
|||||||
{
|
{
|
||||||
"name": "User",
|
"name": "User",
|
||||||
"description": "User management API"
|
"description": "User management API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nginx",
|
||||||
|
"description": "Nginx management API"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,42 @@
|
|||||||
export namespace Schemas {
|
export namespace Schemas {
|
||||||
// <Schemas>
|
// <Schemas>
|
||||||
export type AdminInitRequest = { password: string; setup_secret: string; username: string };
|
export type AdminInitRequest = { password: string; setup_secret: string; username: string };
|
||||||
|
export type UpstreamBasicInfo = {
|
||||||
|
created_at: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
protocol: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
export type UpstreamTargetInfo = {
|
||||||
|
created_at: string;
|
||||||
|
enabled: boolean;
|
||||||
|
id: string;
|
||||||
|
is_backup: boolean;
|
||||||
|
target_host: string;
|
||||||
|
target_port: number;
|
||||||
|
updated_at: string;
|
||||||
|
upstream?: (null | UpstreamBasicInfo) | undefined;
|
||||||
|
upstream_id: string;
|
||||||
|
weight: number;
|
||||||
|
};
|
||||||
|
export type CreateUpstreamRequestBody = {
|
||||||
|
algorithm?: (string | null) | undefined;
|
||||||
|
name: string;
|
||||||
|
protocol: string;
|
||||||
|
sticky_session?: (boolean | null) | undefined;
|
||||||
|
upstream_targets: Array<UpstreamTargetInfo>;
|
||||||
|
};
|
||||||
|
export type CreateUpstreamTargetInfo = {
|
||||||
|
enabled?: (boolean | null) | undefined;
|
||||||
|
host: string;
|
||||||
|
is_backup?: (boolean | null) | undefined;
|
||||||
|
port: number;
|
||||||
|
upstream_id: string;
|
||||||
|
weight?: (number | null) | undefined;
|
||||||
|
};
|
||||||
|
export type GetUpstreamParams = Partial<{ include_targets: boolean | null }>;
|
||||||
|
export type GetUpstreamTargetsParams = Partial<{ include_upstream: boolean | null }>;
|
||||||
export type HealthInfo = {
|
export type HealthInfo = {
|
||||||
errors?: (Array<string> | null) | undefined;
|
errors?: (Array<string> | null) | undefined;
|
||||||
is_initialized: boolean;
|
is_initialized: boolean;
|
||||||
@@ -9,6 +45,77 @@ export namespace Schemas {
|
|||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
export type LoginRequest = { password: string; username: string };
|
export type LoginRequest = { password: string; username: string };
|
||||||
|
export type PaginationInfo = { current_page: number; per_page: number; total_items: number; total_pages: number };
|
||||||
|
export type UpstreamTargetBasicInfo = {
|
||||||
|
created_at: string;
|
||||||
|
enabled: boolean;
|
||||||
|
id: string;
|
||||||
|
is_backup: boolean;
|
||||||
|
target_host: string;
|
||||||
|
target_port: number;
|
||||||
|
updated_at: string;
|
||||||
|
weight: number;
|
||||||
|
};
|
||||||
|
export type UpdateUpstreamInfoResponse = {
|
||||||
|
algorithm: string;
|
||||||
|
created_at: string;
|
||||||
|
created_by?: (string | null) | undefined;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
protocol: string;
|
||||||
|
sticky_session: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
upstream_targets: Array<UpstreamTargetBasicInfo>;
|
||||||
|
};
|
||||||
|
export type UpdateUpstreamRequestBody = Partial<{
|
||||||
|
algorithm: string | null;
|
||||||
|
name: string | null;
|
||||||
|
protocol: string | null;
|
||||||
|
sticky_session: boolean | null;
|
||||||
|
upstream_targets: Array<UpstreamTargetBasicUpdateInfo> | null;
|
||||||
|
}>;
|
||||||
|
export type UpdateUpstreamTargetInfoResponse = {
|
||||||
|
created_at: string;
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
id: string;
|
||||||
|
is_backup: boolean;
|
||||||
|
port: number;
|
||||||
|
updated_at: string;
|
||||||
|
upstream_id: string;
|
||||||
|
weight: number;
|
||||||
|
};
|
||||||
|
export type UpdateUpstreamTargetRequestBody = Partial<{
|
||||||
|
enabled: boolean | null;
|
||||||
|
host: string | null;
|
||||||
|
is_backup: boolean | null;
|
||||||
|
port: number | null;
|
||||||
|
weight: number | null;
|
||||||
|
}>;
|
||||||
|
export type UpstreamInfoResponse = {
|
||||||
|
algorithm: string;
|
||||||
|
created_at: string;
|
||||||
|
created_by?: (string | null) | undefined;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
protocol: string;
|
||||||
|
sticky_session: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
upstream_targets: Array<UpstreamTargetBasicInfo>;
|
||||||
|
};
|
||||||
|
export type UpstreamListResponse = { items: Array<UpstreamInfoResponse>; pagination: PaginationInfo };
|
||||||
|
export type UpstreamTargetBasicUpdateInfo = { enabled: boolean; id: number };
|
||||||
|
export type UpstreamTargetInfoResponse = {
|
||||||
|
created_at: string;
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
id: string;
|
||||||
|
is_backup: boolean;
|
||||||
|
port: number;
|
||||||
|
updated_at: string;
|
||||||
|
upstream_id: string;
|
||||||
|
weight: number;
|
||||||
|
};
|
||||||
export type UserInfo = { id: string; username: string };
|
export type UserInfo = { id: string; username: string };
|
||||||
|
|
||||||
// </Schemas>
|
// </Schemas>
|
||||||
@@ -42,6 +149,95 @@ export namespace Endpoints {
|
|||||||
parameters: never;
|
parameters: never;
|
||||||
responses: { 200: Schemas.HealthInfo; 404: unknown };
|
responses: { 200: Schemas.HealthInfo; 404: unknown };
|
||||||
};
|
};
|
||||||
|
export type get_Get_upstream_target = {
|
||||||
|
method: "GET";
|
||||||
|
path: "/api/upstream_targets/{upstream_target_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { upstream_target_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.UpstreamTargetInfo; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type delete_Remove_upstream_target = {
|
||||||
|
method: "DELETE";
|
||||||
|
path: "/api/upstream_targets/{upstream_target_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { upstream_target_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: unknown; 401: unknown; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type patch_Update_upstream_target = {
|
||||||
|
method: "PATCH";
|
||||||
|
path: "/api/upstream_targets/{upstream_target_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { upstream_target_id: string };
|
||||||
|
|
||||||
|
body: Schemas.UpdateUpstreamTargetRequestBody;
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
200: Schemas.UpdateUpstreamTargetInfoResponse;
|
||||||
|
401: unknown;
|
||||||
|
404: unknown;
|
||||||
|
422: unknown;
|
||||||
|
500: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type get_Get_upstream_list = {
|
||||||
|
method: "GET";
|
||||||
|
path: "/api/upstreams";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: never;
|
||||||
|
responses: { 200: Schemas.UpstreamListResponse; 500: unknown };
|
||||||
|
};
|
||||||
|
export type post_Create_upstream = {
|
||||||
|
method: "POST";
|
||||||
|
path: "/api/upstreams";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
body: Schemas.CreateUpstreamRequestBody;
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.UpstreamInfoResponse; 401: unknown; 422: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type get_Get_upstream = {
|
||||||
|
method: "GET";
|
||||||
|
path: "/api/upstreams/{upstream_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { upstream_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.UpstreamInfoResponse; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type delete_Remove_upstream = {
|
||||||
|
method: "DELETE";
|
||||||
|
path: "/api/upstreams/{upstream_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { upstream_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: unknown; 401: unknown; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type patch_Update_upstream = {
|
||||||
|
method: "PATCH";
|
||||||
|
path: "/api/upstreams/{upstream_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { upstream_id: string };
|
||||||
|
|
||||||
|
body: Schemas.UpdateUpstreamRequestBody;
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.UpdateUpstreamInfoResponse; 401: unknown; 404: unknown; 422: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type post_Add_upstream_target = {
|
||||||
|
method: "POST";
|
||||||
|
path: "/api/upstreams/{upstream_id}/targets";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
body: Schemas.CreateUpstreamTargetInfo;
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.UpstreamTargetInfoResponse; 401: unknown; 422: unknown; 500: unknown };
|
||||||
|
};
|
||||||
export type get_Get_user_info = {
|
export type get_Get_user_info = {
|
||||||
method: "GET";
|
method: "GET";
|
||||||
path: "/api/user/me";
|
path: "/api/user/me";
|
||||||
@@ -58,11 +254,24 @@ export type EndpointByMethod = {
|
|||||||
post: {
|
post: {
|
||||||
"/api/auth/init_admin": Endpoints.post_Init_admin;
|
"/api/auth/init_admin": Endpoints.post_Init_admin;
|
||||||
"/api/auth/login": Endpoints.post_Login;
|
"/api/auth/login": Endpoints.post_Login;
|
||||||
|
"/api/upstreams": Endpoints.post_Create_upstream;
|
||||||
|
"/api/upstreams/{upstream_id}/targets": Endpoints.post_Add_upstream_target;
|
||||||
};
|
};
|
||||||
get: {
|
get: {
|
||||||
"/api/health/info": Endpoints.get_Get_health_info;
|
"/api/health/info": Endpoints.get_Get_health_info;
|
||||||
|
"/api/upstream_targets/{upstream_target_id}": Endpoints.get_Get_upstream_target;
|
||||||
|
"/api/upstreams": Endpoints.get_Get_upstream_list;
|
||||||
|
"/api/upstreams/{upstream_id}": Endpoints.get_Get_upstream;
|
||||||
"/api/user/me": Endpoints.get_Get_user_info;
|
"/api/user/me": Endpoints.get_Get_user_info;
|
||||||
};
|
};
|
||||||
|
delete: {
|
||||||
|
"/api/upstream_targets/{upstream_target_id}": Endpoints.delete_Remove_upstream_target;
|
||||||
|
"/api/upstreams/{upstream_id}": Endpoints.delete_Remove_upstream;
|
||||||
|
};
|
||||||
|
patch: {
|
||||||
|
"/api/upstream_targets/{upstream_target_id}": Endpoints.patch_Update_upstream_target;
|
||||||
|
"/api/upstreams/{upstream_id}": Endpoints.patch_Update_upstream;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// </EndpointByMethod>
|
// </EndpointByMethod>
|
||||||
@@ -70,6 +279,8 @@ export type EndpointByMethod = {
|
|||||||
// <EndpointByMethod.Shorthands>
|
// <EndpointByMethod.Shorthands>
|
||||||
export type PostEndpoints = EndpointByMethod["post"];
|
export type PostEndpoints = EndpointByMethod["post"];
|
||||||
export type GetEndpoints = EndpointByMethod["get"];
|
export type GetEndpoints = EndpointByMethod["get"];
|
||||||
|
export type DeleteEndpoints = EndpointByMethod["delete"];
|
||||||
|
export type PatchEndpoints = EndpointByMethod["patch"];
|
||||||
// </EndpointByMethod.Shorthands>
|
// </EndpointByMethod.Shorthands>
|
||||||
|
|
||||||
// <ApiClientTypes>
|
// <ApiClientTypes>
|
||||||
@@ -364,6 +575,68 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
// </ApiClient.get>
|
// </ApiClient.get>
|
||||||
|
|
||||||
|
// <ApiClient.delete>
|
||||||
|
delete<Path extends keyof DeleteEndpoints, TEndpoint extends DeleteEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<
|
||||||
|
TEndpoint extends { parameters: infer UParams }
|
||||||
|
? NotNever<UParams> extends true
|
||||||
|
? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
||||||
|
>
|
||||||
|
): Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>;
|
||||||
|
|
||||||
|
delete<Path extends keyof DeleteEndpoints, TEndpoint extends DeleteEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<
|
||||||
|
TEndpoint extends { parameters: infer UParams }
|
||||||
|
? NotNever<UParams> extends true
|
||||||
|
? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
||||||
|
>
|
||||||
|
): Promise<SafeApiResponse<TEndpoint>>;
|
||||||
|
|
||||||
|
delete<Path extends keyof DeleteEndpoints, _TEndpoint extends DeleteEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<any>
|
||||||
|
): Promise<any> {
|
||||||
|
return this.request("delete", path, ...params);
|
||||||
|
}
|
||||||
|
// </ApiClient.delete>
|
||||||
|
|
||||||
|
// <ApiClient.patch>
|
||||||
|
patch<Path extends keyof PatchEndpoints, TEndpoint extends PatchEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<
|
||||||
|
TEndpoint extends { parameters: infer UParams }
|
||||||
|
? NotNever<UParams> extends true
|
||||||
|
? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean }
|
||||||
|
>
|
||||||
|
): Promise<Extract<InferResponseByStatus<TEndpoint, SuccessStatusCode>, { data: {} }>["data"]>;
|
||||||
|
|
||||||
|
patch<Path extends keyof PatchEndpoints, TEndpoint extends PatchEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<
|
||||||
|
TEndpoint extends { parameters: infer UParams }
|
||||||
|
? NotNever<UParams> extends true
|
||||||
|
? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
||||||
|
: { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean }
|
||||||
|
>
|
||||||
|
): Promise<SafeApiResponse<TEndpoint>>;
|
||||||
|
|
||||||
|
patch<Path extends keyof PatchEndpoints, _TEndpoint extends PatchEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<any>
|
||||||
|
): Promise<any> {
|
||||||
|
return this.request("patch", path, ...params);
|
||||||
|
}
|
||||||
|
// </ApiClient.patch>
|
||||||
|
|
||||||
// <ApiClient.request>
|
// <ApiClient.request>
|
||||||
/**
|
/**
|
||||||
* Generic request method with full type-safety for any endpoint
|
* Generic request method with full type-safety for any endpoint
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const createQueryKey = <TOptions extends EndpointParameters>(
|
|||||||
// <EndpointByMethod.Shorthands>
|
// <EndpointByMethod.Shorthands>
|
||||||
export type PostEndpoints = EndpointByMethod["post"];
|
export type PostEndpoints = EndpointByMethod["post"];
|
||||||
export type GetEndpoints = EndpointByMethod["get"];
|
export type GetEndpoints = EndpointByMethod["get"];
|
||||||
|
export type DeleteEndpoints = EndpointByMethod["delete"];
|
||||||
|
export type PatchEndpoints = EndpointByMethod["patch"];
|
||||||
// </EndpointByMethod.Shorthands>
|
// </EndpointByMethod.Shorthands>
|
||||||
|
|
||||||
// <ApiClientTypes>
|
// <ApiClientTypes>
|
||||||
@@ -130,6 +132,66 @@ export class TanstackQueryApiClient {
|
|||||||
}
|
}
|
||||||
// </ApiClient.get>
|
// </ApiClient.get>
|
||||||
|
|
||||||
|
// <ApiClient.delete>
|
||||||
|
delete<Path extends keyof DeleteEndpoints, TEndpoint extends DeleteEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<TEndpoint["parameters"]>
|
||||||
|
) {
|
||||||
|
const queryKey = createQueryKey(path as string, params[0]);
|
||||||
|
const query = {
|
||||||
|
/** type-only property if you need easy access to the endpoint params */
|
||||||
|
"~endpoint": {} as TEndpoint,
|
||||||
|
queryKey,
|
||||||
|
queryFn: {} as "You need to pass .queryOptions to the useQuery hook",
|
||||||
|
queryOptions: queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const requestParams = {
|
||||||
|
...(params[0] || {}),
|
||||||
|
...(queryKey[0] || {}),
|
||||||
|
overrides: { signal },
|
||||||
|
withResponse: false as const,
|
||||||
|
};
|
||||||
|
const res = await this.client.delete(path, requestParams as never);
|
||||||
|
return res as InferResponseData<TEndpoint, SuccessStatusCode>;
|
||||||
|
},
|
||||||
|
queryKey: queryKey,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
// </ApiClient.delete>
|
||||||
|
|
||||||
|
// <ApiClient.patch>
|
||||||
|
patch<Path extends keyof PatchEndpoints, TEndpoint extends PatchEndpoints[Path]>(
|
||||||
|
path: Path,
|
||||||
|
...params: MaybeOptionalArg<TEndpoint["parameters"]>
|
||||||
|
) {
|
||||||
|
const queryKey = createQueryKey(path as string, params[0]);
|
||||||
|
const query = {
|
||||||
|
/** type-only property if you need easy access to the endpoint params */
|
||||||
|
"~endpoint": {} as TEndpoint,
|
||||||
|
queryKey,
|
||||||
|
queryFn: {} as "You need to pass .queryOptions to the useQuery hook",
|
||||||
|
queryOptions: queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const requestParams = {
|
||||||
|
...(params[0] || {}),
|
||||||
|
...(queryKey[0] || {}),
|
||||||
|
overrides: { signal },
|
||||||
|
withResponse: false as const,
|
||||||
|
};
|
||||||
|
const res = await this.client.patch(path, requestParams as never);
|
||||||
|
return res as InferResponseData<TEndpoint, SuccessStatusCode>;
|
||||||
|
},
|
||||||
|
queryKey: queryKey,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
// </ApiClient.patch>
|
||||||
|
|
||||||
// <ApiClient.request>
|
// <ApiClient.request>
|
||||||
/**
|
/**
|
||||||
* Generic mutation method with full type-safety for any endpoint; it doesnt require parameters to be passed initially
|
* Generic mutation method with full type-safety for any endpoint; it doesnt require parameters to be passed initially
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
//! `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 {}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
//! `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,8 +3,6 @@
|
|||||||
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;
|
||||||
pub mod upstream_target;
|
pub mod upstream_target;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
//! `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::Entity as Upstream;
|
||||||
pub use super::upstream_target::Entity as UpstreamTarget;
|
pub use super::upstream_target::Entity as UpstreamTarget;
|
||||||
pub use super::user::Entity as User;
|
pub use super::user::Entity as User;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ impl MigratorTrait for Migrator {
|
|||||||
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_000004_create_upstream_table::Migration),
|
||||||
Box::new(m20251223_000005_create_upstream_target_table::Migration),
|
Box::new(m20251223_000005_create_upstream_target_table::Migration),
|
||||||
Box::new(m20251230_000006_create_editing_session_table::Migration),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ 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_000004_create_upstream_table;
|
||||||
pub mod m20251223_000005_create_upstream_target_table;
|
pub mod m20251223_000005_create_upstream_target_table;
|
||||||
pub mod m20251230_000006_create_editing_session_table;
|
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
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