mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-03 10:48:13 +00:00
fix(notify): unify runtime lifecycle coordination (#5088)
* fix(notify): unify runtime lifecycle coordination * fix(notify): repair lifecycle convergence checks * fix(admin): expose effective notify state (#5097)
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::common::{is_target_enabled, split_env_field_and_instance};
|
||||
use crate::TargetError;
|
||||
use rustfs_config::server_config::{Config, KVS};
|
||||
use rustfs_config::{DEFAULT_DELIMITER, ENV_PREFIX};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -27,6 +28,15 @@ pub fn collect_target_configs(
|
||||
collect_target_configs_from_env(config, route_prefix, target_type, valid_fields, std::env::vars())
|
||||
}
|
||||
|
||||
pub fn try_collect_target_configs(
|
||||
config: &Config,
|
||||
route_prefix: &str,
|
||||
target_type: &str,
|
||||
valid_fields: &HashSet<String>,
|
||||
) -> Result<Vec<(String, KVS)>, TargetError> {
|
||||
try_collect_target_configs_from_env(config, route_prefix, target_type, valid_fields, std::env::vars())
|
||||
}
|
||||
|
||||
fn is_sensitive_target_field(field_name: &str) -> bool {
|
||||
let field_name = field_name.to_ascii_lowercase();
|
||||
field_name.contains("password")
|
||||
@@ -123,7 +133,7 @@ pub fn collect_target_configs_from_env<I>(
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
collect_merged_target_configs_from_env(
|
||||
collect_merged_target_configs_compat_from_env(
|
||||
config,
|
||||
&format!("{route_prefix}{target_type}").to_lowercase(),
|
||||
route_prefix,
|
||||
@@ -137,6 +147,30 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn try_collect_target_configs_from_env<I>(
|
||||
config: &Config,
|
||||
route_prefix: &str,
|
||||
target_type: &str,
|
||||
valid_fields: &HashSet<String>,
|
||||
env_vars: I,
|
||||
) -> Result<Vec<(String, KVS)>, TargetError>
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
Ok(collect_merged_target_configs_from_env(
|
||||
config,
|
||||
&format!("{route_prefix}{target_type}").to_lowercase(),
|
||||
route_prefix,
|
||||
target_type,
|
||||
valid_fields,
|
||||
env_vars,
|
||||
)?
|
||||
.into_iter()
|
||||
.filter(|record| record.enabled)
|
||||
.map(|record| (record.instance_id, record.effective_config))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn collect_merged_target_configs_from_env<I>(
|
||||
config: &Config,
|
||||
section_name: &str,
|
||||
@@ -144,7 +178,52 @@ pub(crate) fn collect_merged_target_configs_from_env<I>(
|
||||
target_type: &str,
|
||||
valid_fields: &HashSet<String>,
|
||||
env_vars: I,
|
||||
) -> Result<Vec<MergedTargetConfigRecord>, TargetError>
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
collect_merged_target_config_results_from_env(config, section_name, route_prefix, target_type, valid_fields, env_vars)
|
||||
.into_iter()
|
||||
.map(|result| result.map_err(|(_, err)| err))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn collect_merged_target_configs_compat_from_env<I>(
|
||||
config: &Config,
|
||||
section_name: &str,
|
||||
route_prefix: &str,
|
||||
target_type: &str,
|
||||
valid_fields: &HashSet<String>,
|
||||
env_vars: I,
|
||||
) -> Vec<MergedTargetConfigRecord>
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
collect_merged_target_config_results_from_env(config, section_name, route_prefix, target_type, valid_fields, env_vars)
|
||||
.into_iter()
|
||||
.map(|result| match result {
|
||||
Ok(record) => record,
|
||||
Err((record, err)) => {
|
||||
warn!(
|
||||
target_type,
|
||||
instance_id = %record.instance_id,
|
||||
error = %err,
|
||||
"Treating target instance with invalid enable configuration as disabled"
|
||||
);
|
||||
record
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_merged_target_config_results_from_env<I>(
|
||||
config: &Config,
|
||||
section_name: &str,
|
||||
route_prefix: &str,
|
||||
target_type: &str,
|
||||
valid_fields: &HashSet<String>,
|
||||
env_vars: I,
|
||||
) -> Vec<Result<MergedTargetConfigRecord, (MergedTargetConfigRecord, TargetError)>>
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
@@ -220,14 +299,28 @@ where
|
||||
let redacted_config = redacted_target_config(&merged_config);
|
||||
debug!(instance_id = %id, ?redacted_config, "Merged target configuration");
|
||||
}
|
||||
merged_configs.push(MergedTargetConfigRecord {
|
||||
instance_id: id,
|
||||
enabled: is_target_enabled(&merged_config),
|
||||
effective_config: merged_config,
|
||||
has_file_default,
|
||||
has_file_instance,
|
||||
has_env_default,
|
||||
has_env_instance,
|
||||
merged_configs.push(match is_target_enabled(&merged_config) {
|
||||
Ok(enabled) => Ok(MergedTargetConfigRecord {
|
||||
instance_id: id,
|
||||
enabled,
|
||||
effective_config: merged_config,
|
||||
has_file_default,
|
||||
has_file_instance,
|
||||
has_env_default,
|
||||
has_env_instance,
|
||||
}),
|
||||
Err(err) => Err((
|
||||
MergedTargetConfigRecord {
|
||||
instance_id: id,
|
||||
enabled: false,
|
||||
effective_config: merged_config,
|
||||
has_file_default,
|
||||
has_file_instance,
|
||||
has_env_default,
|
||||
has_env_instance,
|
||||
},
|
||||
err,
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,8 +331,9 @@ where
|
||||
mod tests {
|
||||
use super::{
|
||||
collect_env_target_instance_ids_from_env, collect_target_configs_from_env, redact_target_field_value,
|
||||
redacted_target_config,
|
||||
redacted_target_config, try_collect_target_configs_from_env,
|
||||
};
|
||||
use crate::TargetError;
|
||||
use rustfs_config::notify::{
|
||||
ENV_NOTIFY_REDIS_ENABLE, ENV_NOTIFY_REDIS_RECONNECT_RETRY_ATTEMPTS, ENV_NOTIFY_REDIS_TLS_ALLOW_INSECURE,
|
||||
ENV_NOTIFY_REDIS_URL, NOTIFY_REDIS_KEYS, NOTIFY_ROUTE_PREFIX,
|
||||
@@ -269,7 +363,7 @@ mod tests {
|
||||
|
||||
cfg.0.insert("notify_webhook".to_string(), subsystem);
|
||||
|
||||
let configs = collect_target_configs_from_env(
|
||||
let configs = try_collect_target_configs_from_env(
|
||||
&cfg,
|
||||
NOTIFY_ROUTE_PREFIX,
|
||||
"webhook",
|
||||
@@ -282,7 +376,8 @@ mod tests {
|
||||
("RUSTFS_NOTIFY_WEBHOOK_ENABLE".to_string(), "on".to_string()),
|
||||
("RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT".to_string(), "42".to_string()),
|
||||
],
|
||||
);
|
||||
)
|
||||
.expect("valid env target");
|
||||
|
||||
let configs: HashMap<String, KVS> = configs.into_iter().collect();
|
||||
assert_eq!(configs.len(), 2);
|
||||
@@ -295,7 +390,7 @@ mod tests {
|
||||
#[test]
|
||||
fn collect_target_configs_discovers_enabled_instance_from_env() {
|
||||
let cfg = Config(HashMap::new());
|
||||
let configs = collect_target_configs_from_env(
|
||||
let configs = try_collect_target_configs_from_env(
|
||||
&cfg,
|
||||
NOTIFY_ROUTE_PREFIX,
|
||||
"webhook",
|
||||
@@ -307,7 +402,8 @@ mod tests {
|
||||
"https://example.com/from-env".to_string(),
|
||||
),
|
||||
],
|
||||
);
|
||||
)
|
||||
.expect("valid target configs");
|
||||
|
||||
assert_eq!(configs.len(), 1);
|
||||
assert_eq!(configs[0].0, "primary");
|
||||
@@ -323,7 +419,7 @@ mod tests {
|
||||
subsystem.insert("_".to_string(), default_kvs);
|
||||
cfg.0.insert("notify_webhook".to_string(), subsystem);
|
||||
|
||||
let configs = collect_target_configs_from_env(
|
||||
let configs = try_collect_target_configs_from_env(
|
||||
&cfg,
|
||||
NOTIFY_ROUTE_PREFIX,
|
||||
"webhook",
|
||||
@@ -332,7 +428,8 @@ mod tests {
|
||||
"RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_SECONDARY".to_string(),
|
||||
"https://example.com/secondary".to_string(),
|
||||
)],
|
||||
);
|
||||
)
|
||||
.expect("valid target configs");
|
||||
|
||||
assert!(configs.is_empty());
|
||||
}
|
||||
@@ -361,7 +458,7 @@ mod tests {
|
||||
let cfg = Config(HashMap::new());
|
||||
let valid_fields = NOTIFY_REDIS_KEYS.iter().map(|key| (*key).to_string()).collect();
|
||||
|
||||
let configs = collect_target_configs_from_env(
|
||||
let configs = try_collect_target_configs_from_env(
|
||||
&cfg,
|
||||
NOTIFY_ROUTE_PREFIX,
|
||||
"redis",
|
||||
@@ -372,7 +469,8 @@ mod tests {
|
||||
(format!("{ENV_NOTIFY_REDIS_RECONNECT_RETRY_ATTEMPTS}_PRIMARY"), "9".to_string()),
|
||||
(format!("{ENV_NOTIFY_REDIS_TLS_ALLOW_INSECURE}_PRIMARY"), "off".to_string()),
|
||||
],
|
||||
);
|
||||
)
|
||||
.expect("valid redis target config");
|
||||
|
||||
let configs: HashMap<String, KVS> = configs.into_iter().collect();
|
||||
let redis_config = configs.get("primary").expect("redis env target should be discovered");
|
||||
@@ -383,6 +481,57 @@ mod tests {
|
||||
assert_eq!(redis_config.lookup(REDIS_TLS_ALLOW_INSECURE).as_deref(), Some("off"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_target_configs_rejects_invalid_instance_enable_value() {
|
||||
let err = try_collect_target_configs_from_env(
|
||||
&Config(HashMap::new()),
|
||||
NOTIFY_ROUTE_PREFIX,
|
||||
"webhook",
|
||||
&HashSet::from([ENABLE_KEY.to_string(), WEBHOOK_ENDPOINT.to_string()]),
|
||||
vec![("RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(), "invalid".to_string())],
|
||||
)
|
||||
.expect_err("invalid enable value must not look like a disabled target");
|
||||
|
||||
match err {
|
||||
TargetError::Configuration(detail) => assert_eq!(detail, "Invalid enable value 'invalid'"),
|
||||
other => panic!("expected a configuration error, got {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_collection_keeps_valid_instances_when_one_enable_is_invalid() {
|
||||
let configs = collect_target_configs_from_env(
|
||||
&Config(HashMap::new()),
|
||||
NOTIFY_ROUTE_PREFIX,
|
||||
"webhook",
|
||||
&HashSet::from([ENABLE_KEY.to_string(), WEBHOOK_ENDPOINT.to_string()]),
|
||||
vec![
|
||||
("RUSTFS_NOTIFY_WEBHOOK_ENABLE_GOOD".to_string(), "on".to_string()),
|
||||
("RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_GOOD".to_string(), "https://example.com/good".to_string()),
|
||||
("RUSTFS_NOTIFY_WEBHOOK_ENABLE_BAD".to_string(), "invalid".to_string()),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(configs.len(), 1);
|
||||
assert_eq!(configs[0].0, "good");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_target_configs_preserves_whitespace_padded_legacy_value() {
|
||||
let configs = try_collect_target_configs_from_env(
|
||||
&Config(HashMap::new()),
|
||||
NOTIFY_ROUTE_PREFIX,
|
||||
"webhook",
|
||||
&HashSet::from([ENABLE_KEY.to_string(), WEBHOOK_ENDPOINT.to_string()]),
|
||||
vec![("RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY".to_string(), " on ".to_string())],
|
||||
)
|
||||
.expect("the shared enable parser accepts surrounding whitespace");
|
||||
|
||||
assert_eq!(configs.len(), 1);
|
||||
assert_eq!(configs[0].0, "primary");
|
||||
assert_eq!(configs[0].1.lookup(ENABLE_KEY).as_deref(), Some(" on "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redact_target_field_value_redacts_sensitive_fields() {
|
||||
assert_eq!(redact_target_field_value("password", "secret"), "***redacted***");
|
||||
|
||||
Reference in New Issue
Block a user