mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 09:48:20 +00:00
8ddbf05924
* 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).
322 lines
12 KiB
Rust
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());
|
|
}
|
|
}
|