improve webclient (#2151)

This commit is contained in:
KKRainbow
2026-04-23 13:44:18 +08:00
committed by GitHub
parent 263f4c3bc9
commit 958b246f05
19 changed files with 1585 additions and 188 deletions
+722 -35
View File
@@ -7,17 +7,17 @@ use std::{
use anyhow::Context;
use easytier::{
common::scoped_task::ScopedTask,
common::{config::ConfigSource, scoped_task::ScopedTask},
proto::{
api::manage::{
NetworkConfig, RunNetworkInstanceRequest, WebClientService,
WebClientServiceClientFactory,
ConfigSource as RpcConfigSource, NetworkConfig, NetworkMeta, RunNetworkInstanceRequest,
WebClientService, WebClientServiceClientFactory,
},
rpc_impl::bidirect::BidirectRpcManager,
rpc_types::{self, controller::BaseController},
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
},
rpc_service::remote_client::{ListNetworkProps, Storage as _},
rpc_service::remote_client::{ListNetworkProps, PersistentConfig as _, Storage as _},
tunnel::Tunnel,
};
use tokio::sync::{RwLock, broadcast};
@@ -26,6 +26,50 @@ use super::storage::{Storage, StorageToken, WeakRefStorage};
use crate::FeatureFlags;
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)]
pub struct Location {
pub country: String,
@@ -148,7 +192,7 @@ impl SessionRpcService {
Ok(serde_json::from_value::<NetworkConfig>(network_config)?)
}
async fn reconcile_managed_network_configs(
async fn reconcile_webhook_source_configs(
storage: &Storage,
user_id: i32,
machine_id: uuid::Uuid,
@@ -159,9 +203,19 @@ impl SessionRpcService {
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
.await
.map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?;
let existing_ids = existing_configs
let existing_sources = existing_configs
.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<_>>();
let mut desired_ids = HashSet::with_capacity(desired_configs.len());
@@ -169,10 +223,30 @@ impl SessionRpcService {
for desired in desired_configs {
let inst_id = uuid::Uuid::parse_str(&desired.instance_id).with_context(|| {
format!(
"invalid desired managed instance id: {}",
"invalid desired webhook config 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)?;
desired_ids.insert(inst_id);
normalized.insert(inst_id, config);
@@ -181,18 +255,23 @@ impl SessionRpcService {
for (inst_id, config) in normalized {
storage
.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
.map_err(|e| {
anyhow::anyhow!(
"failed to persist managed network config {}: {:?}",
"failed to persist webhook network config {}: {:?}",
inst_id,
e
)
})?;
}
let stale_ids = existing_ids
let stale_ids = existing_webhook_ids
.difference(&desired_ids)
.copied()
.collect::<Vec<_>>();
@@ -225,7 +304,7 @@ impl SessionRpcService {
let (
user_id,
webhook_managed_network_configs,
webhook_source_configs,
webhook_config_revision,
webhook_validated,
binding_version,
@@ -306,11 +385,11 @@ impl SessionRpcService {
if webhook_validated
&& data.applied_config_revision.as_deref() != Some(webhook_config_revision.as_str())
{
Self::reconcile_managed_network_configs(
Self::reconcile_webhook_source_configs(
&storage,
user_id,
machine_id,
webhook_managed_network_configs,
webhook_source_configs,
)
.await
.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(
mut heartbeat_waiter: broadcast::Receiver<HeartbeatRequest>,
storage: WeakRefStorage,
rpc_client: SessionRpcClient,
) {
let mut cleaned_web_managed_instances = false;
let mut last_desired_inst_ids: Option<HashSet<String>> = None;
let mut cleaned_webhook_source_instances = false;
let mut last_desired_webhook_inst_ids: Option<HashSet<String>> = None;
loop {
heartbeat_waiter = heartbeat_waiter.resubscribe();
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 should_be_alive_inst_ids = local_configs
let should_be_alive_webhook_inst_ids = local_configs
.iter()
.filter(|cfg| cfg.get_runtime_network_config_source() == ConfigSource::Webhook)
.map(|cfg| cfg.network_instance_id.clone())
.collect::<HashSet<_>>();
let desired_changed = last_desired_inst_ids
let desired_changed = last_desired_webhook_inst_ids
.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 {
let all_local_configs = match storage
if !cleaned_webhook_source_instances || desired_changed {
let db_webhook_inst_ids = match storage
.db
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::All)
.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) => {
tracing::error!("Failed to list all network configs, error: {:?}", e);
return;
}
};
let all_inst_ids = all_local_configs
.iter()
.map(|cfg| cfg.network_instance_id.clone())
let running_webhook_inst_ids = if let Some(metas) = running_metas.as_ref() {
Self::collect_webhook_source_instance_ids(metas.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<_>>();
let should_delete_ids = running_inst_ids
let should_delete_ids = should_delete_inst_ids
.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())
.map(Into::into)
.collect::<Vec<_>>();
@@ -556,7 +878,7 @@ impl Session {
.await;
tracing::info!(
?user_id,
"Clean non-web-managed network instances on start: {:?}, user_token: {:?}",
"Clean stale webhook-source network instances on start: {:?}, user_token: {:?}",
ret,
req.user_token
);
@@ -564,8 +886,8 @@ impl Session {
}
if !has_failed {
cleaned_web_managed_instances = true;
last_desired_inst_ids = Some(should_be_alive_inst_ids.clone());
cleaned_webhook_source_instances = true;
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) {
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
.run_network_instance(
BaseController::default(),
@@ -582,6 +914,7 @@ impl Session {
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
),
overwrite: false,
source: source as i32,
},
)
.await;
@@ -596,7 +929,7 @@ impl Session {
}
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)]
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 super::{super::storage::Storage, *};
#[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 user_id = storage
.db()
@@ -662,6 +999,7 @@ mod tests {
network_name: Some("old-name".to_string()),
..Default::default()
},
ConfigSource::Webhook,
)
.await
.unwrap();
@@ -674,11 +1012,12 @@ mod tests {
network_name: Some("stale".to_string()),
..Default::default()
},
ConfigSource::Webhook,
)
.await
.unwrap();
SessionRpcService::reconcile_managed_network_configs(
SessionRpcService::reconcile_webhook_source_configs(
&storage,
user_id,
machine_id,
@@ -729,5 +1068,353 @@ mod tests {
updated_keep_config.network_name.as_deref(),
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)
);
}
}