mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-06 03:59:14 +00:00
feat(admin): add on-demand migration bucket admin API (#7076)
* feat(ecstore): add on-demand migration bucket config model Introduce OnDemandMigrationConfig (deny_unknown_fields, version 1) with typed validation, credential redaction, a secret-free Debug impl, and the OnceLock publish hook the runtime registers into. Exported through the api facade. * feat(ecstore): persist on-demand migration config in bucket metadata Store the config as a RustFS extension entry (on-demand-migration.json) with its update time in .metadata.bin, add the typed BucketMetadataSys accessor, and publish the config through the hook on every cache-install path alongside the durability sync. * refactor(ecstore): extract shared remote S3 client builder Move the aws_sdk_s3 client construction out of bucket_target_sys into bucket/remote_s3_client.rs: endpoint assembly, credential provider, path-style selection, custom CA / skip-TLS transports and the outbound SSRF gate now build from a neutral RemoteS3EndpointSpec so replication targets and the upcoming on-demand migration source client share one policy. Replication builds its client through From<&BucketTarget>; the gate keeps its relaxed semantics (private allowed, loopback only behind RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET) verbatim. The builder also gains optional connect/read timeouts and a User-Agent suffix interceptor, both unset for replication. Refs rustfs/backlog#2149 * feat(ecstore): add on-demand migration SourceClient Add bucket/on_demand_migration/source_client.rs on top of the shared remote S3 builder: HEAD, ranged streaming GET, ListObjectsV2 with source-prefix mapping, GetObjectTagging and an admin probe. Every request carries the x-rustfs-/x-minio-source-proxy-request anti-loop markers and a RustFS-OnDemandMigration/<version> User-Agent suffix; SSE-C source objects are rejected as unsupported. SourceError classifies SDK failures (not found, access denied, throttled, timeout, connect, server error) with retryability and a stable metrics label. Debug output redacts credentials. Refs rustfs/backlog#2149 * docs(operations): point outbound policy at shared remote S3 client builder * chore: integrate ODM-01 and ODM-02 as B1 base (fix facade merge) * feat(admin): add on-demand migration bucket admin API Add the management plane for On-Demand Migration (ODM-07, rustfs/backlog#2154): PUT/GET/DELETE /v3/on-demand-migration/{bucket}, PUT ?dry-run=true, and a GET .../status skeleton. - PUT authorizes SetBucketOnDemandMigration, checks the bucket, the RUSTFS_ON_DEMAND_MIGRATION_ENABLED switch and the license, validates the ODM-01 config against local endpoints and replication targets, probes the source with SourceClient::probe(), then persists through the incarnation gate and asks peers to reload. Responses carry the redacted config and a probe summary; probe failures name only the error class. - GET answers 404 NoSuchConfiguration when unset; DELETE is idempotent (204). - New AdminAction variants admin:SetBucketOnDemandMigration and admin:GetBucketOnDemandMigration, route policy matrix rows, registration and MinIO alias coverage, and a doc row for the extra handler gates. - rustfs-madmin gains on_demand_migration wire types and client methods; golden fixtures under crates/madmin/fixtures/on_demand_migration/ are asserted byte-for-byte by both the handler and the client tests. Anonymous sources still map to a 400 naming source.credentials until the runtime slice adds the credential-less path. * refactor(admin): route on-demand migration handler errors through the s3 facade
This commit is contained in:
@@ -49,6 +49,7 @@ mod notify_runtime_access;
|
||||
pub mod object_data_cache;
|
||||
pub mod object_zip_download;
|
||||
pub mod oidc;
|
||||
pub mod on_demand_migration;
|
||||
pub mod plugins_catalog;
|
||||
pub mod plugins_instances;
|
||||
pub mod policies;
|
||||
@@ -129,6 +130,10 @@ mod tests {
|
||||
let _list_extension_instances = extensions::ListExtensionInstancesHandler {};
|
||||
let _get_plugin_catalog = plugins_catalog::GetPluginCatalogHandler {};
|
||||
let _create_object_zip_download = object_zip_download::CreateObjectZipDownloadHandler {};
|
||||
let _set_on_demand_migration = on_demand_migration::SetBucketOnDemandMigrationHandler {};
|
||||
let _get_on_demand_migration = on_demand_migration::GetBucketOnDemandMigrationHandler {};
|
||||
let _delete_on_demand_migration = on_demand_migration::DeleteBucketOnDemandMigrationHandler {};
|
||||
let _on_demand_migration_status = on_demand_migration::GetBucketOnDemandMigrationStatusHandler {};
|
||||
let _list_plugin_instances = plugins_instances::ListPluginInstancesHandler {};
|
||||
let _get_plugin_instance = plugins_instances::GetPluginInstanceHandler {};
|
||||
let _put_plugin_instance = plugins_instances::PutPluginInstanceHandler {};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,9 +36,9 @@ mod route_registration_test;
|
||||
use handlers::{
|
||||
account, audit, batch_job, bucket_meta, cluster_snapshot, config_admin, diagnostics, durability as durability_handler,
|
||||
extensions, heal, health, idp_compat, ilm_transition, inspect_archive, kms, mfa, module_switch, object_data_cache,
|
||||
object_zip_download, oidc, plugins_catalog, plugins_instances, pools, profile_admin, quota as quota_handler, rebalance,
|
||||
replication as replication_handler, scanner, site_replication, sts, system, table_catalog, tier, tls_debug, usage_prefix,
|
||||
user,
|
||||
object_zip_download, oidc, on_demand_migration, plugins_catalog, plugins_instances, pools, profile_admin,
|
||||
quota as quota_handler, rebalance, replication as replication_handler, scanner, site_replication, sts, system, table_catalog,
|
||||
tier, tls_debug, usage_prefix, user,
|
||||
};
|
||||
use router::{AdminOperation, S3Router};
|
||||
use s3s::route::S3Route;
|
||||
@@ -77,6 +77,7 @@ fn register_admin_routes(r: &mut S3Router<AdminOperation>) -> std::io::Result<()
|
||||
|
||||
quota_handler::register_quota_route(r)?;
|
||||
durability_handler::register_durability_route(r)?;
|
||||
on_demand_migration::register_on_demand_migration_route(r)?;
|
||||
bucket_meta::register_bucket_meta_route(r)?;
|
||||
config_admin::register_config_route(r)?;
|
||||
scanner::register_scanner_route(r)?;
|
||||
|
||||
@@ -38,6 +38,7 @@ const EXPORT_BUCKET_METADATA: AdminActionRef = AdminActionRef::new("ExportBucket
|
||||
const EXPORT_IAM: AdminActionRef = AdminActionRef::new("ExportIAMAction");
|
||||
const FORCE_UNLOCK: AdminActionRef = AdminActionRef::new("ForceUnlockAdminAction");
|
||||
const GET_BUCKET_TARGET: AdminActionRef = AdminActionRef::new("GetBucketTargetAction");
|
||||
const GET_BUCKET_ON_DEMAND_MIGRATION: AdminActionRef = AdminActionRef::new("GetBucketOnDemandMigrationAction");
|
||||
const GET_GROUP: AdminActionRef = AdminActionRef::new("GetGroupAdminAction");
|
||||
const GET_USER: AdminActionRef = AdminActionRef::new("GetUserAdminAction");
|
||||
const GET_METRICS: AdminActionRef = AdminActionRef::new("GetMetricsAction");
|
||||
@@ -88,6 +89,7 @@ const SERVER_INFO: AdminActionRef = AdminActionRef::new("ServerInfoAdminAction")
|
||||
const SERVER_UPDATE: AdminActionRef = AdminActionRef::new("ServerUpdateAdminAction");
|
||||
const SET_BUCKET_QUOTA: AdminActionRef = AdminActionRef::new("SetBucketQuotaAdminAction");
|
||||
const SET_BUCKET_TARGET: AdminActionRef = AdminActionRef::new("SetBucketTargetAction");
|
||||
const SET_BUCKET_ON_DEMAND_MIGRATION: AdminActionRef = AdminActionRef::new("SetBucketOnDemandMigrationAction");
|
||||
const SET_TABLE: AdminActionRef = AdminActionRef::new("SetTableAction");
|
||||
const SET_TABLE_BUCKET: AdminActionRef = AdminActionRef::new("SetTableBucketAction");
|
||||
const SET_TABLE_LIFECYCLE: AdminActionRef = AdminActionRef::new("SetTableLifecycleAction");
|
||||
@@ -390,6 +392,30 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
||||
CONFIG_UPDATE,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}",
|
||||
SET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}",
|
||||
GET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}",
|
||||
SET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}/status",
|
||||
GET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/export-bucket-metadata",
|
||||
@@ -2163,6 +2189,37 @@ mod tests {
|
||||
assert_action(HttpMethod::Get, "/rustfs/admin/v3/metrics", GET_METRICS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_splits_on_demand_migration_into_set_and_get_actions() {
|
||||
assert_action(
|
||||
HttpMethod::Put,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}",
|
||||
SET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Delete,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}",
|
||||
SET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}",
|
||||
GET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
);
|
||||
assert_action(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}/status",
|
||||
GET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
);
|
||||
// Reads never require the write action, and the routes are not bucket-target routes.
|
||||
assert_not_action(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/on-demand-migration/{bucket}",
|
||||
SET_BUCKET_ON_DEMAND_MIGRATION,
|
||||
);
|
||||
assert_not_action(HttpMethod::Put, "/rustfs/admin/v3/on-demand-migration/{bucket}", SET_BUCKET_TARGET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_policy_requires_dedicated_inspect_action_for_encrypted_archive() {
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v4/inspect/archive", INSPECT_DATA);
|
||||
|
||||
@@ -240,6 +240,14 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
||||
admin_route_sample(Method::PUT, "/v3/bucket-durability/{bucket}", "/v3/bucket-durability/test-bucket"),
|
||||
admin_route_sample(Method::GET, "/v3/bucket-durability/{bucket}", "/v3/bucket-durability/test-bucket"),
|
||||
admin_route_sample(Method::DELETE, "/v3/bucket-durability/{bucket}", "/v3/bucket-durability/test-bucket"),
|
||||
admin_route_sample(Method::PUT, "/v3/on-demand-migration/{bucket}", "/v3/on-demand-migration/test-bucket"),
|
||||
admin_route_sample(Method::GET, "/v3/on-demand-migration/{bucket}", "/v3/on-demand-migration/test-bucket"),
|
||||
admin_route_sample(Method::DELETE, "/v3/on-demand-migration/{bucket}", "/v3/on-demand-migration/test-bucket"),
|
||||
admin_route_sample(
|
||||
Method::GET,
|
||||
"/v3/on-demand-migration/{bucket}/status",
|
||||
"/v3/on-demand-migration/test-bucket/status",
|
||||
),
|
||||
admin_route(Method::GET, "/export-bucket-metadata"),
|
||||
admin_route(Method::GET, "/v3/export-bucket-metadata"),
|
||||
admin_route(Method::PUT, "/import-bucket-metadata"),
|
||||
@@ -1274,6 +1282,10 @@ fn test_register_routes_cover_representative_admin_paths() {
|
||||
assert_route(&router, Method::PUT, &admin_path("/v3/bucket-durability/test-bucket"));
|
||||
assert_route(&router, Method::GET, &admin_path("/v3/bucket-durability/test-bucket"));
|
||||
assert_route(&router, Method::DELETE, &admin_path("/v3/bucket-durability/test-bucket"));
|
||||
assert_route(&router, Method::PUT, &admin_path("/v3/on-demand-migration/test-bucket"));
|
||||
assert_route(&router, Method::GET, &admin_path("/v3/on-demand-migration/test-bucket"));
|
||||
assert_route(&router, Method::DELETE, &admin_path("/v3/on-demand-migration/test-bucket"));
|
||||
assert_route(&router, Method::GET, &admin_path("/v3/on-demand-migration/test-bucket/status"));
|
||||
|
||||
assert_route(&router, Method::GET, &admin_path("/export-bucket-metadata"));
|
||||
assert_route(&router, Method::GET, &admin_path("/v3/export-bucket-metadata"));
|
||||
@@ -1403,6 +1415,10 @@ fn test_admin_alias_paths_match_existing_admin_routes() {
|
||||
(Method::POST, compat_admin_alias_path("/v3/scanner/cycle-state/reset")),
|
||||
(Method::POST, compat_admin_alias_path("/v3/scanner/usage-state/reset")),
|
||||
(Method::GET, compat_admin_alias_path("/v3/ilm/expiry/status")),
|
||||
(Method::PUT, compat_admin_alias_path("/v3/on-demand-migration/b")),
|
||||
(Method::GET, compat_admin_alias_path("/v3/on-demand-migration/b")),
|
||||
(Method::DELETE, compat_admin_alias_path("/v3/on-demand-migration/b")),
|
||||
(Method::GET, compat_admin_alias_path("/v3/on-demand-migration/b/status")),
|
||||
] {
|
||||
assert!(
|
||||
router.contains_compatible_route(method.clone(), &path),
|
||||
|
||||
@@ -20,8 +20,8 @@ use time::OffsetDateTime;
|
||||
|
||||
mod ecstore_bucket {
|
||||
pub(crate) use crate::storage::storage_api::ecstore_bucket::{
|
||||
bandwidth, bucket_target_sys, durability, lifecycle, metadata, metadata_sys, object_lock, quota, replication, target,
|
||||
utils, versioning, versioning_sys,
|
||||
bandwidth, bucket_target_sys, durability, lifecycle, metadata, metadata_sys, object_lock, on_demand_migration, quota,
|
||||
remote_s3_client, replication, target, utils, versioning, versioning_sys,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ pub(crate) mod metadata {
|
||||
pub(crate) const BUCKET_TARGETS_FILE: &str = super::ecstore_bucket::metadata::BUCKET_TARGETS_FILE;
|
||||
pub(crate) const BUCKET_VERSIONING_CONFIG: &str = super::ecstore_bucket::metadata::BUCKET_VERSIONING_CONFIG;
|
||||
pub(crate) const BUCKET_DURABILITY_CONFIG: &str = super::ecstore_bucket::metadata::BUCKET_DURABILITY_CONFIG;
|
||||
pub(crate) const BUCKET_ON_DEMAND_MIGRATION_CONFIG: &str = super::ecstore_bucket::metadata::BUCKET_ON_DEMAND_MIGRATION_CONFIG;
|
||||
pub(crate) const OBJECT_LOCK_CONFIG: &str = super::ecstore_bucket::metadata::OBJECT_LOCK_CONFIG;
|
||||
|
||||
pub(crate) type BucketMetadata = super::ecstore_bucket::metadata::BucketMetadata;
|
||||
@@ -282,6 +283,29 @@ pub(crate) mod durability {
|
||||
pub(crate) type BucketDurabilityConfig = super::ecstore_bucket::durability::BucketDurabilityConfig;
|
||||
}
|
||||
|
||||
pub(crate) mod on_demand_migration {
|
||||
pub(crate) type OnDemandMigrationConfig = super::ecstore_bucket::on_demand_migration::OnDemandMigrationConfig;
|
||||
pub(crate) type OnDemandMigrationConfigError = super::ecstore_bucket::on_demand_migration::OnDemandMigrationConfigError;
|
||||
pub(crate) type PathStyle = super::ecstore_bucket::on_demand_migration::PathStyle;
|
||||
pub(crate) type Provider = super::ecstore_bucket::on_demand_migration::Provider;
|
||||
pub(crate) type ValidationContext<'a> = super::ecstore_bucket::on_demand_migration::ValidationContext<'a>;
|
||||
|
||||
pub(crate) mod source_client {
|
||||
pub(crate) type SourceClient = super::super::ecstore_bucket::on_demand_migration::source_client::SourceClient;
|
||||
pub(crate) type SourceClientSpec = super::super::ecstore_bucket::on_demand_migration::source_client::SourceClientSpec;
|
||||
pub(crate) type SourceError = super::super::ecstore_bucket::on_demand_migration::source_client::SourceError;
|
||||
pub(crate) type SourceProbe = super::super::ecstore_bucket::on_demand_migration::source_client::SourceProbe;
|
||||
pub(crate) type SourceProvider = super::super::ecstore_bucket::on_demand_migration::source_client::SourceProvider;
|
||||
pub(crate) type SourceTimeouts = super::super::ecstore_bucket::on_demand_migration::source_client::SourceTimeouts;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod remote_s3_client {
|
||||
pub(crate) type PathStyle = super::ecstore_bucket::remote_s3_client::PathStyle;
|
||||
pub(crate) type RemoteCredentials = super::ecstore_bucket::remote_s3_client::RemoteCredentials;
|
||||
pub(crate) type RemoteS3ClientError = super::ecstore_bucket::remote_s3_client::RemoteS3ClientError;
|
||||
}
|
||||
|
||||
pub(crate) mod metadata_sys {
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -417,6 +441,12 @@ pub(crate) mod metadata_sys {
|
||||
super::ecstore_bucket::metadata_sys::get_durability_config(bucket).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_on_demand_migration_config(
|
||||
bucket: &str,
|
||||
) -> Result<Option<(super::on_demand_migration::OnDemandMigrationConfig, OffsetDateTime)>> {
|
||||
super::ecstore_bucket::metadata_sys::get_on_demand_migration_config(bucket).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_quota_config(bucket: &str) -> Result<(BucketQuota, OffsetDateTime)> {
|
||||
super::ecstore_bucket::metadata_sys::get_quota_config(bucket).await
|
||||
}
|
||||
@@ -869,7 +899,9 @@ pub(crate) mod bucket {
|
||||
pub(crate) use super::lifecycle;
|
||||
pub(crate) use super::metadata;
|
||||
pub(crate) use super::metadata_sys;
|
||||
pub(crate) use super::on_demand_migration;
|
||||
pub(crate) use super::quota;
|
||||
pub(crate) use super::remote_s3_client;
|
||||
pub(crate) use super::replication;
|
||||
pub(crate) use super::target;
|
||||
pub(crate) use super::versioning_sys;
|
||||
@@ -969,7 +1001,7 @@ pub(crate) mod runtime {
|
||||
}
|
||||
|
||||
pub(crate) mod s3 {
|
||||
pub(crate) use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, header};
|
||||
pub(crate) use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, auth, header};
|
||||
|
||||
/// Build an `S3Error` without reaching for the `s3s` error macro.
|
||||
///
|
||||
|
||||
@@ -408,7 +408,7 @@ pub(crate) mod ecstore_bucket {
|
||||
pub(crate) use rustfs_ecstore::api::bucket::lifecycle::tier_delete_journal::test_util::install_all_v6_fleet_capability_proof;
|
||||
pub(crate) use rustfs_ecstore::api::bucket::{
|
||||
bandwidth, bucket_target_sys, durability, lifecycle, metadata, metadata_sys, migration, object_lock, on_demand_migration,
|
||||
policy_sys, replication, tagging, target, utils,
|
||||
policy_sys, remote_s3_client, replication, tagging, target, utils,
|
||||
};
|
||||
pub(crate) use rustfs_ecstore::api::bucket::{quota, versioning, versioning_sys};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user