test: add admin route matrix guard (#3268)

This commit is contained in:
安正超
2026-06-08 05:30:18 +08:00
committed by GitHub
parent dee550a831
commit c03c0ebc36
4 changed files with 385 additions and 97 deletions
+26 -31
View File
@@ -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(<task-id>)` marker and cleanup-register entry.
- The next config-model PR must preserve the current tuple-struct shapes and
+34 -30
View File
@@ -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<impl S3Route> {
let mut r: S3Router<AdminOperation> = 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<AdminOperation>) -> 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(())
}
+309 -36
View File
@@ -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<String>) -> RouteMatrixEntry {
let pattern = pattern.into();
RouteMatrixEntry {
method,
sample: pattern.clone(),
pattern,
}
}
fn route_sample(method: Method, pattern: impl Into<String>, sample: impl Into<String>) -> 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<String> {
routes.iter().map(|route| route_key(&route.method, &route.pattern)).collect()
}
fn assert_route(router: &S3Router<AdminOperation>, method: Method, path: &str) {
assert!(
router.contains_route(method.clone(), path),
@@ -49,40 +94,269 @@ fn assert_route(router: &S3Router<AdminOperation>, method: Method, path: &str) {
);
}
fn register_admin_routes(router: &mut S3Router<AdminOperation>) {
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<AdminOperation> {
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<RouteMatrixEntry> {
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::<BTreeSet<_>>();
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<AdminOperation> = 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<AdminOperation> = 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")),
+16
View File
@@ -2212,6 +2212,8 @@ pub struct S3Router<T> {
router: Router<T>,
console_enabled: bool,
console_router: Option<axum::routing::RouterIntoService<Body>>,
#[cfg(test)]
registered_routes: Vec<String>,
}
fn is_public_health_path(path: &str) -> bool {
@@ -2242,6 +2244,8 @@ impl<T: Operation> S3Router<T> {
router,
console_enabled,
console_router,
#[cfg(test)]
registered_routes: Vec::new(),
}
}
@@ -2250,6 +2254,13 @@ impl<T: Operation> S3Router<T> {
// 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<T: Operation> S3Router<T> {
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<T: Operation> Default for S3Router<T> {