9 Commits

Author SHA1 Message Date
GW_MC
3be9ecc4c1 Refactor: improve config formatting, clean up imports
Some checks failed
Test / test-frontend (pull_request) Successful in 24s
Test / lint-frontend (pull_request) Successful in 27s
Test / frontend-build (pull_request) Successful in 31s
Verify / verify-generated-database-code (pull_request) Successful in 1m9s
Verify / verify-generated-agent-code (pull_request) Successful in 1m12s
Verify / verify-openapi-spec (pull_request) Successful in 1m9s
Verify / verify-frontend-api-client (pull_request) Successful in 22s
Test / test-crates (pull_request) Failing after 58s
Test / lint-crates (pull_request) Successful in 1m8s
2025-12-31 20:10:47 +08:00
GW_MC
545bc66f8c Fix: invalid config when all are backup 2025-12-31 19:17:14 +08:00
GW_MC
75097a661b Add filtering options for upstream targets in get_upstreams 2025-12-31 19:16:55 +08:00
GW_MC
9860dddf60 Fix incorrect upstream_target config 2025-12-31 18:09:30 +08:00
GW_MC
c4634b18f9 Fix CORS method not allowed 2025-12-31 18:09:19 +08:00
GW_MC
a0b4df745e fix upstream does not contain a target when init 2025-12-31 18:03:54 +08:00
GW_MC
46801fba99 improve error logging 2025-12-31 18:03:42 +08:00
GW_MC
cb65d4e9f7 fix incorrect Extension 2025-12-31 18:03:19 +08:00
GW_MC
10cc8f9d97 Fix incorrect path 2025-12-31 18:02:45 +08:00
18 changed files with 147 additions and 57 deletions

View File

@@ -1,14 +1,17 @@
use axum::response::IntoResponse;
use sea_orm::DbErr;
use tracing::error;
use crate::errors::service_error::ServiceError;
#[derive(Debug)]
pub enum ApiError {
ServiceError(ServiceError),
}
impl From<ServiceError> for ApiError {
fn from(err: ServiceError) -> Self {
error!("Service error occurred: {:?}", err);
ApiError::ServiceError(err)
}
}
@@ -21,6 +24,7 @@ impl From<DbErr> for ApiError {
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
error!("API error occurred: {:?}", self);
match self {
ApiError::ServiceError(service_error) => service_error.into_response(),
}

View File

@@ -9,7 +9,7 @@ use axum::{
http::{HeaderValue, Method, StatusCode, Uri},
};
use tower::{ServiceBuilder, timeout::TimeoutLayer};
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};
use tracing::warn;
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 {
let mut cors_layer = CorsLayer::new()
.allow_credentials(true)
.allow_methods(AllowMethods::mirror_request())
.allow_headers(AllowHeaders::mirror_request());
let allowed_origins = &cors_settings.allowed_origins;

View File

@@ -83,7 +83,7 @@ impl From<CreateUpstreamRequestBody> for ConcreteCreateUpstreamRequestBody {
#[axum::debug_handler]
#[utoipa::path(
post,
path = "/api/upstreams",
path = "/api/nginx/upstreams",
request_body = CreateUpstreamRequestBody,
responses(
(status = 200, description = "Upstream created successfully", body = UpstreamInfoResponse),

View File

@@ -51,7 +51,7 @@ impl From<CreateUpstreamTargetInfo> for ConcreteCreateUpstreamTargetInfo {
#[axum::debug_handler]
#[utoipa::path(
post,
path = "/api/upstreams/{upstream_id}/targets",
path = "/api/nginx/upstreams/{upstream_id}/targets",
request_body = CreateUpstreamTargetInfo,
responses(
(status = 200, description = "Upstream target created successfully", body = UpstreamTargetInfoResponse),

View File

@@ -42,7 +42,7 @@ impl From<GetUpstreamParams> for ConcreteGetUpstreamParams {
#[utoipa::path(
get,
path = "/api/upstreams",
path = "/api/nginx/upstreams",
responses(
(status = 200, description = "List upstreams", body = UpstreamListResponse),
(status = 500, description = "Internal server error"),
@@ -55,7 +55,14 @@ pub async fn get_upstream_list(
) -> AxumResult<Json<UpstreamListResponse>, ServiceError> {
let upstream_service = &state.service.nginx.get_upstream_service();
let upstreams = upstream_service
.get_upstreams(Some(pagination.clone().into()), None)
.get_upstreams(
Some(pagination.clone().into()),
Some(GetUpstreamOptions {
include_targets: true,
filter_by_enabled: false,
}),
None,
)
.await?;
//
@@ -72,7 +79,7 @@ pub async fn get_upstream_list(
#[utoipa::path(
get,
path = "/api/upstreams/{upstream_id}",
path = "/api/nginx/upstreams/{upstream_id}",
responses(
(status = 200, description = "Get upstream info", body = UpstreamInfoResponse),
(status = 404, description = "Not found"),
@@ -93,6 +100,7 @@ pub async fn get_upstream(
upstream_id,
Some(GetUpstreamOptions {
include_targets: true,
filter_by_enabled: false,
}),
None,
)

View File

@@ -38,7 +38,7 @@ impl From<GetUpstreamTargetsParams> for ConcreteGetUpstreamTargetsParams {
#[utoipa::path(
get,
path = "/api/upstream_targets/{upstream_target_id}",
path = "/api/nginx/upstream_targets/{upstream_target_id}",
responses(
(status = 200, description = "Get upstream target info", body = UpstreamTargetInfo),
(status = 404, description = "Not found"),

View File

@@ -16,7 +16,7 @@ use crate::{
#[utoipa::path(
delete,
path = "/api/upstreams/{upstream_id}",
path = "/api/nginx/upstreams/{upstream_id}",
responses(
(status = 200, description = "Upstream removed successfully", body = ()),
(status = 401, description = "Unauthorized"),

View File

@@ -16,7 +16,7 @@ use crate::{
#[utoipa::path(
delete,
path = "/api/upstream_targets/{upstream_target_id}",
path = "/api/nginx/upstream_targets/{upstream_target_id}",
responses(
(status = 200, description = "Upstream target removed successfully", body = ()),
(status = 401, description = "Unauthorized"),

View File

@@ -50,7 +50,7 @@ impl From<UpdateUpstreamRequestBody> for UpdateUpstreamInfo {
#[utoipa::path(
patch,
path = "/api/upstreams/{upstream_id}",
path = "/api/nginx/upstreams/{upstream_id}",
request_body = UpdateUpstreamRequestBody,
responses(
(status = 200, description = "Upstream updated successfully", body = UpdateUpstreamInfoResponse),

View File

@@ -42,7 +42,7 @@ impl From<UpdateUpstreamTargetRequestBody> for UpdateUpstreamTargetInfo {
#[utoipa::path(
patch,
path = "/api/upstream_targets/{upstream_target_id}",
path = "/api/nginx/upstream_targets/{upstream_target_id}",
request_body = UpdateUpstreamTargetRequestBody,
responses(
(status = 200, description = "Upstream target updated successfully", body = UpdateUpstreamTargetInfoResponse),

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use axum::{
Extension, Json,
Json,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use tracing::error;
use crate::{
middlewares::request_info::RequestInfo,
middlewares::request_info::AuthenticatedRequestInfo,
routes::{AppState, api::openapi::tag::USER_TAG},
};
@@ -38,15 +38,9 @@ pub struct UserInfo {
)]
pub async fn get_user_info(
State(app_state): State<Arc<AppState>>,
request_info: Extension<Arc<RequestInfo>>,
request_info: AuthenticatedRequestInfo,
) -> Response {
let user_id = match request_info.user_id {
Some(id) => id,
None => {
error!("User ID not found in request info");
return (StatusCode::UNAUTHORIZED).into_response();
}
};
let user_id = request_info.user_id;
match app_state.service.user.get_user_by_id(user_id, None).await {
Ok(user) => {

View File

@@ -8,7 +8,7 @@ use agent_client::{
},
models::{ValidateAndReloadBody, ValidateBody, WriteConfigBody},
};
use tracing::warn;
use tracing::{error, warn};
use crate::{configs::agent::AgentSettings, errors::service_error::ServiceError};
@@ -23,6 +23,7 @@ pub enum AgentError {
impl From<AgentError> for ServiceError {
fn from(err: AgentError) -> Self {
error!("Agent error occurred: {:?}", err);
match err {
AgentError::ValidationFailed(_internal, user) => ServiceError::InternalError(user),
AgentError::ApplicationFailed(_internal, user) => ServiceError::InternalError(user),

View File

@@ -36,7 +36,8 @@ impl NginxConfigProvider for NginxConfigBuilder {
}
// TODO: Add other sections like servers, locations, etc.
// trailing newline for file ending
config.push('\n');
config
}
}

View File

@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
use database::generated::entities::{upstream, upstream_target};
use sea_orm::ActiveValue::{Set, Unchanged};
use tracing::warn;
use uuid::Uuid;
use crate::{
@@ -13,6 +14,8 @@ use crate::{
set_if_some,
};
const PLACEHOLDER_TARGET: &str = "server 127.0.0.1:65535 down; # placeholder target";
#[derive(Clone)]
pub struct UpstreamInfo {
pub id: Uuid,
@@ -55,12 +58,53 @@ impl NginxConfigProvider for UpstreamInfo {
.map(|target| target.to_nginx_config(Some(indent.unwrap_or(0) + INDENT_SIZE)))
.collect();
format!(
"upstream {} {{\n{}\n}}",
self.name,
targets_config.join("\n".indent(indent.unwrap_or(0) + INDENT_SIZE).as_str())
)
.indent(indent.unwrap_or(0))
let mut targets_config_str = {
let config_str = match self.algorithm.as_str() {
"least-conn" => "least_conn",
"ip-hash" => "ip_hash",
"round-robin" => "",
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 {
fn from(data: (upstream::Model, Vec<upstream_target::Model>)) -> Self {
let (upstream_model, upstream_target_models) = data;

View File

@@ -130,7 +130,7 @@ impl From<UpstreamTargetCreateInfo> for upstream_target::ActiveModel {
impl NginxConfigProvider for UpstreamTargetInfo {
fn to_nginx_config(&self, indent: Option<usize>) -> String {
format!(
"{}:{} weight={}{}{}",
"server {}:{} weight={}{}{};",
self.target_host,
self.target_port,
self.weight,

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
ModelTrait, QueryFilter, QuerySelect, TransactionTrait,
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait, ExprTrait,
ModelTrait, QueryFilter, QuerySelect, QueryTrait, TransactionTrait,
};
use database::generated::entities::{upstream, upstream_target};
@@ -38,6 +38,7 @@ pub trait UpstreamService: Send + Sync {
async fn get_upstreams(
&self,
pagination: Option<PaginationFilter>,
options: Option<GetUpstreamOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<Vec<UpstreamInfo>, ServiceError>;
async fn update_upstream(
@@ -93,6 +94,7 @@ pub struct UpstreamServiceImpl {
#[derive(Default)]
pub struct GetUpstreamOptions {
pub include_targets: bool,
pub filter_by_enabled: bool,
}
#[derive(Default)]
@@ -168,6 +170,9 @@ impl UpstreamService for UpstreamServiceImpl {
)))?;
let targets = upstream_target::Entity::find()
.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)
.await?;
(up, targets)
@@ -191,6 +196,7 @@ impl UpstreamService for UpstreamServiceImpl {
async fn get_upstreams(
&self,
pagination: Option<PaginationFilter>,
options: Option<GetUpstreamOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<Vec<UpstreamInfo>, ServiceError> {
let r = with_conn!(&*self.connection, tx, conn, {
@@ -201,7 +207,24 @@ impl UpstreamService for UpstreamServiceImpl {
} else {
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())
@@ -375,7 +398,9 @@ impl UpstreamService for UpstreamServiceImpl {
});
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())
}
@@ -505,6 +530,7 @@ mod tests {
up_id,
Some(GetUpstreamOptions {
include_targets: true,
filter_by_enabled: false,
}),
None,
)
@@ -559,7 +585,7 @@ mod tests {
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());
let list = res.expect("Failed to get upstreams");
assert_eq!(list.len(), 2);

View File

@@ -106,7 +106,7 @@
}
}
},
"/api/upstream_targets/{upstream_target_id}": {
"/api/nginx/upstream_targets/{upstream_target_id}": {
"get": {
"tags": [
"Nginx"
@@ -232,7 +232,7 @@
}
}
},
"/api/upstreams": {
"/api/nginx/upstreams": {
"get": {
"tags": [
"Nginx"
@@ -292,7 +292,7 @@
}
}
},
"/api/upstreams/{upstream_id}": {
"/api/nginx/upstreams/{upstream_id}": {
"get": {
"tags": [
"Nginx"
@@ -418,7 +418,7 @@
}
}
},
"/api/upstreams/{upstream_id}/targets": {
"/api/nginx/upstreams/{upstream_id}/targets": {
"post": {
"tags": [
"Nginx"

View File

@@ -151,7 +151,7 @@ export namespace Endpoints {
};
export type get_Get_upstream_target = {
method: "GET";
path: "/api/upstream_targets/{upstream_target_id}";
path: "/api/nginx/upstream_targets/{upstream_target_id}";
requestFormat: "json";
parameters: {
path: { upstream_target_id: string };
@@ -160,7 +160,7 @@ export namespace Endpoints {
};
export type delete_Remove_upstream_target = {
method: "DELETE";
path: "/api/upstream_targets/{upstream_target_id}";
path: "/api/nginx/upstream_targets/{upstream_target_id}";
requestFormat: "json";
parameters: {
path: { upstream_target_id: string };
@@ -169,7 +169,7 @@ export namespace Endpoints {
};
export type patch_Update_upstream_target = {
method: "PATCH";
path: "/api/upstream_targets/{upstream_target_id}";
path: "/api/nginx/upstream_targets/{upstream_target_id}";
requestFormat: "json";
parameters: {
path: { upstream_target_id: string };
@@ -186,14 +186,14 @@ export namespace Endpoints {
};
export type get_Get_upstream_list = {
method: "GET";
path: "/api/upstreams";
path: "/api/nginx/upstreams";
requestFormat: "json";
parameters: never;
responses: { 200: Schemas.UpstreamListResponse; 500: unknown };
};
export type post_Create_upstream = {
method: "POST";
path: "/api/upstreams";
path: "/api/nginx/upstreams";
requestFormat: "json";
parameters: {
body: Schemas.CreateUpstreamRequestBody;
@@ -202,7 +202,7 @@ export namespace Endpoints {
};
export type get_Get_upstream = {
method: "GET";
path: "/api/upstreams/{upstream_id}";
path: "/api/nginx/upstreams/{upstream_id}";
requestFormat: "json";
parameters: {
path: { upstream_id: string };
@@ -211,7 +211,7 @@ export namespace Endpoints {
};
export type delete_Remove_upstream = {
method: "DELETE";
path: "/api/upstreams/{upstream_id}";
path: "/api/nginx/upstreams/{upstream_id}";
requestFormat: "json";
parameters: {
path: { upstream_id: string };
@@ -220,7 +220,7 @@ export namespace Endpoints {
};
export type patch_Update_upstream = {
method: "PATCH";
path: "/api/upstreams/{upstream_id}";
path: "/api/nginx/upstreams/{upstream_id}";
requestFormat: "json";
parameters: {
path: { upstream_id: string };
@@ -231,7 +231,7 @@ export namespace Endpoints {
};
export type post_Add_upstream_target = {
method: "POST";
path: "/api/upstreams/{upstream_id}/targets";
path: "/api/nginx/upstreams/{upstream_id}/targets";
requestFormat: "json";
parameters: {
body: Schemas.CreateUpstreamTargetInfo;
@@ -254,23 +254,23 @@ export type EndpointByMethod = {
post: {
"/api/auth/init_admin": Endpoints.post_Init_admin;
"/api/auth/login": Endpoints.post_Login;
"/api/upstreams": Endpoints.post_Create_upstream;
"/api/upstreams/{upstream_id}/targets": Endpoints.post_Add_upstream_target;
"/api/nginx/upstreams": Endpoints.post_Create_upstream;
"/api/nginx/upstreams/{upstream_id}/targets": Endpoints.post_Add_upstream_target;
};
get: {
"/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/nginx/upstream_targets/{upstream_target_id}": Endpoints.get_Get_upstream_target;
"/api/nginx/upstreams": Endpoints.get_Get_upstream_list;
"/api/nginx/upstreams/{upstream_id}": Endpoints.get_Get_upstream;
"/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;
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.delete_Remove_upstream_target;
"/api/nginx/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;
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.patch_Update_upstream_target;
"/api/nginx/upstreams/{upstream_id}": Endpoints.patch_Update_upstream;
};
};