feat/web (Patchset 2) (#444)

This patch implement a restful server without any auth.

usage:

```bash
# run easytier-web, which acts as an gateway and registry for all easytier-core
$> easytier-web

# run easytier-core and connect to easytier-web with a token
$> easytier-core --config-server udp://127.0.0.1:22020/fdsafdsa

# use restful api to list session
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/sessions
[{"token":"fdsafdsa","client_url":"udp://127.0.0.1:48915","machine_id":"de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f"}]%

# use restful api to run a network instance
$> curl -H "Content-Type: application/json" -X POST 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f -d '{"config": "listeners = [\"udp://0.0.0.0:12344\"]"}'

# use restful api to get network instance info
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f/65437e50-b286-4098-a624-74429f2cb839 
```
This commit is contained in:
Sijie.Sun
2024-10-26 00:04:22 +08:00
committed by GitHub
parent b5c3726e67
commit a78b759741
33 changed files with 1539 additions and 263 deletions
+171
View File
@@ -0,0 +1,171 @@
use std::collections::BTreeMap;
use dashmap::DashMap;
use crate::{
common::config::{ConfigLoader, TomlConfigLoader},
launcher::NetworkInstance,
proto::{
rpc_types::{self, controller::BaseController},
web::{
CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest,
DeleteNetworkInstanceResponse, ListNetworkInstanceRequest, ListNetworkInstanceResponse,
NetworkInstanceRunningInfoMap, RetainNetworkInstanceRequest,
RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse,
ValidateConfigRequest, ValidateConfigResponse, WebClientService,
},
},
};
pub struct Controller {
token: String,
instance_map: DashMap<uuid::Uuid, NetworkInstance>,
}
impl Controller {
pub fn new(token: String) -> Self {
Controller {
token,
instance_map: DashMap::new(),
}
}
pub fn run_network_instance(&self, cfg: TomlConfigLoader) -> Result<(), anyhow::Error> {
let instance_id = cfg.get_id();
if self.instance_map.contains_key(&instance_id) {
anyhow::bail!("instance {} already exists", instance_id);
}
let mut instance = NetworkInstance::new(cfg);
instance.start()?;
println!("instance {} started", instance_id);
self.instance_map.insert(instance_id, instance);
Ok(())
}
pub fn retain_network_instance(
&self,
instance_ids: Vec<uuid::Uuid>,
) -> Result<RetainNetworkInstanceResponse, anyhow::Error> {
self.instance_map.retain(|k, _| instance_ids.contains(k));
let remain = self
.instance_map
.iter()
.map(|item| item.key().clone().into())
.collect::<Vec<_>>();
println!("instance {:?} retained", remain);
Ok(RetainNetworkInstanceResponse {
remain_inst_ids: remain,
})
}
pub fn collect_network_infos(&self) -> Result<NetworkInstanceRunningInfoMap, anyhow::Error> {
let mut map = BTreeMap::new();
for instance in self.instance_map.iter() {
if let Some(info) = instance.get_running_info() {
map.insert(instance.key().to_string(), info);
}
}
Ok(NetworkInstanceRunningInfoMap { map })
}
pub fn list_network_instance_ids(&self) -> Vec<uuid::Uuid> {
self.instance_map
.iter()
.map(|item| item.key().clone())
.collect()
}
pub fn token(&self) -> String {
self.token.clone()
}
}
#[async_trait::async_trait]
impl WebClientService for Controller {
type Controller = BaseController;
async fn validate_config(
&self,
_: BaseController,
req: ValidateConfigRequest,
) -> Result<ValidateConfigResponse, rpc_types::error::Error> {
let _ = TomlConfigLoader::new_from_str(&req.config)?;
Ok(ValidateConfigResponse {})
}
async fn run_network_instance(
&self,
_: BaseController,
req: RunNetworkInstanceRequest,
) -> Result<RunNetworkInstanceResponse, rpc_types::error::Error> {
let cfg = TomlConfigLoader::new_from_str(&req.config)?;
self.run_network_instance(cfg)?;
Ok(RunNetworkInstanceResponse {})
}
async fn retain_network_instance(
&self,
_: BaseController,
req: RetainNetworkInstanceRequest,
) -> Result<RetainNetworkInstanceResponse, rpc_types::error::Error> {
Ok(self.retain_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?)
}
async fn collect_network_info(
&self,
_: BaseController,
req: CollectNetworkInfoRequest,
) -> Result<CollectNetworkInfoResponse, rpc_types::error::Error> {
let mut ret = self.collect_network_infos()?;
let include_inst_ids = req
.inst_ids
.iter()
.cloned()
.map(|id| id.to_string())
.collect::<Vec<_>>();
if !include_inst_ids.is_empty() {
let mut to_remove = Vec::new();
for (k, _) in ret.map.iter() {
if !include_inst_ids.contains(&k) {
to_remove.push(k.clone());
}
}
for k in to_remove {
ret.map.remove(&k);
}
}
Ok(CollectNetworkInfoResponse { info: Some(ret) })
}
// rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {}
async fn list_network_instance(
&self,
_: BaseController,
_: ListNetworkInstanceRequest,
) -> Result<ListNetworkInstanceResponse, rpc_types::error::Error> {
Ok(ListNetworkInstanceResponse {
inst_ids: self
.list_network_instance_ids()
.into_iter()
.map(Into::into)
.collect(),
})
}
// rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {}
async fn delete_network_instance(
&self,
_: BaseController,
req: DeleteNetworkInstanceRequest,
) -> Result<DeleteNetworkInstanceResponse, rpc_types::error::Error> {
let mut inst_ids = self.list_network_instance_ids();
inst_ids.retain(|id| !req.inst_ids.contains(&(id.clone().into())));
self.retain_network_instance(inst_ids.clone())?;
Ok(DeleteNetworkInstanceResponse {
remain_inst_ids: inst_ids.into_iter().map(Into::into).collect(),
})
}
}
+48
View File
@@ -0,0 +1,48 @@
use std::sync::Arc;
use crate::{common::scoped_task::ScopedTask, tunnel::TunnelConnector};
pub mod controller;
pub mod session;
pub struct WebClient {
controller: Arc<controller::Controller>,
tasks: ScopedTask<()>,
}
impl WebClient {
pub fn new<T: TunnelConnector + 'static, S: ToString>(connector: T, token: S) -> Self {
let controller = Arc::new(controller::Controller::new(token.to_string()));
let controller_clone = controller.clone();
let tasks = ScopedTask::from(tokio::spawn(async move {
Self::routine(controller_clone, Box::new(connector)).await;
}));
WebClient { controller, tasks }
}
async fn routine(
controller: Arc<controller::Controller>,
mut connector: Box<dyn TunnelConnector>,
) {
loop {
let conn = match connector.connect().await {
Ok(conn) => conn,
Err(e) => {
println!(
"Failed to connect to the server ({}), retrying in 5 seconds...",
e
);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
};
println!("Successfully connected to {:?}", conn.info());
let mut session = session::Session::new(conn, controller.clone());
session.wait().await;
}
}
}
+126
View File
@@ -0,0 +1,126 @@
use std::sync::Arc;
use tokio::{
sync::{broadcast, Mutex},
task::JoinSet,
time::interval,
};
use crate::{
common::get_machine_id,
proto::{
rpc_impl::bidirect::BidirectRpcManager,
rpc_types::controller::BaseController,
web::{
HeartbeatRequest, HeartbeatResponse, WebClientServiceServer,
WebServerServiceClientFactory,
},
},
tunnel::Tunnel,
};
use super::controller::Controller;
#[derive(Debug, Clone)]
struct HeartbeatCtx {
notifier: Arc<broadcast::Sender<HeartbeatResponse>>,
resp: Arc<Mutex<Option<HeartbeatResponse>>>,
}
pub struct Session {
rpc_mgr: BidirectRpcManager,
controller: Arc<Controller>,
heartbeat_ctx: HeartbeatCtx,
tasks: Mutex<JoinSet<()>>,
}
impl Session {
pub fn new(tunnel: Box<dyn Tunnel>, controller: Arc<Controller>) -> Self {
let rpc_mgr = BidirectRpcManager::new();
rpc_mgr.run_with_tunnel(tunnel);
rpc_mgr
.rpc_server()
.registry()
.register(WebClientServiceServer::new(controller.clone()), "");
let mut tasks: JoinSet<()> = JoinSet::new();
let heartbeat_ctx = Self::heartbeat_routine(&rpc_mgr, controller.token(), &mut tasks);
Session {
rpc_mgr,
controller,
heartbeat_ctx,
tasks: Mutex::new(tasks),
}
}
fn heartbeat_routine(
rpc_mgr: &BidirectRpcManager,
token: String,
tasks: &mut JoinSet<()>,
) -> HeartbeatCtx {
let (tx, _rx1) = broadcast::channel(2);
let ctx = HeartbeatCtx {
notifier: Arc::new(tx),
resp: Arc::new(Mutex::new(None)),
};
let mid = get_machine_id();
let inst_id = uuid::Uuid::new_v4();
let token = token;
let ctx_clone = ctx.clone();
let mut tick = interval(std::time::Duration::from_secs(1));
let client = rpc_mgr
.rpc_client()
.scoped_client::<WebServerServiceClientFactory<BaseController>>(1, 1, "".to_string());
tasks.spawn(async move {
let req = HeartbeatRequest {
machine_id: Some(mid.into()),
inst_id: Some(inst_id.into()),
user_token: token.to_string(),
};
loop {
tick.tick().await;
match client
.heartbeat(BaseController::default(), req.clone())
.await
{
Err(e) => {
tracing::error!("heartbeat failed: {:?}", e);
break;
}
Ok(resp) => {
tracing::debug!("heartbeat response: {:?}", resp);
let _ = ctx_clone.notifier.send(resp.clone());
ctx_clone.resp.lock().await.replace(resp);
}
}
}
});
ctx
}
async fn wait_routines(&self) {
self.tasks.lock().await.join_next().await;
// if any task failed, we should abort all tasks
self.tasks.lock().await.abort_all();
}
pub async fn wait(&mut self) {
tokio::select! {
_ = self.rpc_mgr.wait() => {}
_ = self.wait_routines() => {}
}
}
pub async fn wait_next_heartbeat(&self) -> Option<HeartbeatResponse> {
let mut rx = self.heartbeat_ctx.notifier.subscribe();
rx.recv().await.ok()
}
}