diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index cefdb0dac..67ee56357 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,15 +5,15 @@ 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-migration-ci-rules` -- Baseline: `upstream/main` at `6f4d0b54a171ff1560b5d892378d2ed407411fb9` -- PR type for this branch: `ci-gate` +- Branch: `overtrue/arch-admin-route-matrix-guard` +- Baseline: `upstream/main` at `dee550a831cbfc24aa5cecd60f6f3a4cfe1a2e30` +- PR type for this branch: `test-only` - Runtime behavior changes: none -- Rust code changes: none -- CI/script changes: add a migration rule check for PR type vocabulary and - temporary compatibility marker/register consistency, plus a lightweight - docs-only workflow so architecture documentation PRs still run the same guard. -- Docs changes: record the new guardrail in the migration handoff. +- Rust code changes: add test-only admin route matrix coverage, test-only + registered route tracking, and a private shared admin route registration helper + so tests exercise the production registration sequence. +- CI/script changes: none +- Docs changes: record the route matrix guard handoff. ## Phase 0 Tasks @@ -31,12 +31,11 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block - Acceptance: `./scripts/check_layer_dependencies.sh` passes on current `upstream/main` while still rejecting new unaccepted layer dependencies. - [~] `G-006` Create migration loss-prevention checks. - - Current branch: add a migration rule check for PR type vocabulary and - temporary compatibility marker/register consistency, with a dedicated - architecture-doc trigger that covers `ARCHITECTURE.md` and - `docs/architecture/**` docs-only PRs. - - Remaining follow-up: add checks for public re-export, route matrix, and - storage trait coverage before pure moves. + - Current branch: add a mechanical admin route matrix guard from + [`admin-route-action-snapshot.md`](admin-route-action-snapshot.md) and + `rustfs/src/admin/route_registration_test.rs`. + - Remaining follow-up: add checks for public re-export and storage trait + coverage before pure moves. - [x] `G-007` Create startup timeline table. - Acceptance: [`startup-timeline.md`](startup-timeline.md) records current binary startup order, side effects, fatal boundaries, and readiness stages. @@ -66,42 +65,38 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Next PRs -1. `test-only`: add a mechanical admin route matrix guard from the current - snapshot and `route_registration_test.rs`. -2. `contract`: define the config-model contract surface while preserving the +1. `contract`: define the config-model contract surface while preserving the existing `Config`, `KV`, and `KVS` behavior. -3. `ci-gate`: add focused checks for public re-export and storage trait coverage +2. `ci-gate`: add focused checks for public re-export and storage trait coverage before pure moves. ## Pre-Push Review Log | Expert | Status | Notes | |---|---|---| -| Quality/architecture | pass | Confirmed the guard script is narrow/readable, fixes single-token PR type detection, and reuses one script across CI/make/docs triggers rather than adding a parallel rule system | -| Migration preservation | pass | Confirmed this is a `ci-gate` PR with only CI/config/docs/script changes and no runtime logic, storage hot-path, global-state, compatibility implementation, or crate-split changes | -| Testing/verification | pass | Confirmed positive checks, staged diff check, and temporary negative checks cover unknown PR type and missing cleanup-register entry failure modes | +| Quality/architecture | pass | Confirmed the matrix helpers keep the snapshot explicit, `S3Router` instrumentation is fully `cfg(test)`, the production registration order is shared through one private helper, and the non-test insertion path keeps existing route construction/insert behavior | +| Migration preservation | pass | Confirmed this remains `test-only` in effect: no route handler/auth/alias semantics changed, no storage hot-path/global-state/crate-split changes, and MinIO admin alias coverage is expanded across all admin-prefix matrix entries | +| Testing/verification | pass | Confirmed focused route tests pin `ENV_HEALTH_ENDPOINT_ENABLE=true`, use the production registration helper, and pass with formatting, non-test cargo check, migration guard scripts, diff check, full `make pre-commit`, and temporary unaccounted-route negative coverage | ## Verification Notes Passed: +- `cargo fmt --all --check` +- `cargo check -p rustfs --lib` +- `cargo test -p rustfs admin::route_registration_test -- --nocapture` - `./scripts/check_architecture_migration_rules.sh` - `./scripts/check_layer_dependencies.sh` - `./scripts/check_metrics_migration_refs.sh` - `git diff --check` -- `git diff --cached --check` -- `bash -n scripts/check_architecture_migration_rules.sh` -- `make architecture-migration-check` -- `ruby -e "require 'yaml'; YAML.load_file('.github/workflows/architecture-migration-rules.yml')"` -- temporary negative check for unknown single-token PR type -- temporary negative check for unknown PR type in `ARCHITECTURE.md` -- temporary negative check for unknown PR type in nested `docs/architecture/**` -- temporary negative check for source compatibility marker without a register entry +- temporary negative check for an unaccounted admin route registration +- `make pre-commit` ## Handoff Notes - Keep Phase 0 PRs small. Do not move Config, Storage API, Runtime, or ECStore - code inside this `ci-gate` branch. -- Keep CI checks in a separate `ci-gate` PR so the PR type rule remains enforceable. + code inside this `test-only` branch. +- Route matrix instrumentation must remain test-only and must not alter dispatch + or auth behavior. - Do not add temporary compatibility code without a matching `RUSTFS_COMPAT_TODO()` marker and cleanup-register entry. - The next config-model PR must preserve the current tuple-struct shapes and diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index 9384356e6..aa11825ce 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -42,35 +42,39 @@ use s3s::route::S3Route; /// An instance of S3Route for admin operations pub fn make_admin_route(console_enabled: bool) -> std::io::Result { let mut r: S3Router = S3Router::new(console_enabled); - - health::register_health_route(&mut r)?; - sts::register_admin_auth_route(&mut r)?; - - user::register_user_route(&mut r)?; - system::register_system_route(&mut r)?; - pools::register_pool_route(&mut r)?; - rebalance::register_rebalance_route(&mut r)?; - - heal::register_heal_route(&mut r)?; - - tier::register_tier_route(&mut r)?; - - quota::register_quota_route(&mut r)?; - bucket_meta::register_bucket_meta_route(&mut r)?; - config_admin::register_config_route(&mut r)?; - scanner::register_scanner_route(&mut r)?; - audit::register_audit_target_route(&mut r)?; - module_switch::register_module_switch_route(&mut r)?; - plugins_catalog::register_plugin_catalog_route(&mut r)?; - plugins_instances::register_plugin_instance_route(&mut r)?; - - replication::register_replication_route(&mut r)?; - site_replication::register_site_replication_route(&mut r)?; - profile_admin::register_profiling_route(&mut r)?; - tls_debug::register_tls_debug_route(&mut r)?; - kms::register_kms_route(&mut r)?; - oidc::register_oidc_route(&mut r)?; - table_catalog::register_table_catalog_route(&mut r)?; - + register_admin_routes(&mut r)?; Ok(r) } + +fn register_admin_routes(r: &mut S3Router) -> std::io::Result<()> { + health::register_health_route(r)?; + sts::register_admin_auth_route(r)?; + + user::register_user_route(r)?; + system::register_system_route(r)?; + pools::register_pool_route(r)?; + rebalance::register_rebalance_route(r)?; + + heal::register_heal_route(r)?; + + tier::register_tier_route(r)?; + + quota::register_quota_route(r)?; + bucket_meta::register_bucket_meta_route(r)?; + config_admin::register_config_route(r)?; + scanner::register_scanner_route(r)?; + audit::register_audit_target_route(r)?; + module_switch::register_module_switch_route(r)?; + plugins_catalog::register_plugin_catalog_route(r)?; + plugins_instances::register_plugin_instance_route(r)?; + + replication::register_replication_route(r)?; + site_replication::register_site_replication_route(r)?; + profile_admin::register_profiling_route(r)?; + tls_debug::register_tls_debug_route(r)?; + kms::register_kms_route(r)?; + oidc::register_oidc_route(r)?; + table_catalog::register_table_catalog_route(r)?; + + Ok(()) +} diff --git a/rustfs/src/admin/route_registration_test.rs b/rustfs/src/admin/route_registration_test.rs index 87b3e1e56..a5c25a195 100644 --- a/rustfs/src/admin/route_registration_test.rs +++ b/rustfs/src/admin/route_registration_test.rs @@ -13,11 +13,7 @@ // limitations under the License. use crate::admin::{ - handlers::{ - audit, bucket_meta, config_admin, heal, health, kms, module_switch, oidc, plugins_catalog, plugins_instances, pools, - profile_admin, quota, rebalance, replication, scanner, site_replication, sts, system, table_catalog, tier, tls_debug, - user, - }, + handlers::health, router::{AdminOperation, S3Router}, }; use crate::server::{ @@ -26,6 +22,7 @@ use crate::server::{ }; use hyper::Method; use serial_test::serial; +use std::collections::BTreeSet; use temp_env::with_var; fn admin_path(path: &str) -> String { @@ -40,6 +37,54 @@ fn table_catalog_path(path: &str) -> String { format!("{}{}", TABLE_CATALOG_PREFIX, path) } +#[derive(Debug)] +struct RouteMatrixEntry { + method: Method, + pattern: String, + sample: String, +} + +fn route(method: Method, pattern: impl Into) -> RouteMatrixEntry { + let pattern = pattern.into(); + RouteMatrixEntry { + method, + sample: pattern.clone(), + pattern, + } +} + +fn route_sample(method: Method, pattern: impl Into, sample: impl Into) -> RouteMatrixEntry { + RouteMatrixEntry { + method, + pattern: pattern.into(), + sample: sample.into(), + } +} + +fn admin_route(method: Method, pattern: &str) -> RouteMatrixEntry { + route(method, admin_path(pattern)) +} + +fn admin_route_sample(method: Method, pattern: &str, sample: &str) -> RouteMatrixEntry { + route_sample(method, admin_path(pattern), admin_path(sample)) +} + +fn table_route(method: Method, pattern: &str) -> RouteMatrixEntry { + route(method, table_catalog_path(pattern)) +} + +fn table_route_sample(method: Method, pattern: &str, sample: &str) -> RouteMatrixEntry { + route_sample(method, table_catalog_path(pattern), table_catalog_path(sample)) +} + +fn route_key(method: &Method, path: &str) -> String { + format!("{}|{}", method.as_str(), path) +} + +fn route_key_set(routes: &[RouteMatrixEntry]) -> BTreeSet { + routes.iter().map(|route| route_key(&route.method, &route.pattern)).collect() +} + fn assert_route(router: &S3Router, method: Method, path: &str) { assert!( router.contains_route(method.clone(), path), @@ -49,40 +94,269 @@ fn assert_route(router: &S3Router, method: Method, path: &str) { ); } -fn register_admin_routes(router: &mut S3Router) { - health::register_health_route(router).expect("register health route"); - sts::register_admin_auth_route(router).expect("register sts route"); - user::register_user_route(router).expect("register user route"); - system::register_system_route(router).expect("register system route"); - pools::register_pool_route(router).expect("register pool route"); - rebalance::register_rebalance_route(router).expect("register rebalance route"); - heal::register_heal_route(router).expect("register heal route"); - tier::register_tier_route(router).expect("register tier route"); - quota::register_quota_route(router).expect("register quota route"); - bucket_meta::register_bucket_meta_route(router).expect("register bucket meta route"); - config_admin::register_config_route(router).expect("register config admin route"); - scanner::register_scanner_route(router).expect("register scanner route"); - audit::register_audit_target_route(router).expect("register audit target route"); - module_switch::register_module_switch_route(router).expect("register module switch route"); - plugins_catalog::register_plugin_catalog_route(router).expect("register plugin catalog route"); - plugins_instances::register_plugin_instance_route(router).expect("register plugin instances route"); - replication::register_replication_route(router).expect("register replication route"); - site_replication::register_site_replication_route(router).expect("register site replication route"); - profile_admin::register_profiling_route(router).expect("register profile route"); - tls_debug::register_tls_debug_route(router).expect("register tls debug route"); - kms::register_kms_route(router).expect("register kms route"); - oidc::register_oidc_route(router).expect("register oidc route"); - table_catalog::register_table_catalog_route(router).expect("register table catalog route"); +fn registered_admin_router() -> S3Router { + with_var(rustfs_config::ENV_HEALTH_ENDPOINT_ENABLE, Some("true"), || { + let mut router = S3Router::new(false); + super::register_admin_routes(&mut router).expect("register production admin routes"); + router + }) +} + +fn expected_admin_route_matrix() -> Vec { + vec![ + route(Method::GET, HEALTH_PREFIX), + route(Method::HEAD, HEALTH_PREFIX), + route(Method::GET, HEALTH_READY_PATH), + route(Method::HEAD, HEALTH_READY_PATH), + route(Method::GET, PROFILE_CPU_PATH), + route(Method::GET, PROFILE_MEMORY_PATH), + route(Method::POST, "/"), + admin_route(Method::GET, "/v3/is-admin"), + admin_route(Method::GET, "/v3/accountinfo"), + admin_route(Method::GET, "/v3/list-users"), + admin_route(Method::GET, "/v3/user-info"), + admin_route(Method::DELETE, "/v3/remove-user"), + admin_route(Method::PUT, "/v3/add-user"), + admin_route(Method::PUT, "/v3/set-user-status"), + admin_route(Method::GET, "/v3/groups"), + admin_route(Method::GET, "/v3/group"), + admin_route_sample(Method::DELETE, "/v3/group/{group}", "/v3/group/test-group"), + admin_route(Method::PUT, "/v3/set-group-status"), + admin_route(Method::PUT, "/v3/update-group-members"), + admin_route(Method::POST, "/v3/update-service-account"), + admin_route(Method::GET, "/v3/info-service-account"), + admin_route(Method::GET, "/v3/temporary-account-info"), + admin_route(Method::GET, "/v3/info-access-key"), + admin_route(Method::GET, "/v3/list-service-accounts"), + admin_route(Method::GET, "/v3/list-access-keys-bulk"), + admin_route(Method::DELETE, "/v3/delete-service-accounts"), + admin_route(Method::DELETE, "/v3/delete-service-account"), + admin_route(Method::PUT, "/v3/add-service-accounts"), + admin_route(Method::PUT, "/v3/add-service-account"), + admin_route(Method::GET, "/v3/export-iam"), + admin_route(Method::PUT, "/v3/import-iam"), + admin_route(Method::GET, "/v3/list-canned-policies"), + admin_route(Method::GET, "/v3/info-canned-policy"), + admin_route(Method::PUT, "/v3/add-canned-policy"), + admin_route(Method::DELETE, "/v3/remove-canned-policy"), + admin_route(Method::PUT, "/v3/set-user-or-group-policy"), + admin_route(Method::PUT, "/v3/set-policy"), + admin_route(Method::POST, "/v3/idp/builtin/policy/attach"), + admin_route(Method::POST, "/v3/idp/builtin/policy/detach"), + admin_route(Method::GET, "/v3/idp/builtin/policy-entities"), + admin_route(Method::GET, "/v3/target/list"), + admin_route_sample(Method::PUT, "/v3/target/{target_type}/{target_name}", "/v3/target/webhook/test-target"), + admin_route_sample( + Method::DELETE, + "/v3/target/{target_type}/{target_name}/reset", + "/v3/target/webhook/test-target/reset", + ), + admin_route(Method::GET, "/v3/target/arns"), + admin_route(Method::POST, "/v3/service"), + admin_route(Method::GET, "/v3/info"), + admin_route(Method::GET, "/v3/inspect-data"), + admin_route(Method::POST, "/v3/inspect-data"), + admin_route(Method::GET, "/v3/storageinfo"), + admin_route(Method::GET, "/v3/datausageinfo"), + admin_route(Method::GET, "/v3/metrics"), + admin_route(Method::GET, "/v3/pools/list"), + admin_route(Method::GET, "/v3/pools/status"), + admin_route(Method::POST, "/v3/pools/decommission"), + admin_route(Method::POST, "/v3/pools/cancel"), + admin_route(Method::POST, "/v3/rebalance/start"), + admin_route(Method::GET, "/v3/rebalance/status"), + admin_route(Method::POST, "/v3/rebalance/stop"), + admin_route(Method::POST, "/v3/heal/"), + admin_route_sample(Method::POST, "/v3/heal/{bucket}", "/v3/heal/test-bucket"), + admin_route_sample(Method::POST, "/v3/heal/{bucket}/{prefix}", "/v3/heal/test-bucket/prefix"), + admin_route(Method::POST, "/v3/background-heal/status"), + admin_route(Method::GET, "/v3/tier"), + admin_route(Method::GET, "/v3/tier-stats"), + admin_route_sample(Method::GET, "/v3/tier/{tier}", "/v3/tier/HOT"), + admin_route_sample(Method::DELETE, "/v3/tier/{tiername}", "/v3/tier/HOT"), + admin_route(Method::PUT, "/v3/tier"), + admin_route_sample(Method::POST, "/v3/tier/{tiername}", "/v3/tier/HOT"), + admin_route(Method::POST, "/v3/tier/clear"), + admin_route(Method::PUT, "/v3/set-bucket-quota"), + admin_route(Method::GET, "/v3/get-bucket-quota"), + admin_route_sample(Method::PUT, "/v3/quota/{bucket}", "/v3/quota/test-bucket"), + admin_route_sample(Method::GET, "/v3/quota/{bucket}", "/v3/quota/test-bucket"), + admin_route_sample(Method::DELETE, "/v3/quota/{bucket}", "/v3/quota/test-bucket"), + admin_route_sample(Method::GET, "/v3/quota-stats/{bucket}", "/v3/quota-stats/test-bucket"), + admin_route_sample(Method::POST, "/v3/quota-check/{bucket}", "/v3/quota-check/test-bucket"), + admin_route(Method::GET, "/export-bucket-metadata"), + admin_route(Method::GET, "/v3/export-bucket-metadata"), + admin_route(Method::PUT, "/import-bucket-metadata"), + admin_route(Method::PUT, "/v3/import-bucket-metadata"), + admin_route(Method::GET, "/v3/get-config-kv"), + admin_route(Method::PUT, "/v3/set-config-kv"), + admin_route(Method::DELETE, "/v3/del-config-kv"), + admin_route(Method::GET, "/v3/help-config-kv"), + admin_route(Method::GET, "/v3/list-config-history-kv"), + admin_route(Method::DELETE, "/v3/clear-config-history-kv"), + admin_route(Method::PUT, "/v3/restore-config-history-kv"), + admin_route(Method::GET, "/v3/config"), + admin_route(Method::PUT, "/v3/config"), + admin_route(Method::GET, "/v3/scanner/status"), + admin_route(Method::GET, "/v3/audit/target/list"), + admin_route_sample( + Method::PUT, + "/v3/audit/target/{target_type}/{target_name}", + "/v3/audit/target/audit_webhook/test-audit", + ), + admin_route_sample( + Method::DELETE, + "/v3/audit/target/{target_type}/{target_name}/reset", + "/v3/audit/target/audit_webhook/test-audit/reset", + ), + admin_route(Method::GET, "/v3/module-switches"), + admin_route(Method::PUT, "/v3/module-switches"), + admin_route(Method::GET, "/v4/plugins/catalog"), + admin_route(Method::GET, "/v4/plugins/instances"), + admin_route_sample(Method::GET, "/v4/plugins/instances/{id}", "/v4/plugins/instances/example-id"), + admin_route_sample(Method::PUT, "/v4/plugins/instances/{id}", "/v4/plugins/instances/example-id"), + admin_route_sample(Method::DELETE, "/v4/plugins/instances/{id}", "/v4/plugins/instances/example-id"), + admin_route(Method::GET, "/v3/list-remote-targets"), + admin_route(Method::GET, "/v3/replicationmetrics"), + admin_route(Method::PUT, "/v3/set-remote-target"), + admin_route(Method::DELETE, "/v3/remove-remote-target"), + admin_route(Method::PUT, "/v3/site-replication/add"), + admin_route(Method::PUT, "/v3/site-replication/remove"), + admin_route(Method::GET, "/v3/site-replication/info"), + admin_route(Method::GET, "/v3/site-replication/metainfo"), + admin_route(Method::GET, "/v3/site-replication/status"), + admin_route(Method::POST, "/v3/site-replication/devnull"), + admin_route(Method::POST, "/v3/site-replication/netperf"), + admin_route(Method::PUT, "/v3/site-replication/peer/join"), + admin_route(Method::PUT, "/v3/site-replication/peer/bucket-ops"), + admin_route(Method::PUT, "/v3/site-replication/peer/iam-item"), + admin_route(Method::PUT, "/v3/site-replication/peer/bucket-meta"), + admin_route(Method::GET, "/v3/site-replication/peer/idp-settings"), + admin_route(Method::PUT, "/v3/site-replication/edit"), + admin_route(Method::PUT, "/v3/site-replication/peer/edit"), + admin_route(Method::PUT, "/v3/site-replication/peer/remove"), + admin_route(Method::PUT, "/v3/site-replication/resync/op"), + admin_route(Method::PUT, "/v3/site-replication/state/edit"), + admin_route(Method::GET, "/debug/pprof/profile"), + admin_route(Method::GET, "/debug/pprof/status"), + admin_route(Method::GET, "/debug/tls/status"), + admin_route(Method::POST, "/v3/kms/create-key"), + admin_route(Method::POST, "/v3/kms/key/create"), + admin_route(Method::GET, "/v3/kms/describe-key"), + admin_route(Method::GET, "/v3/kms/key/status"), + admin_route(Method::GET, "/v3/kms/list-keys"), + admin_route(Method::POST, "/v3/kms/generate-data-key"), + admin_route(Method::GET, "/v3/kms/status"), + admin_route(Method::POST, "/v3/kms/status"), + admin_route(Method::GET, "/v3/kms/config"), + admin_route(Method::POST, "/v3/kms/clear-cache"), + admin_route(Method::POST, "/v3/kms/configure"), + admin_route(Method::POST, "/v3/kms/start"), + admin_route(Method::POST, "/v3/kms/stop"), + admin_route(Method::GET, "/v3/kms/service-status"), + admin_route(Method::POST, "/v3/kms/reconfigure"), + admin_route(Method::POST, "/v3/kms/keys"), + admin_route(Method::DELETE, "/v3/kms/keys/delete"), + admin_route(Method::POST, "/v3/kms/keys/cancel-deletion"), + admin_route(Method::GET, "/v3/kms/keys"), + admin_route_sample(Method::GET, "/v3/kms/keys/{key_id}", "/v3/kms/keys/test-key"), + admin_route(Method::GET, "/v3/oidc/providers"), + admin_route_sample(Method::GET, "/v3/oidc/authorize/{provider_id}", "/v3/oidc/authorize/default"), + admin_route_sample(Method::GET, "/v3/oidc/callback/{provider_id}", "/v3/oidc/callback/default"), + admin_route(Method::GET, "/v3/oidc/logout"), + admin_route(Method::GET, "/v3/oidc/config"), + admin_route_sample(Method::PUT, "/v3/oidc/config/{provider_id}", "/v3/oidc/config/default"), + admin_route_sample(Method::DELETE, "/v3/oidc/config/{provider_id}", "/v3/oidc/config/default"), + admin_route(Method::POST, "/v3/oidc/validate"), + table_route(Method::GET, "/config"), + table_route_sample(Method::GET, "/{warehouse}/namespaces", "/analytics/namespaces"), + table_route_sample(Method::POST, "/{warehouse}/namespaces", "/analytics/namespaces"), + table_route_sample(Method::GET, "/{warehouse}/namespaces/{namespace}", "/analytics/namespaces/sales"), + table_route_sample(Method::DELETE, "/{warehouse}/namespaces/{namespace}", "/analytics/namespaces/sales"), + table_route_sample( + Method::GET, + "/{warehouse}/namespaces/{namespace}/tables", + "/analytics/namespaces/sales/tables", + ), + table_route_sample( + Method::POST, + "/{warehouse}/namespaces/{namespace}/tables", + "/analytics/namespaces/sales/tables", + ), + table_route_sample( + Method::POST, + "/{warehouse}/namespaces/{namespace}/register", + "/analytics/namespaces/sales/register", + ), + table_route_sample( + Method::GET, + "/{warehouse}/namespaces/{namespace}/tables/{table}", + "/analytics/namespaces/sales/tables/orders", + ), + table_route_sample( + Method::POST, + "/{warehouse}/namespaces/{namespace}/tables/{table}", + "/analytics/namespaces/sales/tables/orders", + ), + table_route_sample( + Method::DELETE, + "/{warehouse}/namespaces/{namespace}/tables/{table}", + "/analytics/namespaces/sales/tables/orders", + ), + ] +} + +// registered_admin_router pins ENV_HEALTH_ENDPOINT_ENABLE because the +// production registration helper intentionally honors that environment switch. +#[test] +#[serial] +fn test_admin_route_matrix_matches_registered_routes() { + let router = registered_admin_router(); + + let matrix = expected_admin_route_matrix(); + let actual_routes = router.registered_routes(); + assert_eq!( + actual_routes.len(), + matrix.len(), + "admin route matrix must account for every registered route" + ); + + let actual = actual_routes.iter().cloned().collect::>(); + let expected = route_key_set(&matrix); + assert_eq!(actual, expected, "admin route registration changed without updating the matrix"); + + for route in matrix { + assert_route(&router, route.method, &route.sample); + } +} + +#[test] +#[serial] +fn test_admin_route_matrix_preserves_minio_admin_aliases() { + let router = registered_admin_router(); + + for route in expected_admin_route_matrix() + .into_iter() + .filter(|route| route.sample.starts_with(ADMIN_PREFIX)) + { + let suffix = route + .sample + .strip_prefix(ADMIN_PREFIX) + .expect("matrix route is already filtered by admin prefix"); + let alias = compat_admin_alias_path(suffix); + assert!( + router.contains_compatible_route(route.method.clone(), &alias), + "expected MinIO admin alias path to match: {} {}", + route.method.as_str(), + alias + ); + } } -// register_admin_routes reads ENV_HEALTH_ENDPOINT_ENABLE to decide whether -// to register /health; serialise with the env-mutating test below to avoid -// cross-thread leakage of that override. #[test] #[serial] fn test_register_routes_cover_representative_admin_paths() { - let mut router: S3Router = S3Router::new(false); - register_admin_routes(&mut router); + let router = registered_admin_router(); assert_route(&router, Method::GET, HEALTH_PREFIX); assert_route(&router, Method::HEAD, HEALTH_PREFIX); assert_route(&router, Method::GET, HEALTH_READY_PATH); @@ -221,8 +495,7 @@ fn test_register_routes_cover_representative_admin_paths() { #[test] #[serial] fn test_admin_alias_paths_match_existing_admin_routes() { - let mut router: S3Router = S3Router::new(false); - register_admin_routes(&mut router); + let router = registered_admin_router(); for (method, path) in [ (Method::GET, compat_admin_alias_path("/v3/is-admin")), diff --git a/rustfs/src/admin/router.rs b/rustfs/src/admin/router.rs index 2f1c2fd6d..c038068fe 100644 --- a/rustfs/src/admin/router.rs +++ b/rustfs/src/admin/router.rs @@ -2212,6 +2212,8 @@ pub struct S3Router { router: Router, console_enabled: bool, console_router: Option>, + #[cfg(test)] + registered_routes: Vec, } fn is_public_health_path(path: &str) -> bool { @@ -2242,6 +2244,8 @@ impl S3Router { router, console_enabled, console_router, + #[cfg(test)] + registered_routes: Vec::new(), } } @@ -2250,6 +2254,13 @@ impl S3Router { // warn!("set uri {}", &path); + #[cfg(test)] + { + self.router.insert(path.clone(), operation).map_err(std::io::Error::other)?; + self.registered_routes.push(path); + } + + #[cfg(not(test))] self.router.insert(path, operation).map_err(std::io::Error::other)?; Ok(()) @@ -2272,6 +2283,11 @@ impl S3Router { let route = Self::make_route_str(method, canonical_path.as_ref()); self.router.at(&route).is_ok() } + + #[cfg(test)] + pub(crate) fn registered_routes(&self) -> &[String] { + &self.registered_routes + } } impl Default for S3Router {