mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 13:36:50 +00:00
fix(admin): notify site replication after bucket metadata import (#4562)
fix(admin): replicate imported bucket metadata
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::admin::handlers::site_replication::site_replication_bucket_meta_hook;
|
||||||
use crate::admin::storage_api::bucket::utils::{deserialize, serialize};
|
use crate::admin::storage_api::bucket::utils::{deserialize, serialize};
|
||||||
use crate::admin::storage_api::bucket::{
|
use crate::admin::storage_api::bucket::{
|
||||||
metadata::{
|
metadata::{
|
||||||
@@ -38,6 +39,7 @@ use http::{HeaderMap, StatusCode};
|
|||||||
use hyper::Method;
|
use hyper::Method;
|
||||||
use matchit::Params;
|
use matchit::Params;
|
||||||
use rustfs_config::MAX_BUCKET_METADATA_IMPORT_SIZE;
|
use rustfs_config::MAX_BUCKET_METADATA_IMPORT_SIZE;
|
||||||
|
use rustfs_madmin::{SITE_REPL_API_VERSION, SRBucketMeta};
|
||||||
use rustfs_policy::policy::{
|
use rustfs_policy::policy::{
|
||||||
BucketPolicy,
|
BucketPolicy,
|
||||||
action::{Action, AdminAction},
|
action::{Action, AdminAction},
|
||||||
@@ -833,6 +835,7 @@ impl Operation for ImportBucketMetadata {
|
|||||||
// field and saves it, preserving any configs not present in the import archive.
|
// field and saves it, preserving any configs not present in the import archive.
|
||||||
for (bucket_name, metadata) in &bucket_metadatas {
|
for (bucket_name, metadata) in &bucket_metadatas {
|
||||||
for (config_file, data) in imported_configs_to_persist(metadata) {
|
for (config_file, data) in imported_configs_to_persist(metadata) {
|
||||||
|
let site_replication_item = imported_config_to_site_replication_item(bucket_name, metadata, config_file, &data)?;
|
||||||
if let Err(e) = metadata_sys::update(bucket_name, config_file, data).await {
|
if let Err(e) = metadata_sys::update(bucket_name, config_file, data).await {
|
||||||
warn!(
|
warn!(
|
||||||
event = EVENT_ADMIN_BUCKET_META_STATE,
|
event = EVENT_ADMIN_BUCKET_META_STATE,
|
||||||
@@ -850,11 +853,24 @@ impl Operation for ImportBucketMetadata {
|
|||||||
"failed to persist imported bucket metadata for {bucket_name}/{config_file}: {e}"
|
"failed to persist imported bucket metadata for {bucket_name}/{config_file}: {e}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let Some(item) = site_replication_item
|
||||||
|
&& let Err(err) = site_replication_bucket_meta_hook(item).await
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
event = EVENT_ADMIN_BUCKET_META_STATE,
|
||||||
|
component = LOG_COMPONENT_ADMIN,
|
||||||
|
subsystem = LOG_SUBSYSTEM_BUCKET_META,
|
||||||
|
action = "import_bucket_metadata",
|
||||||
|
result = "site_replication_notify_failed",
|
||||||
|
bucket = %bucket_name,
|
||||||
|
config_name = %config_file,
|
||||||
|
error = %err,
|
||||||
|
"admin bucket meta state"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: site replication notify
|
|
||||||
|
|
||||||
let mut header = HeaderMap::new();
|
let mut header = HeaderMap::new();
|
||||||
header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));
|
header.insert(CONTENT_TYPE, "application/json".parse().expect("valid header value"));
|
||||||
header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value"));
|
header.insert(CONTENT_LENGTH, "0".parse().expect("valid header value"));
|
||||||
@@ -887,6 +903,80 @@ fn imported_configs_to_persist(metadata: &BucketMetadata) -> Vec<(&'static str,
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn imported_config_to_site_replication_item(
|
||||||
|
bucket_name: &str,
|
||||||
|
metadata: &BucketMetadata,
|
||||||
|
config_file: &str,
|
||||||
|
data: &[u8],
|
||||||
|
) -> S3Result<Option<SRBucketMeta>> {
|
||||||
|
let mut item = match config_file {
|
||||||
|
BUCKET_POLICY_CONFIG => site_replication_bucket_meta_item(bucket_name, "policy", metadata.policy_config_updated_at),
|
||||||
|
BUCKET_LIFECYCLE_CONFIG => {
|
||||||
|
site_replication_bucket_meta_item(bucket_name, "lc-config", metadata.lifecycle_config_updated_at)
|
||||||
|
}
|
||||||
|
BUCKET_SSECONFIG => site_replication_bucket_meta_item(bucket_name, "sse-config", metadata.encryption_config_updated_at),
|
||||||
|
BUCKET_TAGGING_CONFIG => site_replication_bucket_meta_item(bucket_name, "tags", metadata.tagging_config_updated_at),
|
||||||
|
BUCKET_QUOTA_CONFIG_FILE => {
|
||||||
|
site_replication_bucket_meta_item(bucket_name, "quota-config", metadata.quota_config_updated_at)
|
||||||
|
}
|
||||||
|
OBJECT_LOCK_CONFIG => {
|
||||||
|
site_replication_bucket_meta_item(bucket_name, "object-lock-config", metadata.object_lock_config_updated_at)
|
||||||
|
}
|
||||||
|
BUCKET_VERSIONING_CONFIG => {
|
||||||
|
site_replication_bucket_meta_item(bucket_name, "version-config", metadata.versioning_config_updated_at)
|
||||||
|
}
|
||||||
|
BUCKET_REPLICATION_CONFIG => {
|
||||||
|
site_replication_bucket_meta_item(bucket_name, "replication-config", metadata.replication_config_updated_at)
|
||||||
|
}
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
match config_file {
|
||||||
|
BUCKET_POLICY_CONFIG => {
|
||||||
|
item.policy =
|
||||||
|
Some(serde_json::from_slice(data).map_err(|e| {
|
||||||
|
s3_error!(InternalError, "failed to encode imported bucket policy for site replication: {e}")
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
BUCKET_LIFECYCLE_CONFIG => {
|
||||||
|
set_imported_config_string(&mut item.expiry_lc_config, config_file, data)?;
|
||||||
|
item.expiry_updated_at = item.updated_at;
|
||||||
|
}
|
||||||
|
BUCKET_SSECONFIG => set_imported_config_string(&mut item.sse_config, config_file, data)?,
|
||||||
|
BUCKET_TAGGING_CONFIG => set_imported_config_string(&mut item.tags, config_file, data)?,
|
||||||
|
BUCKET_QUOTA_CONFIG_FILE => {
|
||||||
|
item.quota = Some(
|
||||||
|
serde_json::from_slice(data)
|
||||||
|
.map_err(|e| s3_error!(InternalError, "failed to encode imported bucket quota for site replication: {e}"))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OBJECT_LOCK_CONFIG => set_imported_config_string(&mut item.object_lock_config, config_file, data)?,
|
||||||
|
BUCKET_VERSIONING_CONFIG => set_imported_config_string(&mut item.versioning, config_file, data)?,
|
||||||
|
BUCKET_REPLICATION_CONFIG => set_imported_config_string(&mut item.replication_config, config_file, data)?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn site_replication_bucket_meta_item(bucket_name: &str, item_type: &str, updated_at: OffsetDateTime) -> SRBucketMeta {
|
||||||
|
SRBucketMeta {
|
||||||
|
bucket: bucket_name.to_string(),
|
||||||
|
r#type: item_type.to_string(),
|
||||||
|
updated_at: Some(updated_at),
|
||||||
|
api_version: Some(SITE_REPL_API_VERSION.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_imported_config_string(target: &mut Option<String>, config_file: &str, data: &[u8]) -> S3Result<()> {
|
||||||
|
*target = Some(
|
||||||
|
String::from_utf8(data.to_vec())
|
||||||
|
.map_err(|e| s3_error!(InternalError, "imported bucket metadata {config_file} is not valid UTF-8: {e}"))?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod import_persist_tests {
|
mod import_persist_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -919,4 +1009,64 @@ mod import_persist_tests {
|
|||||||
let metadata = BucketMetadata::new("untouched-bucket");
|
let metadata = BucketMetadata::new("untouched-bucket");
|
||||||
assert!(imported_configs_to_persist(&metadata).is_empty());
|
assert!(imported_configs_to_persist(&metadata).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn imported_configs_are_scheduled_for_site_replication() {
|
||||||
|
let updated_at = OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(42);
|
||||||
|
let mut metadata = BucketMetadata::new("restored-bucket");
|
||||||
|
metadata.policy_config_json = br#"{"Version":"2012-10-17","Statement":[]}"#.to_vec();
|
||||||
|
metadata.policy_config_updated_at = updated_at;
|
||||||
|
metadata.versioning_config_xml = b"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>".to_vec();
|
||||||
|
metadata.versioning_config_updated_at = updated_at;
|
||||||
|
metadata.lifecycle_config_xml = b"<LifecycleConfiguration></LifecycleConfiguration>".to_vec();
|
||||||
|
metadata.lifecycle_config_updated_at = updated_at;
|
||||||
|
metadata.quota_config_json = br#"{"quota":1024}"#.to_vec();
|
||||||
|
metadata.quota_config_updated_at = updated_at;
|
||||||
|
|
||||||
|
let mut items = imported_configs_to_persist(&metadata)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(config_file, data)| {
|
||||||
|
imported_config_to_site_replication_item("restored-bucket", &metadata, config_file, &data)
|
||||||
|
.expect("site replication item should encode")
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
items.sort_by(|left, right| left.r#type.cmp(&right.r#type));
|
||||||
|
|
||||||
|
assert_eq!(items.len(), 4);
|
||||||
|
assert!(items.iter().all(|item| item.bucket == "restored-bucket"));
|
||||||
|
assert!(items.iter().all(|item| item.updated_at == Some(updated_at)));
|
||||||
|
assert!(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.all(|item| item.api_version.as_deref() == Some(SITE_REPL_API_VERSION))
|
||||||
|
);
|
||||||
|
assert!(items.iter().any(|item| item.r#type == "policy" && item.policy.is_some()));
|
||||||
|
assert!(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.r#type == "version-config" && item.versioning.is_some())
|
||||||
|
);
|
||||||
|
assert!(items.iter().any(|item| {
|
||||||
|
item.r#type == "lc-config" && item.expiry_lc_config.is_some() && item.expiry_updated_at == Some(updated_at)
|
||||||
|
}));
|
||||||
|
assert!(items.iter().any(|item| item.r#type == "quota-config" && item.quota.is_some()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_imported_configs_are_not_site_replicated() {
|
||||||
|
let mut metadata = BucketMetadata::new("restored-bucket");
|
||||||
|
metadata.notification_config_xml = b"<NotificationConfiguration></NotificationConfiguration>".to_vec();
|
||||||
|
metadata.bucket_targets_config_json = b"[]".to_vec();
|
||||||
|
|
||||||
|
let has_site_replication_item = imported_configs_to_persist(&metadata)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(config_file, data)| {
|
||||||
|
imported_config_to_site_replication_item("restored-bucket", &metadata, config_file, &data)
|
||||||
|
.expect("unsupported configs should be skipped")
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
assert!(!has_site_replication_item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user