From 4ba8058dbc3b66fb03fdb5231ae1042d6bb7ed61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Sat, 20 Jun 2026 09:48:49 +0800 Subject: [PATCH] refactor: expose extension runtime snapshots (#3644) --- docs/architecture/migration-progress.md | 49 ++++++++--- rustfs/src/admin/handlers/extensions.rs | 106 +++++++++++++++++++++++- 2 files changed, 140 insertions(+), 15 deletions(-) diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index 1addc0812..635070d51 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,16 +5,16 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Current Context - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) -- Branch: `overtrue/arch-ops-profiler-runtime-contract` -- Baseline: `overtrue/arch-embedded-lifecycle-publication-reuse` - (`e27c079d4c2a577bd9a632a615b2f0b96b468149`). -- Stacked on: local R-031, which is stacked on rustfs/rustfs#3641. +- Branch: `overtrue/arch-extension-runtime-snapshots` +- Baseline: `overtrue/arch-ops-profiler-runtime-contract` + (`6d4ff92f398a9decdce53a7203744c7c2a70a4e9`). +- Stacked on: local R-032, which is stacked on rustfs/rustfs#3642. - PR type for this branch: `contract` - Runtime behavior changes: none. -- Rust code changes: publish the builtin ops profiler catalog contract through - targets and add a read-only profiler registry contract. +- Rust code changes: expose builtin diagnostics/profiler runtime capability + snapshots through the admin extension catalog response. - CI/script changes: none. -- Docs changes: record the R-032 ops profiler runtime contract slice. +- Docs changes: record the R-033 extension runtime snapshot slice. ## Phase 0 Tasks @@ -2198,20 +2198,33 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block migration/layer guards, formatting, diff hygiene, Rust risk scan, branch freshness check, pre-commit quality gate, and three-expert review. +- [x] `R-033` Expose extension runtime capability snapshots. + - Do: add read-only diagnostics/profiler runtime capability snapshots to the + admin extension catalog response using existing schema and contract DTOs. + - Acceptance: `/v4/extensions/catalog` reports builtin diagnostics and + profiler capability contracts with their runtime boundaries, disabled + defaults, and non-fatal startup flags while preserving schema validation. + - Must preserve: existing extension catalog route/auth, plugin instance + listing, profiler/diagnostics execution paths, and external plugin flow + status semantics. + - Verification: focused admin catalog and targets runtime checks, RustFS lib + check, migration/layer guards, formatting, diff hygiene, Rust risk scan, + branch freshness check, pre-commit quality gate, and three-expert review. + ## Next PRs 1. `pure-move`: continue pruning residual embedded startup-only orchestration once the lifecycle helpers are merged. 2. `contract`: add the next read-only extension handoff once admin/reporting - consumers need profiler or diagnostics snapshots. + consumers need additional extension runtime snapshots. ## Pre-Push Review Log | Expert | Status | Notes | |---|---|---| -| Quality/architecture | passed | R-032 keeps profiler execution in the existing admin/profiling paths while adding only catalog and registry contract boundaries. | -| Migration preservation | passed | Existing profile collection routes, startup/shutdown hook behavior, redaction requirements, and disabled external-runtime defaults remain unchanged. | -| Testing/verification | passed | Focused targets/admin extension checks, guards, formatting, diff hygiene, Rust risk scan, and full pre-commit passed. | +| Quality/architecture | passed | R-033 exposes only read-only diagnostics/profiler capability snapshots through the admin extension catalog response. | +| Migration preservation | passed | Catalog auth, plugin instance listing, profiler/diagnostics execution paths, and external plugin flow status semantics remain unchanged. | +| Testing/verification | passed | Focused admin catalog and targets runtime checks, lib check, guards, formatting, diff hygiene, Rust risk scan, and full pre-commit passed. | ## Verification Notes @@ -2247,6 +2260,20 @@ Passed before push: - `make pre-commit`: passed. - Three-expert review: passed. +- Issue #660 R-033 current slice: + - `cargo test -p rustfs --lib extension_catalog -- --nocapture`: passed. + - `cargo test -p rustfs-targets ops_diagnostics -- --nocapture`: passed. + - `cargo test -p rustfs-targets ops_profiler -- --nocapture`: passed. + - `cargo check -p rustfs --lib`: passed. + - `./scripts/check_architecture_migration_rules.sh`: passed. + - `./scripts/check_layer_dependencies.sh`: passed. + - `cargo fmt --all --check`: passed. + - `git diff --check`: passed. + - Rust risk scan on changed Rust files: passed; only test-only + expectations/assertion paths were present. + - `make pre-commit`: passed. + - Three-expert review: passed. + - Issue #660 X-012 current slice: - `cargo test -p rustfs-extension-schema`: passed. - `cargo check -p rustfs-extension-schema`: passed. diff --git a/rustfs/src/admin/handlers/extensions.rs b/rustfs/src/admin/handlers/extensions.rs index 27e28fc82..86431cc98 100644 --- a/rustfs/src/admin/handlers/extensions.rs +++ b/rustfs/src/admin/handlers/extensions.rs @@ -26,11 +26,16 @@ use crate::server::{ADMIN_PREFIX, RemoteAddr}; use http::{HeaderMap, HeaderValue, StatusCode}; use hyper::Method; use matchit::Params; -use rustfs_extension_schema::{ExtensionKind, ExtensionSchema}; +use rustfs_extension_schema::{ + ExtensionCapabilityRef, ExtensionKind, ExtensionRuntimeContract, ExtensionSchema, OPS_DIAGNOSTICS_CAPABILITY, + OPS_PROFILER_CAPABILITY, OpsDiagnosticsContract, OpsProfilerContract, +}; use rustfs_policy::policy::action::{Action, AdminAction}; use rustfs_targets::{ - TargetPluginExternalFlowGate, TargetPluginExternalFlowGateStatus, builtin_extension_schemas, - catalog::example_external_webhook_plugin, target_marketplace_extension_schema, + OpsDiagnosticsRegistry, OpsProfilerRegistry, TargetPluginExternalFlowGate, TargetPluginExternalFlowGateStatus, + builtin_extension_schemas, builtin_ops_diagnostics_contract, builtin_ops_diagnostics_extension_schema, + builtin_ops_profiler_contract, builtin_ops_profiler_extension_schema, catalog::example_external_webhook_plugin, + target_marketplace_extension_schema, }; use s3s::header::CONTENT_TYPE; use s3s::{Body, S3Request, S3Response, S3Result, s3_error}; @@ -55,9 +60,29 @@ pub fn register_extension_route(r: &mut S3Router) -> std::io::Re #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub(crate) struct ExtensionCatalogResponse { pub extensions: Vec, + pub runtime_capabilities: ExtensionRuntimeCapabilitiesResponse, pub external_plugin_flow: TargetPluginExternalFlowGateStatus, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub(crate) struct ExtensionRuntimeCapabilitiesResponse { + pub ops_diagnostics: ExtensionRuntimeCapabilityResponse, + pub ops_profiler: ExtensionRuntimeCapabilityResponse, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub(crate) struct ExtensionRuntimeCapabilityResponse +where + T: Serialize, +{ + pub extension_id: String, + pub capability: ExtensionCapabilityRef, + pub runtime: ExtensionRuntimeContract, + pub disabled_by_default: bool, + pub startup_fatal: bool, + pub contract: T, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub(crate) struct ExtensionInstanceEntry { @@ -95,10 +120,50 @@ fn build_extension_catalog_response() -> ExtensionCatalogResponse { ExtensionCatalogResponse { extensions, + runtime_capabilities: build_extension_runtime_capabilities_response(), external_plugin_flow: TargetPluginExternalFlowGate::default().status(), } } +fn build_extension_runtime_capabilities_response() -> ExtensionRuntimeCapabilitiesResponse { + let ops_diagnostics_schema = builtin_ops_diagnostics_extension_schema(); + let ops_diagnostics_contract = builtin_ops_diagnostics_contract(); + let mut ops_diagnostics_registry = OpsDiagnosticsRegistry::new(); + debug_assert!( + ops_diagnostics_registry + .register_schema(&ops_diagnostics_schema, &ops_diagnostics_contract) + .is_ok() + ); + + let ops_profiler_schema = builtin_ops_profiler_extension_schema(); + let ops_profiler_contract = builtin_ops_profiler_contract(); + let mut ops_profiler_registry = OpsProfilerRegistry::new(); + debug_assert!( + ops_profiler_registry + .register_schema(&ops_profiler_schema, &ops_profiler_contract) + .is_ok() + ); + + ExtensionRuntimeCapabilitiesResponse { + ops_diagnostics: ExtensionRuntimeCapabilityResponse { + extension_id: ops_diagnostics_schema.extension_id, + capability: ExtensionCapabilityRef::new(OPS_DIAGNOSTICS_CAPABILITY), + runtime: ops_diagnostics_schema.runtime, + disabled_by_default: ops_diagnostics_schema.disabled_by_default, + startup_fatal: false, + contract: ops_diagnostics_contract, + }, + ops_profiler: ExtensionRuntimeCapabilityResponse { + extension_id: ops_profiler_schema.extension_id, + capability: ExtensionCapabilityRef::new(OPS_PROFILER_CAPABILITY), + runtime: ops_profiler_schema.runtime, + disabled_by_default: ops_profiler_schema.disabled_by_default, + startup_fatal: false, + contract: ops_profiler_contract, + }, + } +} + fn map_extension_instance(instance: PluginInstanceEntry) -> ExtensionInstanceEntry { ExtensionInstanceEntry { id: instance.id, @@ -206,7 +271,10 @@ mod tests { PluginContractDomain, PluginEnableState, PluginInstallState, PluginInstanceEntry, PluginInstanceSource, PluginOperationalRuntimeState, PluginOperationalStateContract, }; - use rustfs_extension_schema::{ExtensionKind, ExtensionRuntimeBoundary, validate_extension_schemas}; + use rustfs_extension_schema::{ + ExtensionKind, ExtensionRuntimeBoundary, OPS_DIAGNOSTICS_CAPABILITY, OPS_PROFILER_CAPABILITY, validate_extension_schemas, + validate_ops_diagnostics_contract, validate_ops_profiler_contract, + }; use std::collections::HashMap; #[test] @@ -305,6 +373,36 @@ mod tests { assert!(validate_extension_schemas(&response.extensions).is_ok()); } + #[test] + fn extension_catalog_exposes_read_only_runtime_capability_snapshots() { + let response = build_extension_catalog_response(); + + assert_eq!(response.runtime_capabilities.ops_diagnostics.extension_id, "builtin:ops-diagnostics"); + assert_eq!( + response.runtime_capabilities.ops_diagnostics.capability.as_str(), + OPS_DIAGNOSTICS_CAPABILITY + ); + assert_eq!( + response.runtime_capabilities.ops_diagnostics.runtime.boundary, + ExtensionRuntimeBoundary::Builtin + ); + assert!(!response.runtime_capabilities.ops_diagnostics.disabled_by_default); + assert!(!response.runtime_capabilities.ops_diagnostics.startup_fatal); + assert!(response.runtime_capabilities.ops_diagnostics.contract.requires_admin_action); + assert!(!response.runtime_capabilities.ops_diagnostics.contract.mutates_object_data); + assert!(validate_ops_diagnostics_contract(&response.runtime_capabilities.ops_diagnostics.contract).is_ok()); + + assert_eq!(response.runtime_capabilities.ops_profiler.extension_id, "builtin:ops-profiler"); + assert_eq!(response.runtime_capabilities.ops_profiler.capability.as_str(), OPS_PROFILER_CAPABILITY); + assert_eq!( + response.runtime_capabilities.ops_profiler.runtime.boundary, + ExtensionRuntimeBoundary::Builtin + ); + assert!(!response.runtime_capabilities.ops_profiler.disabled_by_default); + assert!(!response.runtime_capabilities.ops_profiler.startup_fatal); + assert!(validate_ops_profiler_contract(&response.runtime_capabilities.ops_profiler.contract).is_ok()); + } + #[test] fn extension_instance_view_maps_plugin_instance_identity() { let instance = PluginInstanceEntry {