mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-18 07:47:22 +08:00
feat(core): Support environment variable parsing in config files (#1640)
This commit is contained in:
@@ -19,6 +19,8 @@ use crate::{
|
||||
tunnel::generate_digest_from_str,
|
||||
};
|
||||
|
||||
use super::env_parser;
|
||||
|
||||
pub type Flags = crate::proto::common::FlagsInConfig;
|
||||
|
||||
pub fn gen_default_flags() -> Flags {
|
||||
@@ -948,6 +950,7 @@ impl ConfigFileControl {
|
||||
pub async fn load_config_from_file(
|
||||
config_file: &PathBuf,
|
||||
config_dir: Option<&PathBuf>,
|
||||
disable_env_parsing: bool,
|
||||
) -> Result<(TomlConfigLoader, ConfigFileControl), anyhow::Error> {
|
||||
if config_file.as_os_str() == "-" {
|
||||
let mut stdin = String::new();
|
||||
@@ -958,10 +961,44 @@ pub async fn load_config_from_file(
|
||||
let config = TomlConfigLoader::new_from_str(&stdin)?;
|
||||
return Ok((config, ConfigFileControl::STATIC_CONFIG));
|
||||
}
|
||||
let config = TomlConfigLoader::new(config_file)
|
||||
|
||||
let config_str = tokio::fs::read_to_string(config_file)
|
||||
.await
|
||||
.with_context(|| format!("failed to read config file: {:?}", config_file))?;
|
||||
|
||||
let (expanded_config_str, uses_env_vars) = if disable_env_parsing {
|
||||
(config_str.clone(), false)
|
||||
} else {
|
||||
env_parser::expand_env_vars(&config_str)
|
||||
};
|
||||
|
||||
if disable_env_parsing {
|
||||
tracing::info!(
|
||||
"Environment variable parsing is disabled for config file: {:?}",
|
||||
config_file
|
||||
);
|
||||
}
|
||||
|
||||
if uses_env_vars {
|
||||
tracing::info!(
|
||||
"Environment variables detected and expanded in config file: {:?}",
|
||||
config_file
|
||||
);
|
||||
}
|
||||
|
||||
let config = TomlConfigLoader::new_from_str(&expanded_config_str)
|
||||
.with_context(|| format!("failed to load config file: {:?}", config_file))?;
|
||||
|
||||
let mut control = ConfigFileControl::from_path(config_file.clone()).await;
|
||||
if control.is_read_only() {
|
||||
|
||||
if uses_env_vars {
|
||||
control.set_read_only(true);
|
||||
control.set_no_delete(true);
|
||||
tracing::info!(
|
||||
"Config file {:?} uses environment variables, marked as READ_ONLY and NO_DELETE",
|
||||
config_file
|
||||
);
|
||||
} else if control.is_read_only() {
|
||||
control.set_no_delete(true);
|
||||
} else if let Some(config_dir) = config_dir {
|
||||
if let Some(config_file_dir) = config_file.parent() {
|
||||
@@ -985,6 +1022,9 @@ pub async fn load_config_from_file(
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_stun_servers_config() {
|
||||
@@ -1088,4 +1128,564 @@ proto = "tcp"
|
||||
);
|
||||
println!("{}", ret.dump());
|
||||
}
|
||||
|
||||
/// 配置文件环境变量解析功能的集成测试
|
||||
///
|
||||
/// 测试范围:
|
||||
/// 1. 配置加载功能测试(环境变量替换、权限标记)
|
||||
/// 2. RPC API 安全测试(只读配置保护)
|
||||
/// 3. CLI 参数测试(--disable-env-parsing 开关)
|
||||
/// 4. 多实例隔离测试
|
||||
/// 5. 实际配置字段测试(network_secret、peer.uri 等)
|
||||
/// 配置加载功能测试(环境变量替换、权限标记)
|
||||
///
|
||||
/// 验证:
|
||||
/// - 环境变量能正确替换到配置中
|
||||
/// - 包含环境变量的配置文件自动标记为只读和禁止删除
|
||||
#[tokio::test]
|
||||
async fn test_env_var_expansion_and_readonly_flag() {
|
||||
// 设置测试环境变量
|
||||
std::env::set_var("TEST_SECRET", "my-test-secret-123");
|
||||
std::env::set_var("TEST_NETWORK", "test-network");
|
||||
|
||||
// 创建临时配置文件,包含环境变量占位符
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "test-instance"
|
||||
|
||||
[network_identity]
|
||||
network_name = "${TEST_NETWORK}"
|
||||
network_secret = "${TEST_SECRET}"
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
|
||||
// 加载配置(启用环境变量解析)
|
||||
let (config, control) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证环境变量已被替换
|
||||
let network_identity = config.get_network_identity();
|
||||
assert_eq!(network_identity.network_name, "test-network");
|
||||
assert_eq!(
|
||||
network_identity.network_secret.as_ref().unwrap(),
|
||||
"my-test-secret-123"
|
||||
);
|
||||
|
||||
// 验证权限标记:包含环境变量的配置应被标记为只读和禁止删除
|
||||
assert!(
|
||||
control.is_read_only(),
|
||||
"Config with env vars should be marked as READ_ONLY"
|
||||
);
|
||||
assert!(
|
||||
control.is_no_delete(),
|
||||
"Config with env vars should be marked as NO_DELETE"
|
||||
);
|
||||
|
||||
// 清理环境变量
|
||||
std::env::remove_var("TEST_SECRET");
|
||||
std::env::remove_var("TEST_NETWORK");
|
||||
}
|
||||
|
||||
/// RPC API 安全测试(只读配置保护)
|
||||
///
|
||||
/// 验证:
|
||||
/// - 只读配置不会通过 RPC API 暴露给远程调用
|
||||
/// - 这需要测试 get_network_instance_config 拒绝返回只读配置
|
||||
///
|
||||
/// 注:这个测试验证权限标记的正确设置,实际的 RPC API 保护已在
|
||||
/// `easytier/src/rpc_service/instance_manage.rs` 中实现
|
||||
#[tokio::test]
|
||||
async fn test_readonly_config_api_protection() {
|
||||
std::env::set_var("API_TEST_SECRET", "secret-value");
|
||||
|
||||
// 创建包含环境变量的配置
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "api-test"
|
||||
|
||||
[network_identity]
|
||||
network_name = "api-network"
|
||||
network_secret = "${API_TEST_SECRET}"
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
|
||||
// 加载配置
|
||||
let (_config, control) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证只读标记已设置(这是 RPC API 保护的前提)
|
||||
assert!(
|
||||
control.is_read_only(),
|
||||
"Config should be marked as READ_ONLY for RPC protection"
|
||||
);
|
||||
assert!(
|
||||
control.permission.has_flag(ConfigFilePermission::READ_ONLY),
|
||||
"Permission flag should be set correctly"
|
||||
);
|
||||
|
||||
std::env::remove_var("API_TEST_SECRET");
|
||||
}
|
||||
|
||||
/// CLI 参数测试(--disable-env-parsing 开关)
|
||||
///
|
||||
/// 验证:
|
||||
/// - disable_env_parsing = true 时,环境变量不会被替换
|
||||
/// - 配置不会被标记为只读
|
||||
#[tokio::test]
|
||||
async fn test_disable_env_parsing_flag() {
|
||||
std::env::set_var("DISABLED_TEST_VAR", "should-not-expand");
|
||||
|
||||
// 创建包含环境变量占位符的配置
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "disable-test"
|
||||
|
||||
[network_identity]
|
||||
network_name = "test"
|
||||
network_secret = "${DISABLED_TEST_VAR}"
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
|
||||
// 以 disable_env_parsing = true 加载配置
|
||||
let (config, control) = load_config_from_file(&config_path, None, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证环境变量未被替换(保持原样)
|
||||
let network_identity = config.get_network_identity();
|
||||
assert_eq!(
|
||||
network_identity.network_secret.as_ref().unwrap(),
|
||||
"${DISABLED_TEST_VAR}",
|
||||
"Env var should not be expanded when parsing is disabled"
|
||||
);
|
||||
|
||||
// 验证配置不因环境变量而被标记为只读
|
||||
// 注:文件系统权限可能使其只读,但不应因环境变量而只读
|
||||
// 这里我们主要验证 NO_DELETE 标记的逻辑
|
||||
// 由于没有 config_dir,文件会被标记为 NO_DELETE,但不是因为环境变量
|
||||
assert!(
|
||||
control.is_no_delete(),
|
||||
"Config should be NO_DELETE due to no config_dir, not env vars"
|
||||
);
|
||||
|
||||
std::env::remove_var("DISABLED_TEST_VAR");
|
||||
}
|
||||
|
||||
/// 多实例隔离测试
|
||||
///
|
||||
/// 验证:
|
||||
/// - 不同实例可以使用不同的环境变量值
|
||||
/// - 环境变量在运行时被解析,支持动态切换
|
||||
#[tokio::test]
|
||||
async fn test_multiple_instances_with_different_env_vars() {
|
||||
// 实例1:使用第一组环境变量
|
||||
std::env::set_var("INSTANCE_SECRET", "instance1-secret");
|
||||
std::env::set_var("INSTANCE_NAME", "instance-one");
|
||||
|
||||
let mut temp_file1 = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "${INSTANCE_NAME}"
|
||||
|
||||
[network_identity]
|
||||
network_name = "multi-test"
|
||||
network_secret = "${INSTANCE_SECRET}"
|
||||
"#;
|
||||
temp_file1.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file1.flush().unwrap();
|
||||
|
||||
let config_path1 = PathBuf::from(temp_file1.path());
|
||||
let (config1, _) = load_config_from_file(&config_path1, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证实例1的配置
|
||||
assert_eq!(config1.get_inst_name(), "instance-one");
|
||||
assert_eq!(
|
||||
config1
|
||||
.get_network_identity()
|
||||
.network_secret
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
"instance1-secret"
|
||||
);
|
||||
|
||||
// 实例2:修改环境变量后加载同一模板
|
||||
std::env::set_var("INSTANCE_SECRET", "instance2-secret");
|
||||
std::env::set_var("INSTANCE_NAME", "instance-two");
|
||||
|
||||
let mut temp_file2 = NamedTempFile::new().unwrap();
|
||||
temp_file2.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file2.flush().unwrap();
|
||||
|
||||
let config_path2 = PathBuf::from(temp_file2.path());
|
||||
let (config2, _) = load_config_from_file(&config_path2, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证实例2使用了不同的环境变量值
|
||||
assert_eq!(config2.get_inst_name(), "instance-two");
|
||||
assert_eq!(
|
||||
config2
|
||||
.get_network_identity()
|
||||
.network_secret
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
"instance2-secret"
|
||||
);
|
||||
|
||||
// 验证两个实例的配置确实不同
|
||||
assert_ne!(config1.get_inst_name(), config2.get_inst_name());
|
||||
assert_ne!(
|
||||
config1.get_network_identity().network_secret,
|
||||
config2.get_network_identity().network_secret
|
||||
);
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("INSTANCE_SECRET");
|
||||
std::env::remove_var("INSTANCE_NAME");
|
||||
}
|
||||
|
||||
/// 实际配置字段测试(network_secret、peer.uri 等)
|
||||
///
|
||||
/// 验证:
|
||||
/// - network_secret 字段支持环境变量
|
||||
/// - peer.uri 字段支持环境变量
|
||||
/// - listeners 字段支持环境变量
|
||||
/// - 其他实际使用的配置字段
|
||||
#[tokio::test]
|
||||
async fn test_real_config_fields_expansion() {
|
||||
// 设置各种实际场景的环境变量
|
||||
std::env::set_var("ET_SECRET", "production-secret-key");
|
||||
std::env::set_var("PEER_HOST", "peer.example.com");
|
||||
std::env::set_var("PEER_PORT", "11011");
|
||||
std::env::set_var("LISTEN_PORT", "11010");
|
||||
std::env::set_var("NETWORK_NAME", "prod-network");
|
||||
|
||||
// 创建包含多个实际字段的完整配置
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "production"
|
||||
ipv4 = "10.144.144.1"
|
||||
listeners = ["tcp://0.0.0.0:${LISTEN_PORT}"]
|
||||
|
||||
[network_identity]
|
||||
network_name = "${NETWORK_NAME}"
|
||||
network_secret = "${ET_SECRET}"
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://${PEER_HOST}:${PEER_PORT}"
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
|
||||
let (config, control) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证 network_identity 字段
|
||||
let identity = config.get_network_identity();
|
||||
assert_eq!(identity.network_name, "prod-network");
|
||||
assert_eq!(
|
||||
identity.network_secret.as_ref().unwrap(),
|
||||
"production-secret-key"
|
||||
);
|
||||
|
||||
// 验证 listeners 字段
|
||||
let listeners = config.get_listener_uris();
|
||||
assert_eq!(listeners.len(), 1);
|
||||
assert_eq!(listeners[0].to_string(), "tcp://0.0.0.0:11010");
|
||||
|
||||
// 验证 peer 字段
|
||||
let peers = config.get_peers();
|
||||
assert_eq!(peers.len(), 1);
|
||||
assert_eq!(peers[0].uri.to_string(), "tcp://peer.example.com:11011");
|
||||
|
||||
// 验证配置被正确标记
|
||||
assert!(control.is_read_only());
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理环境变量
|
||||
std::env::remove_var("ET_SECRET");
|
||||
std::env::remove_var("PEER_HOST");
|
||||
std::env::remove_var("PEER_PORT");
|
||||
std::env::remove_var("LISTEN_PORT");
|
||||
std::env::remove_var("NETWORK_NAME");
|
||||
}
|
||||
|
||||
/// 带默认值的环境变量
|
||||
///
|
||||
/// 验证:
|
||||
/// - ${VAR:-default} 语法在变量未定义时使用默认值
|
||||
#[tokio::test]
|
||||
async fn test_env_var_with_default_value() {
|
||||
// 确保变量未定义
|
||||
std::env::remove_var("UNDEFINED_PORT");
|
||||
std::env::remove_var("UNDEFINED_SECRET");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "default-test"
|
||||
listeners = ["tcp://0.0.0.0:${UNDEFINED_PORT:-11010}"]
|
||||
|
||||
[network_identity]
|
||||
network_name = "test"
|
||||
network_secret = "${UNDEFINED_SECRET:-default-secret}"
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
|
||||
let (config, _) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证使用了默认值
|
||||
assert_eq!(
|
||||
config
|
||||
.get_network_identity()
|
||||
.network_secret
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
"default-secret"
|
||||
);
|
||||
assert_eq!(
|
||||
config.get_listener_uris()[0].to_string(),
|
||||
"tcp://0.0.0.0:11010"
|
||||
);
|
||||
}
|
||||
|
||||
/// 环境变量未定义且无默认值的情况
|
||||
///
|
||||
/// 验证:
|
||||
/// - 未定义的环境变量保持原样(shellexpand 的默认行为)
|
||||
#[tokio::test]
|
||||
async fn test_undefined_env_var_without_default() {
|
||||
std::env::remove_var("COMPLETELY_UNDEFINED");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "undefined-test"
|
||||
|
||||
[network_identity]
|
||||
network_name = "test"
|
||||
network_secret = "${COMPLETELY_UNDEFINED}"
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
let (config, control) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证变量保持原样
|
||||
assert_eq!(
|
||||
config
|
||||
.get_network_identity()
|
||||
.network_secret
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
"${COMPLETELY_UNDEFINED}"
|
||||
);
|
||||
|
||||
// 注意:由于没有实际替换发生,控制标记不应因环境变量而设置
|
||||
// 但会因为其他原因(如没有 config_dir)被标记为 NO_DELETE
|
||||
assert!(control.is_no_delete());
|
||||
}
|
||||
|
||||
/// 布尔类型环境变量
|
||||
///
|
||||
/// 验证:
|
||||
/// - 布尔类型的环境变量能正确解析和反序列化
|
||||
/// - TOML 解析器能将字符串 "true"/"false" 转换为布尔值
|
||||
#[tokio::test]
|
||||
async fn test_boolean_type_env_vars() {
|
||||
// 设置布尔类型的环境变量
|
||||
std::env::set_var("ENABLE_DHCP", "true");
|
||||
std::env::set_var("ENABLE_ENCRYPTION", "false");
|
||||
std::env::set_var("ENABLE_IPV6", "true");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "bool-test"
|
||||
dhcp = ${ENABLE_DHCP}
|
||||
|
||||
[network_identity]
|
||||
network_name = "test"
|
||||
network_secret = "secret"
|
||||
|
||||
[flags]
|
||||
enable_encryption = ${ENABLE_ENCRYPTION}
|
||||
enable_ipv6 = ${ENABLE_IPV6}
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
let (config, control) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证布尔值被正确解析
|
||||
assert!(config.get_dhcp(), "dhcp should be true");
|
||||
|
||||
let flags = config.get_flags();
|
||||
assert!(
|
||||
!flags.enable_encryption,
|
||||
"enable_encryption should be false"
|
||||
);
|
||||
assert!(flags.enable_ipv6, "enable_ipv6 should be true");
|
||||
|
||||
// 验证使用环境变量的配置被标记为只读
|
||||
assert!(control.is_read_only());
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("ENABLE_DHCP");
|
||||
std::env::remove_var("ENABLE_ENCRYPTION");
|
||||
std::env::remove_var("ENABLE_IPV6");
|
||||
}
|
||||
|
||||
/// 数字类型环境变量
|
||||
///
|
||||
/// 验证:
|
||||
/// - 数字类型(整数、端口号)的环境变量能正确解析和反序列化
|
||||
/// - TOML 解析器能将字符串 "1380" 转换为整数
|
||||
#[tokio::test]
|
||||
async fn test_numeric_type_env_vars() {
|
||||
// 设置数字类型的环境变量
|
||||
std::env::set_var("MTU_VALUE", "1400");
|
||||
std::env::set_var("QUIC_PORT", "8080");
|
||||
std::env::set_var("THREAD_COUNT", "4");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "numeric-test"
|
||||
|
||||
[network_identity]
|
||||
network_name = "test"
|
||||
network_secret = "secret"
|
||||
|
||||
[flags]
|
||||
mtu = ${MTU_VALUE}
|
||||
quic_listen_port = ${QUIC_PORT}
|
||||
multi_thread_count = ${THREAD_COUNT}
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
let (config, control) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证数字值被正确解析
|
||||
let flags = config.get_flags();
|
||||
assert_eq!(flags.mtu, 1400, "mtu should be 1400");
|
||||
assert_eq!(
|
||||
flags.quic_listen_port, 8080,
|
||||
"quic_listen_port should be 8080"
|
||||
);
|
||||
assert_eq!(
|
||||
flags.multi_thread_count, 4,
|
||||
"multi_thread_count should be 4"
|
||||
);
|
||||
|
||||
// 验证使用环境变量的配置被标记为只读
|
||||
assert!(control.is_read_only());
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("MTU_VALUE");
|
||||
std::env::remove_var("QUIC_PORT");
|
||||
std::env::remove_var("THREAD_COUNT");
|
||||
}
|
||||
|
||||
/// 混合类型环境变量
|
||||
///
|
||||
/// 验证:
|
||||
/// - 字符串、布尔、数字类型的环境变量可以同时使用
|
||||
/// - 所有类型都能正确解析和反序列化
|
||||
/// - 模拟真实的复杂配置场景
|
||||
#[tokio::test]
|
||||
async fn test_mixed_type_env_vars() {
|
||||
// 设置不同类型的环境变量
|
||||
std::env::set_var("MIXED_SECRET", "mixed-secret-key");
|
||||
std::env::set_var("MIXED_NETWORK", "production");
|
||||
std::env::set_var("MIXED_DHCP", "true");
|
||||
std::env::set_var("MIXED_MTU", "1500");
|
||||
std::env::set_var("MIXED_ENCRYPTION", "false");
|
||||
std::env::set_var("MIXED_LISTEN_PORT", "12345");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
instance_name = "mixed-test"
|
||||
ipv4 = "10.0.0.1"
|
||||
dhcp = ${MIXED_DHCP}
|
||||
listeners = ["tcp://0.0.0.0:${MIXED_LISTEN_PORT}"]
|
||||
|
||||
[network_identity]
|
||||
network_name = "${MIXED_NETWORK}"
|
||||
network_secret = "${MIXED_SECRET}"
|
||||
|
||||
[flags]
|
||||
mtu = ${MIXED_MTU}
|
||||
enable_encryption = ${MIXED_ENCRYPTION}
|
||||
"#;
|
||||
temp_file.write_all(config_content.as_bytes()).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let config_path = PathBuf::from(temp_file.path());
|
||||
let (config, control) = load_config_from_file(&config_path, None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 验证字符串类型
|
||||
let identity = config.get_network_identity();
|
||||
assert_eq!(identity.network_name, "production");
|
||||
assert_eq!(
|
||||
identity.network_secret.as_ref().unwrap(),
|
||||
"mixed-secret-key"
|
||||
);
|
||||
|
||||
// 验证布尔类型
|
||||
assert!(config.get_dhcp());
|
||||
|
||||
let flags = config.get_flags();
|
||||
assert!(!flags.enable_encryption);
|
||||
|
||||
// 验证数字类型
|
||||
assert_eq!(flags.mtu, 1500);
|
||||
|
||||
// 验证 URL 中的端口号(数字)
|
||||
let listeners = config.get_listener_uris();
|
||||
assert_eq!(listeners.len(), 1);
|
||||
assert_eq!(listeners[0].to_string(), "tcp://0.0.0.0:12345");
|
||||
|
||||
// 验证配置被标记为只读
|
||||
assert!(control.is_read_only());
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("MIXED_SECRET");
|
||||
std::env::remove_var("MIXED_NETWORK");
|
||||
std::env::remove_var("MIXED_DHCP");
|
||||
std::env::remove_var("MIXED_MTU");
|
||||
std::env::remove_var("MIXED_ENCRYPTION");
|
||||
std::env::remove_var("MIXED_LISTEN_PORT");
|
||||
}
|
||||
}
|
||||
|
||||
247
easytier/src/common/env_parser.rs
Normal file
247
easytier/src/common/env_parser.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
//! 环境变量解析模块
|
||||
//!
|
||||
//! 提供配置文件中环境变量占位符的解析功能
|
||||
//! 支持 Shell 风格的语法:${VAR}、$VAR、${VAR:-default} 等
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// 解析字符串中的环境变量占位符
|
||||
///
|
||||
/// 支持的语法:
|
||||
/// - `${VAR_NAME}` - 标准格式(推荐)
|
||||
/// - `$VAR_NAME` - 简写格式
|
||||
/// - `${VAR_NAME:-default}` - 带默认值(bash 标准语法)
|
||||
///
|
||||
/// # 参数
|
||||
/// - `text`: 待解析的字符串
|
||||
///
|
||||
/// # 返回值
|
||||
/// - `String`: 替换后的字符串
|
||||
/// - `bool`: 是否检测到并替换了环境变量
|
||||
pub fn expand_env_vars(text: &str) -> (String, bool) {
|
||||
// 使用 shellexpand::env() 解析环境变量
|
||||
// 该函数仅处理环境变量,不处理 tilde (~) 扩展,适合配置文件场景
|
||||
match shellexpand::env(text) {
|
||||
Ok(expanded) => {
|
||||
// 通过比较原始字符串和扩展后的字符串判断是否发生了替换
|
||||
let changed = match &expanded {
|
||||
Cow::Borrowed(_) => false, // 未发生变化,仍是借用
|
||||
Cow::Owned(_) => true, // 发生了变化,产生了新字符串
|
||||
};
|
||||
|
||||
(expanded.into_owned(), changed)
|
||||
}
|
||||
Err(e) => {
|
||||
// 如果解析失败(例如变量引用语法错误),记录警告并返回原字符串
|
||||
tracing::warn!("Failed to expand environment variables in config: {}", e);
|
||||
(text.to_string(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_expand_standard_syntax() {
|
||||
std::env::set_var("TEST_VAR_STANDARD", "test_value");
|
||||
let (result, changed) = expand_env_vars("secret=${TEST_VAR_STANDARD}");
|
||||
assert_eq!(result, "secret=test_value");
|
||||
assert!(changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_short_syntax() {
|
||||
std::env::set_var("TEST_VAR_SHORT", "short_value");
|
||||
let (result, changed) = expand_env_vars("key=$TEST_VAR_SHORT");
|
||||
assert_eq!(result, "key=short_value");
|
||||
assert!(changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_with_default() {
|
||||
// 确保变量未定义
|
||||
std::env::remove_var("UNDEFINED_VAR_WITH_DEFAULT");
|
||||
let (result, changed) = expand_env_vars("port=${UNDEFINED_VAR_WITH_DEFAULT:-8080}");
|
||||
assert_eq!(result, "port=8080");
|
||||
assert!(changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_env_vars() {
|
||||
let (result, changed) = expand_env_vars("plain text without variables");
|
||||
assert_eq!(result, "plain text without variables");
|
||||
assert!(!changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_string() {
|
||||
let (result, changed) = expand_env_vars("");
|
||||
assert_eq!(result, "");
|
||||
assert!(!changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_vars() {
|
||||
std::env::set_var("VAR1", "value1");
|
||||
std::env::set_var("VAR2", "value2");
|
||||
let (result, changed) = expand_env_vars("${VAR1} and ${VAR2}");
|
||||
assert_eq!(result, "value1 and value2");
|
||||
assert!(changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undefined_var_without_default() {
|
||||
// 确保变量未定义
|
||||
std::env::remove_var("COMPLETELY_UNDEFINED_VAR");
|
||||
let (result, changed) = expand_env_vars("value=${COMPLETELY_UNDEFINED_VAR}");
|
||||
// shellexpand::env 对未定义的变量会保持原样
|
||||
assert_eq!(result, "value=${COMPLETELY_UNDEFINED_VAR}");
|
||||
assert!(!changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_toml_config() {
|
||||
std::env::set_var("ET_SECRET", "my-secret-key");
|
||||
std::env::set_var("ET_PORT", "11010");
|
||||
|
||||
let config = r#"
|
||||
[network_identity]
|
||||
network_name = "test-network"
|
||||
network_secret = "${ET_SECRET}"
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
"#;
|
||||
|
||||
let (result, changed) = expand_env_vars(config);
|
||||
assert!(changed);
|
||||
assert!(result.contains(r#"network_secret = "my-secret-key""#));
|
||||
assert!(result.contains(r#"uri = "tcp://127.0.0.1:11010""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_syntax_double_dollar() {
|
||||
std::env::set_var("ESCAPED_VAR", "should_not_expand");
|
||||
// shellexpand 使用 $$ 作为转义序列,表示字面量的单个 $
|
||||
// $$ 会被转义为单个 $,不会触发变量扩展
|
||||
let (result, changed) = expand_env_vars("value=$${ESCAPED_VAR}");
|
||||
assert_eq!(result, "value=${ESCAPED_VAR}");
|
||||
assert!(changed); // $$ -> $ 被视为一次变换
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_syntax_backslash() {
|
||||
std::env::set_var("ESCAPED_VAR", "should_not_expand");
|
||||
// shellexpand 中反斜杠转义的行为:\$ 会展开为 \<变量值>
|
||||
// 这不是推荐的转义方式,此测试仅为记录实际行为
|
||||
let (result, changed) = expand_env_vars(r"value=\${ESCAPED_VAR}");
|
||||
assert_eq!(result, r"value=\should_not_expand");
|
||||
assert!(changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_dollar_signs() {
|
||||
std::env::set_var("TEST_VAR", "value");
|
||||
// 测试多个连续的 $ 符号
|
||||
let (result1, changed1) = expand_env_vars("$$");
|
||||
assert_eq!(result1, "$");
|
||||
assert!(changed1);
|
||||
|
||||
let (result2, changed2) = expand_env_vars("$$$$");
|
||||
assert_eq!(result2, "$$");
|
||||
assert!(changed2);
|
||||
|
||||
// $$ 后跟变量扩展
|
||||
let (result3, changed3) = expand_env_vars("$$$TEST_VAR");
|
||||
assert_eq!(result3, "$value");
|
||||
assert!(changed3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_var_value() {
|
||||
std::env::set_var("EMPTY_VAR", "");
|
||||
let (result, changed) = expand_env_vars("value=${EMPTY_VAR}");
|
||||
// 变量存在但值为空
|
||||
assert_eq!(result, "value=");
|
||||
assert!(changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_with_special_chars() {
|
||||
std::env::remove_var("UNDEFINED_SPECIAL");
|
||||
// 测试默认值包含冒号、等号、空格等特殊字符
|
||||
let (result, changed) = expand_env_vars("url=${UNDEFINED_SPECIAL:-http://localhost:8080}");
|
||||
assert_eq!(result, "url=http://localhost:8080");
|
||||
assert!(changed);
|
||||
|
||||
let (result2, changed2) = expand_env_vars("key=${UNDEFINED_SPECIAL:-name=value}");
|
||||
assert_eq!(result2, "key=name=value");
|
||||
assert!(changed2);
|
||||
|
||||
let (result3, changed3) = expand_env_vars("msg=${UNDEFINED_SPECIAL:-hello world}");
|
||||
assert_eq!(result3, "msg=hello world");
|
||||
assert!(changed3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_var_name_with_numbers_underscores() {
|
||||
std::env::set_var("VAR_123", "num_value");
|
||||
std::env::set_var("_VAR", "underscore_prefix");
|
||||
std::env::set_var("VAR_", "underscore_suffix");
|
||||
|
||||
let (result1, changed1) = expand_env_vars("${VAR_123}");
|
||||
assert_eq!(result1, "num_value");
|
||||
assert!(changed1);
|
||||
|
||||
let (result2, changed2) = expand_env_vars("${_VAR}");
|
||||
assert_eq!(result2, "underscore_prefix");
|
||||
assert!(changed2);
|
||||
|
||||
let (result3, changed3) = expand_env_vars("${VAR_}");
|
||||
assert_eq!(result3, "underscore_suffix");
|
||||
assert!(changed3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_syntax() {
|
||||
// 测试无效语法的处理
|
||||
let (result1, changed1) = expand_env_vars("${}");
|
||||
// shellexpand 会保留无效语法原样
|
||||
assert_eq!(result1, "${}");
|
||||
assert!(!changed1);
|
||||
|
||||
// 注意:未闭合的 ${VAR 实际上 shellexpand 会当作普通文本处理
|
||||
// 它会尝试查找名为 "VAR" 的环境变量(到字符串末尾)
|
||||
std::env::remove_var("VAR");
|
||||
let (result2, _changed2) = expand_env_vars("incomplete ${VAR");
|
||||
// 如果 VAR 未定义,shellexpand 会返回错误或保持原样
|
||||
assert_eq!(result2, "incomplete ${VAR");
|
||||
// 注意:changed2 的值取决于 shellexpand 是否认为这是有效语法
|
||||
// 因此不对 changed2 做断言
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_defined_undefined_vars() {
|
||||
std::env::set_var("DEFINED_VAR", "defined");
|
||||
std::env::remove_var("UNDEFINED_VAR");
|
||||
|
||||
// 混合已定义和未定义的变量
|
||||
// shellexpand::env 在遇到未定义变量时会返回错误(默认行为)
|
||||
// 因此整个字符串会保持不变
|
||||
let (result, changed) = expand_env_vars("${DEFINED_VAR} and ${UNDEFINED_VAR}");
|
||||
assert_eq!(result, "${DEFINED_VAR} and ${UNDEFINED_VAR}");
|
||||
assert!(!changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_braces() {
|
||||
std::env::set_var("OUTER", "outer_value");
|
||||
// 嵌套的大括号是无效语法,shellexpand::env 会返回错误
|
||||
let (result, changed) = expand_env_vars("${OUTER} and ${{INNER}}");
|
||||
// 由于语法错误,整个字符串保持不变
|
||||
assert_eq!(result, "${OUTER} and ${{INNER}}");
|
||||
assert!(!changed);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ pub mod config;
|
||||
pub mod constants;
|
||||
pub mod defer;
|
||||
pub mod dns;
|
||||
pub mod env_parser;
|
||||
pub mod error;
|
||||
pub mod global_ctx;
|
||||
pub mod idn;
|
||||
|
||||
@@ -133,6 +133,9 @@ struct Cli {
|
||||
|
||||
#[clap(long, help = t!("core_clap.daemon").to_string())]
|
||||
daemon: bool,
|
||||
|
||||
#[clap(long, help = t!("core_clap.disable_env_parsing").to_string())]
|
||||
disable_env_parsing: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Default, PartialEq, Eq)]
|
||||
@@ -1209,8 +1212,12 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
}
|
||||
};
|
||||
for config_file in config_files {
|
||||
let (mut cfg, mut control) =
|
||||
load_config_from_file(&config_file, cli.config_dir.as_ref()).await?;
|
||||
let (mut cfg, mut control) = load_config_from_file(
|
||||
&config_file,
|
||||
cli.config_dir.as_ref(),
|
||||
cli.disable_env_parsing,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if cli.network_options.can_merge(&cfg, config_file_count) {
|
||||
cli.network_options
|
||||
|
||||
@@ -213,6 +213,21 @@ impl WebClientService for InstanceManageRpcService {
|
||||
.inst_id
|
||||
.ok_or_else(|| anyhow::anyhow!("instance id is required"))?
|
||||
.into();
|
||||
|
||||
let control = self
|
||||
.manager
|
||||
.get_instance_config_control(&inst_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("instance config control not found"))?;
|
||||
|
||||
if control.is_read_only() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Configuration for instance {} is read-only (uses environment variables) and cannot be retrieved via API. \
|
||||
Please access the configuration file directly on the file system.",
|
||||
inst_id
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let config = self
|
||||
.manager
|
||||
.get_instance_service(&inst_id)
|
||||
|
||||
Reference in New Issue
Block a user