From 75b936faa01ebc0b50e54465f5dec4e4fd149e8c Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 29 Mar 2024 20:24:13 +0800 Subject: [PATCH] refactor: adjust admin web page --- .github/workflows/publish.yml | 8 +++ .github/workflows/test.yml | 4 ++ Cargo.lock | 48 +++++++-------- Cargo.toml | 8 +-- TODO.md | 7 ++- conf/pingap.toml | 3 + docs/config_zh.md | 1 + src/config/load.rs | 1 + src/config/mod.rs | 12 ++++ src/main.rs | 2 + src/proxy/server.rs | 97 +++++++++++++++++++----------- src/serve/admin.rs | 20 ++++-- web/src/components/form-editor.tsx | 39 ++++++++++-- web/src/components/main-header.tsx | 51 ++++++++++++++++ web/src/components/main-nav.tsx | 53 +++++++++++----- web/src/main.tsx | 15 +++-- web/src/pages/location-info.tsx | 19 +++++- web/src/pages/server-info.tsx | 26 +++++++- web/src/pages/upstream-info.tsx | 19 +++++- web/src/states/basic.ts | 31 ++++++++++ web/src/states/config.ts | 20 +++++- 21 files changed, 378 insertions(+), 106 deletions(-) create mode 100644 web/src/components/main-header.tsx create mode 100644 web/src/states/basic.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1749acd..a064379 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,6 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: setup node + uses: actions/setup-node@v4 + - name: build-web + run: make build-web - name: release uses: addnab/docker-run-action@v3 with: @@ -29,6 +33,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: setup node + uses: actions/setup-node@v4 + - name: build-web + run: make build-web - name: release uses: addnab/docker-run-action@v3 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be35f6e..249d830 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: setup node + uses: actions/setup-node@v4 + - name: build-web + run: make build-web - name: release uses: addnab/docker-run-action@v3 with: diff --git a/Cargo.lock b/Cargo.lock index 8ad6db1..2ebf417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,9 +202,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" @@ -332,9 +332,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -484,7 +484,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.3", + "clap 4.5.4", "criterion-plot", "is-terminal", "itertools", @@ -1196,9 +1196,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" @@ -1316,9 +1316,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" @@ -1466,9 +1466,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1557,7 +1557,7 @@ dependencies = [ "bytes", "bytesize", "chrono", - "clap 4.5.3", + "clap 4.5.4", "criterion", "dirs", "env_logger", @@ -2051,9 +2051,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" @@ -2177,9 +2177,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.34.3" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" dependencies = [ "arrayvec", "num-traits", @@ -2410,9 +2410,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -2754,9 +2754,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 87edfef..9732523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ description = "A reverse proxy like nginx" license = "Apache-2.0" homepage = "https://github.com/vicanso/pingap" repository = "https://github.com/vicanso/pingap" -exclude = ["asset/*", "test/*", "Cargo.lock"] +exclude = ["asset/*", "test/*", "Cargo.lock", "web/*", "dist/*", ".github/*"] readme = "./README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -18,8 +18,8 @@ async-trait = "0.1.79" base64 = "0.22.0" bytes = "1.6.0" bytesize = "1.3.0" -chrono = "0.4.35" -clap = { version = "4.5.3", features = ["derive"] } +chrono = "0.4.37" +clap = { version = "4.5.4", features = ["derive"] } dirs = "5.0.1" env_logger = "0.11.3" futures-util = "0.3.30" @@ -37,7 +37,7 @@ pingora = { version = "0.1.0", default-features = false, features = ["lb"] } regex = "1.10.4" rust-embed = { version = "8.3.0", features = ["mime-guess", "compression"] } serde = "1.0.197" -serde_json = "1.0.114" +serde_json = "1.0.115" snafu = "0.8.2" substring = "1.4.5" tempfile = "3.10.1" diff --git a/TODO.md b/TODO.md index d376e22..b1ccd6b 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,8 @@ - [x] support add tls - [ ] log rotate - [x] stats of server -- [ ] start without config -- [ ] static serve for admin +- [x] start without config +- [x] static serve for admin - [ ] status:499 for client abort -- [ ] support get pingap start time +- [x] support get pingap start time +- [ ] custom error for pingora error diff --git a/conf/pingap.toml b/conf/pingap.toml index 8503af1..5609e41 100644 --- a/conf/pingap.toml +++ b/conf/pingap.toml @@ -95,3 +95,6 @@ locations = ["lo"] # Stats path for get the stats of server. Default `None` stats_path = "/stats" + +# Admin path for admin server. Default `None` +admin_path = "/admin" diff --git a/docs/config_zh.md b/docs/config_zh.md index c97fc26..430606a 100644 --- a/docs/config_zh.md +++ b/docs/config_zh.md @@ -45,3 +45,4 @@ Pingap使用toml来配置相关参数,具体参数说明如下: - `access_log`: 可选,默认为不输出访问日志。请求日志格式化,指定输出访问日志的形式。 - `locations`: location的列表,指定该server使用的所有location。 - `stats_path`: 可选,默认无。指定返回server的stats的路由。 +- `admin_path`: 可选,默认无。指定用于转发至admin管理后台的路由。 diff --git a/src/config/load.rs b/src/config/load.rs index ffa36e2..572b866 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -162,6 +162,7 @@ pub struct ServerConf { pub tls_cert: Option, pub tls_key: Option, pub stats_path: Option, + pub admin_path: Option, } impl ServerConf { diff --git a/src/config/mod.rs b/src/config/mod.rs index 66ca0c2..d5a9b23 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,6 @@ +use once_cell::sync::Lazy; use once_cell::sync::OnceCell; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; mod load; @@ -12,3 +14,13 @@ pub fn set_config_path(conf_path: &str) { pub fn get_config_path() -> String { CONFIG_PATH.get_or_init(|| "".to_string()).to_owned() } + +static START_TIME: Lazy = Lazy::new(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() +}); + +pub fn get_start_time() -> u64 { + START_TIME.as_secs() +} diff --git a/src/main.rs b/src/main.rs index a33ab67..3335cc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use crate::config::get_start_time; use crate::proxy::{Server, ServerConf}; use clap::Parser; use config::PingapConf; @@ -116,6 +117,7 @@ fn run() -> Result<(), Box> { my_server.add_service(services.lb); } info!("server is running"); + let _ = get_start_time(); my_server.run_forever(); Ok(()) } diff --git a/src/proxy/server.rs b/src/proxy/server.rs index 4650fcd..e191513 100644 --- a/src/proxy/server.rs +++ b/src/proxy/server.rs @@ -32,6 +32,7 @@ use std::collections::HashMap; use std::fs; use std::sync::atomic::{AtomicI32, AtomicU64, Ordering}; use std::sync::Arc; +use substring::Substring; static ERROR_TEMPLATE: &str = include_str!("../../error.html"); @@ -55,6 +56,7 @@ pub struct ServerConf { pub addr: String, pub admin: bool, pub stats_path: Option, + pub admin_path: Option, pub access_log: Option, pub upstreams: Vec<(String, UpstreamConf)>, pub locations: Vec<(String, LocationConf)>, @@ -119,6 +121,7 @@ impl From for Vec { tls_key, admin: false, stats_path: item.stats_path, + admin_path: item.admin_path, addr: item.addr, access_log: item.access_log, upstreams: filter_upstreams, @@ -147,6 +150,7 @@ pub struct Server { log_parser: Option, error_template: String, stats_path: Option, + admin_path: Option, tls_cert: Option>, tls_key: Option>, } @@ -211,6 +215,7 @@ impl Server { accepted: AtomicU64::new(0), processing: AtomicI32::new(0), stats_path: conf.stats_path, + admin_path: conf.admin_path, addr: conf.addr, log_parser: p, locations, @@ -281,6 +286,8 @@ impl Server { } } +static LOCATION_NOT_FOUND: &str = "Location not found"; + #[async_trait] impl ProxyHttp for Server { type CTX = State; @@ -300,13 +307,13 @@ impl ProxyHttp for Server { if session.is_http2() { ctx.http_version = 2; } - if self.admin { - let result = ADMIN_SERVE.handle(session, ctx).await?; - return Ok(result); - } + + let header = session.req_header_mut(); + let path = header.uri.path(); + let host = header.uri.host().unwrap_or_default(); if let Some(stats_path) = &self.stats_path { - if stats_path == session.req_header().uri.path() { + if stats_path == path { let size = self .get_stats_response() .send(session) @@ -320,6 +327,39 @@ impl ProxyHttp for Server { return Ok(true); } } + + // admin server + if self.admin { + let result = ADMIN_SERVE.handle(session, ctx).await?; + return Ok(result); + } + + // admin path + if let Some(admin_path) = &self.admin_path { + if path.starts_with(admin_path) { + let mut new_path = path.substring(admin_path.len(), path.len()).to_string(); + if let Some(query) = header.uri.query() { + new_path = format!("{new_path}?{query}"); + } + // TODO parse error + if let Ok(uri) = new_path.parse::() { + header.set_uri(uri); + } + let result = ADMIN_SERVE.handle(session, ctx).await?; + return Ok(result); + } + } + + let (location_index, _) = self + .locations + .iter() + .enumerate() + .find(|(_, item)| item.matched(host, path)) + .ok_or_else(|| pingora::Error::new_str(LOCATION_NOT_FOUND))?; + ctx.location_index = Some(location_index); + // TODO get response from cache + // check location support cache + Ok(false) } async fn upstream_peer( @@ -329,14 +369,14 @@ impl ProxyHttp for Server { ) -> pingora::Result> { let header = session.req_header_mut(); let path = header.uri.path(); - let host = header.uri.host().unwrap_or_default(); - let (location_index, lo) = self + let location_index = ctx + .location_index + .ok_or_else(|| pingora::Error::new_str(LOCATION_NOT_FOUND))?; + let lo = self .locations - .iter() - .enumerate() - .find(|(_, item)| item.matched(host, path)) - .ok_or(pingora::Error::new_str("Location not found"))?; - ctx.location_index = Some(location_index); + .get(location_index) + .ok_or_else(|| pingora::Error::new_str(LOCATION_NOT_FOUND))?; + if let Some(mut new_path) = lo.rewrite(path) { if let Some(query) = header.uri.query() { new_path = format!("{new_path}?{query}"); @@ -429,30 +469,19 @@ impl ProxyHttp for Server { { let server_session = session.as_mut(); - let mut code = match e.etype() { + let code = match e.etype() { pingora::HTTPStatus(code) => *code, - _ => { - match e.esource() { - pingora::ErrorSource::Upstream => 502, - pingora::ErrorSource::Downstream => { - match e.etype() { - pingora::ErrorType::WriteError - | pingora::ErrorType::ReadError - | pingora::ErrorType::ConnectionClosed => { - /* conn already dead */ - 0 - } - _ => 400, - } - } - pingora::ErrorSource::Internal | pingora::ErrorSource::Unset => 500, - } - } + _ => match e.esource() { + pingora::ErrorSource::Upstream => 502, + pingora::ErrorSource::Downstream => match e.etype() { + pingora::ErrorType::WriteError | pingora::ErrorType::ReadError => 500, + // client close the connection + pingora::ErrorType::ConnectionClosed => 499, + _ => 400, + }, + pingora::ErrorSource::Internal | pingora::ErrorSource::Unset => 500, + }, }; - // convert code to 500 - if code == 0 { - code = 500; - } // TODO better error handler let mut resp = match code { 502 => error_resp::HTTP_502_RESPONSE.clone(), diff --git a/src/serve/admin.rs b/src/serve/admin.rs index cd0fdf2..1e76de8 100644 --- a/src/serve/admin.rs +++ b/src/serve/admin.rs @@ -1,7 +1,8 @@ use super::static_file::StaticFile; use super::Serve; -use crate::config::{self, save_config, LocationConf, ServerConf, UpstreamConf}; +use crate::config::{self, get_start_time, save_config, LocationConf, ServerConf, UpstreamConf}; use crate::state::State; +use crate::utils::get_pkg_version; use crate::{cache::HttpResponse, config::PingapConf}; use async_trait::async_trait; use http::{Method, StatusCode}; @@ -26,7 +27,7 @@ struct ErrorResponse { #[derive(Serialize, Deserialize)] struct BasicConfParams { - error_template: String, + error_template: Option, pid_file: Option, upgrade_sock: Option, user: Option, @@ -35,6 +36,12 @@ struct BasicConfParams { work_stealing: Option, } +#[derive(Serialize, Deserialize)] +struct BasicInfo { + start_time: u64, + version: String, +} + const CATEGORY_UPSTREAM: &str = "upstream"; const CATEGORY_LOCATION: &str = "location"; const CATEGORY_SERVER: &str = "server"; @@ -119,7 +126,7 @@ impl AdminServe { error!("failed to basic info: {e}"); pingora::Error::new_str("Basic config invalid") })?; - conf.error_template = basic_conf.error_template; + conf.error_template = basic_conf.error_template.unwrap_or_default(); conf.pid_file = basic_conf.pid_file; conf.upgrade_sock = basic_conf.upgrade_sock; conf.user = basic_conf.user; @@ -175,7 +182,6 @@ impl Serve for AdminServe { _ => self.get_config(category).await, } .unwrap_or_else(|err| { - println!("{err:?}"); let mut resp = HttpResponse::try_from_json(&ErrorResponse { message: err.to_string(), }) @@ -183,6 +189,12 @@ impl Serve for AdminServe { resp.status = StatusCode::INTERNAL_SERVER_ERROR; resp }) + } else if path == "/basic" { + HttpResponse::try_from_json(&BasicInfo { + start_time: get_start_time(), + version: get_pkg_version().to_string(), + }) + .unwrap_or(HttpResponse::unknown_error()) } else { let mut file = path.substring(1, path.len()); if file.is_empty() { diff --git a/web/src/components/form-editor.tsx b/web/src/components/form-editor.tsx index 43b8c37..946b8c0 100644 --- a/web/src/components/form-editor.tsx +++ b/web/src/components/form-editor.tsx @@ -66,11 +66,15 @@ export default function FormEditor({ description, items, onUpsert, + created, + currentNames, }: { title: string; description: string; items: FormItem[]; onUpsert: (name: string, data: Record) => Promise; + created?: boolean; + currentNames?: string[]; }) { const theme = useTheme(); const [data, setData] = React.useState(getDefaultValues(items)); @@ -80,7 +84,7 @@ export default function FormEditor({ items.forEach((item) => { switch (item.category) { case FormItemCategory.LOCATION: { - const arr = item.defaultValue as string[]; + const arr = (item.defaultValue as string[]) || []; arr.forEach((lo) => { defaultLocations.push(lo); }); @@ -91,7 +95,7 @@ export default function FormEditor({ break; } case FormItemCategory.ADDRS: { - const arr = item.defaultValue as string[]; + const arr = (item.defaultValue as string[]) || []; arr.forEach((addr) => { defaultAddrs.push(addr); }); @@ -107,6 +111,7 @@ export default function FormEditor({ const [updated, setUpdated] = React.useState(false); const [processing, setProcessing] = React.useState(false); const [showSuccess, setShowSuccess] = React.useState(false); + const [newName, setNewName] = React.useState(""); const [showError, setShowError] = React.useState({ open: false, @@ -274,7 +279,7 @@ export default function FormEditor({ updateValue(item.id, values); }} > - Add + Add Address , ); formItem = {list}; @@ -351,7 +356,15 @@ export default function FormEditor({ } setProcessing(true); try { - await onUpsert("", data); + if (created) { + if (!newName) { + throw new Error("Name is required"); + } + if ((currentNames || []).includes(newName)) { + throw new Error("Name is exists"); + } + } + await onUpsert(newName, data); setShowSuccess(true); } catch (err) { setShowError({ @@ -362,6 +375,23 @@ export default function FormEditor({ setProcessing(false); } }; + let createNewItem = <>; + if (created) { + createNewItem = ( + + + { + setNewName(e.target.value.trim()); + }} + /> + + + ); + } return ( @@ -377,6 +407,7 @@ export default function FormEditor({
+ {createNewItem} {list}