Files
rustfs/crates/audit/src/registry.rs
T
Zhengchao An 8ddbf05924 test(targets,notify,audit): share a builder MockTarget testkit (#6717)
* test(targets): ship a builder MockTarget testkit and retire the in-crate Target mocks

Adds crates/targets/src/testkit.rs with a builder-style MockTarget implementing Target<E> for every E: PluginEvent, with orthogonal off-by-default knobs: disabled/active override, health delay plus health-started signal plus a drop-guard counter proving a cancelled probe future was dropped, an init failure budget (usize::MAX = always fail) plus blocking init plus an init counter, a close counter/signal/semaphore gate with a runtime block toggle, a save counter plus save failure budget, caller-supplied store and failed-store handles, and a shared final-failure counter. Clones and clone_dyn share all counters, so an observer clone keeps watching a target after it is boxed into a runtime.

The module is gated as #[cfg(any(test, feature = "test-support"))]: in-crate unit tests get it via cfg(test), and downstream test suites opt in through the new off-by-default test-support cargo feature (test-support = [], activating no dependencies).

Migrates the five in-crate duplicate mocks onto it: the plugin.rs registry-factory TestTarget, the runtime/adapter.rs lifecycle TestTarget (init/close/store knobs), the runtime/mod.rs TestTarget plus HealthDropGuard (close gating and health-probe tests), the target/mod.rs MoveTestTarget (folded into the test_support helper constructors used by the NATS JetStream failed-store tests), and the target/mod.rs StoreBackedTarget (the default send_from_store purge test; the mock deliberately does not override send_from_store or handle_terminal_failure). The forced init failure now uses TargetError::Initialization instead of the old adapter mock's Configuration; the adapter derives its redacted failure summary from the target id alone, so the migrated assertions are unchanged.

Leak guard: testkit unit tests assert the crate manifest still declares default = [] and that test-support = [] stays a pure cfg gate, complementing the compile-level cfg gate that keeps the mock out of production builds.

Part of rustfs/backlog#1846 (cluster 3, step 1).

* test(notify,audit): migrate the Target mocks onto the shared testkit

Retires the hand-written Target mocks in crates/notify and crates/audit in favor of rustfs_targets::testkit::MockTarget: notify's notifier.rs TestTarget/DeferredTestTarget/ClosableTestTarget, lifecycle.rs BlockingInitTarget/RetryInitTarget (rebuilt as observed MockTarget templates cloned by their plugin-descriptor factories, sharing the init signal, close counter, and single-failure init budget across generations), runtime_view.rs TestTarget, runtime_facade.rs TestTarget, and audit's pipeline.rs MockTarget, system.rs TestTarget, registry.rs CloseTestTarget, plus TestTarget and FailingTarget in audit/tests/pipeline_layer_test.rs. Removing the two integration-test mocks also removes their respelled PluginEvent bound (E: Send + Sync + 'static + Clone + Serialize + DeserializeOwned), which the plugin-contract rules require to be spelled only via PluginEvent.

lifecycle.rs ReplayTarget stays bespoke on purpose: its generation tags, mpsc observation channels, gated send_raw delivery, and ObservedQueueStore model the replay pipeline itself and would contort a general-purpose mock. The other bespoke mocks named out of scope in PR-3a (ProgrammedTarget, ClassifyingTarget, the ReloadableTargetTls fakes) are likewise untouched.

New testkit knobs, each defaulted off and unit-tested: with_id (rename a clone while keeping the shared counters, for factory templates), with_first_save_gate (the first save notifies entered and waits on release; several mocks may share one pair), with_health_gate (is_active waits on a release handle after notifying health_started), with_delivery_snapshot (fixed snapshot overriding the store-derived default), with_close_failures (close-failure budget, default TargetError::Storage) with with_close_failure_error to shape the variant (audit's registry test pins TargetError::Unknown), and an always-on is_enabled call counter exposed as enabled_call_count (notifier's generation tests count dispatcher selections through it).

Both crates enable the testkit through a dev-dependency on rustfs-targets with the test-support feature; the feature stays out of default and activates no dependencies, so production builds are unchanged.

Part of rustfs/backlog#1846 (cluster 3, step 2).
2026-08-27 11:28:16 +08:00

322 lines
12 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::{AuditEntry, AuditError, AuditResult, factory::builtin_target_plugins};
use rustfs_config::audit::AUDIT_ROUTE_PREFIX;
use rustfs_config::server_config::{Config, KVS};
use rustfs_targets::arn::TargetID;
use rustfs_targets::{SharedTarget, Target, TargetError, TargetPluginRegistry, TargetRuntimeManager};
use tracing::info;
const LOG_COMPONENT_AUDIT: &str = "audit";
const LOG_SUBSYSTEM_REGISTRY: &str = "registry";
const EVENT_AUDIT_TARGET_REGISTRY_KEY_CREATED: &str = "audit_target_registry_key_created";
const EVENT_AUDIT_TARGET_REGISTRY_STATE: &str = "audit_target_registry_state";
/// Registry for managing audit targets
pub struct AuditRegistry {
/// Storage for created targets
targets: TargetRuntimeManager<AuditEntry>,
/// Registered plugins for creating targets
plugins: TargetPluginRegistry<AuditEntry>,
}
impl Default for AuditRegistry {
fn default() -> Self {
Self::new()
}
}
impl AuditRegistry {
/// Creates a new AuditRegistry
pub fn new() -> Self {
let mut plugins = TargetPluginRegistry::new();
plugins.register_all(builtin_target_plugins());
AuditRegistry {
targets: TargetRuntimeManager::new(),
plugins,
}
}
pub fn supports_target_type(&self, target_type: &str) -> bool {
self.plugins.supports_target_type(target_type)
}
/// Creates a target of the specified type with the given ID and configuration
///
/// # Arguments
/// * `target_type` - The type of the target (e.g., "webhook", "mqtt").
/// * `id` - The identifier for the target instance.
/// * `config` - The configuration key-value store for the target.
///
/// # Returns
/// * `Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError>` - The created target or an error.
pub async fn create_target(
&self,
target_type: &str,
id: String,
config: &KVS,
) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
self.plugins.create_target(target_type, id, config)
}
/// Creates all targets from a configuration
/// Create all notification targets from system configuration and environment variables.
/// This method processes the creation of each target concurrently as follows:
/// 1. Iterate through all registered target types (e.g. webhooks, mqtt).
/// 2. For each type, resolve its configuration in the configuration file and environment variables.
/// 3. Identify all target instance IDs that need to be created.
/// 4. Combine the default configuration, file configuration, and environment variable configuration for each instance.
/// 5. If the instance is enabled, create an asynchronous task for it to instantiate.
/// 6. Concurrency executes all creation tasks and collects results.
pub async fn create_audit_targets_from_config(
&self,
config: &Config,
) -> AuditResult<Vec<Box<dyn Target<AuditEntry> + Send + Sync>>> {
self.plugins
.create_targets_from_config(config, AUDIT_ROUTE_PREFIX)
.await
.map_err(AuditError::from)
}
/// Adds a target to the registry
///
/// # Arguments
/// * `id` - The identifier for the target.
/// * `target` - The target instance to be added.
pub fn add_target(&mut self, _id: String, target: Box<dyn Target<AuditEntry> + Send + Sync>) {
debug_assert_eq!(_id, target.id().to_string());
self.targets.add_boxed(target);
}
pub fn add_shared_target(&mut self, _id: String, target: SharedTarget<AuditEntry>) {
debug_assert_eq!(_id, target.id().to_string());
self.targets.add_arc(target);
}
/// Removes a target from the registry
///
/// # Arguments
/// * `id` - The identifier for the target to be removed.
///
/// # Returns
/// * `Option<SharedTarget<AuditEntry>>` - The removed target if it existed.
pub async fn remove_target(&mut self, id: &str) -> Option<SharedTarget<AuditEntry>> {
self.targets.remove_and_close(id).await
}
/// Gets a target from the registry
///
/// # Arguments
/// * `id` - The identifier for the target to be retrieved.
///
/// # Returns
/// * `Option<SharedTarget<AuditEntry>>` - The target if it exists.
pub fn get_target(&self, id: &str) -> Option<SharedTarget<AuditEntry>> {
self.targets.get(id)
}
/// Lists cloned target values for runtime inspection without exposing mutable registry access.
pub fn list_target_values(&self) -> Vec<SharedTarget<AuditEntry>> {
self.targets.values()
}
pub fn runtime_manager(&self) -> &TargetRuntimeManager<AuditEntry> {
&self.targets
}
pub fn runtime_manager_mut(&mut self) -> &mut TargetRuntimeManager<AuditEntry> {
&mut self.targets
}
/// Lists all target IDs
///
/// # Returns
/// * `Vec<String>` - A vector of all target IDs in the registry.
pub fn list_targets(&self) -> Vec<String> {
self.targets.keys()
}
/// Closes all targets and clears the registry
///
/// # Returns
/// * `AuditResult<()>` - Result indicating success or failure.
pub async fn close_all(&mut self) -> AuditResult<()> {
let mut first_error = None;
for target_id in self.targets.keys() {
if let Some(target) = self.targets.remove(&target_id)
&& let Err(err) = target.close().await
{
tracing::error!(
event = EVENT_AUDIT_TARGET_REGISTRY_STATE,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_id = %target_id,
state = "close_failed",
error = %err,
"Failed to close target during shutdown"
);
if first_error.is_none() {
first_error = Some(err);
}
}
}
match first_error {
Some(err) => Err(AuditError::Target(err)),
None => Ok(()),
}
}
/// Creates a unique key for a target based on its type and ID
///
/// # Arguments
/// * `target_type` - The type of the target (e.g., "webhook", "mqtt").
/// * `target_id` - The identifier for the target instance.
///
/// # Returns
/// * `String` - The unique key for the target.
pub fn create_key(&self, target_type: &str, target_id: &str) -> String {
let key = TargetID::new(target_id.to_string(), target_type.to_string());
info!(
event = EVENT_AUDIT_TARGET_REGISTRY_KEY_CREATED,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_type = %target_type,
target_id = %target_id,
registry_key = %key,
"audit target registry state"
);
key.to_string()
}
/// Enables a target (placeholder, assumes target exists)
///
/// # Arguments
/// * `target_type` - The type of the target (e.g., "webhook", "mqtt").
/// * `target_id` - The identifier for the target instance.
///
/// # Returns
/// * `AuditResult<()>` - Result indicating success or failure.
pub fn enable_target(&self, target_type: &str, target_id: &str) -> AuditResult<()> {
let key = self.create_key(target_type, target_id);
if self.get_target(&key).is_some() {
info!(
event = EVENT_AUDIT_TARGET_REGISTRY_STATE,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_type = %target_type,
target_id = %target_id,
state = "enabled",
"audit target registry state"
);
Ok(())
} else {
Err(AuditError::Configuration(
format!("Target not found: {}-{}", target_type, target_id),
None,
))
}
}
/// Disables a target (placeholder, assumes target exists)
///
/// # Arguments
/// * `target_type` - The type of the target (e.g., "webhook", "mqtt").
/// * `target_id` - The identifier for the target instance.
///
/// # Returns
/// * `AuditResult<()>` - Result indicating success or failure.
pub fn disable_target(&self, target_type: &str, target_id: &str) -> AuditResult<()> {
let key = self.create_key(target_type, target_id);
if self.get_target(&key).is_some() {
info!(
event = EVENT_AUDIT_TARGET_REGISTRY_STATE,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_type = %target_type,
target_id = %target_id,
state = "disabled",
"audit target registry state"
);
Ok(())
} else {
Err(AuditError::Configuration(
format!("Target not found: {}-{}", target_type, target_id),
None,
))
}
}
/// Upserts a target into the registry
///
/// # Arguments
/// * `target_type` - The type of the target (e.g., "webhook", "mqtt").
/// * `target_id` - The identifier for the target instance.
/// * `target` - The target instance to be upserted.
///
/// # Returns
/// * `AuditResult<()>` - Result indicating success or failure.
pub fn upsert_target(
&mut self,
target_type: &str,
target_id: &str,
target: Box<dyn Target<AuditEntry> + Send + Sync>,
) -> AuditResult<()> {
let key = self.create_key(target_type, target_id);
debug_assert_eq!(key, target.id().to_string());
self.targets.add_boxed(target);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::AuditRegistry;
use crate::AuditError;
use rustfs_targets::TargetError;
use rustfs_targets::target::ChannelTargetType;
use rustfs_targets::testkit::MockTarget;
#[test]
fn registry_registers_amqp_factory() {
let registry = AuditRegistry::new();
assert!(registry.supports_target_type(ChannelTargetType::Amqp.as_str()));
}
#[tokio::test]
async fn close_all_returns_first_error_and_clears_targets() {
let mut registry = AuditRegistry::new();
let ok = MockTarget::new("ok", "webhook");
let ok_observer = ok.clone();
let fail = MockTarget::new("fail", "webhook")
.with_close_failures(usize::MAX)
.with_close_failure_error(|| TargetError::Unknown("close failed".to_string()));
let fail_observer = fail.clone();
registry.add_target(ok.target_id().to_string(), Box::new(ok));
registry.add_target(fail.target_id().to_string(), Box::new(fail));
let result = registry.close_all().await;
assert!(matches!(result, Err(AuditError::Target(TargetError::Unknown(_)))));
assert_eq!(ok_observer.close_call_count(), 1);
assert_eq!(fail_observer.close_call_count(), 1);
assert!(registry.list_targets().is_empty());
}
}