mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-03 03:47:42 +00:00
957080bea5
Swift container and account metadata handlers cloned the cached BucketMetadata, set the tagging fields, and called set_bucket_metadata, which only updates the in-memory cache map. Nothing reached .metadata.bin, so every Swift metadata POST was lost on restart and silently overwritten by the next disk-truth reload (a peer LoadBucketMetadata notification or the 15-minute refresh loop) — while the client had already been told 2xx. Route these writes through a new metadata_sys::update_config_with: a read-modify-write that loads the on-disk metadata and persists the result under the same write guard metadata_sys::update uses, so the rewrite merges against disk truth instead of a possibly stale cache and cannot clobber a concurrent update to another config file. Peers are notified afterwards, matching the S3 config handlers. Persisting these writes required hardening the paths that now produce durable state: - Account metadata writes validate account ownership. This metadata holds the account's TempURL signing key, so an unauthenticated write for someone else's account would have become a durable, cluster-wide takeover of that account's pre-signed URLs. Reads stay open because TempURL signature validation runs before credentials exist. - disable_versioning verifies the container exists. Without it the metadata loader's "no metadata on disk" default would be persisted, creating an orphan metadata file and caching a fabricated default as authoritative. - Container and account metadata are size- and count-limited, reusing the Swift limits object metadata already enforces; these tags land in the bucket metadata file that every later config write rewrites whole. - A rewrite refuses to run when the persisted tagging config is unreadable, instead of merging onto an empty set and wiping the container ACL and versioning tags. It reports 409 naming the remedy. - Storage errors are logged in full and reported generically, since they now carry real disk and quorum detail. The tagging arm of BucketMetadata::update_config also clears the parsed config, as the lifecycle arm does: parse_all_configs skips empty XML rather than clearing, so a cleared config kept serving the old tags. Tagging is serialized with the S3 XML serializer the loader can parse back, not quick_xml, whose output was never round-trippable.
169 lines
7.5 KiB
Rust
169 lines
7.5 KiB
Rust
// Copyright 2024 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 std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use rustfs_ecstore::api::bucket::metadata::BUCKET_TAGGING_CONFIG;
|
|
pub(crate) use rustfs_ecstore::api::bucket::metadata::BucketMetadata as SwiftBucketMetadata;
|
|
use rustfs_ecstore::api::bucket::metadata_sys::{get as get_swift_bucket_metadata_from_backend, update_config_with};
|
|
use rustfs_ecstore::api::bucket::utils::serialize as serialize_bucket_config;
|
|
use rustfs_ecstore::api::error::Error as SwiftStorageError;
|
|
pub(crate) use rustfs_ecstore::api::error::Result as SwiftStorageResult;
|
|
use rustfs_ecstore::api::notification::get_global_notification_sys;
|
|
pub(crate) use rustfs_ecstore::api::runtime::object_store_handle as resolve_swift_object_store_handle;
|
|
use rustfs_ecstore::api::storage::ECStore as SwiftStore;
|
|
use rustfs_storage_api as storage_contracts;
|
|
use s3s::dto::Tagging;
|
|
|
|
use super::{SwiftError, SwiftResult};
|
|
|
|
pub(crate) mod account {
|
|
pub(crate) use super::storage_contracts::{BucketOperations, MakeBucketOptions};
|
|
}
|
|
|
|
pub(crate) mod container {
|
|
pub(crate) use super::storage_contracts::{
|
|
BucketInfo, BucketOperations, BucketOptions, DeleteBucketOptions, ListOperations, MakeBucketOptions,
|
|
};
|
|
}
|
|
|
|
pub(crate) mod large_object {
|
|
pub(crate) use super::storage_contracts::HTTPRangeSpec;
|
|
}
|
|
|
|
pub(crate) mod object {
|
|
pub(crate) use super::storage_contracts::{BucketOperations, BucketOptions, HTTPRangeSpec, ObjectIO, ObjectOperations};
|
|
}
|
|
|
|
pub(crate) mod public_api {
|
|
pub use super::{SwiftGetObjectReader, SwiftObjectInfo, SwiftObjectOptions, SwiftPutObjReader};
|
|
pub(crate) use super::{
|
|
get_swift_bucket_metadata, get_swift_bucket_usage, resolve_swift_object_store_handle, update_swift_bucket_tagging,
|
|
};
|
|
}
|
|
|
|
pub(crate) mod versioning {
|
|
pub(crate) use super::storage_contracts::{ListOperations, ObjectOperations};
|
|
}
|
|
|
|
const LOG_COMPONENT_PROTOCOLS: &str = "protocols";
|
|
const LOG_SUBSYSTEM_SWIFT_STORAGE: &str = "swift_storage";
|
|
const EVENT_SWIFT_BUCKET_TAGGING_UPDATE: &str = "swift_bucket_tagging_update";
|
|
|
|
/// Marks the refusal to rewrite an unreadable persisted tagging config, so the
|
|
/// caller can turn it into an actionable client error rather than a generic
|
|
/// storage failure. Carried through the ecstore error, which is a string type.
|
|
const UNREADABLE_TAGGING_SENTINEL: &str = "swift: persisted tagging config could not be parsed";
|
|
|
|
pub type SwiftGetObjectReader = <SwiftStore as storage_contracts::ObjectIO>::GetObjectReader;
|
|
pub type SwiftObjectInfo = <SwiftStore as storage_contracts::ObjectOperations>::ObjectInfo;
|
|
pub type SwiftObjectOptions = <SwiftStore as storage_contracts::ObjectOperations>::ObjectOptions;
|
|
pub type SwiftPutObjReader = <SwiftStore as storage_contracts::ObjectIO>::PutObjectReader;
|
|
|
|
pub(crate) async fn get_swift_bucket_metadata(bucket: &str) -> SwiftStorageResult<Arc<SwiftBucketMetadata>> {
|
|
get_swift_bucket_metadata_from_backend(bucket).await
|
|
}
|
|
|
|
/// Rewrite the bucket's tagging config through the persisting
|
|
/// bucket-metadata path.
|
|
///
|
|
/// `rewrite` sees the tag set currently persisted on disk (`None` when the
|
|
/// bucket has none) and returns the full replacement; an empty tag set
|
|
/// clears the config. The read-modify-write runs under the bucket metadata
|
|
/// system's write guard — serialized against every other config update —
|
|
/// and the result is written to the bucket metadata file before the cache
|
|
/// is refreshed, so a Swift metadata POST survives process restarts and
|
|
/// disk-truth reloads. Peers are then told to reload, matching what the S3
|
|
/// handlers do after a config write.
|
|
///
|
|
/// Storage failures are logged in full and reported to the client as a
|
|
/// generic error: these now carry real disk and quorum detail, which does not
|
|
/// belong in a Swift response body. The one exception is an unreadable
|
|
/// persisted config, which is reported specifically because the operator has
|
|
/// to act on it.
|
|
pub(crate) async fn update_swift_bucket_tagging<F>(bucket: String, rewrite: F) -> SwiftResult<()>
|
|
where
|
|
F: FnOnce(Option<&Tagging>) -> Tagging + Send,
|
|
{
|
|
let result = update_config_with(&bucket, BUCKET_TAGGING_CONFIG, |bm| {
|
|
// Merging onto an unparseable tag set would silently drop every tag
|
|
// the bucket has — including the container ACL and versioning tags —
|
|
// because the rewrite closures treat "no parsed tags" as "no tags".
|
|
// Refuse instead: the persisted config is intact, just unreadable.
|
|
if !bm.tagging_config_xml.is_empty() && bm.tagging_config.is_none() {
|
|
return Err(SwiftStorageError::other(UNREADABLE_TAGGING_SENTINEL));
|
|
}
|
|
|
|
let tagging = rewrite(bm.tagging_config.as_ref());
|
|
if tagging.tag_set.is_empty() {
|
|
Ok(Vec::new())
|
|
} else {
|
|
// The S3 XML serializer, not quick_xml: the metadata loader's
|
|
// parse step must be able to round-trip what we persist.
|
|
serialize_bucket_config(&tagging)
|
|
.map_err(|e| SwiftStorageError::other(format!("failed to serialize bucket tagging: {e}")))
|
|
}
|
|
})
|
|
.await;
|
|
|
|
if let Err(err) = result {
|
|
let unreadable = err.to_string().contains(UNREADABLE_TAGGING_SENTINEL);
|
|
tracing::error!(
|
|
event = EVENT_SWIFT_BUCKET_TAGGING_UPDATE,
|
|
component = LOG_COMPONENT_PROTOCOLS,
|
|
subsystem = LOG_SUBSYSTEM_SWIFT_STORAGE,
|
|
bucket = %bucket,
|
|
error = %err,
|
|
reason = if unreadable { "unreadable_persisted_config" } else { "storage_failure" },
|
|
result = "failed",
|
|
"swift bucket tagging update failed"
|
|
);
|
|
// A Swift-only client has no way to repair this itself, so say what
|
|
// happened and name the remedy instead of a bare storage error.
|
|
return Err(if unreadable {
|
|
SwiftError::Conflict(format!(
|
|
"The persisted tagging configuration for container store '{bucket}' cannot be parsed, so metadata cannot be updated without discarding it. Reset it with the S3 DeleteBucketTagging API."
|
|
))
|
|
} else {
|
|
SwiftError::InternalServerError("Metadata update operation failed".to_string())
|
|
});
|
|
}
|
|
|
|
if let Some(notification_sys) = get_global_notification_sys() {
|
|
tokio::spawn(async move {
|
|
if let Err(err) = notification_sys.load_bucket_metadata(&bucket).await {
|
|
tracing::warn!(bucket = %bucket, error = %err, "failed to notify peers after swift bucket tagging update");
|
|
}
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn get_swift_bucket_usage() -> SwiftStorageResult<Option<HashMap<String, (u64, u64)>>> {
|
|
let Some(store) = resolve_swift_object_store_handle() else {
|
|
return Ok(None);
|
|
};
|
|
let mut data_usage = rustfs_ecstore::api::data_usage::load_data_usage_from_backend_cached(store).await?;
|
|
rustfs_ecstore::api::data_usage::apply_bucket_usage_memory_overlay(&mut data_usage).await;
|
|
Ok(Some(
|
|
data_usage
|
|
.buckets_usage
|
|
.into_iter()
|
|
.map(|(bucket, usage)| (bucket, (usage.objects_count, usage.size)))
|
|
.collect(),
|
|
))
|
|
}
|