Compare commits
11 Commits
d184261027
...
feature/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d21459802c | ||
|
|
5e1a8364c7 | ||
|
|
3be9ecc4c1 | ||
|
|
545bc66f8c | ||
|
|
75097a661b | ||
|
|
9860dddf60 | ||
|
|
c4634b18f9 | ||
|
|
a0b4df745e | ||
|
|
46801fba99 | ||
|
|
cb65d4e9f7 | ||
|
|
10cc8f9d97 |
@@ -1,14 +1,17 @@
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use sea_orm::DbErr;
|
use sea_orm::DbErr;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::errors::service_error::ServiceError;
|
use crate::errors::service_error::ServiceError;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ApiError {
|
pub enum ApiError {
|
||||||
ServiceError(ServiceError),
|
ServiceError(ServiceError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ServiceError> for ApiError {
|
impl From<ServiceError> for ApiError {
|
||||||
fn from(err: ServiceError) -> Self {
|
fn from(err: ServiceError) -> Self {
|
||||||
|
error!("Service error occurred: {:?}", err);
|
||||||
ApiError::ServiceError(err)
|
ApiError::ServiceError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +24,7 @@ impl From<DbErr> for ApiError {
|
|||||||
|
|
||||||
impl IntoResponse for ApiError {
|
impl IntoResponse for ApiError {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
error!("API error occurred: {:?}", self);
|
||||||
match self {
|
match self {
|
||||||
ApiError::ServiceError(service_error) => service_error.into_response(),
|
ApiError::ServiceError(service_error) => service_error.into_response(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use axum::{
|
|||||||
http::{HeaderValue, Method, StatusCode, Uri},
|
http::{HeaderValue, Method, StatusCode, Uri},
|
||||||
};
|
};
|
||||||
use tower::{ServiceBuilder, timeout::TimeoutLayer};
|
use tower::{ServiceBuilder, timeout::TimeoutLayer};
|
||||||
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
|
use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{configs::server::CORSSettings, routes::AppState};
|
use crate::{configs::server::CORSSettings, routes::AppState};
|
||||||
@@ -34,6 +34,7 @@ pub fn apply_root_middleware(
|
|||||||
pub fn get_cors_layer(cors_settings: Arc<CORSSettings>) -> CorsLayer {
|
pub fn get_cors_layer(cors_settings: Arc<CORSSettings>) -> CorsLayer {
|
||||||
let mut cors_layer = CorsLayer::new()
|
let mut cors_layer = CorsLayer::new()
|
||||||
.allow_credentials(true)
|
.allow_credentials(true)
|
||||||
|
.allow_methods(AllowMethods::mirror_request())
|
||||||
.allow_headers(AllowHeaders::mirror_request());
|
.allow_headers(AllowHeaders::mirror_request());
|
||||||
|
|
||||||
let allowed_origins = &cors_settings.allowed_origins;
|
let allowed_origins = &cors_settings.allowed_origins;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ impl From<CreateUpstreamRequestBody> for ConcreteCreateUpstreamRequestBody {
|
|||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/api/upstreams",
|
path = "/api/nginx/upstreams",
|
||||||
request_body = CreateUpstreamRequestBody,
|
request_body = CreateUpstreamRequestBody,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Upstream created successfully", body = UpstreamInfoResponse),
|
(status = 200, description = "Upstream created successfully", body = UpstreamInfoResponse),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl From<CreateUpstreamTargetInfo> for ConcreteCreateUpstreamTargetInfo {
|
|||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/api/upstreams/{upstream_id}/targets",
|
path = "/api/nginx/upstreams/{upstream_id}/targets",
|
||||||
request_body = CreateUpstreamTargetInfo,
|
request_body = CreateUpstreamTargetInfo,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Upstream target created successfully", body = UpstreamTargetInfoResponse),
|
(status = 200, description = "Upstream target created successfully", body = UpstreamTargetInfoResponse),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ impl From<GetUpstreamParams> for ConcreteGetUpstreamParams {
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/upstreams",
|
path = "/api/nginx/upstreams",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "List upstreams", body = UpstreamListResponse),
|
(status = 200, description = "List upstreams", body = UpstreamListResponse),
|
||||||
(status = 500, description = "Internal server error"),
|
(status = 500, description = "Internal server error"),
|
||||||
@@ -54,16 +54,32 @@ pub async fn get_upstream_list(
|
|||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> AxumResult<Json<UpstreamListResponse>, ServiceError> {
|
) -> AxumResult<Json<UpstreamListResponse>, ServiceError> {
|
||||||
let upstream_service = &state.service.nginx.get_upstream_service();
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
let upstreams = upstream_service
|
|
||||||
.get_upstreams(Some(pagination.clone().into()), None)
|
let (upstreams_res, upstream_count_res) = tokio::join!(
|
||||||
.await?;
|
upstream_service.get_upstreams(
|
||||||
|
Some(pagination.clone().into()),
|
||||||
|
Some(GetUpstreamOptions {
|
||||||
|
include_targets: true,
|
||||||
|
filter_by_enabled: false,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
upstream_service.get_total_upstreams(None, None),
|
||||||
|
);
|
||||||
|
|
||||||
|
let upstreams = upstreams_res?;
|
||||||
|
let upstream_count = upstream_count_res?;
|
||||||
|
|
||||||
//
|
//
|
||||||
Ok(Json(UpstreamListResponse {
|
Ok(Json(UpstreamListResponse {
|
||||||
items: upstreams.into_iter().map(|u| u.into()).collect(),
|
items: upstreams.into_iter().map(|u| u.into()).collect(),
|
||||||
pagination: PaginationInfo {
|
pagination: PaginationInfo {
|
||||||
total_items: 0,
|
total_items: upstream_count,
|
||||||
total_pages: 0,
|
total_pages: if upstream_count == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(upstream_count as f32 / pagination.per_page as f32).ceil() as u32
|
||||||
|
},
|
||||||
current_page: pagination.page,
|
current_page: pagination.page,
|
||||||
per_page: pagination.per_page,
|
per_page: pagination.per_page,
|
||||||
},
|
},
|
||||||
@@ -72,7 +88,7 @@ pub async fn get_upstream_list(
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/upstreams/{upstream_id}",
|
path = "/api/nginx/upstreams/{upstream_id}",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get upstream info", body = UpstreamInfoResponse),
|
(status = 200, description = "Get upstream info", body = UpstreamInfoResponse),
|
||||||
(status = 404, description = "Not found"),
|
(status = 404, description = "Not found"),
|
||||||
@@ -93,6 +109,7 @@ pub async fn get_upstream(
|
|||||||
upstream_id,
|
upstream_id,
|
||||||
Some(GetUpstreamOptions {
|
Some(GetUpstreamOptions {
|
||||||
include_targets: true,
|
include_targets: true,
|
||||||
|
filter_by_enabled: false,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -110,19 +127,21 @@ pub async fn get_upstream(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::Arc;
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, Value};
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
use crate::configs::{FromConfig, ProgramSettings};
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
use crate::routes::api::restricted::nginx::upstream::get_upstream_router;
|
routes::api::restricted::nginx::upstream::{
|
||||||
use crate::routes::api::restricted::nginx::upstream::info::response::UpstreamInfoResponse;
|
get_upstream_router, info::response::UpstreamInfoResponse,
|
||||||
use crate::services::get_app_service;
|
},
|
||||||
|
services::get_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();
|
||||||
@@ -159,7 +178,14 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![u1.clone(), u2.clone()]])
|
.append_query_results(vec![vec![
|
||||||
|
(u1.clone(), None::<upstream_target::Model>),
|
||||||
|
(u2.clone(), None::<upstream_target::Model>),
|
||||||
|
]])
|
||||||
|
.append_query_results(vec![vec![BTreeMap::from([(
|
||||||
|
"count".to_string(),
|
||||||
|
Value::BigInt(Some(2)),
|
||||||
|
)])]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
@@ -170,6 +196,7 @@ mod tests {
|
|||||||
let body = res.json::<UpstreamListResponse>();
|
let body = res.json::<UpstreamListResponse>();
|
||||||
assert_eq!(body.items.len(), 2);
|
assert_eq!(body.items.len(), 2);
|
||||||
assert_eq!(body.pagination.current_page, 1u32);
|
assert_eq!(body.pagination.current_page, 1u32);
|
||||||
|
assert_eq!(body.pagination.total_pages, 1u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -222,6 +249,10 @@ mod tests {
|
|||||||
async fn extractor_pagination_validation_rejects_bad_values() {
|
async fn extractor_pagination_validation_rejects_bad_values() {
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.append_query_results(vec![vec![BTreeMap::from([(
|
||||||
|
"count".to_string(),
|
||||||
|
Value::BigInt(Some(0)),
|
||||||
|
)])]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
let server = TestServer::new(router).expect("failed to create test server");
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
@@ -293,6 +324,10 @@ mod tests {
|
|||||||
async fn handler_get_upstream_list_empty_returns_empty_items() {
|
async fn handler_get_upstream_list_empty_returns_empty_items() {
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.append_query_results(vec![vec![BTreeMap::from([(
|
||||||
|
"count".to_string(),
|
||||||
|
Value::BigInt(Some(0)),
|
||||||
|
)])]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ impl From<GetUpstreamTargetsParams> for ConcreteGetUpstreamTargetsParams {
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/upstream_targets/{upstream_target_id}",
|
path = "/api/nginx/upstream_targets/{upstream_target_id}",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Get upstream target info", body = UpstreamTargetInfo),
|
(status = 200, description = "Get upstream target info", body = UpstreamTargetInfo),
|
||||||
(status = 404, description = "Not found"),
|
(status = 404, description = "Not found"),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use crate::{
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
path = "/api/upstreams/{upstream_id}",
|
path = "/api/nginx/upstreams/{upstream_id}",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Upstream removed successfully", body = ()),
|
(status = 200, description = "Upstream removed successfully", body = ()),
|
||||||
(status = 401, description = "Unauthorized"),
|
(status = 401, description = "Unauthorized"),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use crate::{
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
path = "/api/upstream_targets/{upstream_target_id}",
|
path = "/api/nginx/upstream_targets/{upstream_target_id}",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Upstream target removed successfully", body = ()),
|
(status = 200, description = "Upstream target removed successfully", body = ()),
|
||||||
(status = 401, description = "Unauthorized"),
|
(status = 401, description = "Unauthorized"),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ impl From<UpdateUpstreamRequestBody> for UpdateUpstreamInfo {
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
patch,
|
patch,
|
||||||
path = "/api/upstreams/{upstream_id}",
|
path = "/api/nginx/upstreams/{upstream_id}",
|
||||||
request_body = UpdateUpstreamRequestBody,
|
request_body = UpdateUpstreamRequestBody,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Upstream updated successfully", body = UpdateUpstreamInfoResponse),
|
(status = 200, description = "Upstream updated successfully", body = UpdateUpstreamInfoResponse),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ impl From<UpdateUpstreamTargetRequestBody> for UpdateUpstreamTargetInfo {
|
|||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
patch,
|
patch,
|
||||||
path = "/api/upstream_targets/{upstream_target_id}",
|
path = "/api/nginx/upstream_targets/{upstream_target_id}",
|
||||||
request_body = UpdateUpstreamTargetRequestBody,
|
request_body = UpdateUpstreamTargetRequestBody,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Upstream target updated successfully", body = UpdateUpstreamTargetInfoResponse),
|
(status = 200, description = "Upstream target updated successfully", body = UpdateUpstreamTargetInfoResponse),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Json,
|
Json,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
middlewares::request_info::RequestInfo,
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
routes::{AppState, api::openapi::tag::USER_TAG},
|
routes::{AppState, api::openapi::tag::USER_TAG},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,15 +38,9 @@ pub struct UserInfo {
|
|||||||
)]
|
)]
|
||||||
pub async fn get_user_info(
|
pub async fn get_user_info(
|
||||||
State(app_state): State<Arc<AppState>>,
|
State(app_state): State<Arc<AppState>>,
|
||||||
request_info: Extension<Arc<RequestInfo>>,
|
request_info: AuthenticatedRequestInfo,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let user_id = match request_info.user_id {
|
let user_id = request_info.user_id;
|
||||||
Some(id) => id,
|
|
||||||
None => {
|
|
||||||
error!("User ID not found in request info");
|
|
||||||
return (StatusCode::UNAUTHORIZED).into_response();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match app_state.service.user.get_user_by_id(user_id, None).await {
|
match app_state.service.user.get_user_by_id(user_id, None).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use agent_client::{
|
|||||||
},
|
},
|
||||||
models::{ValidateAndReloadBody, ValidateBody, WriteConfigBody},
|
models::{ValidateAndReloadBody, ValidateBody, WriteConfigBody},
|
||||||
};
|
};
|
||||||
use tracing::warn;
|
use tracing::{error, warn};
|
||||||
|
|
||||||
use crate::{configs::agent::AgentSettings, errors::service_error::ServiceError};
|
use crate::{configs::agent::AgentSettings, errors::service_error::ServiceError};
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ pub enum AgentError {
|
|||||||
|
|
||||||
impl From<AgentError> for ServiceError {
|
impl From<AgentError> for ServiceError {
|
||||||
fn from(err: AgentError) -> Self {
|
fn from(err: AgentError) -> Self {
|
||||||
|
error!("Agent error occurred: {:?}", err);
|
||||||
match err {
|
match err {
|
||||||
AgentError::ValidationFailed(_internal, user) => ServiceError::InternalError(user),
|
AgentError::ValidationFailed(_internal, user) => ServiceError::InternalError(user),
|
||||||
AgentError::ApplicationFailed(_internal, user) => ServiceError::InternalError(user),
|
AgentError::ApplicationFailed(_internal, user) => ServiceError::InternalError(user),
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ impl NginxConfigProvider for NginxConfigBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add other sections like servers, locations, etc.
|
// TODO: Add other sections like servers, locations, etc.
|
||||||
|
// trailing newline for file ending
|
||||||
|
config.push('\n');
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
use sea_orm::ActiveValue::{Set, Unchanged};
|
use sea_orm::ActiveValue::{Set, Unchanged};
|
||||||
|
use tracing::warn;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -13,6 +14,8 @@ use crate::{
|
|||||||
set_if_some,
|
set_if_some,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PLACEHOLDER_TARGET: &str = "server 127.0.0.1:65535 down; # placeholder target";
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UpstreamInfo {
|
pub struct UpstreamInfo {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
@@ -55,12 +58,53 @@ impl NginxConfigProvider for UpstreamInfo {
|
|||||||
.map(|target| target.to_nginx_config(Some(indent.unwrap_or(0) + INDENT_SIZE)))
|
.map(|target| target.to_nginx_config(Some(indent.unwrap_or(0) + INDENT_SIZE)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
format!(
|
let mut targets_config_str = {
|
||||||
"upstream {} {{\n{}\n}}",
|
let config_str = match self.algorithm.as_str() {
|
||||||
self.name,
|
"least-conn" => "least_conn",
|
||||||
targets_config.join("\n".indent(indent.unwrap_or(0) + INDENT_SIZE).as_str())
|
"ip-hash" => "ip_hash",
|
||||||
)
|
"round-robin" => "",
|
||||||
.indent(indent.unwrap_or(0))
|
v => {
|
||||||
|
// TODO: allow arbitrary algorithms via config extensions/plugins
|
||||||
|
warn!(
|
||||||
|
"Unknown upstream algorithm '{}', defaulting to 'round-robin'",
|
||||||
|
v
|
||||||
|
);
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
// TODO: add support for sticky session / checking for nginx sticky module existence
|
||||||
|
// if self.sticky_session {
|
||||||
|
// config_str.push_str("sticky")
|
||||||
|
// }
|
||||||
|
if config_str.trim().is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
config_str + ";"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.indent(indent.unwrap_or(0) + INDENT_SIZE * 2);
|
||||||
|
targets_config_str.push('\n');
|
||||||
|
|
||||||
|
targets_config_str.push_str(
|
||||||
|
&(if targets_config.is_empty() {
|
||||||
|
// add placeholder if no targets
|
||||||
|
PLACEHOLDER_TARGET.to_string()
|
||||||
|
} else {
|
||||||
|
// normal targets
|
||||||
|
targets_config.join("\n")
|
||||||
|
}
|
||||||
|
.indent(indent.unwrap_or(0) + INDENT_SIZE)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// add placeholder if all targets are backup
|
||||||
|
if self.upstream_targets.iter().all(|v| v.is_backup) {
|
||||||
|
targets_config_str.push('\n');
|
||||||
|
targets_config_str
|
||||||
|
.push_str(&PLACEHOLDER_TARGET.indent(indent.unwrap_or(0) + INDENT_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("upstream {} {{\n{}\n}}", self.name, targets_config_str).indent(indent.unwrap_or(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +150,17 @@ impl From<upstream::Model> for UpstreamInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<(upstream::Model, Option<Vec<upstream_target::Model>>)> for UpstreamInfo {
|
||||||
|
fn from(data: (upstream::Model, Option<Vec<upstream_target::Model>>)) -> Self {
|
||||||
|
let (upstream_model, upstream_target_models) = data;
|
||||||
|
if let Some(targets) = upstream_target_models {
|
||||||
|
UpstreamInfo::from((upstream_model, targets))
|
||||||
|
} else {
|
||||||
|
UpstreamInfo::from(upstream_model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<(upstream::Model, Vec<upstream_target::Model>)> for UpstreamInfo {
|
impl From<(upstream::Model, Vec<upstream_target::Model>)> for UpstreamInfo {
|
||||||
fn from(data: (upstream::Model, Vec<upstream_target::Model>)) -> Self {
|
fn from(data: (upstream::Model, Vec<upstream_target::Model>)) -> Self {
|
||||||
let (upstream_model, upstream_target_models) = data;
|
let (upstream_model, upstream_target_models) = data;
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ impl From<UpstreamTargetCreateInfo> for upstream_target::ActiveModel {
|
|||||||
impl NginxConfigProvider for UpstreamTargetInfo {
|
impl NginxConfigProvider for UpstreamTargetInfo {
|
||||||
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}:{} weight={}{}{}",
|
"server {}:{} weight={}{}{};",
|
||||||
self.target_host,
|
self.target_host,
|
||||||
self.target_port,
|
self.target_port,
|
||||||
self.weight,
|
self.weight,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
|
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait, ExprTrait,
|
||||||
ModelTrait, QueryFilter, QuerySelect, TransactionTrait,
|
FromQueryResult, ModelTrait, QueryFilter, QuerySelect, QueryTrait, TransactionTrait,
|
||||||
};
|
};
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
@@ -29,6 +29,11 @@ pub trait UpstreamService: Send + Sync {
|
|||||||
create_info: UpstreamCreateInfo,
|
create_info: UpstreamCreateInfo,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<UpstreamInfo, ServiceError>;
|
) -> Result<UpstreamInfo, ServiceError>;
|
||||||
|
async fn get_total_upstreams(
|
||||||
|
&self,
|
||||||
|
options: Option<UpstreamTotalCountOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<u64, ServiceError>;
|
||||||
async fn get_upstream(
|
async fn get_upstream(
|
||||||
&self,
|
&self,
|
||||||
upstream_id: uuid::Uuid,
|
upstream_id: uuid::Uuid,
|
||||||
@@ -38,6 +43,7 @@ pub trait UpstreamService: Send + Sync {
|
|||||||
async fn get_upstreams(
|
async fn get_upstreams(
|
||||||
&self,
|
&self,
|
||||||
pagination: Option<PaginationFilter>,
|
pagination: Option<PaginationFilter>,
|
||||||
|
options: Option<GetUpstreamOptions>,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<Vec<UpstreamInfo>, ServiceError>;
|
) -> Result<Vec<UpstreamInfo>, ServiceError>;
|
||||||
async fn update_upstream(
|
async fn update_upstream(
|
||||||
@@ -93,8 +99,12 @@ pub struct UpstreamServiceImpl {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GetUpstreamOptions {
|
pub struct GetUpstreamOptions {
|
||||||
pub include_targets: bool,
|
pub include_targets: bool,
|
||||||
|
pub filter_by_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct UpstreamTotalCountOptions {}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GetUpstreamTargetOptions {
|
pub struct GetUpstreamTargetOptions {
|
||||||
pub include_upstream: bool,
|
pub include_upstream: bool,
|
||||||
@@ -150,6 +160,27 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
Ok(r.into())
|
Ok(r.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_total_upstreams(
|
||||||
|
&self,
|
||||||
|
_options: Option<UpstreamTotalCountOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<u64, ServiceError> {
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
struct CountResult {
|
||||||
|
// The field name must match the column alias in the query
|
||||||
|
count: i64,
|
||||||
|
}
|
||||||
|
let count_info = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
upstream::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column_as(upstream::Column::Id, "count")
|
||||||
|
.into_model::<CountResult>()
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
});
|
||||||
|
Ok(count_info.map_or(0, |c| c.count) as u64)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_upstream(
|
async fn get_upstream(
|
||||||
&self,
|
&self,
|
||||||
upstream_id: uuid::Uuid,
|
upstream_id: uuid::Uuid,
|
||||||
@@ -168,6 +199,9 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
)))?;
|
)))?;
|
||||||
let targets = upstream_target::Entity::find()
|
let targets = upstream_target::Entity::find()
|
||||||
.filter(upstream_target::Column::UpstreamId.eq(upstream_id))
|
.filter(upstream_target::Column::UpstreamId.eq(upstream_id))
|
||||||
|
.apply_if(Some(concrete_options.filter_by_enabled), |query, _v| {
|
||||||
|
query.filter(upstream_target::Column::Enabled.eq(true))
|
||||||
|
})
|
||||||
.all(*conn)
|
.all(*conn)
|
||||||
.await?;
|
.await?;
|
||||||
(up, targets)
|
(up, targets)
|
||||||
@@ -191,6 +225,7 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
async fn get_upstreams(
|
async fn get_upstreams(
|
||||||
&self,
|
&self,
|
||||||
pagination: Option<PaginationFilter>,
|
pagination: Option<PaginationFilter>,
|
||||||
|
options: Option<GetUpstreamOptions>,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<Vec<UpstreamInfo>, ServiceError> {
|
) -> Result<Vec<UpstreamInfo>, ServiceError> {
|
||||||
let r = with_conn!(&*self.connection, tx, conn, {
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
@@ -201,7 +236,24 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
} else {
|
} else {
|
||||||
find_query
|
find_query
|
||||||
};
|
};
|
||||||
find_query.all(*conn).await?
|
let find_query = match options {
|
||||||
|
Some(opts) => {
|
||||||
|
if opts.include_targets && opts.filter_by_enabled {
|
||||||
|
find_query.filter(
|
||||||
|
upstream_target::Column::Enabled
|
||||||
|
.eq(true)
|
||||||
|
.or(upstream_target::Column::Id.is_null()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
find_query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => find_query,
|
||||||
|
};
|
||||||
|
find_query
|
||||||
|
.find_with_related(upstream_target::Entity)
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(r.into_iter().map(|m| m.into()).collect())
|
Ok(r.into_iter().map(|m| m.into()).collect())
|
||||||
@@ -375,7 +427,9 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
});
|
});
|
||||||
let active_model = target.apply_to_model(current_model);
|
let active_model = target.apply_to_model(current_model);
|
||||||
|
|
||||||
let r = active_model.update(&*self.connection).await?;
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
active_model.update(*conn).await?
|
||||||
|
});
|
||||||
Ok(r.into())
|
Ok(r.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +559,7 @@ mod tests {
|
|||||||
up_id,
|
up_id,
|
||||||
Some(GetUpstreamOptions {
|
Some(GetUpstreamOptions {
|
||||||
include_targets: true,
|
include_targets: true,
|
||||||
|
filter_by_enabled: false,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -554,12 +609,15 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![u1.clone(), u2.clone()]])
|
.append_query_results(vec![vec![
|
||||||
|
(u1.clone(), None::<upstream_target::Model>),
|
||||||
|
(u2.clone(), None::<upstream_target::Model>),
|
||||||
|
]])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let svc = UpstreamServiceImpl::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, None).await;
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
let list = res.expect("Failed to get upstreams");
|
let list = res.expect("Failed to get upstreams");
|
||||||
assert_eq!(list.len(), 2);
|
assert_eq!(list.len(), 2);
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/upstream_targets/{upstream_target_id}": {
|
"/api/nginx/upstream_targets/{upstream_target_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Nginx"
|
"Nginx"
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/upstreams": {
|
"/api/nginx/upstreams": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Nginx"
|
"Nginx"
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/upstreams/{upstream_id}": {
|
"/api/nginx/upstreams/{upstream_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Nginx"
|
"Nginx"
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/upstreams/{upstream_id}/targets": {
|
"/api/nginx/upstreams/{upstream_id}/targets": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Nginx"
|
"Nginx"
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type get_Get_upstream_target = {
|
export type get_Get_upstream_target = {
|
||||||
method: "GET";
|
method: "GET";
|
||||||
path: "/api/upstream_targets/{upstream_target_id}";
|
path: "/api/nginx/upstream_targets/{upstream_target_id}";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
path: { upstream_target_id: string };
|
path: { upstream_target_id: string };
|
||||||
@@ -160,7 +160,7 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type delete_Remove_upstream_target = {
|
export type delete_Remove_upstream_target = {
|
||||||
method: "DELETE";
|
method: "DELETE";
|
||||||
path: "/api/upstream_targets/{upstream_target_id}";
|
path: "/api/nginx/upstream_targets/{upstream_target_id}";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
path: { upstream_target_id: string };
|
path: { upstream_target_id: string };
|
||||||
@@ -169,7 +169,7 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type patch_Update_upstream_target = {
|
export type patch_Update_upstream_target = {
|
||||||
method: "PATCH";
|
method: "PATCH";
|
||||||
path: "/api/upstream_targets/{upstream_target_id}";
|
path: "/api/nginx/upstream_targets/{upstream_target_id}";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
path: { upstream_target_id: string };
|
path: { upstream_target_id: string };
|
||||||
@@ -186,14 +186,14 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type get_Get_upstream_list = {
|
export type get_Get_upstream_list = {
|
||||||
method: "GET";
|
method: "GET";
|
||||||
path: "/api/upstreams";
|
path: "/api/nginx/upstreams";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: never;
|
parameters: never;
|
||||||
responses: { 200: Schemas.UpstreamListResponse; 500: unknown };
|
responses: { 200: Schemas.UpstreamListResponse; 500: unknown };
|
||||||
};
|
};
|
||||||
export type post_Create_upstream = {
|
export type post_Create_upstream = {
|
||||||
method: "POST";
|
method: "POST";
|
||||||
path: "/api/upstreams";
|
path: "/api/nginx/upstreams";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
body: Schemas.CreateUpstreamRequestBody;
|
body: Schemas.CreateUpstreamRequestBody;
|
||||||
@@ -202,7 +202,7 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type get_Get_upstream = {
|
export type get_Get_upstream = {
|
||||||
method: "GET";
|
method: "GET";
|
||||||
path: "/api/upstreams/{upstream_id}";
|
path: "/api/nginx/upstreams/{upstream_id}";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
path: { upstream_id: string };
|
path: { upstream_id: string };
|
||||||
@@ -211,7 +211,7 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type delete_Remove_upstream = {
|
export type delete_Remove_upstream = {
|
||||||
method: "DELETE";
|
method: "DELETE";
|
||||||
path: "/api/upstreams/{upstream_id}";
|
path: "/api/nginx/upstreams/{upstream_id}";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
path: { upstream_id: string };
|
path: { upstream_id: string };
|
||||||
@@ -220,7 +220,7 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type patch_Update_upstream = {
|
export type patch_Update_upstream = {
|
||||||
method: "PATCH";
|
method: "PATCH";
|
||||||
path: "/api/upstreams/{upstream_id}";
|
path: "/api/nginx/upstreams/{upstream_id}";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
path: { upstream_id: string };
|
path: { upstream_id: string };
|
||||||
@@ -231,7 +231,7 @@ export namespace Endpoints {
|
|||||||
};
|
};
|
||||||
export type post_Add_upstream_target = {
|
export type post_Add_upstream_target = {
|
||||||
method: "POST";
|
method: "POST";
|
||||||
path: "/api/upstreams/{upstream_id}/targets";
|
path: "/api/nginx/upstreams/{upstream_id}/targets";
|
||||||
requestFormat: "json";
|
requestFormat: "json";
|
||||||
parameters: {
|
parameters: {
|
||||||
body: Schemas.CreateUpstreamTargetInfo;
|
body: Schemas.CreateUpstreamTargetInfo;
|
||||||
@@ -254,23 +254,23 @@ 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/nginx/upstreams": Endpoints.post_Create_upstream;
|
||||||
"/api/upstreams/{upstream_id}/targets": Endpoints.post_Add_upstream_target;
|
"/api/nginx/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/nginx/upstream_targets/{upstream_target_id}": Endpoints.get_Get_upstream_target;
|
||||||
"/api/upstreams": Endpoints.get_Get_upstream_list;
|
"/api/nginx/upstreams": Endpoints.get_Get_upstream_list;
|
||||||
"/api/upstreams/{upstream_id}": Endpoints.get_Get_upstream;
|
"/api/nginx/upstreams/{upstream_id}": Endpoints.get_Get_upstream;
|
||||||
"/api/user/me": Endpoints.get_Get_user_info;
|
"/api/user/me": Endpoints.get_Get_user_info;
|
||||||
};
|
};
|
||||||
delete: {
|
delete: {
|
||||||
"/api/upstream_targets/{upstream_target_id}": Endpoints.delete_Remove_upstream_target;
|
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.delete_Remove_upstream_target;
|
||||||
"/api/upstreams/{upstream_id}": Endpoints.delete_Remove_upstream;
|
"/api/nginx/upstreams/{upstream_id}": Endpoints.delete_Remove_upstream;
|
||||||
};
|
};
|
||||||
patch: {
|
patch: {
|
||||||
"/api/upstream_targets/{upstream_target_id}": Endpoints.patch_Update_upstream_target;
|
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.patch_Update_upstream_target;
|
||||||
"/api/upstreams/{upstream_id}": Endpoints.patch_Update_upstream;
|
"/api/nginx/upstreams/{upstream_id}": Endpoints.patch_Update_upstream;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user