fix: improve env compatibility and observability startup

This commit is contained in:
weisd
2026-03-15 23:38:54 +08:00
parent f2212a2650
commit e52d3fb5d7
10 changed files with 256 additions and 95 deletions

2
Cargo.lock generated
View File

@@ -7406,6 +7406,7 @@ dependencies = [
"starshard",
"subtle",
"sysinfo",
"temp-env",
"tempfile",
"thiserror 2.0.18",
"tikv-jemalloc-ctl",
@@ -8173,6 +8174,7 @@ dependencies = [
"siphasher",
"snap",
"sysinfo",
"temp-env",
"tempfile",
"thiserror 2.0.18",
"tokio",

View File

@@ -89,7 +89,14 @@ pub(super) fn init_local_logging(
let log_dir_str = config.log_directory.as_deref().filter(|s| !s.is_empty());
if let Some(log_directory) = log_dir_str {
init_file_logging_internal(config, log_directory, logger_level, is_production)
match init_file_logging_internal(config, log_directory, logger_level, is_production) {
Ok(guard) => Ok(guard),
Err(error) if should_fallback_to_stdout(&error) => {
emit_file_logging_fallback_warning(log_directory, &error);
Ok(init_stdout_only(config, logger_level, is_production))
}
Err(error) => Err(error),
}
} else {
Ok(init_stdout_only(config, logger_level, is_production))
}
@@ -316,6 +323,24 @@ pub fn ensure_dir_permissions(log_directory: &str) -> Result<(), TelemetryError>
}
}
pub(super) fn should_fallback_to_stdout(error: &TelemetryError) -> bool {
match error {
TelemetryError::SetPermissions(_) => true,
TelemetryError::Io(message) => {
let message = message.to_ascii_lowercase();
message.contains("permission denied") || message.contains("os error 13")
}
_ => false,
}
}
pub(super) fn emit_file_logging_fallback_warning(log_directory: &str, error: &TelemetryError) {
eprintln!(
"[WARN] Failed to initialize file observability logging at '{}': {}. Falling back to stdout logging.",
log_directory, error
);
}
// ─── Cleanup task ─────────────────────────────────────────────────────────────
/// Spawn a background task that periodically cleans up old log files.
@@ -435,4 +460,17 @@ mod tests {
assert!(result.is_err(), "invalid filename must return Err, not panic");
});
}
#[test]
fn test_permission_denied_errors_fall_back_to_stdout() {
assert!(should_fallback_to_stdout(&TelemetryError::Io(
"Permission denied (os error 13)".to_string()
)));
assert!(should_fallback_to_stdout(&TelemetryError::SetPermissions(
"dir='/logs', want=0o755, have=0o777, err=Permission denied (os error 13)".to_string()
)));
assert!(!should_fallback_to_stdout(&TelemetryError::Io(
"No such file or directory (os error 2)".to_string()
)));
}
}

View File

@@ -166,6 +166,7 @@ pub(super) fn init_observability_http(
let mut cleanup_handle = None;
let mut tracing_guard = None; // Guard for file writer
let mut stdout_guard = None; // Guard for stdout writer (File mode)
let mut force_stdout_logging = false;
// ── Case 1: OTLP Logging
if !log_ep.is_empty() {
@@ -189,43 +190,36 @@ pub(super) fn init_observability_http(
{
let log_filename = config.log_filename.as_deref().unwrap_or(&service_name);
let keep_files = config.log_keep_files.unwrap_or(DEFAULT_LOG_KEEP_FILES);
let file_logging_result = (|| -> Result<_, TelemetryError> {
fs::create_dir_all(log_directory).map_err(|e| TelemetryError::Io(e.to_string()))?;
// 1. Ensure dir exists
if let Err(e) = fs::create_dir_all(log_directory) {
return Err(TelemetryError::Io(e.to_string()));
}
// 2. Permissions
#[cfg(unix)]
crate::telemetry::local::ensure_dir_permissions(log_directory)?;
#[cfg(unix)]
crate::telemetry::local::ensure_dir_permissions(log_directory)?;
// 3. Rotation
let rotation_str = config
.log_rotation_time
.as_deref()
.unwrap_or(DEFAULT_LOG_ROTATION_TIME)
.to_lowercase();
let match_mode = match config.log_match_mode.as_deref().map(|s| s.to_lowercase()).as_deref() {
Some("prefix") => FileMatchMode::Prefix,
_ => FileMatchMode::Suffix,
};
let rotation = match rotation_str.as_str() {
"minutely" => Rotation::Minutely,
"hourly" => Rotation::Hourly,
"daily" => Rotation::Daily,
_ => Rotation::Daily,
};
let max_single_file_size = config
.log_max_single_file_size_bytes
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
let rotation_str = config
.log_rotation_time
.as_deref()
.unwrap_or(DEFAULT_LOG_ROTATION_TIME)
.to_lowercase();
let match_mode = match config.log_match_mode.as_deref().map(|s| s.to_lowercase()).as_deref() {
Some("prefix") => FileMatchMode::Prefix,
_ => FileMatchMode::Suffix,
};
let rotation = match rotation_str.as_str() {
"minutely" => Rotation::Minutely,
"hourly" => Rotation::Hourly,
"daily" => Rotation::Daily,
_ => Rotation::Daily,
};
let max_single_file_size = config
.log_max_single_file_size_bytes
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
let file_appender =
RollingAppender::new(log_directory, log_filename.to_string(), rotation, max_single_file_size, match_mode)?;
let file_appender =
RollingAppender::new(log_directory, log_filename.to_string(), rotation, max_single_file_size, match_mode)?;
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
tracing_guard = Some(guard);
file_layer_opt = Some(
tracing_subscriber::fmt::layer()
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let file_layer = tracing_subscriber::fmt::layer()
.with_timer(LocalTime::rfc_3339())
.with_target(true)
.with_ansi(false)
@@ -238,16 +232,29 @@ pub(super) fn init_observability_http(
.with_current_span(true)
.with_span_list(true)
.with_span_events(span_events.clone())
.with_filter(build_env_filter(logger_level, None)),
);
.with_filter(build_env_filter(logger_level, None));
let cleanup_handle = spawn_cleanup_task(config, log_directory, log_filename, keep_files);
// Cleanup task
cleanup_handle = Some(spawn_cleanup_task(config, log_directory, log_filename, keep_files));
Ok((file_layer, guard, cleanup_handle, rotation_str))
})();
info!(
"Init file logging at '{}', rotation: {}, keep {} files",
log_directory, rotation_str, keep_files
);
match file_logging_result {
Ok((file_layer, guard, new_cleanup_handle, rotation_str)) => {
tracing_guard = Some(guard);
file_layer_opt = Some(file_layer);
cleanup_handle = Some(new_cleanup_handle);
info!(
"Init file logging at '{}', rotation: {}, keep {} files",
log_directory, rotation_str, keep_files
);
}
Err(error) if crate::telemetry::local::should_fallback_to_stdout(&error) => {
crate::telemetry::local::emit_file_logging_fallback_warning(log_directory, &error);
force_stdout_logging = true;
}
Err(error) => return Err(error),
}
}
// ── Tracing subscriber registry ───────────────────────────────────────────
@@ -258,7 +265,7 @@ pub(super) fn init_observability_http(
// Optional stdout mirror (matching init_file_logging_internal logic)
// This is separate from OTLP stdout logic. If file logging is enabled, we honor its stdout rules.
if config.log_stdout_enabled.unwrap_or(DEFAULT_OBS_LOG_STDOUT_ENABLED) || !is_production {
if force_stdout_logging || config.log_stdout_enabled.unwrap_or(DEFAULT_OBS_LOG_STDOUT_ENABLED) || !is_production {
let (stdout_nb, stdout_g) = tracing_appender::non_blocking(std::io::stdout());
stdout_guard = Some(stdout_g);
stdout_layer_opt = Some(

View File

@@ -70,6 +70,7 @@ zstd = { workspace = true, optional = true }
tempfile = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
temp-env = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true, optional = true, features = ["Win32_Storage_FileSystem", "Win32_Foundation"] }

View File

@@ -31,7 +31,7 @@ use tracing::warn;
/// - `i8`: The parsed value as i8 if successful, otherwise the default value.
///
pub fn get_env_i8(key: &str, default: i8) -> i8 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -44,7 +44,7 @@ pub fn get_env_i8(key: &str, default: i8) -> i8 {
/// - `Option<i8>`: The parsed value as i8 if successful, otherwise None
///
pub fn get_env_opt_i8(key: &str) -> Option<i8> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -58,7 +58,7 @@ pub fn get_env_opt_i8(key: &str) -> Option<i8> {
/// - `u8`: The parsed value as u8 if successful, otherwise the default value.
///
pub fn get_env_u8(key: &str, default: u8) -> u8 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -71,7 +71,7 @@ pub fn get_env_u8(key: &str, default: u8) -> u8 {
/// - `Option<u8>`: The parsed value as u8 if successful, otherwise None
///
pub fn get_env_opt_u8(key: &str) -> Option<u8> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
static WARNED_ENV_MESSAGES: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
@@ -87,19 +87,38 @@ fn log_once(key: &str, message: impl FnOnce() -> String) {
}
}
fn external_alias_for_key(key: &str) -> Option<String> {
let suffix = key.strip_prefix("RUSTFS_")?;
if is_external_compatible_suffix(suffix) {
Some(format!("{}{}", external_env_prefix(), suffix))
} else {
None
}
}
fn resolve_env_with_aliases(key: &str, deprecated: &[&str]) -> Option<(String, String)> {
if let Ok(value) = env::var(key) {
return Some((key.to_string(), value));
}
let (alias, value) = deprecated
if let Some((alias, value)) = deprecated
.iter()
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))?;
.find_map(|alias| env::var(alias).ok().map(|value| (*alias, value)))
{
let deprecated_key = format!("env_alias:{alias}->{key}");
log_once(&deprecated_key, || {
format!("Environment variable {alias} is deprecated, use {key} instead")
});
return Some((alias.to_string(), value));
}
let alias = external_alias_for_key(key)?;
let value = env::var(&alias).ok()?;
let deprecated_key = format!("env_alias:{alias}->{key}");
log_once(&deprecated_key, || {
format!("Environment variable {alias} is deprecated, use {key} instead")
});
Some((alias.to_string(), value))
Some((alias, value))
}
const EXTERNAL_ENV_PREFIX_BYTES: [u8; 6] = [77, 73, 78, 73, 79, 95];
@@ -239,6 +258,13 @@ pub fn build_external_env_compat_report() -> ExternalEnvCompatReport {
build_external_env_compat_report_from_entries(env::vars())
}
fn parse_env_value<T>(key: &str) -> Option<T>
where
T: std::str::FromStr,
{
resolve_env_with_aliases(key, &[]).and_then(|(_, value)| value.parse().ok())
}
pub fn get_env_str_with_aliases(key: &str, deprecated: &[&str], default: &str) -> String {
resolve_env_with_aliases(key, deprecated).map_or_else(|| default.to_string(), |(_, value)| value)
}
@@ -272,7 +298,7 @@ pub fn get_env_bool_with_aliases(key: &str, deprecated: &[&str], default: bool)
/// - `i16`: The parsed value as i16 if successful, otherwise the default value.
///
pub fn get_env_i16(key: &str, default: i16) -> i16 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 16-bit type: signed i16
@@ -284,7 +310,7 @@ pub fn get_env_i16(key: &str, default: i16) -> i16 {
/// - `Option<i16>`: The parsed value as i16 if successful, otherwise None
///
pub fn get_env_opt_i16(key: &str) -> Option<i16> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -298,7 +324,7 @@ pub fn get_env_opt_i16(key: &str) -> Option<i16> {
/// - `u16`: The parsed value as u16 if successful, otherwise the default value.
///
pub fn get_env_u16(key: &str, default: u16) -> u16 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 16-bit type: unsigned u16
@@ -310,7 +336,7 @@ pub fn get_env_u16(key: &str, default: u16) -> u16 {
/// - `Option<u16>`: The parsed value as u16 if successful, otherwise None
///
pub fn get_env_u16_opt(key: &str) -> Option<u16> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 16-bit type: unsigned u16
@@ -335,7 +361,7 @@ pub fn get_env_opt_u16(key: &str) -> Option<u16> {
/// - `i32`: The parsed value as i32 if successful, otherwise the default value.
///
pub fn get_env_i32(key: &str, default: i32) -> i32 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 32-bit type: signed i32
@@ -347,7 +373,7 @@ pub fn get_env_i32(key: &str, default: i32) -> i32 {
/// - `Option<i32>`: The parsed value as i32 if successful, otherwise None
///
pub fn get_env_opt_i32(key: &str) -> Option<i32> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -361,7 +387,7 @@ pub fn get_env_opt_i32(key: &str) -> Option<i32> {
/// - `u32`: The parsed value as u32 if successful, otherwise the default value.
///
pub fn get_env_u32(key: &str, default: u32) -> u32 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
/// 32-bit type: unsigned u32
@@ -373,7 +399,7 @@ pub fn get_env_u32(key: &str, default: u32) -> u32 {
/// - `Option<u32>`: The parsed value as u32 if successful, otherwise None
///
pub fn get_env_opt_u32(key: &str) -> Option<u32> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
///
@@ -385,7 +411,7 @@ pub fn get_env_opt_u32(key: &str) -> Option<u32> {
/// - `f32`: The parsed value as f32 if successful, otherwise the default value
///
pub fn get_env_f32(key: &str, default: f32) -> f32 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
///
@@ -396,7 +422,7 @@ pub fn get_env_f32(key: &str, default: f32) -> f32 {
/// - `Option<f32>`: The parsed value as f32 if successful, otherwise None
///
pub fn get_env_opt_f32(key: &str) -> Option<f32> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -409,7 +435,7 @@ pub fn get_env_opt_f32(key: &str) -> Option<f32> {
/// - `i64`: The parsed value as i64 if successful, otherwise the default value
///
pub fn get_env_i64(key: &str, default: i64) -> i64 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
///
@@ -420,7 +446,7 @@ pub fn get_env_i64(key: &str, default: i64) -> i64 {
/// - `Option<i64>`: The parsed value as i64 if successful, otherwise None
///
pub fn get_env_opt_i64(key: &str) -> Option<i64> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, returning Option<Option<i64>> if not set or parsing fails.
@@ -432,7 +458,7 @@ pub fn get_env_opt_i64(key: &str) -> Option<i64> {
/// - `Option<Option<i64>>`: The parsed value as i64 if successful, otherwise None
///
pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
env::var(key).ok().map(|v| v.parse().ok())
resolve_env_with_aliases(key, &[]).map(|(_, value)| value.parse().ok())
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -445,7 +471,7 @@ pub fn get_env_opt_opt_i64(key: &str) -> Option<Option<i64>> {
/// - `u64`: The parsed value as u64 if successful, otherwise the default value.
///
pub fn get_env_u64(key: &str, default: u64) -> u64 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as an unsigned 64-bit integer, returning `None` if not set or parsing fails.
@@ -479,7 +505,7 @@ pub fn get_env_opt_u64_with_aliases(key: &str, deprecated: &[&str]) -> Option<u6
/// - `Option<u64>`: The parsed value as u64 if successful, otherwise None
///
pub fn get_env_opt_u64(key: &str) -> Option<u64> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -492,7 +518,7 @@ pub fn get_env_opt_u64(key: &str) -> Option<u64> {
/// - `f64`: The parsed value as f64 if successful, otherwise the default value.
///
pub fn get_env_f64(key: &str, default: f64) -> f64 {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -504,7 +530,7 @@ pub fn get_env_f64(key: &str, default: f64) -> f64 {
/// - `Option<f64>`: The parsed value as f64 if successful, otherwise None
///
pub fn get_env_opt_f64(key: &str) -> Option<f64> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, with a default value if not set or parsing fails.
@@ -517,7 +543,7 @@ pub fn get_env_opt_f64(key: &str) -> Option<f64> {
/// - `usize`: The parsed value as usize if successful, otherwise the default value.
///
pub fn get_env_usize(key: &str, default: usize) -> usize {
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
parse_env_value(key).unwrap_or(default)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
///
@@ -528,7 +554,7 @@ pub fn get_env_usize(key: &str, default: usize) -> usize {
/// - `Option<usize>`: The parsed value as usize if successful, otherwise None
///
pub fn get_env_usize_opt(key: &str) -> Option<usize> {
env::var(key).ok().and_then(|v| v.parse().ok())
parse_env_value(key)
}
/// Retrieve an environment variable as a specific type, returning None if not set or parsing fails.
@@ -565,7 +591,7 @@ pub fn get_env_str(key: &str, default: &str) -> String {
/// - `Option<String>`: The environment variable value if set, otherwise None.
///
pub fn get_env_opt_str(key: &str) -> Option<String> {
env::var(key).ok()
resolve_env_with_aliases(key, &[]).map(|(_, value)| value)
}
/// Retrieve an environment variable as a boolean, with a default value if not set or parsing fails.
@@ -615,9 +641,27 @@ pub fn get_env_opt_bool(key: &str) -> Option<bool> {
})
}
/// Copy supported external-prefix variables such as `MINIO_*` into their
/// canonical `RUSTFS_*` names in the current process when the canonical key is
/// missing.
#[allow(unsafe_code)]
pub fn apply_external_env_compat() -> ExternalEnvCompatReport {
let report = build_external_env_compat_report();
for (source_key, rustfs_key) in &report.mapped_pairs {
if let Ok(value) = env::var(source_key) {
// Safety: this helper is intended for early startup bootstrap
// before any background threads are created.
unsafe {
env::set_var(rustfs_key, value);
}
}
}
report
}
#[cfg(test)]
mod tests {
use super::build_external_env_compat_report_from_entries;
use super::{apply_external_env_compat, build_external_env_compat_report_from_entries, get_env_str};
fn source_key(suffix: &str) -> String {
let mut key = super::external_env_prefix().to_string();
@@ -672,4 +716,29 @@ mod tests {
assert_eq!(report.mapped_count(), 0);
assert_eq!(report.conflict_count(), 0);
}
#[test]
fn minio_alias_is_used_for_rustfs_reads() {
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
assert_eq!(get_env_str("RUSTFS_ROOT_USER", "default-user"), "compat-admin");
});
});
}
#[test]
fn apply_external_env_compat_copies_missing_rustfs_keys() {
temp_env::with_var("MINIO_ROOT_USER", Some("compat-admin"), || {
temp_env::with_var_unset("RUSTFS_ROOT_USER", || {
let report = apply_external_env_compat();
assert!(
report
.mapped_pairs
.iter()
.any(|(source_key, rustfs_key)| source_key == "MINIO_ROOT_USER" && rustfs_key == "RUSTFS_ROOT_USER")
);
assert_eq!(std::env::var("RUSTFS_ROOT_USER").as_deref(), Ok("compat-admin"));
});
});
}
}

View File

@@ -167,6 +167,7 @@ aws-sdk-s3 = { workspace = true }
aws-config = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true, features = ["test-util"] }
temp-env = { workspace = true }
[build-dependencies]
http.workspace = true

View File

@@ -121,6 +121,26 @@ mod tests {
assert_eq!(console_port, 9001);
}
#[test]
#[serial]
fn test_minio_prefixed_envs_are_accepted_by_parser() {
temp_env::with_vars(
[
("MINIO_VOLUMES", Some("/compat/vol1")),
("MINIO_ADDRESS", Some(":9100")),
("RUSTFS_VOLUMES", None),
("RUSTFS_ADDRESS", None),
],
|| {
let opt = Opt::parse_from(["rustfs"]);
assert_eq!(opt.volumes, vec!["/compat/vol1"]);
assert_eq!(opt.address, ":9100");
assert_eq!(std::env::var("RUSTFS_VOLUMES").as_deref(), Ok("/compat/vol1"));
assert_eq!(std::env::var("RUSTFS_ADDRESS").as_deref(), Ok(":9100"));
},
);
}
#[test]
#[serial]
fn test_volumes_and_disk_layout_parsing() {

View File

@@ -16,6 +16,7 @@ use clap::builder::NonEmptyStringValueParser;
use clap::{Args, Parser, Subcommand};
use const_str::concat;
use rustfs_config::RUSTFS_REGION;
use rustfs_utils::apply_external_env_compat;
use std::path::PathBuf;
use std::string::ToString;
@@ -262,6 +263,7 @@ impl Opt {
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let _ = apply_external_env_compat();
let args: Vec<String> = args.into_iter().map(|a| a.into().to_string_lossy().into_owned()).collect();
let args = preprocess_args_for_legacy(args);
let cli = Cli::parse_from(args);
@@ -276,6 +278,7 @@ impl Opt {
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let _ = apply_external_env_compat();
let args: Vec<String> = args.into_iter().map(|a| a.into().to_string_lossy().into_owned()).collect();
let args = preprocess_args_for_legacy(args);
let cli = Cli::try_parse_from(args)?;

View File

@@ -70,10 +70,11 @@ use rustfs_iam::{init_iam_sys, init_oidc_sys};
use rustfs_metrics::init_metrics_system;
use rustfs_obs::{init_obs, set_global_guard};
use rustfs_scanner::init_data_scanner;
use rustfs_utils::{build_external_env_compat_report, get_env_bool_with_aliases, net::parse_and_resolve_address};
use rustfs_utils::{
ExternalEnvCompatReport, apply_external_env_compat, get_env_bool_with_aliases, net::parse_and_resolve_address,
};
use rustls::crypto::aws_lc_rs::default_provider;
use std::io::{Error, Result};
use std::process::Command;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, instrument, warn};
@@ -82,7 +83,6 @@ const ENV_SCANNER_ENABLED: &str = "RUSTFS_SCANNER_ENABLED";
const ENV_SCANNER_ENABLED_DEPRECATED: &str = "RUSTFS_ENABLE_SCANNER";
const ENV_HEAL_ENABLED: &str = "RUSTFS_HEAL_ENABLED";
const ENV_HEAL_ENABLED_DEPRECATED: &str = "RUSTFS_ENABLE_HEAL";
const ENV_EXTERNAL_COMPAT_BOOTSTRAPPED: &str = "_RUSTFS_EXTERNAL_PREFIX_COMPAT_BOOTSTRAPPED";
#[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))]
#[global_allocator]
@@ -117,11 +117,7 @@ fn main() {
}
fn bootstrap_external_prefix_compat() -> Result<()> {
if std::env::var_os(ENV_EXTERNAL_COMPAT_BOOTSTRAPPED).is_some() {
return Ok(());
}
let env_compat_report = build_external_env_compat_report();
let env_compat_report = apply_external_env_compat();
if env_compat_report.conflict_count() > 0 {
// RUSTFS_* is the canonical namespace in this codebase, so on key conflicts we keep RUSTFS_*
// to preserve explicit user/operator overrides and avoid changing existing runtime behavior.
@@ -137,23 +133,21 @@ fn bootstrap_external_prefix_compat() -> Result<()> {
}
eprintln!(
"[INFO] Applying external-prefix compatibility for {} variable(s) and re-executing process.",
env_compat_report.mapped_count()
"[INFO] Applying external-prefix compatibility in-process for {} variable(s): {}",
env_compat_report.mapped_count(),
format_external_prefix_mappings(&env_compat_report)
);
// Re-exec ensures the child process starts with RUSTFS_* already present in its environment,
// so all subsequent config/env reads (including early startup paths) observe consistent keys.
let mut cmd = Command::new(std::env::current_exe()?);
cmd.args(std::env::args_os().skip(1));
cmd.env(ENV_EXTERNAL_COMPAT_BOOTSTRAPPED, "1");
for (source_key, rustfs_key) in env_compat_report.mapped_pairs {
if let Some(value) = std::env::var_os(source_key.as_str()) {
cmd.env(rustfs_key, value);
}
}
Ok(())
}
let status = cmd.status()?;
std::process::exit(status.code().unwrap_or(1));
fn format_external_prefix_mappings(report: &ExternalEnvCompatReport) -> String {
report
.mapped_pairs
.iter()
.map(|(source_key, rustfs_key)| format!("{source_key}->{rustfs_key}"))
.collect::<Vec<_>>()
.join(", ")
}
async fn async_main() -> Result<()> {
@@ -221,6 +215,32 @@ async fn async_main() -> Result<()> {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_external_prefix_mappings_lists_mapped_pairs() {
let report = ExternalEnvCompatReport {
mapped_pairs: vec![
("MINIO_ROOT_USER".to_string(), "RUSTFS_ROOT_USER".to_string()),
(
"MINIO_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(),
"RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(),
),
],
conflict_keys: Vec::new(),
};
let formatted = format_external_prefix_mappings(&report);
assert_eq!(
formatted,
"MINIO_ROOT_USER->RUSTFS_ROOT_USER, MINIO_NOTIFY_WEBHOOK_ENABLE_PRIMARY->RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY"
);
}
}
#[instrument(skip(config))]
async fn run(config: config::Config) -> Result<()> {
debug!("config: {:?}", &config);

View File

@@ -5,7 +5,7 @@ RUSTFS_VOLUMES="http://node{1...4}:7000/data/rustfs{0...3} http://node{5...8
RUSTFS_ADDRESS=":7000"
RUSTFS_CONSOLE_ENABLE=true
RUST_LOG=warn
RUSTFS_OBS_LOG_DIRECTORY="/var/logs/rustfs/"
RUSTFS_OBS_LOG_DIRECTORY="./deploy/logs"
RUSTFS_NS_SCANNER_INTERVAL=60
#RUSTFS_SKIP_BACKGROUND_TASK=true
RUSTFS_COMPRESSION_ENABLED=true
RUSTFS_COMPRESSION_ENABLED=true