mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 10:14:35 +00:00
improve webclient (#2151)
This commit is contained in:
@@ -15,7 +15,9 @@ use easytier::rpc_service::remote_client::{
|
|||||||
use easytier::web_client::{self, WebClient};
|
use easytier::web_client::{self, WebClient};
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::{
|
common::{
|
||||||
config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
config::{
|
||||||
|
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
|
||||||
|
},
|
||||||
log,
|
log,
|
||||||
},
|
},
|
||||||
instance_manager::NetworkInstanceManager,
|
instance_manager::NetworkInstanceManager,
|
||||||
@@ -118,7 +120,7 @@ async fn run_network_instance(
|
|||||||
let client_manager = get_client_manager!()?;
|
let client_manager = get_client_manager!()?;
|
||||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
client_manager
|
client_manager
|
||||||
.pre_run_network_instance_hook(&app, &toml_config)
|
.pre_run_network_instance_hook(&app, &toml_config, manager::PersistedConfigSource::User)
|
||||||
.await?;
|
.await?;
|
||||||
client_manager
|
client_manager
|
||||||
.handle_run_network_instance(app.clone(), cfg, save)
|
.handle_run_network_instance(app.clone(), cfg, save)
|
||||||
@@ -207,13 +209,17 @@ async fn update_network_config_state(
|
|||||||
.map_err(|e: uuid::Error| e.to_string())?;
|
.map_err(|e: uuid::Error| e.to_string())?;
|
||||||
let client_manager = get_client_manager!()?;
|
let client_manager = get_client_manager!()?;
|
||||||
if !disabled {
|
if !disabled {
|
||||||
let cfg = client_manager
|
let (cfg, source) = client_manager
|
||||||
.handle_get_network_config(app.clone(), instance_id)
|
.handle_get_network_config_with_source(app.clone(), instance_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
client_manager
|
client_manager
|
||||||
.pre_run_network_instance_hook(&app, &toml_config)
|
.pre_run_network_instance_hook(
|
||||||
|
&app,
|
||||||
|
&toml_config,
|
||||||
|
manager::PersistedConfigSource::from_runtime_source(source),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
client_manager
|
client_manager
|
||||||
@@ -272,7 +278,7 @@ async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_configs(
|
async fn load_configs(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
configs: Vec<NetworkConfig>,
|
configs: Vec<manager::StoredGuiConfig>,
|
||||||
enabled_networks: Vec<String>,
|
enabled_networks: Vec<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
get_client_manager!()?
|
get_client_manager!()?
|
||||||
@@ -612,7 +618,11 @@ mod manager {
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let client_manager = get_client_manager!()?;
|
let client_manager = get_client_manager!()?;
|
||||||
client_manager
|
client_manager
|
||||||
.pre_run_network_instance_hook(&self.app, cfg)
|
.pre_run_network_instance_hook(
|
||||||
|
&self.app,
|
||||||
|
cfg,
|
||||||
|
PersistedConfigSource::from_runtime_source(cfg.get_network_config_source()),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,14 +641,87 @@ mod manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) enum PersistedConfigSource {
|
||||||
|
User,
|
||||||
|
Webhook,
|
||||||
|
#[serde(other)]
|
||||||
|
#[default]
|
||||||
|
Legacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistedConfigSource {
|
||||||
|
pub(super) fn from_runtime_source(source: ConfigSource) -> Self {
|
||||||
|
match source {
|
||||||
|
ConfigSource::User => Self::User,
|
||||||
|
ConfigSource::Webhook => Self::Webhook,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_persisted(self, incoming: Self) -> Self {
|
||||||
|
match (self, incoming) {
|
||||||
|
// Older runtimes report missing source as `user`. Keep the stronger persisted
|
||||||
|
// ownership until webhook sync or an explicit user save repairs it.
|
||||||
|
(Self::Webhook, Self::User) | (Self::Legacy, Self::User) => self,
|
||||||
|
(_, next) => next,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_runtime_source(self) -> ConfigSource {
|
||||||
|
match self {
|
||||||
|
Self::User | Self::Legacy => ConfigSource::User,
|
||||||
|
Self::Webhook => ConfigSource::Webhook,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, target_os = "android"))]
|
||||||
|
fn is_webhook_like(self) -> bool {
|
||||||
|
matches!(self, Self::Webhook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
|
pub(super) struct GUIConfig {
|
||||||
|
inst_id: String,
|
||||||
|
pub(crate) config: NetworkConfig,
|
||||||
|
source: PersistedConfigSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(super) struct StoredGuiConfig {
|
||||||
|
config: NetworkConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
source: PersistedConfigSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GUIConfig {
|
||||||
|
fn new(inst_id: String, config: NetworkConfig, source: PersistedConfigSource) -> Self {
|
||||||
|
Self {
|
||||||
|
inst_id,
|
||||||
|
config,
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_stored(self) -> StoredGuiConfig {
|
||||||
|
StoredGuiConfig {
|
||||||
|
config: self.config,
|
||||||
|
source: self.source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PersistentConfig<anyhow::Error> for GUIConfig {
|
impl PersistentConfig<anyhow::Error> for GUIConfig {
|
||||||
fn get_network_inst_id(&self) -> &str {
|
fn get_network_inst_id(&self) -> &str {
|
||||||
&self.0
|
&self.inst_id
|
||||||
}
|
}
|
||||||
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
||||||
Ok(self.1.clone())
|
Ok(self.config.clone())
|
||||||
|
}
|
||||||
|
fn get_network_config_source(&self) -> ConfigSource {
|
||||||
|
self.source.to_runtime_source()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,13 +738,12 @@ mod manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
||||||
let configs: Result<Vec<String>, _> = self
|
let configs = self
|
||||||
.network_configs
|
.network_configs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| serde_json::to_string(&entry.value().1))
|
.map(|entry| entry.value().clone().into_stored())
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
let payload = format!("[{}]", configs?.join(","));
|
app.emit("save_configs", configs)?;
|
||||||
app.emit_str("save_configs", payload)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,8 +762,14 @@ mod manager {
|
|||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
inst_id: Uuid,
|
inst_id: Uuid,
|
||||||
cfg: NetworkConfig,
|
cfg: NetworkConfig,
|
||||||
|
source: PersistedConfigSource,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let config = GUIConfig(inst_id.to_string(), cfg);
|
let source = self
|
||||||
|
.network_configs
|
||||||
|
.get(&inst_id)
|
||||||
|
.map(|existing| existing.source.merge_persisted(source))
|
||||||
|
.unwrap_or(source);
|
||||||
|
let config = GUIConfig::new(inst_id.to_string(), cfg, source);
|
||||||
self.network_configs.insert(inst_id, config);
|
self.network_configs.insert(inst_id, config);
|
||||||
self.save_configs(app)
|
self.save_configs(app)
|
||||||
}
|
}
|
||||||
@@ -693,8 +781,14 @@ mod manager {
|
|||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
network_inst_id: Uuid,
|
network_inst_id: Uuid,
|
||||||
network_config: NetworkConfig,
|
network_config: NetworkConfig,
|
||||||
|
source: ConfigSource,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
self.save_config(&app, network_inst_id, network_config)?;
|
self.save_config(
|
||||||
|
&app,
|
||||||
|
network_inst_id,
|
||||||
|
network_config,
|
||||||
|
PersistedConfigSource::from_runtime_source(source),
|
||||||
|
)?;
|
||||||
self.enabled_networks.insert(network_inst_id);
|
self.enabled_networks.insert(network_inst_id);
|
||||||
self.save_enabled_networks(&app)?;
|
self.save_enabled_networks(&app)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -811,17 +905,36 @@ mod manager {
|
|||||||
.network_configs
|
.network_configs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||||
.filter(|v| !v.1.no_tun())
|
.filter(|v| !v.config.no_tun())
|
||||||
.filter_map(|c| c.1.instance_id().parse::<uuid::Uuid>().ok())
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn get_enabled_instances_with_webhook_like_tun_ids(
|
||||||
|
&self,
|
||||||
|
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||||
|
self.storage
|
||||||
|
.network_configs
|
||||||
|
.iter()
|
||||||
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||||
|
.filter(|v| !v.config.no_tun())
|
||||||
|
.filter(|v| v.source.is_webhook_like())
|
||||||
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub(super) async fn disable_instances_with_tun(
|
pub(super) async fn disable_instances_with_tun(
|
||||||
&self,
|
&self,
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
|
webhook_only: bool,
|
||||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||||
{
|
{
|
||||||
let inst_ids: Vec<uuid::Uuid> = self.get_enabled_instances_with_tun_ids().collect();
|
let inst_ids: Vec<uuid::Uuid> = if webhook_only {
|
||||||
|
self.get_enabled_instances_with_webhook_like_tun_ids()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
self.get_enabled_instances_with_tun_ids().collect()
|
||||||
|
};
|
||||||
for inst_id in inst_ids {
|
for inst_id in inst_ids {
|
||||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -842,6 +955,7 @@ mod manager {
|
|||||||
&self,
|
&self,
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
cfg: &easytier::common::config::TomlConfigLoader,
|
cfg: &easytier::common::config::TomlConfigLoader,
|
||||||
|
source: PersistedConfigSource,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let instance_id = cfg.get_id();
|
let instance_id = cfg.get_id();
|
||||||
app.emit("pre_run_network_instance", instance_id.to_string())
|
app.emit("pre_run_network_instance", instance_id.to_string())
|
||||||
@@ -849,9 +963,24 @@ mod manager {
|
|||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
if !cfg.get_flags().no_tun {
|
if !cfg.get_flags().no_tun {
|
||||||
self.disable_instances_with_tun(app)
|
match source {
|
||||||
.await
|
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
||||||
.map_err(|e| e.to_string())?;
|
self.disable_instances_with_tun(app, false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
PersistedConfigSource::Webhook => {
|
||||||
|
self.disable_instances_with_tun(app, true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||||
|
return Err(
|
||||||
|
"Android only supports one active TUN network; user-managed VPN remains active"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.storage
|
self.storage
|
||||||
@@ -859,6 +988,7 @@ mod manager {
|
|||||||
app,
|
app,
|
||||||
instance_id,
|
instance_id,
|
||||||
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
|
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
|
||||||
|
source,
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@@ -962,15 +1092,15 @@ mod manager {
|
|||||||
pub(super) async fn load_configs(
|
pub(super) async fn load_configs(
|
||||||
&self,
|
&self,
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
configs: Vec<NetworkConfig>,
|
configs: Vec<StoredGuiConfig>,
|
||||||
enabled_networks: Vec<String>,
|
enabled_networks: Vec<String>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
self.storage.network_configs.clear();
|
self.storage.network_configs.clear();
|
||||||
for cfg in configs {
|
for stored in configs {
|
||||||
let instance_id = cfg.instance_id();
|
let instance_id = stored.config.instance_id();
|
||||||
self.storage.network_configs.insert(
|
self.storage.network_configs.insert(
|
||||||
instance_id.parse()?,
|
instance_id.parse()?,
|
||||||
GUIConfig(instance_id.to_string(), cfg),
|
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,12 +1116,12 @@ mod manager {
|
|||||||
.storage
|
.storage
|
||||||
.network_configs
|
.network_configs
|
||||||
.get(&uuid)
|
.get(&uuid)
|
||||||
.map(|i| i.value().1.clone());
|
.map(|i| (i.value().config.clone(), i.value().source));
|
||||||
let Some(config) = config else {
|
let Some((config, source)) = config else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let toml_config = config.gen_config()?;
|
let toml_config = config.gen_config()?;
|
||||||
self.pre_run_network_instance_hook(&app, &toml_config)
|
self.pre_run_network_instance_hook(&app, &toml_config, source)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!(e))?;
|
.map_err(|e| anyhow::anyhow!(e))?;
|
||||||
client
|
client
|
||||||
@@ -1001,6 +1131,7 @@ mod manager {
|
|||||||
inst_id: None,
|
inst_id: None,
|
||||||
config: Some(config),
|
config: Some(config),
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
|
source: source.to_runtime_source().to_rpc(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1032,6 +1163,44 @@ mod manager {
|
|||||||
&self.storage
|
&self.storage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{PersistedConfigSource, StoredGuiConfig};
|
||||||
|
use easytier::proto::api::manage::NetworkConfig;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_gui_config_defaults_missing_source_to_legacy() {
|
||||||
|
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
||||||
|
"config": NetworkConfig::default(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(stored.source, PersistedConfigSource::Legacy);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persisted_source_merge_keeps_legacy_and_webhook_over_ambiguous_user() {
|
||||||
|
assert_eq!(
|
||||||
|
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User),
|
||||||
|
PersistedConfigSource::Legacy
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PersistedConfigSource::Webhook.merge_persisted(PersistedConfigSource::User),
|
||||||
|
PersistedConfigSource::Webhook
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Webhook),
|
||||||
|
PersistedConfigSource::Webhook
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_webhook_configs_are_webhook_like() {
|
||||||
|
assert!(!PersistedConfigSource::Legacy.is_webhook_like());
|
||||||
|
assert!(!PersistedConfigSource::User.is_webhook_like());
|
||||||
|
assert!(PersistedConfigSource::Webhook.is_webhook_like());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist
|
|||||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||||
|
type ConfigSource = 'user' | 'webhook' | 'legacy'
|
||||||
interface ServiceOptions {
|
interface ServiceOptions {
|
||||||
config_dir: string
|
config_dir: string
|
||||||
rpc_portal: string
|
rpc_portal: string
|
||||||
@@ -16,6 +17,39 @@ interface ServiceOptions {
|
|||||||
|
|
||||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
||||||
|
|
||||||
|
interface StoredGuiConfig {
|
||||||
|
config: NetworkConfig
|
||||||
|
source: ConfigSource
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStoredConfigs(raw: string | null): StoredGuiConfig[] {
|
||||||
|
const parsed: unknown = JSON.parse(raw || '[]')
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.flatMap((entry): StoredGuiConfig[] => {
|
||||||
|
if (entry && typeof entry === 'object' && 'config' in entry) {
|
||||||
|
const { config, source } = entry as {
|
||||||
|
config?: NetworkConfig
|
||||||
|
source?: ConfigSource
|
||||||
|
}
|
||||||
|
if (!config) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [{
|
||||||
|
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||||
|
source: source === 'user' || source === 'webhook' ? source : 'legacy',
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
config: NetworkTypes.normalizeNetworkConfig(entry as NetworkConfig),
|
||||||
|
source: 'legacy',
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||||
return invoke<string>('parse_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
return invoke<string>('parse_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
||||||
}
|
}
|
||||||
@@ -71,9 +105,12 @@ export async function getConfig(instanceId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function sendConfigs(enabledNetworks: string[]) {
|
export async function sendConfigs(enabledNetworks: string[]) {
|
||||||
const networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
const networkList = parseStoredConfigs(localStorage.getItem('networkList'))
|
||||||
return await invoke('load_configs', {
|
return await invoke('load_configs', {
|
||||||
configs: networkList.map((config) => NetworkTypes.toBackendNetworkConfig(NetworkTypes.normalizeNetworkConfig(config))),
|
configs: networkList.map(({ config, source }) => ({
|
||||||
|
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||||
|
source,
|
||||||
|
})),
|
||||||
enabledNetworks
|
enabledNetworks
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { type } from "@tauri-apps/plugin-os";
|
|||||||
import { NetworkTypes } from "easytier-frontend-lib"
|
import { NetworkTypes } from "easytier-frontend-lib"
|
||||||
import { Utils } from "easytier-frontend-lib";
|
import { Utils } from "easytier-frontend-lib";
|
||||||
|
|
||||||
|
interface StoredGuiConfig {
|
||||||
|
config: NetworkTypes.NetworkConfig
|
||||||
|
source?: 'user' | 'webhook' | 'legacy'
|
||||||
|
}
|
||||||
|
|
||||||
const EVENTS = Object.freeze({
|
const EVENTS = Object.freeze({
|
||||||
SAVE_CONFIGS: 'save_configs',
|
SAVE_CONFIGS: 'save_configs',
|
||||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||||
@@ -13,9 +18,15 @@ const EVENTS = Object.freeze({
|
|||||||
EVENT_LAGGED: 'event_lagged',
|
EVENT_LAGGED: 'event_lagged',
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
function onSaveConfigs(event: Event<StoredGuiConfig[]>) {
|
||||||
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
|
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
|
||||||
localStorage.setItem('networkList', JSON.stringify(event.payload.map((config) => NetworkTypes.normalizeNetworkConfig(config))));
|
localStorage.setItem(
|
||||||
|
'networkList',
|
||||||
|
JSON.stringify(event.payload.map(({ config, source }) => ({
|
||||||
|
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||||
|
source: source ?? 'legacy',
|
||||||
|
}))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeInstanceIdPayload(payload: unknown): string {
|
function normalizeInstanceIdPayload(payload: unknown): string {
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::scoped_task::ScopedTask,
|
common::{config::ConfigSource, scoped_task::ScopedTask},
|
||||||
proto::{
|
proto::{
|
||||||
api::manage::{
|
api::manage::{
|
||||||
NetworkConfig, RunNetworkInstanceRequest, WebClientService,
|
ConfigSource as RpcConfigSource, NetworkConfig, NetworkMeta, RunNetworkInstanceRequest,
|
||||||
WebClientServiceClientFactory,
|
WebClientService, WebClientServiceClientFactory,
|
||||||
},
|
},
|
||||||
rpc_impl::bidirect::BidirectRpcManager,
|
rpc_impl::bidirect::BidirectRpcManager,
|
||||||
rpc_types::{self, controller::BaseController},
|
rpc_types::{self, controller::BaseController},
|
||||||
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
|
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
|
||||||
},
|
},
|
||||||
rpc_service::remote_client::{ListNetworkProps, Storage as _},
|
rpc_service::remote_client::{ListNetworkProps, PersistentConfig as _, Storage as _},
|
||||||
tunnel::Tunnel,
|
tunnel::Tunnel,
|
||||||
};
|
};
|
||||||
use tokio::sync::{RwLock, broadcast};
|
use tokio::sync::{RwLock, broadcast};
|
||||||
@@ -26,6 +26,50 @@ use super::storage::{Storage, StorageToken, WeakRefStorage};
|
|||||||
use crate::FeatureFlags;
|
use crate::FeatureFlags;
|
||||||
use crate::webhook::SharedWebhookConfig;
|
use crate::webhook::SharedWebhookConfig;
|
||||||
|
|
||||||
|
const LEGACY_NETWORK_CONFIG_SOURCE: &str = "legacy";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum PersistedConfigSource {
|
||||||
|
User,
|
||||||
|
Webhook,
|
||||||
|
Legacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistedConfigSource {
|
||||||
|
fn from_db(source: &str) -> Self {
|
||||||
|
match source {
|
||||||
|
"webhook" => Self::Webhook,
|
||||||
|
"user" => Self::User,
|
||||||
|
LEGACY_NETWORK_CONFIG_SOURCE => Self::Legacy,
|
||||||
|
_ => Self::User,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_update_from_runtime(self, runtime_source: ConfigSource) -> bool {
|
||||||
|
match (self, runtime_source) {
|
||||||
|
// Older clients report missing source as `user`, which is not authoritative enough
|
||||||
|
// to downgrade an existing webhook-owned or legacy row.
|
||||||
|
(Self::Webhook | Self::Legacy, ConfigSource::User) => false,
|
||||||
|
_ => self.as_runtime_source() != runtime_source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_runtime_source(self) -> ConfigSource {
|
||||||
|
match self {
|
||||||
|
Self::User | Self::Legacy => ConfigSource::User,
|
||||||
|
Self::Webhook => ConfigSource::Webhook,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_run_rpc_source(self) -> Option<RpcConfigSource> {
|
||||||
|
match self {
|
||||||
|
Self::User => Some(RpcConfigSource::User),
|
||||||
|
Self::Webhook => Some(RpcConfigSource::Webhook),
|
||||||
|
Self::Legacy => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Location {
|
pub struct Location {
|
||||||
pub country: String,
|
pub country: String,
|
||||||
@@ -148,7 +192,7 @@ impl SessionRpcService {
|
|||||||
Ok(serde_json::from_value::<NetworkConfig>(network_config)?)
|
Ok(serde_json::from_value::<NetworkConfig>(network_config)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reconcile_managed_network_configs(
|
async fn reconcile_webhook_source_configs(
|
||||||
storage: &Storage,
|
storage: &Storage,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
machine_id: uuid::Uuid,
|
machine_id: uuid::Uuid,
|
||||||
@@ -159,9 +203,19 @@ impl SessionRpcService {
|
|||||||
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
|
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?;
|
.map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?;
|
||||||
let existing_ids = existing_configs
|
let existing_sources = existing_configs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|cfg| uuid::Uuid::parse_str(&cfg.network_instance_id).ok())
|
.filter_map(|cfg| {
|
||||||
|
uuid::Uuid::parse_str(&cfg.network_instance_id)
|
||||||
|
.ok()
|
||||||
|
.map(|inst_id| (inst_id, PersistedConfigSource::from_db(&cfg.source)))
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
let existing_webhook_ids = existing_sources
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(inst_id, source)| {
|
||||||
|
(*source == PersistedConfigSource::Webhook).then_some(*inst_id)
|
||||||
|
})
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let mut desired_ids = HashSet::with_capacity(desired_configs.len());
|
let mut desired_ids = HashSet::with_capacity(desired_configs.len());
|
||||||
@@ -169,10 +223,30 @@ impl SessionRpcService {
|
|||||||
for desired in desired_configs {
|
for desired in desired_configs {
|
||||||
let inst_id = uuid::Uuid::parse_str(&desired.instance_id).with_context(|| {
|
let inst_id = uuid::Uuid::parse_str(&desired.instance_id).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"invalid desired managed instance id: {}",
|
"invalid desired webhook config instance id: {}",
|
||||||
desired.instance_id
|
desired.instance_id
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
match existing_sources.get(&inst_id) {
|
||||||
|
Some(PersistedConfigSource::User) => {
|
||||||
|
tracing::warn!(
|
||||||
|
?user_id,
|
||||||
|
?machine_id,
|
||||||
|
instance_id = %inst_id,
|
||||||
|
"skip webhook config because a user-owned config already exists"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Some(PersistedConfigSource::Legacy) => {
|
||||||
|
tracing::info!(
|
||||||
|
?user_id,
|
||||||
|
?machine_id,
|
||||||
|
instance_id = %inst_id,
|
||||||
|
"adopt legacy config as webhook-owned during reconciliation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
let config = Self::normalize_network_config(desired.network_config, inst_id)?;
|
let config = Self::normalize_network_config(desired.network_config, inst_id)?;
|
||||||
desired_ids.insert(inst_id);
|
desired_ids.insert(inst_id);
|
||||||
normalized.insert(inst_id, config);
|
normalized.insert(inst_id, config);
|
||||||
@@ -181,18 +255,23 @@ impl SessionRpcService {
|
|||||||
for (inst_id, config) in normalized {
|
for (inst_id, config) in normalized {
|
||||||
storage
|
storage
|
||||||
.db()
|
.db()
|
||||||
.insert_or_update_user_network_config((user_id, machine_id), inst_id, config)
|
.insert_or_update_user_network_config(
|
||||||
|
(user_id, machine_id),
|
||||||
|
inst_id,
|
||||||
|
config,
|
||||||
|
ConfigSource::Webhook,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"failed to persist managed network config {}: {:?}",
|
"failed to persist webhook network config {}: {:?}",
|
||||||
inst_id,
|
inst_id,
|
||||||
e
|
e
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stale_ids = existing_ids
|
let stale_ids = existing_webhook_ids
|
||||||
.difference(&desired_ids)
|
.difference(&desired_ids)
|
||||||
.copied()
|
.copied()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -225,7 +304,7 @@ impl SessionRpcService {
|
|||||||
|
|
||||||
let (
|
let (
|
||||||
user_id,
|
user_id,
|
||||||
webhook_managed_network_configs,
|
webhook_source_configs,
|
||||||
webhook_config_revision,
|
webhook_config_revision,
|
||||||
webhook_validated,
|
webhook_validated,
|
||||||
binding_version,
|
binding_version,
|
||||||
@@ -306,11 +385,11 @@ impl SessionRpcService {
|
|||||||
if webhook_validated
|
if webhook_validated
|
||||||
&& data.applied_config_revision.as_deref() != Some(webhook_config_revision.as_str())
|
&& data.applied_config_revision.as_deref() != Some(webhook_config_revision.as_str())
|
||||||
{
|
{
|
||||||
Self::reconcile_managed_network_configs(
|
Self::reconcile_webhook_source_configs(
|
||||||
&storage,
|
&storage,
|
||||||
user_id,
|
user_id,
|
||||||
machine_id,
|
machine_id,
|
||||||
webhook_managed_network_configs,
|
webhook_source_configs,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(rpc_types::error::Error::from)?;
|
.map_err(rpc_types::error::Error::from)?;
|
||||||
@@ -448,13 +527,133 @@ impl Session {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_webhook_source_instance_ids(
|
||||||
|
metas: Vec<easytier::proto::api::manage::NetworkMeta>,
|
||||||
|
) -> HashSet<String> {
|
||||||
|
metas
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|meta| {
|
||||||
|
(RpcConfigSource::try_from(meta.source).ok() == Some(RpcConfigSource::Webhook))
|
||||||
|
.then(|| {
|
||||||
|
meta.inst_id
|
||||||
|
.map(|inst_id| Into::<uuid::Uuid>::into(inst_id).to_string())
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_running_config_sources(
|
||||||
|
db: &crate::db::Db,
|
||||||
|
user_id: i32,
|
||||||
|
machine_id: uuid::Uuid,
|
||||||
|
local_configs: &[crate::db::entity::user_running_network_configs::Model],
|
||||||
|
metas: &[NetworkMeta],
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let local_configs_by_id = local_configs
|
||||||
|
.iter()
|
||||||
|
.map(|cfg| (cfg.network_instance_id.clone(), cfg))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
for meta in metas {
|
||||||
|
let Some(inst_id) = meta.inst_id.as_ref().map(|inst_id| {
|
||||||
|
let inst_id: uuid::Uuid = (*inst_id).into();
|
||||||
|
inst_id
|
||||||
|
}) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let inst_id_str = inst_id.to_string();
|
||||||
|
let Some(local_cfg) = local_configs_by_id.get(&inst_id_str) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(running_source) = ConfigSource::from_rpc(meta.source) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let local_source = PersistedConfigSource::from_db(&local_cfg.source);
|
||||||
|
if !local_source.should_update_from_runtime(running_source) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.insert_or_update_user_network_config(
|
||||||
|
(user_id, machine_id),
|
||||||
|
inst_id,
|
||||||
|
local_cfg.get_network_config().map_err(|e| {
|
||||||
|
anyhow::anyhow!("failed to decode local network config {}: {:?}", inst_id, e)
|
||||||
|
})?,
|
||||||
|
running_source,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"failed to sync running network config source {}: {:?}",
|
||||||
|
inst_id,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repair_legacy_running_config_sources(
|
||||||
|
db: &crate::db::Db,
|
||||||
|
user_id: i32,
|
||||||
|
machine_id: uuid::Uuid,
|
||||||
|
local_configs: &[crate::db::entity::user_running_network_configs::Model],
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
let legacy_configs = local_configs
|
||||||
|
.iter()
|
||||||
|
.filter(|cfg| {
|
||||||
|
PersistedConfigSource::from_db(&cfg.source) == PersistedConfigSource::Legacy
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if legacy_configs.is_empty() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for local_cfg in legacy_configs {
|
||||||
|
let inst_id =
|
||||||
|
uuid::Uuid::parse_str(&local_cfg.network_instance_id).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse legacy network config instance id {}",
|
||||||
|
local_cfg.network_instance_id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
db.insert_or_update_user_network_config(
|
||||||
|
(user_id, machine_id),
|
||||||
|
inst_id,
|
||||||
|
local_cfg.get_network_config().map_err(|e| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"failed to decode legacy network config {}: {:?}",
|
||||||
|
inst_id,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
ConfigSource::User,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"failed to repair legacy network config source {}: {:?}",
|
||||||
|
inst_id,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_network_on_start(
|
async fn run_network_on_start(
|
||||||
mut heartbeat_waiter: broadcast::Receiver<HeartbeatRequest>,
|
mut heartbeat_waiter: broadcast::Receiver<HeartbeatRequest>,
|
||||||
storage: WeakRefStorage,
|
storage: WeakRefStorage,
|
||||||
rpc_client: SessionRpcClient,
|
rpc_client: SessionRpcClient,
|
||||||
) {
|
) {
|
||||||
let mut cleaned_web_managed_instances = false;
|
let mut cleaned_webhook_source_instances = false;
|
||||||
let mut last_desired_inst_ids: Option<HashSet<String>> = None;
|
let mut last_desired_webhook_inst_ids: Option<HashSet<String>> = None;
|
||||||
loop {
|
loop {
|
||||||
heartbeat_waiter = heartbeat_waiter.resubscribe();
|
heartbeat_waiter = heartbeat_waiter.resubscribe();
|
||||||
let req = heartbeat_waiter.recv().await;
|
let req = heartbeat_waiter.recv().await;
|
||||||
@@ -510,37 +709,160 @@ impl Session {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut local_configs = local_configs;
|
||||||
|
let running_metas = if req.support_config_source {
|
||||||
|
let ret = if running_inst_ids.is_empty() {
|
||||||
|
Ok(Vec::new())
|
||||||
|
} else {
|
||||||
|
rpc_client
|
||||||
|
.list_network_instance_meta(
|
||||||
|
BaseController::default(),
|
||||||
|
easytier::proto::api::manage::ListNetworkInstanceMetaRequest {
|
||||||
|
inst_ids: running_inst_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok())
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|resp| resp.metas)
|
||||||
|
};
|
||||||
|
|
||||||
|
match ret {
|
||||||
|
Ok(metas) => {
|
||||||
|
if let Err(e) = Self::sync_running_config_sources(
|
||||||
|
&storage.db,
|
||||||
|
user_id,
|
||||||
|
machine_id.into(),
|
||||||
|
&local_configs,
|
||||||
|
&metas,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
?user_id,
|
||||||
|
?machine_id,
|
||||||
|
%e,
|
||||||
|
"Failed to sync running network config sources"
|
||||||
|
);
|
||||||
|
} else if !metas.is_empty() {
|
||||||
|
local_configs = match storage
|
||||||
|
.db
|
||||||
|
.list_network_configs(
|
||||||
|
(user_id, machine_id.into()),
|
||||||
|
ListNetworkProps::EnabledOnly,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(configs) => configs,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to reload network configs after source sync, error: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(metas)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
?user_id,
|
||||||
|
%e,
|
||||||
|
"Failed to list running network instance metadata"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match Self::repair_legacy_running_config_sources(
|
||||||
|
&storage.db,
|
||||||
|
user_id,
|
||||||
|
machine_id.into(),
|
||||||
|
&local_configs,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(true) => {
|
||||||
|
local_configs = match storage
|
||||||
|
.db
|
||||||
|
.list_network_configs(
|
||||||
|
(user_id, machine_id.into()),
|
||||||
|
ListNetworkProps::EnabledOnly,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(configs) => configs,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to reload network configs after legacy source repair, error: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
?user_id,
|
||||||
|
?machine_id,
|
||||||
|
%e,
|
||||||
|
"Failed to repair legacy running network config sources"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut has_failed = false;
|
let mut has_failed = false;
|
||||||
let should_be_alive_inst_ids = local_configs
|
let should_be_alive_webhook_inst_ids = local_configs
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|cfg| cfg.get_runtime_network_config_source() == ConfigSource::Webhook)
|
||||||
.map(|cfg| cfg.network_instance_id.clone())
|
.map(|cfg| cfg.network_instance_id.clone())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
let desired_changed = last_desired_inst_ids
|
let desired_changed = last_desired_webhook_inst_ids
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|last| last != &should_be_alive_inst_ids);
|
.is_none_or(|last| last != &should_be_alive_webhook_inst_ids);
|
||||||
|
|
||||||
if !cleaned_web_managed_instances || desired_changed {
|
if !cleaned_webhook_source_instances || desired_changed {
|
||||||
let all_local_configs = match storage
|
let db_webhook_inst_ids = match storage
|
||||||
.db
|
.db
|
||||||
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::All)
|
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::All)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(configs) => configs,
|
Ok(configs) => configs
|
||||||
|
.iter()
|
||||||
|
.filter(|cfg| {
|
||||||
|
cfg.get_runtime_network_config_source() == ConfigSource::Webhook
|
||||||
|
})
|
||||||
|
.map(|cfg| cfg.network_instance_id.clone())
|
||||||
|
.collect::<HashSet<_>>(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to list all network configs, error: {:?}", e);
|
tracing::error!("Failed to list all network configs, error: {:?}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let all_inst_ids = all_local_configs
|
let running_webhook_inst_ids = if let Some(metas) = running_metas.as_ref() {
|
||||||
.iter()
|
Self::collect_webhook_source_instance_ids(metas.clone())
|
||||||
.map(|cfg| cfg.network_instance_id.clone())
|
} else {
|
||||||
|
running_inst_ids
|
||||||
|
.intersection(&db_webhook_inst_ids)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_delete_inst_ids = running_webhook_inst_ids
|
||||||
|
.difference(&should_be_alive_webhook_inst_ids)
|
||||||
|
.cloned()
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let should_delete_ids = running_inst_ids
|
let should_delete_ids = should_delete_inst_ids
|
||||||
.iter()
|
.iter()
|
||||||
.chain(all_inst_ids.iter())
|
|
||||||
.filter(|inst_id| !should_be_alive_inst_ids.contains(*inst_id))
|
|
||||||
.filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok())
|
.filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok())
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -556,7 +878,7 @@ impl Session {
|
|||||||
.await;
|
.await;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
?user_id,
|
?user_id,
|
||||||
"Clean non-web-managed network instances on start: {:?}, user_token: {:?}",
|
"Clean stale webhook-source network instances on start: {:?}, user_token: {:?}",
|
||||||
ret,
|
ret,
|
||||||
req.user_token
|
req.user_token
|
||||||
);
|
);
|
||||||
@@ -564,8 +886,8 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !has_failed {
|
if !has_failed {
|
||||||
cleaned_web_managed_instances = true;
|
cleaned_webhook_source_instances = true;
|
||||||
last_desired_inst_ids = Some(should_be_alive_inst_ids.clone());
|
last_desired_webhook_inst_ids = Some(should_be_alive_webhook_inst_ids.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,6 +895,16 @@ impl Session {
|
|||||||
if running_inst_ids.contains(&c.network_instance_id) {
|
if running_inst_ids.contains(&c.network_instance_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let Some(source) = PersistedConfigSource::from_db(&c.source).auto_run_rpc_source()
|
||||||
|
else {
|
||||||
|
tracing::warn!(
|
||||||
|
?user_id,
|
||||||
|
?machine_id,
|
||||||
|
instance_id = %c.network_instance_id,
|
||||||
|
"skip auto-run for legacy config until source ownership is repaired"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let ret = rpc_client
|
let ret = rpc_client
|
||||||
.run_network_instance(
|
.run_network_instance(
|
||||||
BaseController::default(),
|
BaseController::default(),
|
||||||
@@ -582,6 +914,7 @@ impl Session {
|
|||||||
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
||||||
),
|
),
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
|
source: source as i32,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -596,7 +929,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !has_failed {
|
if !has_failed {
|
||||||
last_desired_inst_ids = Some(should_be_alive_inst_ids);
|
last_desired_webhook_inst_ids = Some(should_be_alive_webhook_inst_ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,13 +967,17 @@ impl Session {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use easytier::rpc_service::remote_client::{ListNetworkProps, Storage as _};
|
use easytier::{
|
||||||
|
common::config::ConfigSource,
|
||||||
|
rpc_service::remote_client::{ListNetworkProps, PersistentConfig as _, Storage as _},
|
||||||
|
};
|
||||||
|
use sea_orm::{ActiveModelTrait, Set};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{super::storage::Storage, *};
|
use super::{super::storage::Storage, *};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn reconcile_managed_network_configs_upserts_and_deletes_exact_set() {
|
async fn reconcile_webhook_source_configs_upserts_and_deletes_exact_set() {
|
||||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||||
let user_id = storage
|
let user_id = storage
|
||||||
.db()
|
.db()
|
||||||
@@ -662,6 +999,7 @@ mod tests {
|
|||||||
network_name: Some("old-name".to_string()),
|
network_name: Some("old-name".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
ConfigSource::Webhook,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -674,11 +1012,12 @@ mod tests {
|
|||||||
network_name: Some("stale".to_string()),
|
network_name: Some("stale".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
ConfigSource::Webhook,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
SessionRpcService::reconcile_managed_network_configs(
|
SessionRpcService::reconcile_webhook_source_configs(
|
||||||
&storage,
|
&storage,
|
||||||
user_id,
|
user_id,
|
||||||
machine_id,
|
machine_id,
|
||||||
@@ -729,5 +1068,353 @@ mod tests {
|
|||||||
updated_keep_config.network_name.as_deref(),
|
updated_keep_config.network_name.as_deref(),
|
||||||
Some("updated-name")
|
Some("updated-name")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
updated_keep.get_network_config_source(),
|
||||||
|
ConfigSource::Webhook
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reconcile_webhook_source_configs_keep_user_owned_configs() {
|
||||||
|
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||||
|
let user_id = storage
|
||||||
|
.db()
|
||||||
|
.auto_create_user("webhook-user-keep-user")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let machine_id = uuid::Uuid::new_v4();
|
||||||
|
let user_owned_id = uuid::Uuid::new_v4();
|
||||||
|
let webhook_owned_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
storage
|
||||||
|
.db()
|
||||||
|
.insert_or_update_user_network_config(
|
||||||
|
(user_id, machine_id),
|
||||||
|
user_owned_id,
|
||||||
|
NetworkConfig {
|
||||||
|
network_name: Some("user-owned".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
ConfigSource::User,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
storage
|
||||||
|
.db()
|
||||||
|
.insert_or_update_user_network_config(
|
||||||
|
(user_id, machine_id),
|
||||||
|
webhook_owned_id,
|
||||||
|
NetworkConfig {
|
||||||
|
network_name: Some("webhook-owned".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
ConfigSource::Webhook,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
SessionRpcService::reconcile_webhook_source_configs(
|
||||||
|
&storage,
|
||||||
|
user_id,
|
||||||
|
machine_id,
|
||||||
|
vec![crate::webhook::ManagedNetworkConfig {
|
||||||
|
instance_id: user_owned_id.to_string(),
|
||||||
|
network_config: json!({
|
||||||
|
"instance_id": user_owned_id.to_string(),
|
||||||
|
"network_name": "webhook-tries-to-take-over"
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let user_owned = storage
|
||||||
|
.db()
|
||||||
|
.get_network_config((user_id, machine_id), &user_owned_id.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(user_owned.get_network_config_source(), ConfigSource::User);
|
||||||
|
let user_owned_cfg: NetworkConfig =
|
||||||
|
serde_json::from_str(&user_owned.network_config).unwrap();
|
||||||
|
assert_eq!(user_owned_cfg.network_name.as_deref(), Some("user-owned"));
|
||||||
|
|
||||||
|
let webhook_owned = storage
|
||||||
|
.db()
|
||||||
|
.get_network_config((user_id, machine_id), &webhook_owned_id.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(webhook_owned.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reconcile_webhook_source_configs_adopts_legacy_rows_for_webhook() {
|
||||||
|
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||||
|
let user_id = storage
|
||||||
|
.db()
|
||||||
|
.auto_create_user("webhook-user-legacy")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let machine_id = uuid::Uuid::new_v4();
|
||||||
|
let legacy_match_id = uuid::Uuid::new_v4();
|
||||||
|
let legacy_user_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
crate::db::entity::user_running_network_configs::ActiveModel {
|
||||||
|
user_id: Set(user_id),
|
||||||
|
device_id: Set(machine_id.to_string()),
|
||||||
|
network_instance_id: Set(legacy_match_id.to_string()),
|
||||||
|
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||||
|
network_name: Some("legacy-webhook".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap()),
|
||||||
|
source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()),
|
||||||
|
disabled: Set(false),
|
||||||
|
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(storage.db().orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
crate::db::entity::user_running_network_configs::ActiveModel {
|
||||||
|
user_id: Set(user_id),
|
||||||
|
device_id: Set(machine_id.to_string()),
|
||||||
|
network_instance_id: Set(legacy_user_id.to_string()),
|
||||||
|
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||||
|
network_name: Some("legacy-user".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap()),
|
||||||
|
source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()),
|
||||||
|
disabled: Set(false),
|
||||||
|
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(storage.db().orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
SessionRpcService::reconcile_webhook_source_configs(
|
||||||
|
&storage,
|
||||||
|
user_id,
|
||||||
|
machine_id,
|
||||||
|
vec![crate::webhook::ManagedNetworkConfig {
|
||||||
|
instance_id: legacy_match_id.to_string(),
|
||||||
|
network_config: json!({
|
||||||
|
"instance_id": legacy_match_id.to_string(),
|
||||||
|
"network_name": "managed-by-webhook"
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let adopted = storage
|
||||||
|
.db()
|
||||||
|
.get_network_config((user_id, machine_id), &legacy_match_id.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(adopted.source, ConfigSource::Webhook.as_str());
|
||||||
|
let adopted_cfg: NetworkConfig = serde_json::from_str(&adopted.network_config).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
adopted_cfg.network_name.as_deref(),
|
||||||
|
Some("managed-by-webhook")
|
||||||
|
);
|
||||||
|
|
||||||
|
let untouched_legacy = storage
|
||||||
|
.db()
|
||||||
|
.get_network_config((user_id, machine_id), &legacy_user_id.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(untouched_legacy.source, LEGACY_NETWORK_CONFIG_SOURCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sync_running_config_sources_updates_enabled_config_source_from_runtime() {
|
||||||
|
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||||
|
let user_id = storage
|
||||||
|
.db()
|
||||||
|
.auto_create_user("webhook-user-sync-source")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let machine_id = uuid::Uuid::new_v4();
|
||||||
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
storage
|
||||||
|
.db()
|
||||||
|
.insert_or_update_user_network_config(
|
||||||
|
(user_id, machine_id),
|
||||||
|
inst_id,
|
||||||
|
NetworkConfig {
|
||||||
|
network_name: Some("webhook-owned".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
ConfigSource::Webhook,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let local_configs = storage
|
||||||
|
.db()
|
||||||
|
.list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Session::sync_running_config_sources(
|
||||||
|
storage.db(),
|
||||||
|
user_id,
|
||||||
|
machine_id,
|
||||||
|
&local_configs,
|
||||||
|
&[easytier::proto::api::manage::NetworkMeta {
|
||||||
|
inst_id: Some(inst_id.into()),
|
||||||
|
source: RpcConfigSource::User as i32,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let updated = storage
|
||||||
|
.db()
|
||||||
|
.get_network_config((user_id, machine_id), &inst_id.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(updated.get_network_config_source(), ConfigSource::Webhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sync_running_config_sources_keeps_legacy_rows_when_runtime_source_is_user() {
|
||||||
|
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||||
|
let user_id = storage
|
||||||
|
.db()
|
||||||
|
.auto_create_user("webhook-user-sync-legacy")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let machine_id = uuid::Uuid::new_v4();
|
||||||
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
crate::db::entity::user_running_network_configs::ActiveModel {
|
||||||
|
user_id: Set(user_id),
|
||||||
|
device_id: Set(machine_id.to_string()),
|
||||||
|
network_instance_id: Set(inst_id.to_string()),
|
||||||
|
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||||
|
network_name: Some("legacy".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap()),
|
||||||
|
source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()),
|
||||||
|
disabled: Set(false),
|
||||||
|
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(storage.db().orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let local_configs = storage
|
||||||
|
.db()
|
||||||
|
.list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Session::sync_running_config_sources(
|
||||||
|
storage.db(),
|
||||||
|
user_id,
|
||||||
|
machine_id,
|
||||||
|
&local_configs,
|
||||||
|
&[easytier::proto::api::manage::NetworkMeta {
|
||||||
|
inst_id: Some(inst_id.into()),
|
||||||
|
source: RpcConfigSource::User as i32,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let updated = storage
|
||||||
|
.db()
|
||||||
|
.get_network_config((user_id, machine_id), &inst_id.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(updated.source, LEGACY_NETWORK_CONFIG_SOURCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn repair_legacy_running_config_sources_promotes_remaining_legacy_rows_to_user() {
|
||||||
|
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||||
|
let user_id = storage
|
||||||
|
.db()
|
||||||
|
.auto_create_user("webhook-user-repair-legacy")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let machine_id = uuid::Uuid::new_v4();
|
||||||
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
crate::db::entity::user_running_network_configs::ActiveModel {
|
||||||
|
user_id: Set(user_id),
|
||||||
|
device_id: Set(machine_id.to_string()),
|
||||||
|
network_instance_id: Set(inst_id.to_string()),
|
||||||
|
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||||
|
network_name: Some("legacy".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap()),
|
||||||
|
source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()),
|
||||||
|
disabled: Set(false),
|
||||||
|
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(storage.db().orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let local_configs = storage
|
||||||
|
.db()
|
||||||
|
.list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
Session::repair_legacy_running_config_sources(
|
||||||
|
storage.db(),
|
||||||
|
user_id,
|
||||||
|
machine_id,
|
||||||
|
&local_configs,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let updated = storage
|
||||||
|
.db()
|
||||||
|
.get_network_config((user_id, machine_id), &inst_id.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(updated.source, ConfigSource::User.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_configs_are_not_auto_run_until_repaired() {
|
||||||
|
assert_eq!(PersistedConfigSource::Legacy.auto_run_rpc_source(), None);
|
||||||
|
assert_eq!(
|
||||||
|
PersistedConfigSource::Webhook.auto_run_rpc_source(),
|
||||||
|
Some(RpcConfigSource::Webhook)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
PersistedConfigSource::User.auto_run_rpc_source(),
|
||||||
|
Some(RpcConfigSource::User)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use easytier::{launcher::NetworkConfig, rpc_service::remote_client::PersistentConfig};
|
use easytier::{
|
||||||
|
common::config::ConfigSource, launcher::NetworkConfig,
|
||||||
|
rpc_service::remote_client::PersistentConfig,
|
||||||
|
};
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -12,10 +15,12 @@ pub struct Model {
|
|||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
#[sea_orm(column_type = "Text", unique)]
|
#[sea_orm(column_type = "Text")]
|
||||||
pub network_instance_id: String,
|
pub network_instance_id: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
pub network_config: String,
|
pub network_config: String,
|
||||||
|
#[sea_orm(column_type = "Text")]
|
||||||
|
pub source: String,
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
pub create_time: DateTimeWithTimeZone,
|
pub create_time: DateTimeWithTimeZone,
|
||||||
pub update_time: DateTimeWithTimeZone,
|
pub update_time: DateTimeWithTimeZone,
|
||||||
@@ -48,4 +53,7 @@ impl PersistentConfig<DbErr> for Model {
|
|||||||
fn get_network_config(&self) -> Result<NetworkConfig, DbErr> {
|
fn get_network_config(&self) -> Result<NetworkConfig, DbErr> {
|
||||||
serde_json::from_str(&self.network_config).map_err(|e| DbErr::Json(e.to_string()))
|
serde_json::from_str(&self.network_config).map_err(|e| DbErr::Json(e.to_string()))
|
||||||
}
|
}
|
||||||
|
fn get_network_config_source(&self) -> ConfigSource {
|
||||||
|
self.source.parse().unwrap_or(ConfigSource::User)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
pub mod entity;
|
pub mod entity;
|
||||||
|
|
||||||
use easytier::{
|
use easytier::{
|
||||||
|
common::config::ConfigSource,
|
||||||
launcher::NetworkConfig,
|
launcher::NetworkConfig,
|
||||||
rpc_service::remote_client::{ListNetworkProps, Storage},
|
rpc_service::remote_client::{ListNetworkProps, Storage},
|
||||||
};
|
};
|
||||||
@@ -149,6 +150,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
(user_id, device_id): (UserIdInDb, Uuid),
|
(user_id, device_id): (UserIdInDb, Uuid),
|
||||||
network_inst_id: Uuid,
|
network_inst_id: Uuid,
|
||||||
network_config: NetworkConfig,
|
network_config: NetworkConfig,
|
||||||
|
source: ConfigSource,
|
||||||
) -> Result<(), DbErr> {
|
) -> Result<(), DbErr> {
|
||||||
let txn = self.orm_db().begin().await?;
|
let txn = self.orm_db().begin().await?;
|
||||||
|
|
||||||
@@ -161,6 +163,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
])
|
])
|
||||||
.update_columns([
|
.update_columns([
|
||||||
urnc::Column::NetworkConfig,
|
urnc::Column::NetworkConfig,
|
||||||
|
urnc::Column::Source,
|
||||||
urnc::Column::Disabled,
|
urnc::Column::Disabled,
|
||||||
urnc::Column::UpdateTime,
|
urnc::Column::UpdateTime,
|
||||||
])
|
])
|
||||||
@@ -172,6 +175,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
network_config: sea_orm::Set(
|
network_config: sea_orm::Set(
|
||||||
serde_json::to_string(&network_config).map_err(|e| DbErr::Json(e.to_string()))?,
|
serde_json::to_string(&network_config).map_err(|e| DbErr::Json(e.to_string()))?,
|
||||||
),
|
),
|
||||||
|
source: sea_orm::Set(source.as_str().to_string()),
|
||||||
disabled: sea_orm::Set(false),
|
disabled: sea_orm::Set(false),
|
||||||
create_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
create_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||||
update_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
update_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||||
@@ -277,8 +281,12 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage};
|
use easytier::{
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
common::config::ConfigSource,
|
||||||
|
proto::api::manage::NetworkConfig,
|
||||||
|
rpc_service::remote_client::{PersistentConfig, Storage},
|
||||||
|
};
|
||||||
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter as _, Set};
|
||||||
|
|
||||||
use crate::db::{Db, ListNetworkProps, entity::user_running_network_configs};
|
use crate::db::{Db, ListNetworkProps, entity::user_running_network_configs};
|
||||||
|
|
||||||
@@ -294,9 +302,14 @@ mod tests {
|
|||||||
let inst_id = uuid::Uuid::new_v4();
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
let device_id = uuid::Uuid::new_v4();
|
let device_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
db.insert_or_update_user_network_config(
|
||||||
.await
|
(user_id, device_id),
|
||||||
.unwrap();
|
inst_id,
|
||||||
|
network_config,
|
||||||
|
ConfigSource::User,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let result = user_running_network_configs::Entity::find()
|
let result = user_running_network_configs::Entity::find()
|
||||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||||
@@ -306,6 +319,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
println!("{:?}", result);
|
println!("{:?}", result);
|
||||||
assert_eq!(result.network_config, network_config_json);
|
assert_eq!(result.network_config, network_config_json);
|
||||||
|
assert_eq!(result.get_network_config_source(), ConfigSource::User);
|
||||||
|
|
||||||
// overwrite the config
|
// overwrite the config
|
||||||
let network_config = NetworkConfig {
|
let network_config = NetworkConfig {
|
||||||
@@ -313,9 +327,14 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let network_config_json = serde_json::to_string(&network_config).unwrap();
|
let network_config_json = serde_json::to_string(&network_config).unwrap();
|
||||||
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
db.insert_or_update_user_network_config(
|
||||||
.await
|
(user_id, device_id),
|
||||||
.unwrap();
|
inst_id,
|
||||||
|
network_config,
|
||||||
|
ConfigSource::Webhook,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let result2 = user_running_network_configs::Entity::find()
|
let result2 = user_running_network_configs::Entity::find()
|
||||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||||
@@ -325,6 +344,11 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
println!("device: {}, {:?}", device_id, result2);
|
println!("device: {}, {:?}", device_id, result2);
|
||||||
assert_eq!(result2.network_config, network_config_json);
|
assert_eq!(result2.network_config, network_config_json);
|
||||||
|
assert_eq!(result2.get_network_config_source(), ConfigSource::Webhook);
|
||||||
|
assert_eq!(
|
||||||
|
result2.get_runtime_network_config_source(),
|
||||||
|
ConfigSource::Webhook
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(result.create_time, result2.create_time);
|
assert_eq!(result.create_time, result2.create_time);
|
||||||
assert_ne!(result.update_time, result2.update_time);
|
assert_ne!(result.update_time, result2.update_time);
|
||||||
@@ -348,6 +372,45 @@ mod tests {
|
|||||||
assert!(result3.is_none());
|
assert!(result3.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_legacy_network_config_defaults_to_user_runtime_source() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
let user_id = 1;
|
||||||
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
|
let device_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
user_running_network_configs::ActiveModel {
|
||||||
|
user_id: Set(user_id),
|
||||||
|
device_id: Set(device_id.to_string()),
|
||||||
|
network_instance_id: Set(inst_id.to_string()),
|
||||||
|
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||||
|
network_name: Some("legacy".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap()),
|
||||||
|
source: Set("legacy".to_string()),
|
||||||
|
disabled: Set(false),
|
||||||
|
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(db.orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = user_running_network_configs::Entity::find()
|
||||||
|
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||||
|
.one(db.orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.get_network_config_source(), ConfigSource::User);
|
||||||
|
assert_eq!(
|
||||||
|
result.get_runtime_network_config_source(),
|
||||||
|
ConfigSource::User
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_user_network_config_same_instance_id_is_scoped_by_device() {
|
async fn test_user_network_config_same_instance_id_is_scoped_by_device() {
|
||||||
let db = Db::memory_db().await;
|
let db = Db::memory_db().await;
|
||||||
@@ -363,6 +426,7 @@ mod tests {
|
|||||||
network_name: Some("cfg-1".to_string()),
|
network_name: Some("cfg-1".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
ConfigSource::User,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -373,6 +437,7 @@ mod tests {
|
|||||||
network_name: Some("cfg-2".to_string()),
|
network_name: Some("cfg-2".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
ConfigSource::User,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
impl MigrationName for Migration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"m20260421_000003_add_network_config_source"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
let db = manager.get_connection();
|
||||||
|
|
||||||
|
db.execute_unprepared(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE user_running_network_configs_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
device_id TEXT NOT NULL,
|
||||||
|
network_instance_id TEXT NOT NULL,
|
||||||
|
network_config TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL DEFAULT 'user',
|
||||||
|
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
create_time TEXT NOT NULL,
|
||||||
|
update_time TEXT NOT NULL,
|
||||||
|
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO user_running_network_configs_new (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
network_instance_id,
|
||||||
|
network_config,
|
||||||
|
source,
|
||||||
|
disabled,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
network_instance_id,
|
||||||
|
network_config,
|
||||||
|
'legacy',
|
||||||
|
disabled,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
FROM user_running_network_configs;
|
||||||
|
|
||||||
|
DROP TABLE user_running_network_configs;
|
||||||
|
ALTER TABLE user_running_network_configs_new RENAME TO user_running_network_configs;
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_running_network_configs_user_id
|
||||||
|
ON user_running_network_configs(user_id);
|
||||||
|
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||||
|
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
let db = manager.get_connection();
|
||||||
|
|
||||||
|
db.execute_unprepared(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE user_running_network_configs_old (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
device_id TEXT NOT NULL,
|
||||||
|
network_instance_id TEXT NOT NULL,
|
||||||
|
network_config TEXT NOT NULL,
|
||||||
|
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
create_time TEXT NOT NULL,
|
||||||
|
update_time TEXT NOT NULL,
|
||||||
|
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO user_running_network_configs_old (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
network_instance_id,
|
||||||
|
network_config,
|
||||||
|
disabled,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
network_instance_id,
|
||||||
|
network_config,
|
||||||
|
disabled,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
FROM user_running_network_configs;
|
||||||
|
|
||||||
|
DROP TABLE user_running_network_configs;
|
||||||
|
ALTER TABLE user_running_network_configs_old RENAME TO user_running_network_configs;
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_running_network_configs_user_id
|
||||||
|
ON user_running_network_configs(user_id);
|
||||||
|
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||||
|
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ use sea_orm_migration::prelude::*;
|
|||||||
|
|
||||||
mod m20241029_000001_init;
|
mod m20241029_000001_init;
|
||||||
mod m20260403_000002_scope_network_config_unique;
|
mod m20260403_000002_scope_network_config_unique;
|
||||||
|
mod m20260421_000003_add_network_config_source;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
|
|||||||
vec![
|
vec![
|
||||||
Box::new(m20241029_000001_init::Migration),
|
Box::new(m20241029_000001_init::Migration),
|
||||||
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
||||||
|
Box::new(m20260421_000003_add_network_config_source::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use crate::{
|
|||||||
instance::dns_server::DEFAULT_ET_DNS_ZONE,
|
instance::dns_server::DEFAULT_ET_DNS_ZONE,
|
||||||
proto::{
|
proto::{
|
||||||
acl::Acl,
|
acl::Acl,
|
||||||
|
api::manage::ConfigSource as RpcConfigSource,
|
||||||
common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType},
|
common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType},
|
||||||
},
|
},
|
||||||
tunnel::generate_digest_from_str,
|
tunnel::generate_digest_from_str,
|
||||||
@@ -206,6 +207,11 @@ pub trait ConfigLoader: Send + Sync {
|
|||||||
}
|
}
|
||||||
fn set_credential_file(&self, _path: Option<std::path::PathBuf>) {}
|
fn set_credential_file(&self, _path: Option<std::path::PathBuf>) {}
|
||||||
|
|
||||||
|
fn get_network_config_source(&self) -> ConfigSource {
|
||||||
|
ConfigSource::User
|
||||||
|
}
|
||||||
|
fn set_network_config_source(&self, _source: Option<ConfigSource>) {}
|
||||||
|
|
||||||
fn dump(&self) -> String;
|
fn dump(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +231,55 @@ pub struct NetworkIdentity {
|
|||||||
pub network_secret_digest: Option<NetworkSecretDigest>,
|
pub network_secret_digest: Option<NetworkSecretDigest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ConfigSource {
|
||||||
|
#[default]
|
||||||
|
User,
|
||||||
|
Webhook,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigSource {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::User => "user",
|
||||||
|
Self::Webhook => "webhook",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_rpc(source: i32) -> Option<Self> {
|
||||||
|
match RpcConfigSource::try_from(source).ok() {
|
||||||
|
Some(RpcConfigSource::Webhook) => Some(Self::Webhook),
|
||||||
|
Some(RpcConfigSource::User) => Some(Self::User),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_rpc(self) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::User => RpcConfigSource::User as i32,
|
||||||
|
Self::Webhook => RpcConfigSource::Webhook as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for ConfigSource {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"user" => Ok(Self::User),
|
||||||
|
"webhook" => Ok(Self::Webhook),
|
||||||
|
other => Err(format!("unknown network config source: {other}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
|
struct ConfigSourceConfig {
|
||||||
|
source: ConfigSource,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash)]
|
#[derive(Eq, PartialEq, Hash)]
|
||||||
struct NetworkIdentityWithOnlyDigest {
|
struct NetworkIdentityWithOnlyDigest {
|
||||||
network_name: String,
|
network_name: String,
|
||||||
@@ -466,6 +521,7 @@ struct Config {
|
|||||||
stun_servers_v6: Option<Vec<String>>,
|
stun_servers_v6: Option<Vec<String>>,
|
||||||
|
|
||||||
credential_file: Option<PathBuf>,
|
credential_file: Option<PathBuf>,
|
||||||
|
source: Option<ConfigSourceConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -480,10 +536,21 @@ impl Default for TomlConfigLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TomlConfigLoader {
|
impl TomlConfigLoader {
|
||||||
|
fn normalize_config_source(config: &mut Config) {
|
||||||
|
if matches!(
|
||||||
|
config.source.as_ref().map(|source| source.source),
|
||||||
|
Some(ConfigSource::User)
|
||||||
|
) {
|
||||||
|
config.source = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
||||||
let mut config = toml::de::from_str::<Config>(config_str)
|
let mut config = toml::de::from_str::<Config>(config_str)
|
||||||
.with_context(|| format!("failed to parse config file: {}", config_str))?;
|
.with_context(|| format!("failed to parse config file: {}", config_str))?;
|
||||||
|
|
||||||
|
Self::normalize_config_source(&mut config);
|
||||||
|
|
||||||
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
||||||
|
|
||||||
let config = TomlConfigLoader {
|
let config = TomlConfigLoader {
|
||||||
@@ -867,6 +934,23 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
self.config.lock().unwrap().credential_file = path;
|
self.config.lock().unwrap().credential_file = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_network_config_source(&self) -> ConfigSource {
|
||||||
|
self.config
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.source
|
||||||
|
.as_ref()
|
||||||
|
.map(|source| source.source)
|
||||||
|
.unwrap_or(ConfigSource::User)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_network_config_source(&self, source: Option<ConfigSource>) {
|
||||||
|
self.config.lock().unwrap().source = source.and_then(|source| match source {
|
||||||
|
ConfigSource::User => None,
|
||||||
|
other => Some(ConfigSourceConfig { source: other }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn dump(&self) -> String {
|
fn dump(&self) -> String {
|
||||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||||
let default_flags_hashmap =
|
let default_flags_hashmap =
|
||||||
@@ -888,6 +972,7 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut config = self.config.lock().unwrap().clone();
|
let mut config = self.config.lock().unwrap().clone();
|
||||||
|
Self::normalize_config_source(&mut config);
|
||||||
config.flags = Some(flag_map);
|
config.flags = Some(flag_map);
|
||||||
if config.stun_servers == Some(StunInfoCollector::get_default_servers()) {
|
if config.stun_servers == Some(StunInfoCollector::get_default_servers()) {
|
||||||
config.stun_servers = None;
|
config.stun_servers = None;
|
||||||
@@ -1126,6 +1211,46 @@ stun_servers = [
|
|||||||
assert_eq!(stun_servers[2], "txt:stun.easytier.cn");
|
assert_eq!(stun_servers[2], "txt:stun.easytier.cn");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_config_source_toml_roundtrip() {
|
||||||
|
let config = TomlConfigLoader::default();
|
||||||
|
assert_eq!(config.get_network_config_source(), ConfigSource::User);
|
||||||
|
|
||||||
|
config.set_network_config_source(Some(ConfigSource::Webhook));
|
||||||
|
let dumped = config.dump();
|
||||||
|
|
||||||
|
assert!(dumped.contains("[source]"));
|
||||||
|
assert!(dumped.contains("source = \"webhook\""));
|
||||||
|
|
||||||
|
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
||||||
|
assert_eq!(loaded.get_network_config_source(), ConfigSource::Webhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_config_source_user_is_implicit() {
|
||||||
|
let config = TomlConfigLoader::default();
|
||||||
|
config.set_network_config_source(Some(ConfigSource::User));
|
||||||
|
let dumped = config.dump();
|
||||||
|
|
||||||
|
assert!(!dumped.contains("[source]"));
|
||||||
|
|
||||||
|
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
||||||
|
assert_eq!(loaded.get_network_config_source(), ConfigSource::User);
|
||||||
|
|
||||||
|
let explicit_user = TomlConfigLoader::new_from_str(
|
||||||
|
r#"
|
||||||
|
[source]
|
||||||
|
source = "user"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
explicit_user.get_network_config_source(),
|
||||||
|
ConfigSource::User
|
||||||
|
);
|
||||||
|
assert!(!explicit_user.dump().contains("[source]"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn full_example_test() {
|
async fn full_example_test() {
|
||||||
let config_str = r#"
|
let config_str = r#"
|
||||||
|
|||||||
@@ -813,7 +813,7 @@ impl NetworkOptions {
|
|||||||
fn can_merge(
|
fn can_merge(
|
||||||
&self,
|
&self,
|
||||||
cfg: &TomlConfigLoader,
|
cfg: &TomlConfigLoader,
|
||||||
source: ConfigSource,
|
source: ConfigFileSource,
|
||||||
explicit_config_file_count: usize,
|
explicit_config_file_count: usize,
|
||||||
config_dir_file_count: usize,
|
config_dir_file_count: usize,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@@ -821,7 +821,7 @@ impl NetworkOptions {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == ConfigSource::CliConfigFile
|
if source == ConfigFileSource::CliConfigFile
|
||||||
&& explicit_config_file_count == 1
|
&& explicit_config_file_count == 1
|
||||||
&& config_dir_file_count == 0
|
&& config_dir_file_count == 0
|
||||||
{
|
{
|
||||||
@@ -832,7 +832,7 @@ impl NetworkOptions {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if source == ConfigSource::ConfigDir {
|
if source == ConfigFileSource::ConfigDir {
|
||||||
return cfg.get_network_identity().network_name == *network_name;
|
return cfg.get_network_identity().network_name == *network_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1161,7 +1161,7 @@ impl NetworkOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum ConfigSource {
|
enum ConfigFileSource {
|
||||||
CliConfigFile,
|
CliConfigFile,
|
||||||
ConfigDir,
|
ConfigDir,
|
||||||
}
|
}
|
||||||
@@ -1353,7 +1353,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
|||||||
let mut config_files = if let Some(v) = cli.config_file {
|
let mut config_files = if let Some(v) = cli.config_file {
|
||||||
v.iter()
|
v.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|path| (path, ConfigSource::CliConfigFile))
|
.map(|path| (path, ConfigFileSource::CliConfigFile))
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
@@ -1376,7 +1376,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
config_dir_file_count += 1;
|
config_dir_file_count += 1;
|
||||||
config_files.push((path, ConfigSource::ConfigDir));
|
config_files.push((path, ConfigFileSource::ConfigDir));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let config_file_count = config_files.len();
|
let config_file_count = config_files.len();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{
|
common::{
|
||||||
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
|
config::{ConfigFileControl, ConfigLoader, ConfigSource, TomlConfigLoader},
|
||||||
global_ctx::{EventBusSubscriber, GlobalCtxEvent},
|
global_ctx::{EventBusSubscriber, GlobalCtxEvent},
|
||||||
log,
|
log,
|
||||||
scoped_task::ScopedTask,
|
scoped_task::ScopedTask,
|
||||||
@@ -217,6 +217,15 @@ impl NetworkInstanceManager {
|
|||||||
.map(|instance| instance.value().get_config_file_control().clone())
|
.map(|instance| instance.value().get_config_file_control().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_instance_network_config_source(
|
||||||
|
&self,
|
||||||
|
instance_id: &uuid::Uuid,
|
||||||
|
) -> Option<ConfigSource> {
|
||||||
|
self.instance_map
|
||||||
|
.get(instance_id)
|
||||||
|
.map(|instance| instance.value().get_network_config_source())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_instance_service(
|
pub fn get_instance_service(
|
||||||
&self,
|
&self,
|
||||||
instance_id: &uuid::Uuid,
|
instance_id: &uuid::Uuid,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::common::config::{ConfigFileControl, PortForwardConfig, process_secure_mode_cfg};
|
use crate::common::config::{
|
||||||
|
ConfigFileControl, ConfigSource, PortForwardConfig, process_secure_mode_cfg,
|
||||||
|
};
|
||||||
use crate::proto::api::{self, manage};
|
use crate::proto::api::{self, manage};
|
||||||
use crate::proto::rpc_types::controller::BaseController;
|
use crate::proto::rpc_types::controller::BaseController;
|
||||||
use crate::rpc_service::InstanceRpcService;
|
use crate::rpc_service::InstanceRpcService;
|
||||||
@@ -434,6 +436,10 @@ impl NetworkInstance {
|
|||||||
&self.config_file_control
|
&self.config_file_control
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_network_config_source(&self) -> ConfigSource {
|
||||||
|
self.config.get_network_config_source()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_latest_error_msg(&self) -> Option<String> {
|
pub fn get_latest_error_msg(&self) -> Option<String> {
|
||||||
if let Some(launcher) = self.launcher.as_ref() {
|
if let Some(launcher) = self.launcher.as_ref() {
|
||||||
launcher.error_msg.read().unwrap().clone()
|
launcher.error_msg.read().unwrap().clone()
|
||||||
|
|||||||
@@ -1575,6 +1575,41 @@ pub mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn secure_center_can_serve_legacy_and_secure_foreign_networks() {
|
||||||
|
let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||||
|
set_secure_mode_cfg(&pm_center.get_global_ctx(), true);
|
||||||
|
|
||||||
|
let legacy_a = create_mock_peer_manager_for_foreign_network("legacy-net").await;
|
||||||
|
let legacy_b = create_mock_peer_manager_for_foreign_network("legacy-net").await;
|
||||||
|
connect_peer_manager(legacy_a.clone(), pm_center.clone()).await;
|
||||||
|
connect_peer_manager(legacy_b.clone(), pm_center.clone()).await;
|
||||||
|
wait_route_appear(legacy_a.clone(), legacy_b.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let secure_a = create_mock_peer_manager_for_secure_foreign_network("secure-net").await;
|
||||||
|
let secure_b = create_mock_peer_manager_for_secure_foreign_network("secure-net").await;
|
||||||
|
connect_peer_manager(secure_a.clone(), pm_center.clone()).await;
|
||||||
|
connect_peer_manager(secure_b.clone(), pm_center.clone()).await;
|
||||||
|
wait_route_appear(secure_a.clone(), secure_b.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(2, legacy_a.list_routes().await.len());
|
||||||
|
assert_eq!(2, legacy_b.list_routes().await.len());
|
||||||
|
assert_eq!(2, secure_a.list_routes().await.len());
|
||||||
|
assert_eq!(2, secure_b.list_routes().await.len());
|
||||||
|
|
||||||
|
let rpc_resp = pm_center
|
||||||
|
.get_foreign_network_manager()
|
||||||
|
.list_foreign_networks()
|
||||||
|
.await;
|
||||||
|
assert_eq!(2, rpc_resp.foreign_networks.len());
|
||||||
|
assert_eq!(2, rpc_resp.foreign_networks["legacy-net"].peers.len());
|
||||||
|
assert_eq!(2, rpc_resp.foreign_networks["secure-net"].peers.len());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn credential_pubkey_trust_requires_ospf_credential_source() {
|
async fn credential_pubkey_trust_requires_ospf_credential_source() {
|
||||||
let global_ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new(
|
let global_ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new(
|
||||||
|
|||||||
@@ -536,6 +536,21 @@ impl PeerManager {
|
|||||||
async fn add_new_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> {
|
async fn add_new_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> {
|
||||||
let my_identity = self.global_ctx.get_network_identity();
|
let my_identity = self.global_ctx.get_network_identity();
|
||||||
let peer_identity = peer_conn.get_network_identity();
|
let peer_identity = peer_conn.get_network_identity();
|
||||||
|
let conn_info = peer_conn.get_conn_info();
|
||||||
|
let local_secure_mode = self
|
||||||
|
.global_ctx
|
||||||
|
.config
|
||||||
|
.get_secure_mode()
|
||||||
|
.as_ref()
|
||||||
|
.map(|cfg| cfg.enabled)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let peer_secure_mode = !conn_info.noise_remote_static_pubkey.is_empty();
|
||||||
|
|
||||||
|
if local_secure_mode != peer_secure_mode {
|
||||||
|
return Err(Error::SecretKeyError(
|
||||||
|
"same-network peers must use the same secure mode".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// For credential nodes, network_secret_digest is either None or all-zeros
|
// For credential nodes, network_secret_digest is either None or all-zeros
|
||||||
// (all-zeros when received over the wire via handshake).
|
// (all-zeros when received over the wire via handshake).
|
||||||
@@ -2717,7 +2732,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn peer_manager_safe_server_accept_legacy_client() {
|
async fn peer_manager_same_network_secure_mode_mismatch_rejected() {
|
||||||
let peer_mgr_client = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
let peer_mgr_client = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||||
let peer_mgr_server = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
let peer_mgr_server = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||||
|
|
||||||
@@ -2737,64 +2752,65 @@ mod tests {
|
|||||||
peer_mgr_client.add_client_tunnel(c_ring, false),
|
peer_mgr_client.add_client_tunnel(c_ring, false),
|
||||||
peer_mgr_server.add_tunnel_as_server(s_ring, true)
|
peer_mgr_server.add_tunnel_as_server(s_ring, true)
|
||||||
);
|
);
|
||||||
let (server_id, _) = c_ret.unwrap();
|
let _ = c_ret;
|
||||||
s_ret.unwrap();
|
assert!(
|
||||||
|
s_ret.is_err(),
|
||||||
|
"same-network peer with mismatched secure mode should be rejected"
|
||||||
|
);
|
||||||
|
|
||||||
wait_for_condition(
|
|
||||||
|| {
|
|
||||||
let peer_mgr_client = peer_mgr_client.clone();
|
|
||||||
async move {
|
|
||||||
if !peer_mgr_client
|
|
||||||
.get_peer_map()
|
|
||||||
.list_peers_with_conn()
|
|
||||||
.await
|
|
||||||
.contains(&server_id)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let Some(conns) = peer_mgr_client
|
|
||||||
.get_peer_map()
|
|
||||||
.list_peer_conns(server_id)
|
|
||||||
.await
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
conns.iter().any(|c| {
|
|
||||||
c.noise_local_static_pubkey.is_empty()
|
|
||||||
&& c.noise_remote_static_pubkey.is_empty()
|
|
||||||
&& c.secure_auth_level == SecureAuthLevel::None as i32
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Duration::from_secs(10),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let client_id = peer_mgr_client.my_peer_id();
|
|
||||||
wait_for_condition(
|
wait_for_condition(
|
||||||
|| {
|
|| {
|
||||||
let peer_mgr_server = peer_mgr_server.clone();
|
let peer_mgr_server = peer_mgr_server.clone();
|
||||||
async move {
|
async move {
|
||||||
if !peer_mgr_server
|
peer_mgr_server
|
||||||
.get_peer_map()
|
.get_peer_map()
|
||||||
.list_peers_with_conn()
|
.list_peers_with_conn()
|
||||||
.await
|
.await
|
||||||
.contains(&client_id)
|
.is_empty()
|
||||||
{
|
}
|
||||||
return false;
|
},
|
||||||
}
|
Duration::from_secs(5),
|
||||||
let Some(conns) = peer_mgr_server
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn credential_node_rejects_legacy_client() {
|
||||||
|
let peer_mgr_client = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||||
|
let peer_mgr_server = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||||
|
|
||||||
|
peer_mgr_client
|
||||||
|
.get_global_ctx()
|
||||||
|
.config
|
||||||
|
.set_network_identity(NetworkIdentity::new("net1".to_string(), "sec1".to_string()));
|
||||||
|
peer_mgr_server
|
||||||
|
.get_global_ctx()
|
||||||
|
.config
|
||||||
|
.set_network_identity(NetworkIdentity::new_credential("net1".to_string()));
|
||||||
|
|
||||||
|
set_secure_mode_cfg(&peer_mgr_server.get_global_ctx(), true);
|
||||||
|
|
||||||
|
let (c_ring, s_ring) = create_ring_tunnel_pair();
|
||||||
|
let (c_ret, s_ret) = tokio::join!(
|
||||||
|
peer_mgr_client.add_client_tunnel(c_ring, false),
|
||||||
|
peer_mgr_server.add_tunnel_as_server(s_ring, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = c_ret;
|
||||||
|
assert!(
|
||||||
|
s_ret.is_err(),
|
||||||
|
"credential server should reject legacy client"
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_for_condition(
|
||||||
|
|| {
|
||||||
|
let peer_mgr_server = peer_mgr_server.clone();
|
||||||
|
async move {
|
||||||
|
peer_mgr_server
|
||||||
.get_peer_map()
|
.get_peer_map()
|
||||||
.list_peer_conns(client_id)
|
.list_peers_with_conn()
|
||||||
.await
|
.await
|
||||||
else {
|
.is_empty()
|
||||||
return false;
|
|
||||||
};
|
|
||||||
conns.iter().any(|c| {
|
|
||||||
c.noise_local_static_pubkey.is_empty()
|
|
||||||
&& c.noise_remote_static_pubkey.is_empty()
|
|
||||||
&& c.secure_auth_level == SecureAuthLevel::None as i32
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ enum NetworkingMethod {
|
|||||||
Standalone = 2;
|
Standalone = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ConfigSource {
|
||||||
|
ConfigSourceUnspecified = 0;
|
||||||
|
ConfigSourceUser = 1;
|
||||||
|
ConfigSourceWebhook = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message NetworkConfig {
|
message NetworkConfig {
|
||||||
optional string instance_id = 1;
|
optional string instance_id = 1;
|
||||||
|
|
||||||
@@ -132,6 +138,7 @@ message NetworkMeta {
|
|||||||
string network_name = 2;
|
string network_name = 2;
|
||||||
uint32 config_permission = 3;
|
uint32 config_permission = 3;
|
||||||
string instance_name = 4;
|
string instance_name = 4;
|
||||||
|
ConfigSource source = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ValidateConfigRequest { NetworkConfig config = 1; }
|
message ValidateConfigRequest { NetworkConfig config = 1; }
|
||||||
@@ -142,6 +149,7 @@ message RunNetworkInstanceRequest {
|
|||||||
common.UUID inst_id = 1;
|
common.UUID inst_id = 1;
|
||||||
NetworkConfig config = 2;
|
NetworkConfig config = 2;
|
||||||
bool overwrite = 3;
|
bool overwrite = 3;
|
||||||
|
ConfigSource source = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RunNetworkInstanceResponse { common.UUID inst_id = 1; }
|
message RunNetworkInstanceResponse { common.UUID inst_id = 1; }
|
||||||
@@ -168,7 +176,10 @@ message DeleteNetworkInstanceResponse {
|
|||||||
|
|
||||||
message GetNetworkInstanceConfigRequest { common.UUID inst_id = 1; }
|
message GetNetworkInstanceConfigRequest { common.UUID inst_id = 1; }
|
||||||
|
|
||||||
message GetNetworkInstanceConfigResponse { NetworkConfig config = 1; }
|
message GetNetworkInstanceConfigResponse {
|
||||||
|
NetworkConfig config = 1;
|
||||||
|
ConfigSource source = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListNetworkInstanceMetaRequest { repeated common.UUID inst_ids = 1; }
|
message ListNetworkInstanceMetaRequest { repeated common.UUID inst_ids = 1; }
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ message HeartbeatRequest {
|
|||||||
|
|
||||||
repeated common.UUID running_network_instances = 7;
|
repeated common.UUID running_network_instances = 7;
|
||||||
DeviceOsInfo device_os = 8;
|
DeviceOsInfo device_os = 8;
|
||||||
|
bool support_config_source = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HeartbeatResponse {}
|
message HeartbeatResponse {}
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
use std::{collections::HashSet, sync::Arc};
|
use std::{collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::config::{ConfigFileControl, ConfigFilePermission, ConfigLoader},
|
common::config::{ConfigFileControl, ConfigFilePermission, ConfigLoader, ConfigSource},
|
||||||
instance_manager::NetworkInstanceManager,
|
instance_manager::NetworkInstanceManager,
|
||||||
proto::{
|
proto::{
|
||||||
api::{config::GetConfigRequest, manage::*},
|
api::{
|
||||||
|
config::GetConfigRequest,
|
||||||
|
manage::{
|
||||||
|
CollectNetworkInfoRequest, CollectNetworkInfoResponse,
|
||||||
|
DeleteNetworkInstanceRequest, DeleteNetworkInstanceResponse,
|
||||||
|
GetNetworkInstanceConfigRequest, GetNetworkInstanceConfigResponse,
|
||||||
|
ListNetworkInstanceMetaRequest, ListNetworkInstanceMetaResponse,
|
||||||
|
ListNetworkInstanceRequest, ListNetworkInstanceResponse,
|
||||||
|
NetworkInstanceRunningInfoMap, NetworkMeta, RetainNetworkInstanceRequest,
|
||||||
|
RetainNetworkInstanceResponse, RunNetworkInstanceRequest,
|
||||||
|
RunNetworkInstanceResponse, ValidateConfigRequest, ValidateConfigResponse,
|
||||||
|
WebClientService,
|
||||||
|
},
|
||||||
|
},
|
||||||
rpc_types::{self, controller::BaseController},
|
rpc_types::{self, controller::BaseController},
|
||||||
},
|
},
|
||||||
web_client::WebClientHooks,
|
web_client::WebClientHooks,
|
||||||
@@ -44,53 +57,64 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
return Err(anyhow::anyhow!("config is required").into());
|
return Err(anyhow::anyhow!("config is required").into());
|
||||||
}
|
}
|
||||||
let cfg = req.config.unwrap().gen_config()?;
|
let cfg = req.config.unwrap().gen_config()?;
|
||||||
let id = cfg.get_id();
|
let mut effective_id = cfg.get_id();
|
||||||
if let Some(inst_id) = req.inst_id {
|
if let Some(inst_id) = req.inst_id {
|
||||||
cfg.set_id(inst_id.into());
|
effective_id = inst_id.into();
|
||||||
|
cfg.set_id(effective_id);
|
||||||
}
|
}
|
||||||
|
let requested_source = ConfigSource::from_rpc(req.source);
|
||||||
let resp = RunNetworkInstanceResponse {
|
let resp = RunNetworkInstanceResponse {
|
||||||
inst_id: Some(id.into()),
|
inst_id: Some(effective_id.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut control = if let Some(control) = self.manager.get_instance_config_control(&id) {
|
let mut control =
|
||||||
let error_msg = self
|
if let Some(control) = self.manager.get_instance_config_control(&effective_id) {
|
||||||
.manager
|
let existing_source = self
|
||||||
.get_network_info(&id)
|
.manager
|
||||||
.await
|
.get_instance_network_config_source(&effective_id);
|
||||||
.and_then(|i| i.error_msg)
|
let error_msg = self
|
||||||
.unwrap_or_default();
|
.manager
|
||||||
|
.get_network_info(&effective_id)
|
||||||
|
.await
|
||||||
|
.and_then(|i| i.error_msg)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
if !req.overwrite && error_msg.is_empty() {
|
if !req.overwrite && error_msg.is_empty() {
|
||||||
return Ok(resp);
|
return Ok(resp);
|
||||||
}
|
}
|
||||||
if control.is_read_only() {
|
if control.is_read_only() {
|
||||||
return Err(
|
|
||||||
anyhow::anyhow!("instance {} is read-only, cannot be overwritten", id).into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(path) = control.path.as_ref() {
|
|
||||||
let real_control = ConfigFileControl::from_path(path.clone()).await;
|
|
||||||
if real_control.is_read_only() {
|
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"config file {} is read-only, cannot be overwritten",
|
"instance {} is read-only, cannot be overwritten",
|
||||||
path.display()
|
effective_id
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.manager.delete_network_instance(vec![id])?;
|
if let Some(path) = control.path.as_ref() {
|
||||||
|
let real_control = ConfigFileControl::from_path(path.clone()).await;
|
||||||
|
if real_control.is_read_only() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"config file {} is read-only, cannot be overwritten",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
control.clone()
|
self.manager.delete_network_instance(vec![effective_id])?;
|
||||||
} else if let Some(config_dir) = self.manager.get_config_dir() {
|
|
||||||
ConfigFileControl::new(
|
cfg.set_network_config_source(requested_source.or(existing_source));
|
||||||
Some(config_dir.join(format!("{}.toml", id))),
|
control.clone()
|
||||||
ConfigFilePermission::default(),
|
} else if let Some(config_dir) = self.manager.get_config_dir() {
|
||||||
)
|
cfg.set_network_config_source(requested_source);
|
||||||
} else {
|
ConfigFileControl::new(
|
||||||
ConfigFileControl::new(None, ConfigFilePermission::default())
|
Some(config_dir.join(format!("{}.toml", effective_id))),
|
||||||
};
|
ConfigFilePermission::default(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
cfg.set_network_config_source(requested_source);
|
||||||
|
ConfigFileControl::new(None, ConfigFilePermission::default())
|
||||||
|
};
|
||||||
|
|
||||||
if !control.is_read_only()
|
if !control.is_read_only()
|
||||||
&& let Some(config_file) = control.path.as_ref()
|
&& let Some(config_file) = control.path.as_ref()
|
||||||
@@ -109,9 +133,9 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.manager.run_network_instance(cfg, true, control)?;
|
self.manager.run_network_instance(cfg, true, control)?;
|
||||||
println!("instance {} started", id);
|
println!("instance {} started", effective_id);
|
||||||
|
|
||||||
if let Err(e) = self.hooks.post_run_network_instance(&id).await {
|
if let Err(e) = self.hooks.post_run_network_instance(&effective_id).await {
|
||||||
tracing::warn!("post-run hook failed: {}", e);
|
tracing::warn!("post-run hook failed: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +285,14 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
.get_config(BaseController::default(), GetConfigRequest::default())
|
.get_config(BaseController::default(), GetConfigRequest::default())
|
||||||
.await?
|
.await?
|
||||||
.config;
|
.config;
|
||||||
Ok(GetNetworkInstanceConfigResponse { config })
|
Ok(GetNetworkInstanceConfigResponse {
|
||||||
|
config,
|
||||||
|
source: self
|
||||||
|
.manager
|
||||||
|
.get_instance_network_config_source(&inst_id)
|
||||||
|
.unwrap_or(ConfigSource::User)
|
||||||
|
.to_rpc(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_network_instance_meta(
|
async fn list_network_instance_meta(
|
||||||
@@ -286,6 +317,11 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
network_name,
|
network_name,
|
||||||
config_permission: control.permission.into(),
|
config_permission: control.permission.into(),
|
||||||
instance_name,
|
instance_name,
|
||||||
|
source: self
|
||||||
|
.manager
|
||||||
|
.get_instance_network_config_source(&inst_id)
|
||||||
|
.unwrap_or(ConfigSource::User)
|
||||||
|
.to_rpc(),
|
||||||
};
|
};
|
||||||
metas.push(meta);
|
metas.push(meta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::proto::{api::manage::*, rpc_types::controller::BaseController};
|
use crate::{
|
||||||
|
common::config::ConfigSource,
|
||||||
|
proto::{
|
||||||
|
api::manage::{
|
||||||
|
CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest,
|
||||||
|
GetNetworkInstanceConfigRequest, ListNetworkInstanceMetaRequest,
|
||||||
|
ListNetworkInstanceRequest, NetworkConfig, NetworkMeta, RunNetworkInstanceRequest,
|
||||||
|
ValidateConfigRequest, ValidateConfigResponse, WebClientService,
|
||||||
|
},
|
||||||
|
rpc_types::controller::BaseController,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait RemoteClientManager<T, C, E>
|
pub trait RemoteClientManager<T, C, E>
|
||||||
@@ -52,6 +63,7 @@ where
|
|||||||
inst_id: None,
|
inst_id: None,
|
||||||
config: Some(config.clone()),
|
config: Some(config.clone()),
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
|
source: ConfigSource::User.to_rpc(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -62,6 +74,7 @@ where
|
|||||||
identify,
|
identify,
|
||||||
resp.inst_id.unwrap_or_default().into(),
|
resp.inst_id.unwrap_or_default().into(),
|
||||||
config,
|
config,
|
||||||
|
ConfigSource::User,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(RemoteClientError::PersistentError)?;
|
.map_err(RemoteClientError::PersistentError)?;
|
||||||
@@ -162,13 +175,18 @@ where
|
|||||||
.get_rpc_client(identify.clone())
|
.get_rpc_client(identify.clone())
|
||||||
.ok_or(RemoteClientError::ClientNotFound)?;
|
.ok_or(RemoteClientError::ClientNotFound)?;
|
||||||
|
|
||||||
let cfg = self
|
let (cfg, source) = self
|
||||||
.handle_get_network_config(identify.clone(), inst_id)
|
.handle_get_network_config_with_source(identify.clone(), inst_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if disabled {
|
if disabled {
|
||||||
self.get_storage()
|
self.get_storage()
|
||||||
.insert_or_update_user_network_config(identify.clone(), inst_id, cfg.clone())
|
.insert_or_update_user_network_config(
|
||||||
|
identify.clone(),
|
||||||
|
inst_id,
|
||||||
|
cfg.clone(),
|
||||||
|
source,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(RemoteClientError::PersistentError)?;
|
.map_err(RemoteClientError::PersistentError)?;
|
||||||
|
|
||||||
@@ -188,6 +206,7 @@ where
|
|||||||
inst_id: Some(inst_id.into()),
|
inst_id: Some(inst_id.into()),
|
||||||
config: Some(cfg),
|
config: Some(cfg),
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
|
source: source.to_rpc(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -230,8 +249,8 @@ where
|
|||||||
if metas.contains_key(&instance_id) {
|
if metas.contains_key(&instance_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let config = self
|
let (config, source) = self
|
||||||
.handle_get_network_config(identify.clone(), instance_id)
|
.handle_get_network_config_with_source(identify.clone(), instance_id)
|
||||||
.await?;
|
.await?;
|
||||||
let network_name = config.network_name.unwrap_or_default();
|
let network_name = config.network_name.unwrap_or_default();
|
||||||
metas.insert(
|
metas.insert(
|
||||||
@@ -241,6 +260,7 @@ where
|
|||||||
network_name: network_name.clone(),
|
network_name: network_name.clone(),
|
||||||
config_permission: 0,
|
config_permission: 0,
|
||||||
instance_name: network_name,
|
instance_name: network_name,
|
||||||
|
source: source.to_rpc(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -255,7 +275,12 @@ where
|
|||||||
config: NetworkConfig,
|
config: NetworkConfig,
|
||||||
) -> Result<(), RemoteClientError<E>> {
|
) -> Result<(), RemoteClientError<E>> {
|
||||||
self.get_storage()
|
self.get_storage()
|
||||||
.insert_or_update_user_network_config(identify.clone(), inst_id, config)
|
.insert_or_update_user_network_config(
|
||||||
|
identify.clone(),
|
||||||
|
inst_id,
|
||||||
|
config,
|
||||||
|
ConfigSource::User,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(RemoteClientError::PersistentError)?;
|
.map_err(RemoteClientError::PersistentError)?;
|
||||||
self.get_storage()
|
self.get_storage()
|
||||||
@@ -270,6 +295,16 @@ where
|
|||||||
identify: T,
|
identify: T,
|
||||||
inst_id: uuid::Uuid,
|
inst_id: uuid::Uuid,
|
||||||
) -> Result<NetworkConfig, RemoteClientError<E>> {
|
) -> Result<NetworkConfig, RemoteClientError<E>> {
|
||||||
|
self.handle_get_network_config_with_source(identify, inst_id)
|
||||||
|
.await
|
||||||
|
.map(|(config, _)| config)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_get_network_config_with_source(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
inst_id: uuid::Uuid,
|
||||||
|
) -> Result<(NetworkConfig, ConfigSource), RemoteClientError<E>> {
|
||||||
if let Some(client) = self.get_rpc_client(identify.clone())
|
if let Some(client) = self.get_rpc_client(identify.clone())
|
||||||
&& let Ok(resp) = client
|
&& let Ok(resp) = client
|
||||||
.get_network_instance_config(
|
.get_network_instance_config(
|
||||||
@@ -281,7 +316,17 @@ where
|
|||||||
.await
|
.await
|
||||||
&& let Some(config) = resp.config
|
&& let Some(config) = resp.config
|
||||||
{
|
{
|
||||||
return Ok(config);
|
let source = if let Some(source) = ConfigSource::from_rpc(resp.source) {
|
||||||
|
source
|
||||||
|
} else {
|
||||||
|
self.get_storage()
|
||||||
|
.get_network_config(identify.clone(), &inst_id.to_string())
|
||||||
|
.await
|
||||||
|
.map_err(RemoteClientError::PersistentError)?
|
||||||
|
.map(|cfg| cfg.get_runtime_network_config_source())
|
||||||
|
.unwrap_or(ConfigSource::User)
|
||||||
|
};
|
||||||
|
return Ok((config, source));
|
||||||
}
|
}
|
||||||
|
|
||||||
let inst_id = inst_id.to_string();
|
let inst_id = inst_id.to_string();
|
||||||
@@ -296,9 +341,12 @@ where
|
|||||||
inst_id
|
inst_id
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
Ok(db_row
|
Ok((
|
||||||
.get_network_config()
|
db_row
|
||||||
.map_err(RemoteClientError::PersistentError)?)
|
.get_network_config()
|
||||||
|
.map_err(RemoteClientError::PersistentError)?,
|
||||||
|
db_row.get_runtime_network_config_source(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +384,10 @@ pub struct GetNetworkMetasResponse {
|
|||||||
pub trait PersistentConfig<E> {
|
pub trait PersistentConfig<E> {
|
||||||
fn get_network_inst_id(&self) -> &str;
|
fn get_network_inst_id(&self) -> &str;
|
||||||
fn get_network_config(&self) -> Result<NetworkConfig, E>;
|
fn get_network_config(&self) -> Result<NetworkConfig, E>;
|
||||||
|
fn get_network_config_source(&self) -> ConfigSource;
|
||||||
|
fn get_runtime_network_config_source(&self) -> ConfigSource {
|
||||||
|
self.get_network_config_source()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -348,6 +400,7 @@ where
|
|||||||
identify: T,
|
identify: T,
|
||||||
network_inst_id: Uuid,
|
network_inst_id: Uuid,
|
||||||
network_config: NetworkConfig,
|
network_config: NetworkConfig,
|
||||||
|
source: ConfigSource,
|
||||||
) -> Result<(), E>;
|
) -> Result<(), E>;
|
||||||
|
|
||||||
async fn delete_network_configs(&self, identify: T, network_inst_ids: &[Uuid])
|
async fn delete_network_configs(&self, identify: T, network_inst_ids: &[Uuid])
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ impl Session {
|
|||||||
hostname: hostname.clone(),
|
hostname: hostname.clone(),
|
||||||
report_time: chrono::Local::now().to_rfc3339(),
|
report_time: chrono::Local::now().to_rfc3339(),
|
||||||
device_os: Some(device_os.clone()),
|
device_os: Some(device_os.clone()),
|
||||||
|
support_config_source: true,
|
||||||
|
|
||||||
running_network_instances: controller
|
running_network_instances: controller
|
||||||
.list_network_instance_ids()
|
.list_network_instance_ids()
|
||||||
|
|||||||
Reference in New Issue
Block a user