feature/upstream-service #13
@@ -3,6 +3,8 @@ 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 update_upstream;
|
||||||
|
pub mod update_upstream_target;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -19,14 +21,18 @@ 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),
|
||||||
|
)
|
||||||
.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),
|
||||||
)
|
)
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,203 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{
|
||||||
|
AppState, 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 r = upstream_service
|
||||||
|
.update_upstream(upstream_id, update_info, None)
|
||||||
|
.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;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
|
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||||
|
routes::api::restricted::nginx::upstream::get_upstream_router,
|
||||||
|
services::get_app_service,
|
||||||
|
};
|
||||||
|
use super::UpdateUpstreamRequestBody;
|
||||||
|
|
||||||
|
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_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 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)
|
||||||
|
.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_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,206 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Result as AxumResult,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::api_error::ApiError,
|
||||||
|
middlewares::request_info::AuthenticatedRequestInfo,
|
||||||
|
routes::{
|
||||||
|
AppState,
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 r = upstream_service
|
||||||
|
.update_upstream_target(upstream_target_id, update_info, None)
|
||||||
|
.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_target;
|
||||||
|
|
||||||
|
use super::UpdateUpstreamTargetRequestBody;
|
||||||
|
use crate::{
|
||||||
|
configs::{FromConfig, ProgramSettings},
|
||||||
|
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
|
||||||
|
let second: Vec<Vec<upstream_target::Model>> = vec![vec![updated_model.clone()]];
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(first)
|
||||||
|
.append_query_results(second)
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use optfield::optfield;
|
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 +14,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 +33,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 +152,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,16 @@ impl UpstreamService {
|
|||||||
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,9 +160,36 @@ 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,14 +530,10 @@ mod tests {
|
|||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamService::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;
|
||||||
@@ -522,14 +554,11 @@ mod tests {
|
|||||||
.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,
|
||||||
@@ -650,16 +679,11 @@ mod tests {
|
|||||||
let svc = UpstreamService::new(Arc::new(db));
|
let svc = UpstreamService::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());
|
||||||
|
|||||||
Reference in New Issue
Block a user