feature/upstream-service #13
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod create_upstream;
|
||||||
pub mod get_upstream;
|
pub mod get_upstream;
|
||||||
pub mod get_upstream_target;
|
pub mod get_upstream_target;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
@@ -10,7 +11,10 @@ use crate::routes::AppState;
|
|||||||
|
|
||||||
pub fn get_upstream_router(state: Arc<AppState>) -> Router {
|
pub fn get_upstream_router(state: Arc<AppState>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/upstreams", get(get_upstream::get_upstream_list))
|
.route(
|
||||||
|
"/upstreams",
|
||||||
|
get(get_upstream::get_upstream_list).post(create_upstream::create_upstream),
|
||||||
|
)
|
||||||
.route("/upstreams/{upstream_id}", get(get_upstream::get_upstream))
|
.route("/upstreams/{upstream_id}", get(get_upstream::get_upstream))
|
||||||
.route(
|
.route(
|
||||||
"/upstream_targets/{upstream_target_id}",
|
"/upstream_targets/{upstream_target_id}",
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{Json, extract::State, response::Result as AxumResult};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{AppState, api::restricted::nginx::upstream::info::response::UpstreamInfoResponse},
|
||||||
|
services::nginx::info::upstream::UpstreamCreateInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||||
|
pub struct UpstreamTargetInfo {
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
pub weight: Option<i64>,
|
||||||
|
pub is_backup: Option<bool>,
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConcreteUpstreamTargetInfo {
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
pub weight: i64,
|
||||||
|
pub is_backup: bool,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UpstreamTargetInfo> for ConcreteUpstreamTargetInfo {
|
||||||
|
fn from(info: UpstreamTargetInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
host: info.host,
|
||||||
|
port: info.port,
|
||||||
|
weight: info.weight.unwrap_or(1),
|
||||||
|
is_backup: info.is_backup.unwrap_or(false),
|
||||||
|
enabled: info.enabled.unwrap_or(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||||
|
pub struct CreateUpstreamRequestBody {
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: Option<String>,
|
||||||
|
pub sticky_session: Option<bool>,
|
||||||
|
pub upstream_targets: Vec<UpstreamTargetInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConcreteCreateUpstreamRequestBody {
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub sticky_session: bool,
|
||||||
|
pub upstream_targets: Vec<ConcreteUpstreamTargetInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateUpstreamRequestBody> for ConcreteCreateUpstreamRequestBody {
|
||||||
|
fn from(payload: CreateUpstreamRequestBody) -> Self {
|
||||||
|
Self {
|
||||||
|
name: payload.name,
|
||||||
|
protocol: payload.protocol,
|
||||||
|
algorithm: payload
|
||||||
|
.algorithm
|
||||||
|
.unwrap_or_else(|| "round_robin".to_string()),
|
||||||
|
sticky_session: payload.sticky_session.unwrap_or(false),
|
||||||
|
upstream_targets: payload
|
||||||
|
.upstream_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|target| target.into())
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
pub async fn create_upstream(
|
||||||
|
request_info: AuthenticatedRequestInfo,
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(payload): Json<CreateUpstreamRequestBody>,
|
||||||
|
) -> AxumResult<Json<UpstreamInfoResponse>, ApiError> {
|
||||||
|
let upstream_service = &state.service.nginx.get_upstream_service();
|
||||||
|
let concrete_payload: ConcreteCreateUpstreamRequestBody = payload.into();
|
||||||
|
|
||||||
|
let create_info = UpstreamCreateInfo {
|
||||||
|
name: concrete_payload.name,
|
||||||
|
protocol: concrete_payload.protocol,
|
||||||
|
algorithm: concrete_payload.algorithm,
|
||||||
|
sticky_session: concrete_payload.sticky_session,
|
||||||
|
created_by: Some(request_info.user_id),
|
||||||
|
upstream_targets: concrete_payload
|
||||||
|
.upstream_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(
|
||||||
|
|target| crate::services::nginx::info::upstream_target::UpstreamTargetCreateInfo {
|
||||||
|
target_host: target.host,
|
||||||
|
target_port: target.port,
|
||||||
|
weight: target.weight,
|
||||||
|
is_backup: target.is_backup,
|
||||||
|
enabled: target.enabled,
|
||||||
|
upstream_id: uuid::Uuid::nil(), // Placeholder, will be set in service
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let upstream_info = upstream_service.create_upstream(create_info, None).await?;
|
||||||
|
|
||||||
|
Ok(Json(upstream_info.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
|
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||||
|
routes::api::restricted::nginx::upstream::{
|
||||||
|
create_upstream::{CreateUpstreamRequestBody, UpstreamTargetInfo as ReqTarget},
|
||||||
|
get_upstream_router,
|
||||||
|
},
|
||||||
|
services::get_app_service,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||||
|
let program_settings = ProgramSettings::mock();
|
||||||
|
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||||
|
let state = Arc::new(crate::routes::AppState {
|
||||||
|
database_connection: Arc::new(db),
|
||||||
|
service: Arc::new(app_service),
|
||||||
|
config: Arc::new(program_settings),
|
||||||
|
});
|
||||||
|
get_upstream_router(state).layer(axum::middleware::from_fn(
|
||||||
|
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_create_upstream_succeeds_returns_created() {
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let up_model = upstream::Model {
|
||||||
|
id: up_id,
|
||||||
|
name: "new_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: "round_robin".to_string(),
|
||||||
|
sticky_session: false,
|
||||||
|
created_by: Some(uuid::Uuid::new_v4()),
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_id = uuid::Uuid::new_v4();
|
||||||
|
let target_model = upstream_target::Model {
|
||||||
|
id: target_id,
|
||||||
|
upstream_id: up_id,
|
||||||
|
target_host: "127.0.0.1".to_string(),
|
||||||
|
target_port: 8080,
|
||||||
|
weight: 1,
|
||||||
|
is_backup: false,
|
||||||
|
enabled: true,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// service will likely perform an insert and then query to return created models
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![up_model.clone()]])
|
||||||
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateUpstreamRequestBody {
|
||||||
|
name: "new_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: vec![ReqTarget {
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
weight: None,
|
||||||
|
is_backup: None,
|
||||||
|
enabled: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server.post("/upstreams").json(&payload).await;
|
||||||
|
|
||||||
|
res.assert_status_ok();
|
||||||
|
let text = res.text();
|
||||||
|
let body: crate::routes::api::restricted::nginx::upstream::info::response::UpstreamInfoResponse =
|
||||||
|
serde_json::from_str(&text).expect("failed to parse json");
|
||||||
|
|
||||||
|
assert_eq!(body.id, up_id);
|
||||||
|
assert_eq!(body.name, "new_upstream");
|
||||||
|
assert_eq!(body.protocol, "http");
|
||||||
|
assert_eq!(body.upstream_targets.len(), 1);
|
||||||
|
assert_eq!(body.upstream_targets[0].id, target_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_create_upstream_invalid_payload_returns_bad_request() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
// missing required fields -> send empty object
|
||||||
|
let res = server.post("/upstreams").json(&serde_json::json!({})).await;
|
||||||
|
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn handler_create_upstream_unauthenticated_returns_unauthorized() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateUpstreamRequestBody {
|
||||||
|
name: "new_upstream".to_string(),
|
||||||
|
protocol: "http".to_string(),
|
||||||
|
algorithm: None,
|
||||||
|
sticky_session: None,
|
||||||
|
upstream_targets: vec![ReqTarget {
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: 8080,
|
||||||
|
weight: None,
|
||||||
|
is_backup: None,
|
||||||
|
enabled: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.post("/upstreams")
|
||||||
|
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user