From c8e9b04dd777f856857deed4e352065469aaa104 Mon Sep 17 00:00:00 2001 From: RWDai <27391645+RWDai@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:03:49 +0800 Subject: [PATCH] add package in process --- Cargo.toml | 2 +- admin/Cargo.toml | 26 ++ admin/README.md | 5 + admin/config/conf-default.toml | 15 + admin/src/api.rs | 3 + admin/src/api/gateway_api.rs | 50 +++ admin/src/api/plugin_api.rs | 26 ++ admin/src/api/route_api.rs | 7 + admin/src/config.rs | 43 +++ admin/src/constants.rs | 1 + admin/src/dto.rs | 11 + admin/src/dto/base_dto.rs | 23 ++ admin/src/dto/filter_dto.rs | 1 + admin/src/dto/query_dto.rs | 44 +++ admin/src/initializer.rs | 18 ++ admin/src/main.rs | 18 ++ admin/src/service.rs | 3 + admin/src/service/gateway_service.rs | 200 ++++++++++++ admin/src/service/helper.rs | 24 ++ admin/src/service/plugin_service.rs | 61 ++++ kernel-dto/src/constants.rs | 13 + kernel-dto/src/dto.rs | 3 + kernel-dto/src/dto/gateway_dto.rs | 292 ++++++++++++++++++ .../src/dto}/http_route_dto.rs | 0 .../src/dto}/plugin_filter_dto.rs | 24 +- .../src/config => kernel-dto/src}/k8s_crd.rs | 1 + kernel/Cargo.toml | 3 +- kernel/res/spacegate-gateway.yaml | 12 + kernel/src/config.rs | 9 +- kernel/src/config/config_by_k8s.rs | 93 +++--- kernel/src/config/config_by_local.rs | 2 +- kernel/src/config/config_by_redis.rs | 2 +- kernel/src/config/gateway_dto.rs | 116 ------- kernel/src/constants.rs | 5 - kernel/src/functions/http_client.rs | 3 +- kernel/src/functions/http_route.rs | 9 +- kernel/src/functions/server.rs | 2 +- kernel/src/functions/websocket.rs | 2 +- kernel/src/instance.rs | 10 +- kernel/src/lib.rs | 2 +- kernel/src/plugins/context.rs | 5 +- kernel/src/plugins/filters.rs | 7 +- kernel/src/plugins/filters/redirect.rs | 2 +- kernel/src/plugins/filters/rewrite.rs | 2 +- kernel/tests/test_file.rs | 58 ++++ 45 files changed, 1060 insertions(+), 198 deletions(-) create mode 100644 admin/Cargo.toml create mode 100644 admin/README.md create mode 100644 admin/config/conf-default.toml create mode 100644 admin/src/api.rs create mode 100644 admin/src/api/gateway_api.rs create mode 100644 admin/src/api/plugin_api.rs create mode 100644 admin/src/api/route_api.rs create mode 100644 admin/src/config.rs create mode 100644 admin/src/constants.rs create mode 100644 admin/src/dto.rs create mode 100644 admin/src/dto/base_dto.rs create mode 100644 admin/src/dto/filter_dto.rs create mode 100644 admin/src/dto/query_dto.rs create mode 100644 admin/src/initializer.rs create mode 100644 admin/src/main.rs create mode 100644 admin/src/service.rs create mode 100644 admin/src/service/gateway_service.rs create mode 100644 admin/src/service/helper.rs create mode 100644 admin/src/service/plugin_service.rs create mode 100644 kernel-dto/src/constants.rs create mode 100644 kernel-dto/src/dto.rs create mode 100644 kernel-dto/src/dto/gateway_dto.rs rename {kernel/src/config => kernel-dto/src/dto}/http_route_dto.rs (100%) rename {kernel/src/config => kernel-dto/src/dto}/plugin_filter_dto.rs (64%) rename {kernel/src/config => kernel-dto/src}/k8s_crd.rs (93%) delete mode 100644 kernel/src/config/gateway_dto.rs create mode 100644 kernel/tests/test_file.rs diff --git a/Cargo.toml b/Cargo.toml index 32494811..6bf801b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["kernel", "services/*"] +members = ["kernel","kernel-dto","services/*","admin"] resolver="2" [profile.release] diff --git a/admin/Cargo.toml b/admin/Cargo.toml new file mode 100644 index 00000000..2fd08098 --- /dev/null +++ b/admin/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "spacegate-admin" +version.workspace = true +authors.workspace = true +description.workspace = true +keywords.workspace = true +categories.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +readme = "./README.md" + +[features] +k8s = ["kube", "k8s-openapi", "k8s-gateway-api","kernel-dto/k8s"] + +[dependencies] +tardis = { workspace = true ,features = ["web-server"]} +serde.workspace = true +serde_json.workspace = true + +kernel-dto={path = "../kernel-dto",features = ["admin-support"]} +kube = { workspace = true, optional = true } +k8s-openapi = { workspace = true, optional = true } +k8s-gateway-api = { workspace = true, optional = true } \ No newline at end of file diff --git a/admin/README.md b/admin/README.md new file mode 100644 index 00000000..5ef22620 --- /dev/null +++ b/admin/README.md @@ -0,0 +1,5 @@ +--- + +**spacegate admin serverđŸȘ** + +--- \ No newline at end of file diff --git a/admin/config/conf-default.toml b/admin/config/conf-default.toml new file mode 100644 index 00000000..35e3f039 --- /dev/null +++ b/admin/config/conf-default.toml @@ -0,0 +1,15 @@ +[fw] +[fw.app] +id = "spacegate-admin" +name = "admin of spacegate" +desc = "admin of spacegate" +version = "0.1.0" +default_lang = "zh-cn" + + +[fw.web_server.modules.admin] +title = "spacegate admin" +doc_urls = [["test env", "http://localhost:9080/"],["remote env", "http://192.168.31.164:9080/"]] + +[fw.web_server] +port = 9080 \ No newline at end of file diff --git a/admin/src/api.rs b/admin/src/api.rs new file mode 100644 index 00000000..337d5081 --- /dev/null +++ b/admin/src/api.rs @@ -0,0 +1,3 @@ +pub(crate) mod gateway_api; +pub(crate) mod plugin_api; +pub(crate) mod route_api; diff --git a/admin/src/api/gateway_api.rs b/admin/src/api/gateway_api.rs new file mode 100644 index 00000000..440c3ad9 --- /dev/null +++ b/admin/src/api/gateway_api.rs @@ -0,0 +1,50 @@ +use crate::dto::base_dto::CommonPageDto; +use crate::dto::query_dto::GatewayQueryDto; +use crate::service::gateway_service::GatewayService; +use kernel_dto::dto::gateway_dto::SgGateway; +use tardis::web::poem_openapi; +use tardis::web::poem_openapi::param::Query; +use tardis::web::poem_openapi::payload::Json; +use tardis::web::web_resp::{TardisApiResult, TardisPage, TardisResp, Void}; + +#[derive(Clone, Default)] +pub struct GatewayApi; + +/// Gateway API +#[poem_openapi::OpenApi(prefix_path = "/gateway")] +impl GatewayApi { + /// Get Gateway List + #[oai(path = "/", method = "get")] + async fn list( + &self, + name: Query>, + namespace: Query>, + port: Query>, + hostname: Query>, + ) -> TardisApiResult> { + let result = GatewayService::list( + namespace.0.clone(), + GatewayQueryDto { + name: name.0, + namespace: namespace.0, + port: port.0, + hostname: hostname.0, + }, + ) + .await?; + TardisResp::ok(result) + } + + /// Add Gateway + #[oai(path = "/", method = "post")] + async fn add(&self, namespace: Query>, gateway: Json) -> TardisApiResult { + TardisResp::ok(GatewayService::add(namespace.0, gateway.0).await?) + } + + /// Delete Gateway + #[oai(path = "/", method = "delete")] + async fn delete(&self, namespace: Query>, name: Query) -> TardisApiResult { + GatewayService::delete(namespace.0, &name.0).await?; + TardisResp::ok(Void {}) + } +} diff --git a/admin/src/api/plugin_api.rs b/admin/src/api/plugin_api.rs new file mode 100644 index 00000000..4bfc3681 --- /dev/null +++ b/admin/src/api/plugin_api.rs @@ -0,0 +1,26 @@ +use crate::dto::base_dto::CommonPageDto; +use crate::dto::query_dto::PluginQueryDto; +use crate::service::plugin_service::PluginService; +use tardis::web::poem_openapi; +use tardis::web::poem_openapi::param::Query; +use tardis::web::web_resp::{TardisApiResult, TardisResp, Void}; + +#[derive(Clone, Default)] +pub struct PluginApi; + +#[poem_openapi::OpenApi(prefix_path = "/plugin")] +impl PluginApi { + /// Get Plugin List + #[oai(path = "/", method = "get")] + async fn list(&self, ids: Query>, name: Query>, namespace: Query>, code: Query>) -> TardisApiResult { + let _ = PluginService::list(PluginQueryDto { + ids: ids.0.map(|s| s.split(',').map(|s| s.to_string()).collect::>()), + name: name.0, + namespace: namespace.0, + code: code.0, + target: None, + }) + .await; + TardisResp::ok(Void {}) + } +} diff --git a/admin/src/api/route_api.rs b/admin/src/api/route_api.rs new file mode 100644 index 00000000..943c9c02 --- /dev/null +++ b/admin/src/api/route_api.rs @@ -0,0 +1,7 @@ +use tardis::web::poem_openapi; + +#[derive(Clone, Default)] +pub struct HttprouteApi; + +#[poem_openapi::OpenApi(prefix_path = "/httproute")] +impl HttprouteApi {} diff --git a/admin/src/config.rs b/admin/src/config.rs new file mode 100644 index 00000000..04210ff8 --- /dev/null +++ b/admin/src/config.rs @@ -0,0 +1,43 @@ +use tardis::serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(default)] +pub struct SpacegateAdminConfig { + #[cfg(feature = "k8s")] + pub k8s_config: Option, +} + +#[cfg(feature = "k8s")] +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(default)] +pub struct K8sConfig { + /// The configured cluster url + pub cluster_url: String, + /// The configured default namespace + pub default_namespace: String, + /// The configured root certificate + pub root_cert: Option>>, + /// Set the timeout for connecting to the Kubernetes API. + /// + /// A value of `None` means no timeout + pub connect_timeout: Option, + /// Set the timeout for the Kubernetes API response. + /// + /// A value of `None` means no timeout + pub read_timeout: Option, + /// Set the timeout for the Kubernetes API request. + /// + /// A value of `None` means no timeout + pub write_timeout: Option, + /// Whether to accept invalid certificates + pub accept_invalid_certs: bool, + /// Stores information to tell the cluster who you are. + pub auth_info: kube::config::AuthInfo, + // TODO Actually support proxy or create an example with custom client + /// Optional proxy URL. + pub proxy_url: Option, + /// If set, apiserver certificate will be validated to contain this string + /// + /// If not set, the `cluster_url` is used instead + pub tls_server_name: Option, +} diff --git a/admin/src/constants.rs b/admin/src/constants.rs new file mode 100644 index 00000000..b2357049 --- /dev/null +++ b/admin/src/constants.rs @@ -0,0 +1 @@ +pub const DOMAIN_CODE: &str = "admin"; diff --git a/admin/src/dto.rs b/admin/src/dto.rs new file mode 100644 index 00000000..8e4b7127 --- /dev/null +++ b/admin/src/dto.rs @@ -0,0 +1,11 @@ +pub mod base_dto; +pub mod filter_dto; +pub mod query_dto; + +#[cfg(feature = "k8s")] +pub trait ToFields { + fn to_fields_vec(&self) -> Vec; + fn to_fields(&self) -> String { + self.to_fields_vec().join(",") + } +} diff --git a/admin/src/dto/base_dto.rs b/admin/src/dto/base_dto.rs new file mode 100644 index 00000000..b8ddf246 --- /dev/null +++ b/admin/src/dto/base_dto.rs @@ -0,0 +1,23 @@ +pub struct CommonPageDto { + pub page_size: u32, + pub page_number: u32, +} + +pub struct TargetRefDTO { + pub name: String, + pub kind: Option, + pub namespace: Option, +} + +pub struct BackendRefDTO { + /// Name is the kubernetes service name OR url host. + pub name_or_host: String, + /// Namespace is the kubernetes namespace + pub namespace: Option, + /// Port specifies the destination port number to use for this resource. + pub port: Option, + // pub protocol: Option, + // + // /// Filters define the filters that are applied to backend that match this hostnames. + // pub filters: Option>, +} diff --git a/admin/src/dto/filter_dto.rs b/admin/src/dto/filter_dto.rs new file mode 100644 index 00000000..d202a734 --- /dev/null +++ b/admin/src/dto/filter_dto.rs @@ -0,0 +1 @@ +use kernel_dto::k8s_crd::{K8sSgFilterSpecFilter, K8sSgFilterSpecTargetRef}; diff --git a/admin/src/dto/query_dto.rs b/admin/src/dto/query_dto.rs new file mode 100644 index 00000000..7394ce97 --- /dev/null +++ b/admin/src/dto/query_dto.rs @@ -0,0 +1,44 @@ +use crate::dto::base_dto::{CommonPageDto, TargetRefDTO}; +#[cfg(feature = "k8s")] +use crate::dto::ToFields; + +#[derive(Default)] +pub struct GatewayQueryDto { + pub name: Option, + pub namespace: Option, + pub port: Option, + pub hostname: Option, +} + +#[cfg(feature = "k8s")] +impl ToFields for GatewayQueryDto { + fn to_fields_vec(&self) -> Vec { + let mut result = vec![]; + if let Some(name) = &self.name { + result.push(format!("metadata.name={}", name)) + }; + if let Some(namespace) = &self.namespace { + result.push(format!("metadata.namespace={}", namespace)) + }; + result + } +} + +pub struct PluginQueryDto { + pub ids: Option>, + pub name: Option, + pub code: Option, + pub namespace: Option, + pub target: Option, +} + +#[cfg(feature = "k8s")] +impl ToFields for PluginQueryDto { + fn to_fields_vec(&self) -> Vec { + let mut result = vec![]; + if let Some(name) = &self.name { + result.push(format!("metadata.name={}", name)) + }; + result + } +} diff --git a/admin/src/initializer.rs b/admin/src/initializer.rs new file mode 100644 index 00000000..dd892155 --- /dev/null +++ b/admin/src/initializer.rs @@ -0,0 +1,18 @@ +use crate::api::{gateway_api, plugin_api, route_api}; +use crate::constants; +use tardis::basic::result::TardisResult; +use tardis::web::web_server::{TardisWebServer, WebServerModule}; + +pub(crate) async fn init(web_server: &TardisWebServer) -> TardisResult<()> { + init_api(web_server).await +} + +async fn init_api(web_server: &TardisWebServer) -> TardisResult<()> { + web_server + .add_module( + constants::DOMAIN_CODE, + WebServerModule::from((gateway_api::GatewayApi, plugin_api::PluginApi, route_api::HttprouteApi)), + ) + .await; + Ok(()) +} diff --git a/admin/src/main.rs b/admin/src/main.rs new file mode 100644 index 00000000..c0f9332a --- /dev/null +++ b/admin/src/main.rs @@ -0,0 +1,18 @@ +use tardis::{basic::result::TardisResult, tokio, TardisFuns}; + +mod api; +mod config; +mod constants; +mod dto; +mod initializer; +mod service; + +#[tokio::main] +async fn main() -> TardisResult<()> { + TardisFuns::init(Some("config")).await?; + let web_server = TardisFuns::web_server(); + initializer::init(web_server).await?; + web_server.start().await?; + web_server.await; + Ok(()) +} diff --git a/admin/src/service.rs b/admin/src/service.rs new file mode 100644 index 00000000..6d0e3d6b --- /dev/null +++ b/admin/src/service.rs @@ -0,0 +1,3 @@ +pub(crate) mod gateway_service; +mod helper; +pub(crate) mod plugin_service; diff --git a/admin/src/service/gateway_service.rs b/admin/src/service/gateway_service.rs new file mode 100644 index 00000000..d7677927 --- /dev/null +++ b/admin/src/service/gateway_service.rs @@ -0,0 +1,200 @@ +use crate::dto::query_dto::GatewayQueryDto; +use crate::dto::ToFields; +#[cfg(feature = "k8s")] +use crate::service::helper::get_k8s_client; +use crate::service::plugin_service; +use crate::service::plugin_service::PluginService; +#[cfg(feature = "k8s")] +use k8s_gateway_api::Gateway; +use k8s_gateway_api::{GatewaySpec, Listener}; +#[cfg(feature = "k8s")] +use k8s_openapi::api::core::v1::Secret; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference}; +use kernel_dto::constants::{DEFAULT_NAMESPACE, GATEWAY_CLASS_NAME}; +use kernel_dto::dto::gateway_dto::{SgGateway, SgListener, SgParameters, SgProtocol, SgTlsConfig, SgTlsMode}; +use kube::api::{DeleteParams, PostParams}; +use kube::Resource; +#[cfg(feature = "k8s")] +use kube::{api::ListParams, Api, ResourceExt}; +use std::fmt::format; +use tardis::basic::error::TardisError; +use tardis::basic::result::TardisResult; +use tardis::futures_util::future::join_all; + +pub struct GatewayService; + +impl GatewayService { + pub async fn list(namespace: Option, query: GatewayQueryDto) -> TardisResult> { + let mut result = vec![]; + #[cfg(feature = "k8s")] + { + let gateway_api: Api = if let Some(namespace) = &namespace { + Api::namespaced(get_k8s_client().await?, &namespace) + } else { + Api::all(get_k8s_client().await?) + }; + + let gateway_list = gateway_api.list(&ListParams::default().fields(&query.to_fields())).await.map_err(|e| TardisError::io_error(&format!("err:{e}"), ""))?; + + result = Self::kube_to( + gateway_list + .items + .into_iter() + .filter(|g| { + query.hostname.as_ref().map(|hostname| g.spec.listeners.iter().any(|l| l.hostname == Some(hostname.to_string()))).unwrap_or(true) + && query.port.map(|port| g.spec.listeners.iter().any(|l| l.port == port)).unwrap_or(true) + }) + .collect(), + ) + .await?; + } + #[cfg(not(feature = "k8s"))] + {} + + Ok(result) + } + + pub async fn add(namespace: Option, add: SgGateway) -> TardisResult { + let result; + #[cfg(feature = "k8s")] + { + let namespace = namespace.unwrap_or(DEFAULT_NAMESPACE.to_string()); + + let (gateway_api, secret_api): (Api, Api) = + (Api::namespaced(get_k8s_client().await?, &namespace), Api::namespaced(get_k8s_client().await?, &namespace)); + + let (gateway, secrets, sgfilters) = add.to_kube_gateway(&namespace); + + let result_gateway = gateway_api.create(&PostParams::default(), &gateway).await.map_err(|e| TardisError::io_error(&format!("[SG.admin] error:{e}"), ""))?; + + for mut s in secrets { + s.metadata.owner_references = Some(vec![OwnerReference { + api_version: "gateway.networking.k8s.io/v1beta1".to_string(), + kind: "Gateway".to_string(), + name: result_gateway.name_any(), + uid: result_gateway.uid().expect("Can not get create gateway uid"), + ..Default::default() + }]); + secret_api.create(&PostParams::default(), &s).await.map_err(|e| TardisError::io_error(&format!("[SG.admin] error:{e}"), ""))?; + } + + PluginService::add_sgfilter_vec(sgfilters).await?; + + result = Self::kube_to(vec![result_gateway]).await?.remove(0); + } + #[cfg(not(feature = "k8s"))] + {} + Ok(result) + } + + pub async fn edit(namespace: Option, edit: SgGateway) -> TardisResult { + #[cfg(feature = "k8s")] + { + let gateway_api: Api = Self::get_gateway_api(&namespace).await?; + + //todo ćŻčæŻ”z + // let (gateway, secrets, sgfilters) = edit.to_kube_gateway(); + // gateway_api.replace(&edit.name, &DeleteParams::default(), gateway).await.map_err(|e| TardisError::io_error(&format!("[SG.admin] delete error:{e}"), ""))?; + } + #[cfg(not(feature = "k8s"))] + {} + Ok(edit) + } + + pub async fn delete(namespace: Option, name: &str) -> TardisResult<()> { + #[cfg(feature = "k8s")] + { + let gateway_api: Api = Self::get_gateway_api(&namespace).await?; + + gateway_api.delete(name, &DeleteParams::default()).await.map_err(|e| TardisError::io_error(&format!("[SG.admin] delete error:{e}"), ""))?; + } + #[cfg(not(feature = "k8s"))] + {} + Ok(()) + } + + #[cfg(feature = "k8s")] + async fn get_gateway_api(namespace: &Option) -> TardisResult> { + Ok(Api::namespaced(get_k8s_client().await?, &namespace.as_ref().unwrap_or(&DEFAULT_NAMESPACE.to_string()))) + } + //todo try to compress with kernel::config::config_by_k8s + #[cfg(feature = "k8s")] + pub async fn kube_to(gateway_list: Vec) -> TardisResult> { + let mut result = vec![]; + for g in gateway_list { + result.push(SgGateway { + name: g.name_any(), + parameters: SgParameters::from_kube_gateway(&g), + listeners: join_all( + g.spec + .listeners + .into_iter() + .map(|listener| async move { + let tls = match listener.tls { + Some(tls) => { + let certificate_ref = tls + .certificate_refs + .as_ref() + .ok_or_else(|| TardisError::format_error("[SG.Config] Gateway [spec.listener.tls.certificateRefs] is required", ""))? + .get(0) + .ok_or_else(|| TardisError::format_error("[SG.Config] Gateway [spec.listener.tls.certificateRefs] is empty", ""))?; + let secret_api: Api = + Api::namespaced(get_k8s_client().await?, certificate_ref.namespace.as_ref().unwrap_or(&DEFAULT_NAMESPACE.to_string())); + if let Some(secret_obj) = secret_api + .get_opt(&certificate_ref.name) + .await + .map_err(|error| TardisError::wrap(&format!("[SG.Config] Kubernetes error: {error:?}"), ""))? + { + let secret_data = secret_obj + .data + .ok_or_else(|| TardisError::format_error(&format!("[SG.Config] Gateway tls secret [{}] data is required", certificate_ref.name), ""))?; + let tls_crt = secret_data.get("tls.crt").ok_or_else(|| { + TardisError::format_error(&format!("[SG.Config] Gateway tls secret [{}] data [tls.crt] is required", certificate_ref.name), "") + })?; + let tls_key = secret_data.get("tls.key").ok_or_else(|| { + TardisError::format_error(&format!("[SG.Config] Gateway tls secret [{}] data [tls.key] is required", certificate_ref.name), "") + })?; + Some(SgTlsConfig { + mode: SgTlsMode::from(tls.mode).unwrap_or_default(), + key: String::from_utf8(tls_key.0.clone()).expect("[SG.Config] Gateway tls secret [tls.key] is not valid utf8"), + cert: String::from_utf8(tls_crt.0.clone()).expect("[SG.Config] Gateway tls secret [tls.cert] is not valid utf8"), + }) + } else { + TardisError::not_found(&format!("[SG.admin] Gateway have tls secret [{}], but not found!", certificate_ref.name), ""); + None + } + } + None => None, + }; + let sg_listener = SgListener { + name: listener.name, + ip: None, + port: listener.port, + protocol: match listener.protocol.to_lowercase().as_str() { + "http" => SgProtocol::Http, + "https" => SgProtocol::Https, + "ws" => SgProtocol::Ws, + _ => { + return Err(TardisError::not_implemented( + &format!("[SG.Config] Gateway [spec.listener.protocol={}] not supported yet", listener.protocol), + "", + )) + } + }, + tls, + hostname: listener.hostname, + }; + Ok(sg_listener) + }) + .collect::>(), + ) + .await + .into_iter() + .map(|listener| listener.expect("[SG.Config] Unexpected none: listener")) + .collect(), + filters: None, + }) + } + Ok(result) + } +} diff --git a/admin/src/service/helper.rs b/admin/src/service/helper.rs new file mode 100644 index 00000000..83cecefe --- /dev/null +++ b/admin/src/service/helper.rs @@ -0,0 +1,24 @@ +#[cfg(feature = "k8s")] +use kube::Client; +use tardis::basic::error::TardisError; +use tardis::basic::result::TardisResult; + +#[cfg(feature = "k8s")] +pub async fn get_k8s_client() -> TardisResult { + Client::try_default().await.map_err(|error| TardisError::wrap(&format!("[SG.admin] Get kubernetes client error: {error:?}"), "")) +} + +pub trait WarpKubeResult { + fn warp_result(self) -> TardisResult; + fn warp_result_by_method(self, method: &str) -> TardisResult; +} + +impl WarpKubeResult for kube::Result { + fn warp_result(self) -> TardisResult { + self.map_err(|e| TardisError::wrap(&format!("[SG.admin] kube api error:{e}"), "")) + } + + fn warp_result_by_method(self, method: &str) -> TardisResult { + self.map_err(|e| TardisError::wrap(&format!("[SG.admin] kube api [{method}] error:{e}"), "")) + } +} diff --git a/admin/src/service/plugin_service.rs b/admin/src/service/plugin_service.rs new file mode 100644 index 00000000..6f70483f --- /dev/null +++ b/admin/src/service/plugin_service.rs @@ -0,0 +1,61 @@ +use crate::dto::query_dto::PluginQueryDto; +use crate::dto::ToFields; +#[cfg(feature = "k8s")] +use crate::service::helper::get_k8s_client; +use crate::service::helper::WarpKubeResult; +use kernel_dto::constants::DEFAULT_NAMESPACE; +use kernel_dto::dto::plugin_filter_dto::{SgRouteFilter, SgSingeFilter}; +#[cfg(feature = "k8s")] +use kernel_dto::k8s_crd::SgFilter; +use kube::api::PostParams; +use kube::ResourceExt; +#[cfg(feature = "k8s")] +use kube::{api::ListParams, Api}; +use std::collections::HashSet; +use tardis::basic::error::TardisError; +use tardis::basic::result::TardisResult; + +pub struct PluginService; + +impl PluginService { + pub async fn list(query: PluginQueryDto) -> TardisResult> { + let result = vec![]; + #[cfg(feature = "k8s")] + { + let filter_api: Api = Self::get_filter_api(&query.namespace).await?; + + let filter_list = filter_api.list(&ListParams::default().fields(&query.to_fields())).await.map_err(|e| TardisError::io_error(&format!("err:{e}"), ""))?; + } + #[cfg(not(feature = "k8s"))] + {} + + Ok(result) + } + + #[cfg(feature = "k8s")] + pub async fn add_sgfilter_vec(sgfilters: Vec) -> TardisResult> { + let add_filter = sgfilters.iter().map(|f| f.spec.filters.iter().map(|f_f| (f_f.name.clone(), f_f.code.clone())).collect::>()).flatten().collect::>(); + let add_target = sgfilters + .iter() + .map(|f| {let f_t=f.target_refs.clone(); (f_t.name.clone(), f_t.namespace.clone(), f_t.kind.clone())}).collect::>() + .flatten() + .collect::>(); + for sf in sgfilters { + let filter_api: Api = Self::get_filter_api(&sf.namespace).await?; + if let Some(mut query_sf) = filter_api.get_opt(&sf.name_any()).await.warp_result_by_method("get")? { + sf.spec.filters.iter().filter(|&x| query_sf.spec.filters.iter().any(|qsf| qsf.code == x.code)).collect(); + filter_api.replace(&query_sf.name_any(), &PostParams::default(), &query_sf).await.warp_result_by_method("replace")?; + } else { + filter_api.create(&PostParams::default(), &sf).await.warp_result_by_method("create")?; + } + } + + Ok(sgfilters) + } + + #[cfg(feature = "k8s")] + #[inline] + pub async fn get_filter_api(namespace: &Option) -> TardisResult> { + Ok(Api::namespaced(get_k8s_client().await?, &namespace.as_ref().unwrap_or(&DEFAULT_NAMESPACE.to_string()))) + } +} diff --git a/kernel-dto/src/constants.rs b/kernel-dto/src/constants.rs new file mode 100644 index 00000000..9420daf3 --- /dev/null +++ b/kernel-dto/src/constants.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "k8s")] +pub const GATEWAY_ANNOTATION_REDIS_URL: &str = "redis_url"; +#[cfg(feature = "k8s")] +pub const GATEWAY_ANNOTATION_LOG_LEVEL: &str = "log_level"; +#[cfg(feature = "k8s")] +pub const GATEWAY_ANNOTATION_LANGUAGE: &str = "lang"; +#[cfg(feature = "k8s")] +pub const GATEWAY_ANNOTATION_IGNORE_TLS_VERIFICATION: &str = "ignore_tls_verification"; + +#[cfg(feature = "k8s")] +pub const GATEWAY_CLASS_NAME: &str = "spacegate"; +#[cfg(feature = "k8s")] +pub const DEFAULT_NAMESPACE: &str = "default"; diff --git a/kernel-dto/src/dto.rs b/kernel-dto/src/dto.rs new file mode 100644 index 00000000..c08db431 --- /dev/null +++ b/kernel-dto/src/dto.rs @@ -0,0 +1,3 @@ +pub mod gateway_dto; +pub mod http_route_dto; +pub mod plugin_filter_dto; diff --git a/kernel-dto/src/dto/gateway_dto.rs b/kernel-dto/src/dto/gateway_dto.rs new file mode 100644 index 00000000..0d336f7f --- /dev/null +++ b/kernel-dto/src/dto/gateway_dto.rs @@ -0,0 +1,292 @@ +#[cfg(feature = "k8s")] +use k8s_gateway_api::{Gateway, Listener}; +use k8s_gateway_api::{GatewaySpec, GatewayTlsConfig, SecretObjectReference, TlsModeType}; +use k8s_openapi::api::core::v1::Secret; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use k8s_openapi::ByteString; +use std::collections::BTreeMap; +use std::{fmt::Display, str::FromStr}; + +use super::plugin_filter_dto::SgRouteFilter; +use crate::constants::GATEWAY_CLASS_NAME; +use crate::dto::plugin_filter_dto::SgSingeFilter; +use crate::k8s_crd::{K8sSgFilterSpec, K8sSgFilterSpecFilter, K8sSgFilterSpecTargetRef, SgFilter}; +use serde::{Deserialize, Serialize}; +use tardis::basic::error::TardisError; +#[cfg(feature = "admin-support")] +use tardis::web::poem_openapi; +use tardis::TardisFuns; + +/// Gateway represents an instance of a service-traffic handling infrastructure +/// by binding Listeners to a set of IP addresses. +/// +/// Reference: [Kubernetes Gateway](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Gateway) +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "admin-support", derive(poem_openapi::Object))] +pub struct SgGateway { + /// Name of the Gateway. Global Unique. + pub name: String, + /// Some parameters necessary for the gateway. + pub parameters: SgParameters, + /// Listeners associated with this Gateway. Listeners define logical endpoints + /// that are bound on this Gateway’s addresses. + pub listeners: Vec, + /// Filters define the filters that are applied to requests that match this gateway. + pub filters: Option>, +} + +impl SgGateway { + #[cfg(feature = "k8s")] + pub fn to_kube_gateway(self, namespace: &str) -> (Gateway, Vec, Vec) { + let mut secrets: Vec = vec![]; + + let gateway = Gateway { + metadata: ObjectMeta { + annotations: Some(self.parameters.to_kube_gateway()), + labels: None, + name: Some(self.name.clone()), + owner_references: None, + self_link: None, + ..Default::default() + }, + spec: GatewaySpec { + gateway_class_name: GATEWAY_CLASS_NAME.to_string(), + listeners: self + .listeners + .into_iter() + .map(|l| Listener { + name: l.name, + hostname: l.hostname, + port: l.port, + protocol: l.protocol.to_string(), + tls: l.tls.map(|l| { + let (tls_config, secret) = l.to_kube_tls(namespace); + secrets.push(secret); + tls_config + }), + allowed_routes: None, + }) + .collect(), + addresses: None, + }, + status: None, + }; + + let sgfilters: Vec = if let Some(filters) = self.filters { + filters + .into_iter() + .map(|f| SgSingeFilter { + name: f.name, + namespace: namespace.to_string(), + filters: K8sSgFilterSpecFilter { + code: f.code, + name: None, + enable: true, + config: f.spec, + }, + target_refs: K8sSgFilterSpecTargetRef { + kind: "Gateway".to_string(), + name: self.name.clone(), + namespace: Some(namespace.to_string()), + }, + }) + .collect() + } else { + vec![] + }; + + (gateway, secrets, sgfilters) + } +} + +/// Gateway parameter configuration. +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "admin-support", derive(poem_openapi::Object))] +pub struct SgParameters { + /// Redis access Url, Url with permission information. + pub redis_url: Option, + /// Gateway Log_Level + pub log_level: Option, + /// Gateway language + pub lang: Option, + /// Ignore backend tls verification + pub ignore_tls_verification: Option, +} + +impl SgParameters { + #[cfg(feature = "k8s")] + pub fn from_kube_gateway(gateway: &Gateway) -> Self { + let gateway_annotations = gateway.metadata.annotations.clone(); + if let Some(gateway_annotations) = gateway_annotations { + SgParameters { + redis_url: gateway_annotations.get(crate::constants::GATEWAY_ANNOTATION_REDIS_URL).map(|v| v.to_string()), + log_level: gateway_annotations.get(crate::constants::GATEWAY_ANNOTATION_LOG_LEVEL).map(|v| v.to_string()), + lang: gateway_annotations.get(crate::constants::GATEWAY_ANNOTATION_LANGUAGE).map(|v| v.to_string()), + ignore_tls_verification: gateway_annotations.get(crate::constants::GATEWAY_ANNOTATION_IGNORE_TLS_VERIFICATION).and_then(|v| v.parse::().ok()), + } + } else { + SgParameters { + redis_url: None, + log_level: None, + lang: None, + ignore_tls_verification: None, + } + } + } + + #[cfg(feature = "k8s")] + pub(crate) fn to_kube_gateway(self) -> BTreeMap { + let mut ann = BTreeMap::new(); + if let Some(redis_url) = self.redis_url { + ann.insert(crate::constants::GATEWAY_ANNOTATION_REDIS_URL.to_string(), redis_url); + } + if let Some(log_level) = self.log_level { + ann.insert(crate::constants::GATEWAY_ANNOTATION_LOG_LEVEL.to_string(), log_level); + } + if let Some(lang) = self.lang { + ann.insert(crate::constants::GATEWAY_ANNOTATION_LANGUAGE.to_string(), lang); + } + if let Some(ignore_tls_verification) = self.ignore_tls_verification { + ann.insert( + crate::constants::GATEWAY_ANNOTATION_IGNORE_TLS_VERIFICATION.to_string(), + ignore_tls_verification.to_string(), + ); + } + ann + } +} + +/// Listener embodies the concept of a logical endpoint where a Gateway accepts network connections. +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "admin-support", derive(poem_openapi::Object))] +pub struct SgListener { + /// Name is the name of the Listener. This name MUST be unique within a Gateway. + pub name: String, + /// Ip bound to the Listener. Default is 0.0.0.0 + pub ip: Option, + /// Port is the network port. Multiple listeners may use the same port, subject + /// to the Listener compatibility rules. + pub port: u16, + /// Protocol specifies the network protocol this listener expects to receive. + pub protocol: SgProtocol, + /// TLS is the TLS configuration for the Listener. + /// This field is required if the Protocol field is “HTTPS” or “TLS”. It is invalid + /// to set this field if the Protocol field is “HTTP”, “TCP”, or “UDP”. + pub tls: Option, + /// `HostName` is used to define the host on which the listener accepts requests. + pub hostname: Option, +} + +/// ProtocolType defines the application protocol accepted by a Listener. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] +#[cfg_attr(feature = "admin-support", derive(poem_openapi::Enum))] +#[serde(rename_all = "lowercase")] +pub enum SgProtocol { + /// Accepts cleartext HTTP/1.1 sessions over TCP. Implementations MAY also support + /// HTTP/2 over cleartext. + /// If implementations support HTTP/2 over cleartext on “HTTP” listeners, that + /// MUST be clearly documented by the implementation. + #[default] + Http, + /// Accepts HTTP/1.1 or HTTP/2 sessions over TLS. + Https, + Ws, + Wss, +} + +impl Display for SgProtocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SgProtocol::Http => write!(f, "http"), + SgProtocol::Https => write!(f, "https"), + SgProtocol::Ws => write!(f, "ws"), + SgProtocol::Wss => write!(f, "wss"), + } + } +} + +/// GatewayTLSConfig describes a TLS configuration. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "admin-support", derive(poem_openapi::Object))] +pub struct SgTlsConfig { + pub mode: SgTlsMode, + pub key: String, + pub cert: String, +} + +impl SgTlsConfig { + pub fn to_kube_tls(self, namespace: &str) -> (GatewayTlsConfig, Secret) { + let tls_name = TardisFuns::field.nanoid_custom( + 10, + &[ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', + ], + ); + ( + GatewayTlsConfig { + mode: Some(self.mode.to_kube_tls_mode_type()), + certificate_refs: Some(vec![SecretObjectReference { + kind: Some("Secret".to_string()), + name: tls_name.clone(), + namespace: Some(namespace.to_string()), + ..Default::default() + }]), + ..Default::default() + }, + Secret { + data: Some(BTreeMap::from([ + ("tls.key".to_string(), ByteString(self.key.as_bytes().to_vec())), + ("tls.crt".to_string(), ByteString(self.cert.as_bytes().to_vec())), + ])), + metadata: ObjectMeta { + annotations: None, + labels: None, + name: Some(tls_name), + ..Default::default() + }, + type_: Some("kubernetes.io/tls".to_string()), + ..Default::default() + }, + ) + } +} + +#[derive(Debug, Serialize, PartialEq, Deserialize, Clone, Default)] +#[cfg_attr(feature = "admin-support", derive(poem_openapi::Enum))] +pub enum SgTlsMode { + Terminate, + #[default] + Passthrough, +} + +impl FromStr for SgTlsMode { + type Err = TardisError; + fn from_str(mode: &str) -> Result { + let level = mode.to_lowercase(); + match level.as_str() { + "terminate" => Ok(SgTlsMode::Terminate), + "passthrough" => Ok(SgTlsMode::Passthrough), + _ => Err(TardisError::bad_request("SgTlsMode parse error", "")), + } + } +} + +impl SgTlsMode { + pub fn from(mode: Option) -> Option { + if let Some(mode) = mode { + if let Ok(mode) = SgTlsMode::from_str(&mode) { + return Some(mode); + } + } + None + } + + #[cfg(feature = "k8s")] + pub(crate) fn to_kube_tls_mode_type(self) -> TlsModeType { + match self { + SgTlsMode::Terminate => "Terminate".to_string(), + SgTlsMode::Passthrough => "Passthrough".to_string(), + } + } +} diff --git a/kernel/src/config/http_route_dto.rs b/kernel-dto/src/dto/http_route_dto.rs similarity index 100% rename from kernel/src/config/http_route_dto.rs rename to kernel-dto/src/dto/http_route_dto.rs diff --git a/kernel/src/config/plugin_filter_dto.rs b/kernel-dto/src/dto/plugin_filter_dto.rs similarity index 64% rename from kernel/src/config/plugin_filter_dto.rs rename to kernel-dto/src/dto/plugin_filter_dto.rs index a494c57c..9a60da90 100644 --- a/kernel/src/config/plugin_filter_dto.rs +++ b/kernel-dto/src/dto/plugin_filter_dto.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +#[cfg(feature = "admin-support")] +use tardis::web::poem_openapi; /// RouteFilter defines processing steps that must be completed during the request or response lifecycle. /// @@ -9,10 +11,12 @@ use serde_json::Value; /// 3. Rule level, which works on all requests under the same gateway routing rule /// 4. Backend level, which works on all requests under the same backend #[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "admin-support", derive(poem_openapi::Object))] pub struct SgRouteFilter { /// Filter code, Used to match the corresponding filter. pub code: String, - /// Filter name, the name of the same filter exists at different levels of configuration, only the child nodes take effectBackend Level > Rule Level > Routing Level > Global Level + /// Filter name. If the name of the same filter exists at different levels of configuration, + /// only the child nodes take effectBackend Level > Rule Level > Routing Level > Global Level pub name: Option, /// filter parameters. pub spec: Value, @@ -29,10 +33,22 @@ pub struct SgHttpPathModifier { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum SgHttpPathModifierType { - /// This type of modifier indicates that the full path will be replaced by the specified value. + /// This type of modifier indicates that the full path will be replaced by the + /// specified value. ReplaceFullPath, - /// This type of modifier indicates that any prefix path matches will be replaced by the substitution value. - /// For example, a path with a prefix match of “/foo” and a ReplacePrefixMatch substitution of “/bar” will have the “/foo” prefix replaced with “/bar” in matching requests. + /// This type of modifier indicates that any prefix path matches will be + /// replaced by the substitution value. + /// For example, a path with a prefix match of “/foo” and a ReplacePrefixMatch + /// substitution of “/bar” will have the “/foo” prefix replaced with “/bar” in + /// matching requests. #[default] ReplacePrefixMatch, } + +#[cfg(feature = "k8s")] +pub struct SgSingeFilter { + pub name: Option, + pub namespace: String, + pub filters: crate::k8s_crd::K8sSgFilterSpecFilter, + pub target_refs: crate::k8s_crd::K8sSgFilterSpecTargetRef, +} diff --git a/kernel/src/config/k8s_crd.rs b/kernel-dto/src/k8s_crd.rs similarity index 93% rename from kernel/src/config/k8s_crd.rs rename to kernel-dto/src/k8s_crd.rs index d3e4eb3c..60cd7182 100644 --- a/kernel/src/config/k8s_crd.rs +++ b/kernel-dto/src/k8s_crd.rs @@ -15,6 +15,7 @@ pub struct K8sSgFilterSpec { #[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct K8sSgFilterSpecFilter { + /// see [crate::dto::plugin_filter_dto::SgRouteFilter].code pub code: String, pub name: Option, pub enable: bool, diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 68e250e1..aacb2c9f 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -20,7 +20,7 @@ path = "src/lib.rs" local = ["tardis/fs"] cache = ["tardis/cache"] ws = ["tardis/ws-client"] -k8s = ["kube", "k8s-openapi", "k8s-gateway-api", "schemars", "cache"] +k8s = ["kube", "k8s-openapi", "k8s-gateway-api", "schemars", "cache","kernel-dto/k8s"] [dependencies] serde.workspace = true @@ -31,6 +31,7 @@ itertools.workspace = true urlencoding.workspace = true async-compression.workspace = true +kernel-dto={path = "../kernel-dto"} tardis = { workspace = true, features = ["future", "crypto", "tls"] } http.workspace = true rustls = { workspace = true, features = ["dangerous_configuration"] } diff --git a/kernel/res/spacegate-gateway.yaml b/kernel/res/spacegate-gateway.yaml index 1f5b1aea..a2bd2c41 100644 --- a/kernel/res/spacegate-gateway.yaml +++ b/kernel/res/spacegate-gateway.yaml @@ -62,6 +62,18 @@ spec: required: - kind - name + additionalPrinterColumns: + - name: Code + type: string + description: The code of SgFilter + jsonPath: .spec.filters[*].code + - name: target + type: string + description: The target of SgFilter + jsonPath: .spec.targetRefs[*].kind + - name: Age + type: date + jsonPath: .metadata.creationTimestamp --- apiVersion: v1 kind: ServiceAccount diff --git a/kernel/src/config.rs b/kernel/src/config.rs index ef695ccf..46287c0f 100644 --- a/kernel/src/config.rs +++ b/kernel/src/config.rs @@ -3,9 +3,9 @@ use tardis::{ log, }; -use crate::config::gateway_dto::SgGateway; +use kernel_dto::dto::gateway_dto::SgGateway; -use self::http_route_dto::SgHttpRoute; +use kernel_dto::dto::http_route_dto::SgHttpRoute; #[cfg(feature = "k8s")] pub mod config_by_k8s; @@ -13,11 +13,6 @@ pub mod config_by_k8s; pub mod config_by_local; #[cfg(feature = "cache")] pub mod config_by_redis; -pub mod gateway_dto; -pub mod http_route_dto; -#[cfg(feature = "k8s")] -pub mod k8s_crd; -pub mod plugin_filter_dto; #[allow(unreachable_code)] #[allow(unused_variables)] diff --git a/kernel/src/config/config_by_k8s.rs b/kernel/src/config/config_by_k8s.rs index 8af05000..7212dc6b 100644 --- a/kernel/src/config/config_by_k8s.rs +++ b/kernel/src/config/config_by_k8s.rs @@ -16,31 +16,27 @@ use tardis::{ TardisFuns, }; -use crate::{ - constants::{self, GATEWAY_ANNOTATION_IGNORE_TLS_VERIFICATION}, - do_startup, - functions::http_route, - shutdown, -}; +use crate::{do_startup, functions::http_route, shutdown}; -use super::{ +use crate::constants::{BANCKEND_KIND_EXTERNAL_HTTP, BANCKEND_KIND_EXTERNAL_HTTPS}; +use kernel_dto::constants::GATEWAY_CLASS_NAME; +use kernel_dto::dto::{ gateway_dto::{SgGateway, SgListener, SgParameters, SgProtocol, SgTlsConfig, SgTlsMode}, http_route_dto::{ SgBackendRef, SgHttpHeaderMatch, SgHttpHeaderMatchType, SgHttpPathMatch, SgHttpPathMatchType, SgHttpQueryMatch, SgHttpQueryMatchType, SgHttpRoute, SgHttpRouteMatch, SgHttpRouteRule, }, - k8s_crd::SgFilter, + plugin_filter_dto, plugin_filter_dto::SgRouteFilter, }; -use crate::constants::{BANCKEND_KIND_EXTERNAL_HTTP, BANCKEND_KIND_EXTERNAL_HTTPS, GATEWAY_ANNOTATION_LANGUAGE, GATEWAY_ANNOTATION_LOG_LEVEL, GATEWAY_ANNOTATION_REDIS_URL}; +use kernel_dto::k8s_crd::SgFilter; use lazy_static::lazy_static; +use crate::plugins::filters::header_modifier::SgFilterHeaderModifierKind; lazy_static! { static ref GATEWAY_NAMES: Arc>> = Arc::new(RwLock::new(Vec::new())); } -const GATEWAY_CLASS_NAME: &str = "spacegate"; - pub async fn init(namespaces: Option) -> TardisResult)>> { let (gateway_api, http_route_api, filter_api): (Api, Api, Api) = if let Some(namespaces) = namespaces { ( @@ -493,23 +489,7 @@ async fn process_gateway_config(gateway_objs: Vec) -> TardisResult| ann.get(GATEWAY_ANNOTATION_LOG_LEVEL).map(|v| v.to_string())), - lang: gateway_obj - .metadata - .annotations - .clone() - .and_then(|ann: std::collections::BTreeMap| ann.get(GATEWAY_ANNOTATION_LANGUAGE).map(|v| v.to_string())), - ignore_tls_verification: gateway_obj - .metadata - .annotations - .and_then(|ann: std::collections::BTreeMap| ann.get(GATEWAY_ANNOTATION_IGNORE_TLS_VERIFICATION).and_then(|v| v.parse::().ok())), - }, + parameters: SgParameters::from_kube_gateway(&gateway_obj), listeners: join_all( gateway_obj .spec @@ -549,7 +529,7 @@ async fn process_gateway_config(gateway_objs: Vec) -> TardisResult None, }; let sg_listener = SgListener { - name: Some(listener.name), + name: listener.name, ip: None, port: listener.port, protocol: match listener.protocol.to_lowercase().as_str() { @@ -585,8 +565,8 @@ async fn process_http_route_config(mut http_route_objs: Vec) -> Tardi let mut http_route_configs = Vec::new(); http_route_objs.sort_by(|http_route_a, http_route_b| { let (a_priority, b_priority) = ( - http_route_a.annotations().get(constants::ANNOTATION_RESOURCE_PRIORITY).and_then(|a| a.parse::().ok()).unwrap_or(0), - http_route_b.annotations().get(constants::ANNOTATION_RESOURCE_PRIORITY).and_then(|a| a.parse::().ok()).unwrap_or(0), + http_route_a.annotations().get(crate::constants::ANNOTATION_RESOURCE_PRIORITY).and_then(|a| a.parse::().ok()).unwrap_or(0), + http_route_b.annotations().get(crate::constants::ANNOTATION_RESOURCE_PRIORITY).and_then(|a| a.parse::().ok()).unwrap_or(0), ); match b_priority.cmp(&a_priority) { Ordering::Equal => http_route_a.metadata.creation_timestamp.cmp(&http_route_b.metadata.creation_timestamp), @@ -869,12 +849,12 @@ fn convert_filters(filters: Option>) -> Option super::plugin_filter_dto::SgHttpPathModifier { - kind: super::plugin_filter_dto::SgHttpPathModifierType::ReplaceFullPath, + k8s_gateway_api::HttpPathModifier::ReplaceFullPath { replace_full_path } => plugin_filter_dto::SgHttpPathModifier { + kind: plugin_filter_dto::SgHttpPathModifierType::ReplaceFullPath, value: replace_full_path, }, - k8s_gateway_api::HttpPathModifier::ReplacePrefixMatch { replace_prefix_match } => super::plugin_filter_dto::SgHttpPathModifier { - kind: super::plugin_filter_dto::SgHttpPathModifierType::ReplacePrefixMatch, + k8s_gateway_api::HttpPathModifier::ReplacePrefixMatch { replace_prefix_match } => plugin_filter_dto::SgHttpPathModifier { + kind: plugin_filter_dto::SgHttpPathModifierType::ReplacePrefixMatch, value: replace_prefix_match, }, }), @@ -888,12 +868,12 @@ fn convert_filters(filters: Option>) -> Option super::plugin_filter_dto::SgHttpPathModifier { - kind: super::plugin_filter_dto::SgHttpPathModifierType::ReplaceFullPath, + k8s_gateway_api::HttpPathModifier::ReplaceFullPath { replace_full_path } => plugin_filter_dto::SgHttpPathModifier { + kind: plugin_filter_dto::SgHttpPathModifierType::ReplaceFullPath, value: replace_full_path, }, - k8s_gateway_api::HttpPathModifier::ReplacePrefixMatch { replace_prefix_match } => super::plugin_filter_dto::SgHttpPathModifier { - kind: super::plugin_filter_dto::SgHttpPathModifierType::ReplacePrefixMatch, + k8s_gateway_api::HttpPathModifier::ReplacePrefixMatch { replace_prefix_match } => plugin_filter_dto::SgHttpPathModifier { + kind: plugin_filter_dto::SgHttpPathModifierType::ReplacePrefixMatch, value: replace_prefix_match, }, }), @@ -919,6 +899,41 @@ fn convert_filters(filters: Option>) -> Option>) -> Option> { + filters.map(|filters| { + filters + .into_iter() + .map(|filter| { + let http_route_filter=match filter.code.as_str() { + crate::plugins::filters::header_modifier::CODE=>{ + let header_modifier=TardisFuns::json.json_to_obj::(filter.spec)?; + match header_modifier.kind{ + SgFilterHeaderModifierKind::Request => {HttpRouteFilter::RequestHeaderModifier { request_header_modifier: k8s_gateway_api::HttpRequestHeaderFilter { + set: header_modifier.sets.map(|set|set.into_iter().map(|(name,value)| k8s_gateway_api::HttpHeader {name,value}).collect::>()), + add: None, + remove: header_modifier.remove, + } }} + SgFilterHeaderModifierKind::Response => {HttpRouteFilter::ResponseHeaderModifier { response_header_modifier: k8s_gateway_api::HttpRequestHeaderFilter { + set: header_modifier.sets.map(|set|set.into_iter().map(|(name,value)| k8s_gateway_api::HttpHeader {name,value}).collect::>()), + add: None, + remove: header_modifier.remove, + } }} + } + }, + crate::plugins::filters::rewrite::CODE=>{ + let header_modifier=TardisFuns::json.json_to_obj::<>(filter.spec)?; + HttpRouteFilter::URLRewrite { + + } + }, + _ => {} + }; + HttpRouteFilter {} + }) + .collect_vec() + }) +} + async fn get_client() -> TardisResult { Client::try_default().await.map_err(|error| TardisError::wrap(&format!("[SG.Config] Kubernetes error: {error:?}"), "")) } diff --git a/kernel/src/config/config_by_local.rs b/kernel/src/config/config_by_local.rs index 18e014ee..68ee30bf 100644 --- a/kernel/src/config/config_by_local.rs +++ b/kernel/src/config/config_by_local.rs @@ -9,7 +9,7 @@ use tardis::{ use crate::{do_startup, functions::http_route, shutdown}; -use super::{gateway_dto::SgGateway, http_route_dto::SgHttpRoute}; +use kernel_dto::dto::{gateway_dto::SgGateway, http_route_dto::SgHttpRoute}; use lazy_static::lazy_static; lazy_static! { diff --git a/kernel/src/config/config_by_redis.rs b/kernel/src/config/config_by_redis.rs index 587bfa69..79f2ed73 100644 --- a/kernel/src/config/config_by_redis.rs +++ b/kernel/src/config/config_by_redis.rs @@ -10,7 +10,7 @@ use tardis::{ use crate::{do_startup, functions::http_route, shutdown}; -use super::{gateway_dto::SgGateway, http_route_dto::SgHttpRoute}; +use kernel_dto::dto::{gateway_dto::SgGateway, http_route_dto::SgHttpRoute}; use lazy_static::lazy_static; lazy_static! { diff --git a/kernel/src/config/gateway_dto.rs b/kernel/src/config/gateway_dto.rs deleted file mode 100644 index 3efd8557..00000000 --- a/kernel/src/config/gateway_dto.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::{fmt::Display, str::FromStr}; - -use serde::{Deserialize, Serialize}; -use tardis::basic::error::TardisError; - -use super::plugin_filter_dto::SgRouteFilter; - -/// Gateway represents an instance of a service-traffic handling infrastructure -/// by binding Listeners to a set of IP addresses. -/// -/// Reference: [Kubernetes Gateway](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Gateway) -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct SgGateway { - /// Name of the Gateway. Global Unique. - pub name: String, - /// Some parameters necessary for the gateway. - pub parameters: SgParameters, - /// Listeners associated with this Gateway. Listeners define logical endpoints that are bound on this Gateway’s addresses. - pub listeners: Vec, - /// Filters define the filters that are applied to requests that match this gateway. - pub filters: Option>, -} - -/// Gateway parameter configuration. -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct SgParameters { - /// Redis access Url, Url with permission information. - pub redis_url: Option, - /// Gateway Log_Level - pub log_level: Option, - /// Gateway language - pub lang: Option, - /// Ignore backend tls verification - pub ignore_tls_verification: Option, -} - -/// Listener embodies the concept of a logical endpoint where a Gateway accepts network connections. -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct SgListener { - /// Name is the name of the Listener. This name MUST be unique within a Gateway. - pub name: Option, - /// Ip bound to the Listener. Default is 0.0.0.0 - pub ip: Option, - /// Port is the network port. Multiple listeners may use the same port, subject to the Listener compatibility rules. - pub port: u16, - /// Protocol specifies the network protocol this listener expects to receive. - pub protocol: SgProtocol, - /// TLS is the TLS configuration for the Listener. - /// This field is required if the Protocol field is “HTTPS” or “TLS”. It is invalid to set this field if the Protocol field is “HTTP”, “TCP”, or “UDP”. - pub tls: Option, - /// `HostName` is used to define the host on which the listener accepts requests. - pub hostname: Option, -} - -/// ProtocolType defines the application protocol accepted by a Listener. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] -#[serde(rename_all = "lowercase")] -pub enum SgProtocol { - /// Accepts cleartext HTTP/1.1 sessions over TCP. Implementations MAY also support HTTP/2 over cleartext. - /// If implementations support HTTP/2 over cleartext on “HTTP” listeners, that MUST be clearly documented by the implementation. - #[default] - Http, - /// Accepts HTTP/1.1 or HTTP/2 sessions over TLS. - Https, - Ws, - Wss, -} - -impl Display for SgProtocol { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SgProtocol::Http => write!(f, "http"), - SgProtocol::Https => write!(f, "https"), - SgProtocol::Ws => write!(f, "ws"), - SgProtocol::Wss => write!(f, "wss"), - } - } -} - -/// GatewayTLSConfig describes a TLS configuration. -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct SgTlsConfig { - pub mode: SgTlsMode, - pub key: String, - pub cert: String, -} - -#[derive(Debug, Serialize, PartialEq, Deserialize, Clone, Default)] -pub enum SgTlsMode { - Terminate, - #[default] - Passthrough, -} - -impl FromStr for SgTlsMode { - type Err = TardisError; - fn from_str(mode: &str) -> Result { - let level = mode.to_lowercase(); - match level.as_str() { - "terminate" => Ok(SgTlsMode::Terminate), - "passthrough" => Ok(SgTlsMode::Passthrough), - _ => Err(TardisError::bad_request("SgTlsMode parse error", "")), - } - } -} - -impl SgTlsMode { - pub fn from(mode: Option) -> Option { - if let Some(mode) = mode { - if let Ok(mode) = SgTlsMode::from_str(&mode) { - return Some(mode); - } - } - None - } -} diff --git a/kernel/src/constants.rs b/kernel/src/constants.rs index 1ea70c26..c4939162 100644 --- a/kernel/src/constants.rs +++ b/kernel/src/constants.rs @@ -2,10 +2,5 @@ pub const DOMAIN_CODE: &str = "spacegate_kernel"; pub const ANNOTATION_RESOURCE_PRIORITY: &str = "priority"; -pub const GATEWAY_ANNOTATION_REDIS_URL: &str = "redis_url"; -pub const GATEWAY_ANNOTATION_LOG_LEVEL: &str = "log_level"; -pub const GATEWAY_ANNOTATION_LANGUAGE: &str = "lang"; -pub const GATEWAY_ANNOTATION_IGNORE_TLS_VERIFICATION: &str = "ignore_tls_verification"; - pub const BANCKEND_KIND_EXTERNAL_HTTP: &str = "ExternalHttp"; pub const BANCKEND_KIND_EXTERNAL_HTTPS: &str = "ExternalHttps"; diff --git a/kernel/src/functions/http_client.rs b/kernel/src/functions/http_client.rs index 63b49bdf..c15ac886 100644 --- a/kernel/src/functions/http_client.rs +++ b/kernel/src/functions/http_client.rs @@ -3,10 +3,11 @@ use std::{ time::Duration, }; -use crate::{config::gateway_dto::SgProtocol, plugins::context::SgRoutePluginContext}; +use crate::plugins::context::SgRoutePluginContext; use http::{HeaderMap, HeaderValue, Method, Request, Response, StatusCode}; use hyper::{client::HttpConnector, Body, Client, Error}; use hyper_rustls::{ConfigBuilderExt, HttpsConnector}; +use kernel_dto::dto::gateway_dto::SgProtocol; use tardis::{ basic::{error::TardisError, result::TardisResult}, log, diff --git a/kernel/src/functions/http_route.rs b/kernel/src/functions/http_route.rs index 9a3849ed..d12f1143 100644 --- a/kernel/src/functions/http_route.rs +++ b/kernel/src/functions/http_route.rs @@ -2,10 +2,6 @@ use std::{collections::HashMap, net::SocketAddr}; use crate::instance::{SgBackendInst, SgGatewayInst, SgHttpHeaderMatchInst, SgHttpQueryMatchInst}; use crate::{ - config::{ - gateway_dto::{SgGateway, SgListener}, - http_route_dto::{SgHttpHeaderMatchType, SgHttpPathMatchType, SgHttpQueryMatchType, SgHttpRoute}, - }, instance::{SgHttpPathMatchInst, SgHttpRouteInst, SgHttpRouteMatchInst, SgHttpRouteRuleInst}, plugins::{ context::{ChosenHttpRouteRuleInst, SgRouteFilterRequestAction, SgRoutePluginContext}, @@ -16,6 +12,8 @@ use http::{header::UPGRADE, HeaderValue, Request, Response}; use hyper::{Body, StatusCode}; use itertools::Itertools; +use kernel_dto::dto::gateway_dto::{SgGateway, SgListener}; +use kernel_dto::dto::http_route_dto::{SgHttpHeaderMatchType, SgHttpPathMatchType, SgHttpQueryMatchType, SgHttpRoute}; use std::sync::{Arc, OnceLock}; use std::vec::Vec; use tardis::tokio::sync::RwLock; @@ -446,7 +444,8 @@ async fn process_response_headers(mut ctx: SgRoutePluginContext) -> TardisResult } else { false }; - if !is_chunked { + + if !is_chunked && ctx.response.get_headers().get(http::header::CONTENT_LENGTH).is_some() { let response_body: Vec = ctx.response.take_body_into_bytes().await?.into(); ctx.response.set_header(http::header::CONTENT_LENGTH, response_body.len().to_string().as_str())?; ctx.response.set_body(response_body); diff --git a/kernel/src/functions/server.rs b/kernel/src/functions/server.rs index 45d8723c..04853ae3 100644 --- a/kernel/src/functions/server.rs +++ b/kernel/src/functions/server.rs @@ -5,13 +5,13 @@ use std::{ net::{Ipv4Addr, Ipv6Addr, SocketAddr}, }; -use crate::config::gateway_dto::{SgGateway, SgProtocol, SgTlsMode}; use core::task::{Context, Poll}; use http::{HeaderValue, Request, Response, StatusCode}; use hyper::server::conn::{AddrIncoming, AddrStream}; use hyper::service::{make_service_fn, service_fn}; use hyper::Server; use hyper::{server::accept::Accept, Body}; +use kernel_dto::dto::gateway_dto::{SgGateway, SgProtocol, SgTlsMode}; use lazy_static::lazy_static; use rustls::{PrivateKey, ServerConfig}; diff --git a/kernel/src/functions/websocket.rs b/kernel/src/functions/websocket.rs index 8d8d4175..260ccff8 100644 --- a/kernel/src/functions/websocket.rs +++ b/kernel/src/functions/websocket.rs @@ -1,10 +1,10 @@ use std::net::SocketAddr; -use crate::config::gateway_dto::SgProtocol; use http::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_VERSION, UPGRADE}; use hyper::header::HeaderValue; use hyper::{self}; use hyper::{Body, Request, Response, StatusCode}; +use kernel_dto::dto::gateway_dto::SgProtocol; use std::sync::Arc; use tardis::basic::{error::TardisError, result::TardisResult}; use tardis::futures::stream::StreamExt; diff --git a/kernel/src/instance.rs b/kernel/src/instance.rs index 9e5918d6..b2adcc9f 100644 --- a/kernel/src/instance.rs +++ b/kernel/src/instance.rs @@ -1,15 +1,11 @@ -use crate::{ - config::{ - gateway_dto::{SgListener, SgProtocol}, - http_route_dto::{SgHttpHeaderMatchType, SgHttpPathMatchType, SgHttpQueryMatchType}, - }, - plugins::filters::BoxSgPluginFilter, -}; +use crate::plugins::filters::BoxSgPluginFilter; use http::Method; use hyper::{client::HttpConnector, Client}; use hyper_rustls::HttpsConnector; +use kernel_dto::dto::gateway_dto::{SgListener, SgProtocol}; +use kernel_dto::dto::http_route_dto::{SgHttpHeaderMatchType, SgHttpPathMatchType, SgHttpQueryMatchType}; use std::{fmt, vec::Vec}; use tardis::regex::Regex; diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index d2e4d087..9a49d0ad 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -20,10 +20,10 @@ //! logs at most. #![warn(clippy::unwrap_used)] -use config::{gateway_dto::SgGateway, http_route_dto::SgHttpRoute}; use functions::{http_route, server}; pub use http; pub use hyper; +use kernel_dto::dto::{gateway_dto::SgGateway, http_route_dto::SgHttpRoute}; use plugins::filters::{self, SgPluginFilterDef}; use tardis::{basic::result::TardisResult, log, tokio::signal}; diff --git a/kernel/src/plugins/context.rs b/kernel/src/plugins/context.rs index 350d6857..e5b34329 100644 --- a/kernel/src/plugins/context.rs +++ b/kernel/src/plugins/context.rs @@ -9,7 +9,7 @@ use tardis::basic::result::TardisResult; use tardis::TardisFuns; -use crate::config::gateway_dto::SgProtocol; +use kernel_dto::dto::gateway_dto::SgProtocol; use crate::instance::{SgBackendInst, SgHttpRouteMatchInst, SgHttpRouteRuleInst}; @@ -77,7 +77,8 @@ pub enum SgRouteFilterRequestAction { None, /// Forwarding the current request. Redirect, - /// Constructing a response directly based on the information in the context , without making a backend request. + /// Constructing a response directly based on the information in the context , + /// without making a backend request. Response, } diff --git a/kernel/src/plugins/filters.rs b/kernel/src/plugins/filters.rs index f322e312..d96e6fd8 100644 --- a/kernel/src/plugins/filters.rs +++ b/kernel/src/plugins/filters.rs @@ -19,10 +19,10 @@ use tardis::basic::result::TardisResult; use tardis::url::Url; use tardis::{log, TardisFuns}; -use crate::config::gateway_dto::{SgGateway, SgParameters}; -use crate::config::http_route_dto::{SgBackendRef, SgHttpPathMatchType, SgHttpRoute, SgHttpRouteRule}; -use crate::config::plugin_filter_dto::{SgHttpPathModifier, SgHttpPathModifierType, SgRouteFilter}; use crate::instance::SgHttpRouteMatchInst; +use kernel_dto::dto::gateway_dto::{SgGateway, SgParameters}; +use kernel_dto::dto::http_route_dto::{SgBackendRef, SgHttpPathMatchType, SgHttpRoute, SgHttpRouteRule}; +use kernel_dto::dto::plugin_filter_dto::{SgHttpPathModifier, SgHttpPathModifierType, SgRouteFilter}; use super::context::SgRoutePluginContext; @@ -105,6 +105,7 @@ pub async fn init(filter_configs: Vec, init_dto: SgPluginFilterIn let mut elements_to_remove = vec![]; for filter_conf in filter_configs { let name = filter_conf.name.unwrap_or(TardisFuns::field.nanoid()); + //todo k8s update sgfilter.name let filter_def = get_filter_def(&filter_conf.code)?; let filter_inst = filter_def.inst(filter_conf.spec)?; plugin_filters.push((format!("{}_{name}", filter_conf.code), filter_inst)); diff --git a/kernel/src/plugins/filters/redirect.rs b/kernel/src/plugins/filters/redirect.rs index 144fac24..f366f919 100644 --- a/kernel/src/plugins/filters/redirect.rs +++ b/kernel/src/plugins/filters/redirect.rs @@ -4,10 +4,10 @@ use serde::{Deserialize, Serialize}; use tardis::basic::{error::TardisError, result::TardisResult}; use tardis::url::Url; -use crate::config::plugin_filter_dto::SgHttpPathModifier; use crate::def_filter; use crate::helpers::url_helper::UrlToUri; use crate::plugins::context::SgRouteFilterRequestAction; +use kernel_dto::dto::plugin_filter_dto::SgHttpPathModifier; use super::{http_common_modify_path, SgPluginFilter, SgPluginFilterInitDto, SgRoutePluginContext}; diff --git a/kernel/src/plugins/filters/rewrite.rs b/kernel/src/plugins/filters/rewrite.rs index 413126c0..d67f0c21 100644 --- a/kernel/src/plugins/filters/rewrite.rs +++ b/kernel/src/plugins/filters/rewrite.rs @@ -1,7 +1,7 @@ -use crate::config::plugin_filter_dto::SgHttpPathModifier; use crate::def_filter; use crate::helpers::url_helper::UrlToUri; use async_trait::async_trait; +use kernel_dto::dto::plugin_filter_dto::SgHttpPathModifier; use serde::{Deserialize, Serialize}; use tardis::basic::{error::TardisError, result::TardisResult}; use tardis::url::Url; diff --git a/kernel/tests/test_file.rs b/kernel/tests/test_file.rs new file mode 100644 index 00000000..34f92ef3 --- /dev/null +++ b/kernel/tests/test_file.rs @@ -0,0 +1,58 @@ +// use std::{env, mem}; +// use std::time::Duration; +// use http::{header, HeaderMap, Method}; +// use hyper::{Body, Client}; +// use hyper_rustls::ConfigBuilderExt; +// use tardis::tokio; +// use tardis::tokio::time::sleep; +// use spacegate_kernel::config::gateway_dto::{SgGateway, SgListener, SgProtocol}; +// use spacegate_kernel::config::http_route_dto::{SgBackendRef, SgHttpRoute, SgHttpRouteRule}; +// use spacegate_kernel::functions::http_client; +// +// #[tokio::test] +// async fn test_https() { +// env::set_var("RUST_LOG", "info,spacegate_kernel=trace"); +// tracing_subscriber::fmt::init(); +// spacegate_kernel::do_startup( +// SgGateway { +// name: "test_gw".to_string(), +// listeners: vec![ +// SgListener { +// port: 8888, +// ..Default::default() +// },], +// ..Default::default() +// }, +// vec![SgHttpRoute { +// gateway_name: "test_gw".to_string(), +// rules: Some(vec![SgHttpRouteRule { +// backends: Some(vec![SgBackendRef { +// name_or_host: "postman-echo.com".to_string(), +// port: 443, +// protocol: Some(SgProtocol::Https), +// ..Default::default() +// }]), +// ..Default::default() +// }]), +// ..Default::default() +// }], +// ) +// .await.unwrap(); +// sleep(Duration::from_millis(500)).await; +// +// let https = hyper_rustls::HttpsConnectorBuilder::new().with_tls_config(rustls::ClientConfig::builder().with_safe_defaults().with_native_roots().with_no_client_auth()).https_or_http().enable_http1().build(); +// let client = Client::builder().build(https); +// +// let body=Body::empty(); +// let mut headers=HeaderMap::new(); +// headers.insert(header::CONTENT_TYPE,"multipart/form-data; boundary=--------------------------734948368826402972978072".parse().unwrap()); +// let resp=http_client::raw_request(Some(&client),Method::POST,"http://localhost:8888/post",body,&headers,None).await.unwrap(); +// +// let mut resp_body =resp.body(); +// let mut swap_body=resp_body +// mem::swap(&mut resp_body); +// let bytes = hyper::body::to_bytes().await.unwrap(); +// +// println!("{:?}",resp_body); +// +// }