diff --git a/.config/nextest.toml b/.config/nextest.toml index 6f4545317..06e69a29d 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -192,7 +192,7 @@ test-group = 'ecstore-serial-flaky' [profile.e2e-smoke] default-filter = """ package(e2e_test) & ( - test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_source_invalid_date|content_encoding|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud)_test::|^fake_s3_target::/) + test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_source_invalid_date|content_encoding|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools)_test::|^fake_s3_target::/) | test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/) | test(/^reliant::lifecycle::/) | test(/^reliant::tiering::/) diff --git a/crates/e2e_test/src/admin_pools_test.rs b/crates/e2e_test/src/admin_pools_test.rs new file mode 100644 index 000000000..8686eb810 --- /dev/null +++ b/crates/e2e_test/src/admin_pools_test.rs @@ -0,0 +1,79 @@ +// Copyright 2026 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::common::{RustFSTestEnvironment, init_logging, local_http_client}; +use http::header::HOST; +use reqwest::StatusCode; +use rustfs_signer::constants::UNSIGNED_PAYLOAD; +use rustfs_signer::sign_v4; +use s3s::Body; +use serde::Deserialize; +use std::error::Error; + +#[derive(Debug, Deserialize)] +struct PoolListItem { + id: usize, + cmdline: String, + status: String, +} + +async fn signed_admin_get(env: &RustFSTestEnvironment, path: &str) -> Result> { + let url = format!("{}{path}", env.url); + let uri = url.parse::()?; + let authority = uri.authority().ok_or("admin URL missing authority")?.to_string(); + let request = http::Request::builder() + .method(http::Method::GET) + .uri(uri) + .header(HOST, authority) + .header("x-amz-content-sha256", UNSIGNED_PAYLOAD) + .body(Body::empty())?; + let signed = sign_v4(request, 0, &env.access_key, &env.secret_key, "", "us-east-1"); + + let mut request = local_http_client().get(&url); + for (name, value) in signed.headers() { + request = request.header(name, value); + } + Ok(request.send().await?) +} + +#[tokio::test] +async fn single_drive_pools_list_succeeds_without_enabling_decommission_status() -> Result<(), Box> { + init_logging(); + + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + + let response = signed_admin_get(&env, "/rustfs/admin/v3/pools/list").await?; + let status = response.status(); + let body = response.bytes().await?; + + assert_eq!(status, StatusCode::OK, "pools list failed: {}", String::from_utf8_lossy(&body)); + let pools: Vec = serde_json::from_slice(&body)?; + assert_eq!(pools.len(), 1); + assert_eq!(pools[0].id, 0); + assert_eq!(pools[0].cmdline, env.temp_dir); + assert_eq!(pools[0].status, "active"); + + let response = signed_admin_get(&env, "/rustfs/admin/v3/decommission/status").await?; + let status = response.status(); + let body = response.text().await?; + assert_eq!( + status, + StatusCode::NOT_IMPLEMENTED, + "decommission status changed for a single pool: {body}" + ); + assert!(body.contains("NotImplemented"), "unexpected decommission error body: {body}"); + + Ok(()) +} diff --git a/crates/e2e_test/src/lib.rs b/crates/e2e_test/src/lib.rs index d8c6523d7..8363305ba 100644 --- a/crates/e2e_test/src/lib.rs +++ b/crates/e2e_test/src/lib.rs @@ -242,6 +242,9 @@ mod console_smoke_test; #[cfg(test)] mod admin_iam_crud_test; +#[cfg(test)] +mod admin_pools_test; + // Replication extension end-to-end regression tests #[cfg(test)] mod replication_extension_test; diff --git a/rustfs/src/app/admin_usecase.rs b/rustfs/src/app/admin_usecase.rs index bd32ee91a..ac1898f50 100644 --- a/rustfs/src/app/admin_usecase.rs +++ b/rustfs/src/app/admin_usecase.rs @@ -330,10 +330,6 @@ impl DefaultAdminUsecase { return Err(Self::app_error_default(S3ErrorCode::NotImplemented)); }; - if endpoints.legacy() { - return Err(Self::app_error_default(S3ErrorCode::NotImplemented)); - } - Self::refresh_pool_status_snapshot(store.as_ref()).await?; let mut pool_statuses = Vec::new(); @@ -400,6 +396,12 @@ impl DefaultAdminUsecase { let Some(store) = self.object_store() else { return Err(Self::app_error(S3ErrorCode::InternalError, "Not init")); }; + let Some(endpoints) = self.endpoints() else { + return Err(Self::app_error_default(S3ErrorCode::NotImplemented)); + }; + if endpoints.legacy() { + return Err(Self::app_error_default(S3ErrorCode::NotImplemented)); + } let pool_statuses = self.execute_list_pool_statuses().await?; Self::refresh_rebalance_status_snapshot(store.as_ref()).await?; let mut pools = Vec::with_capacity(pool_statuses.len());