mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 08:49:26 +00:00
refactor(lifecycle): extract core rule contracts (#4258)
This commit is contained in:
Generated
+20
@@ -9073,6 +9073,7 @@ dependencies = [
|
|||||||
"rustfs-filemeta",
|
"rustfs-filemeta",
|
||||||
"rustfs-io-metrics",
|
"rustfs-io-metrics",
|
||||||
"rustfs-kms",
|
"rustfs-kms",
|
||||||
|
"rustfs-lifecycle",
|
||||||
"rustfs-lock",
|
"rustfs-lock",
|
||||||
"rustfs-madmin",
|
"rustfs-madmin",
|
||||||
"rustfs-object-capacity",
|
"rustfs-object-capacity",
|
||||||
@@ -9360,6 +9361,25 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustfs-lifecycle"
|
||||||
|
version = "1.0.0-beta.8"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"rustfs-common",
|
||||||
|
"rustfs-config",
|
||||||
|
"rustfs-replication",
|
||||||
|
"rustfs-storage-api",
|
||||||
|
"s3s",
|
||||||
|
"serial_test",
|
||||||
|
"temp-env",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-lock"
|
name = "rustfs-lock"
|
||||||
version = "1.0.0-beta.8"
|
version = "1.0.0-beta.8"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ members = [
|
|||||||
"crates/heal", # Erasure set and object healing
|
"crates/heal", # Erasure set and object healing
|
||||||
"crates/iam", # Identity and Access Management
|
"crates/iam", # Identity and Access Management
|
||||||
"crates/keystone", # OpenStack Keystone integration
|
"crates/keystone", # OpenStack Keystone integration
|
||||||
|
"crates/lifecycle", # Lifecycle rule evaluation contracts
|
||||||
"crates/kms", # Key Management Service
|
"crates/kms", # Key Management Service
|
||||||
"crates/lock", # Distributed locking implementation
|
"crates/lock", # Distributed locking implementation
|
||||||
"crates/madmin", # Management dashboard and admin API interface
|
"crates/madmin", # Management dashboard and admin API interface
|
||||||
@@ -97,6 +98,7 @@ rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-beta.8" }
|
|||||||
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-beta.8" }
|
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-beta.8" }
|
||||||
rustfs-iam = { path = "crates/iam", version = "1.0.0-beta.8" }
|
rustfs-iam = { path = "crates/iam", version = "1.0.0-beta.8" }
|
||||||
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-beta.8" }
|
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-beta.8" }
|
||||||
|
rustfs-lifecycle = { path = "crates/lifecycle", version = "1.0.0-beta.8" }
|
||||||
rustfs-kms = { path = "crates/kms", version = "1.0.0-beta.8" }
|
rustfs-kms = { path = "crates/kms", version = "1.0.0-beta.8" }
|
||||||
rustfs-lock = { path = "crates/lock", version = "1.0.0-beta.8" }
|
rustfs-lock = { path = "crates/lock", version = "1.0.0-beta.8" }
|
||||||
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-beta.8" }
|
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-beta.8" }
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ rustfs-common.workspace = true
|
|||||||
rustfs-policy.workspace = true
|
rustfs-policy.workspace = true
|
||||||
rustfs-protos.workspace = true
|
rustfs-protos.workspace = true
|
||||||
rustfs-replication.workspace = true
|
rustfs-replication.workspace = true
|
||||||
|
rustfs-lifecycle.workspace = true
|
||||||
rustfs-kms.workspace = true
|
rustfs-kms.workspace = true
|
||||||
rustfs-s3-types = { workspace = true }
|
rustfs-s3-types = { workspace = true }
|
||||||
rustfs-data-usage.workspace = true
|
rustfs-data-usage.workspace = true
|
||||||
|
|||||||
@@ -1965,7 +1965,7 @@ pub async fn enqueue_immediate_expiry(oi: &ObjectInfo, src: LcEventSrc) {
|
|||||||
};
|
};
|
||||||
let object_opts = object_infos
|
let object_opts = object_infos
|
||||||
.iter()
|
.iter()
|
||||||
.map(ObjectOpts::from_object_info)
|
.map(lifecycle::object_opts_from_object_info)
|
||||||
.collect::<Vec<ObjectOpts>>();
|
.collect::<Vec<ObjectOpts>>();
|
||||||
let Ok(events) = Evaluator::new(Arc::new(lifecycle))
|
let Ok(events) = Evaluator::new(Arc::new(lifecycle))
|
||||||
.with_lock_retention(lock_config)
|
.with_lock_retention(lock_config)
|
||||||
@@ -2086,7 +2086,7 @@ async fn enqueue_expiry_for_existing_object_group(
|
|||||||
|
|
||||||
let object_opts = object_infos
|
let object_opts = object_infos
|
||||||
.iter()
|
.iter()
|
||||||
.map(ObjectOpts::from_object_info)
|
.map(lifecycle::object_opts_from_object_info)
|
||||||
.collect::<Vec<ObjectOpts>>();
|
.collect::<Vec<ObjectOpts>>();
|
||||||
let events = match Evaluator::new(context.lc.clone())
|
let events = match Evaluator::new(context.lc.clone())
|
||||||
.with_lock_retention(context.lock_config.clone())
|
.with_lock_retention(context.lock_config.clone())
|
||||||
@@ -2559,7 +2559,7 @@ pub trait LifecycleOps {
|
|||||||
|
|
||||||
impl LifecycleOps for ObjectInfo {
|
impl LifecycleOps for ObjectInfo {
|
||||||
fn to_lifecycle_opts(&self) -> lifecycle::ObjectOpts {
|
fn to_lifecycle_opts(&self) -> lifecycle::ObjectOpts {
|
||||||
lifecycle::ObjectOpts::from_object_info(self)
|
lifecycle::object_opts_from_object_info(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_remote(&self) -> bool {
|
fn is_remote(&self) -> bool {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,406 +12,4 @@
|
|||||||
// 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 std::sync::Arc;
|
pub use rustfs_lifecycle::Evaluator;
|
||||||
|
|
||||||
use s3s::dto::{BucketLifecycleConfiguration, ObjectLockConfiguration, ObjectLockEnabled};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use super::object_lock_boundary;
|
|
||||||
use crate::bucket::lifecycle::lifecycle::{Event, Lifecycle, ObjectOpts};
|
|
||||||
use crate::bucket::lifecycle::replication_sink::{self, LifecycleReplicationConfig};
|
|
||||||
use rustfs_common::metrics::IlmAction;
|
|
||||||
|
|
||||||
const LOG_COMPONENT_ECSTORE: &str = "ecstore";
|
|
||||||
const LOG_SUBSYSTEM_LIFECYCLE: &str = "lifecycle";
|
|
||||||
const EVENT_LIFECYCLE_VERSION_SCAN_SKIPPED: &str = "lifecycle_version_scan_skipped";
|
|
||||||
|
|
||||||
/// Evaluator - evaluates lifecycle policy on objects for the given lifecycle
|
|
||||||
/// configuration and lock retention configuration.
|
|
||||||
pub struct Evaluator {
|
|
||||||
policy: Arc<BucketLifecycleConfiguration>,
|
|
||||||
lock_retention: Option<Arc<ObjectLockConfiguration>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Evaluator {
|
|
||||||
/// NewEvaluator - creates a new evaluator with the given lifecycle
|
|
||||||
pub fn new(policy: Arc<BucketLifecycleConfiguration>) -> Self {
|
|
||||||
Self {
|
|
||||||
policy,
|
|
||||||
lock_retention: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WithLockRetention - sets the lock retention configuration for the evaluator
|
|
||||||
pub fn with_lock_retention(mut self, lr: Option<Arc<ObjectLockConfiguration>>) -> Self {
|
|
||||||
self.lock_retention = lr;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WithReplicationConfig is retained for caller compatibility.
|
|
||||||
/// Lifecycle replication guards are evaluated from per-object replication state.
|
|
||||||
pub fn with_replication_config(self, _rcfg: Option<Arc<LifecycleReplicationConfig>>) -> Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// IsPendingReplication checks if the object is pending replication.
|
|
||||||
pub fn is_pending_replication(&self, obj: &ObjectOpts) -> bool {
|
|
||||||
replication_sink::has_pending_lifecycle_replication(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn any_version_has_pending_replication(&self, objs: &[ObjectOpts]) -> bool {
|
|
||||||
objs.iter().any(|obj| self.is_pending_replication(obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// IsObjectLocked checks if it is appropriate to remove an
|
|
||||||
/// object according to locking configuration when this is lifecycle/bucket quota asking.
|
|
||||||
/// Uses the common `is_object_locked_by_metadata` function for consistency.
|
|
||||||
pub fn is_object_locked(&self, obj: &ObjectOpts) -> bool {
|
|
||||||
// First check if object lock is enabled for this bucket
|
|
||||||
if self.lock_retention.as_ref().is_none_or(|v| {
|
|
||||||
v.object_lock_enabled
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|v| v.as_str() != ObjectLockEnabled::ENABLED)
|
|
||||||
}) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the common function to check if the object is locked
|
|
||||||
object_lock_boundary::is_object_locked_by_metadata(&obj.user_defined, obj.delete_marker)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// eval will return a lifecycle event for each object in objs for a given time.
|
|
||||||
async fn eval_inner(&self, objs: &[ObjectOpts], now: OffsetDateTime) -> Vec<Event> {
|
|
||||||
let mut events = vec![Event::default(); objs.len()];
|
|
||||||
let mut newer_noncurrent_versions = 0;
|
|
||||||
|
|
||||||
'top_loop: {
|
|
||||||
for (i, obj) in objs.iter().enumerate() {
|
|
||||||
let mut event = self.policy.eval_inner(obj, now, newer_noncurrent_versions).await;
|
|
||||||
if replication_sink::lifecycle_action_waits_for_replication(event.action) && self.is_pending_replication(obj) {
|
|
||||||
event = Event::default();
|
|
||||||
}
|
|
||||||
match event.action {
|
|
||||||
IlmAction::DeleteAllVersionsAction | IlmAction::DelMarkerDeleteAllVersionsAction => {
|
|
||||||
// Skip if bucket has object locking enabled; To prevent the
|
|
||||||
// possibility of violating an object retention on one of the
|
|
||||||
// noncurrent versions of this object.
|
|
||||||
if self.lock_retention.as_ref().is_some_and(|v| {
|
|
||||||
v.object_lock_enabled
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|v| v.as_str() == ObjectLockEnabled::ENABLED)
|
|
||||||
}) || self.any_version_has_pending_replication(objs)
|
|
||||||
{
|
|
||||||
event = Event::default();
|
|
||||||
} else {
|
|
||||||
// No need to evaluate remaining versions' lifecycle
|
|
||||||
// events after DeleteAllVersionsAction*
|
|
||||||
events[i] = event;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
event = EVENT_LIFECYCLE_VERSION_SCAN_SKIPPED,
|
|
||||||
component = LOG_COMPONENT_ECSTORE,
|
|
||||||
subsystem = LOG_SUBSYSTEM_LIFECYCLE,
|
|
||||||
reason = "delete_all_versions_action",
|
|
||||||
action = ?events[i].action,
|
|
||||||
"Skipped remaining lifecycle version scan"
|
|
||||||
);
|
|
||||||
|
|
||||||
break 'top_loop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IlmAction::DeleteVersionAction | IlmAction::DeleteRestoredVersionAction => {
|
|
||||||
// Defensive code, should never happen
|
|
||||||
if obj.version_id.is_none_or(|v| v.is_nil()) {
|
|
||||||
event.action = IlmAction::NoneAction;
|
|
||||||
}
|
|
||||||
if self.is_object_locked(obj) {
|
|
||||||
event = Event::default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !obj.is_latest {
|
|
||||||
match event.action {
|
|
||||||
IlmAction::DeleteVersionAction => {
|
|
||||||
// this noncurrent version will be expired, nothing to add
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// this noncurrent version will be spared
|
|
||||||
newer_noncurrent_versions += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
events[i] = event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
events
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Eval will return a lifecycle event for each object in objs
|
|
||||||
pub async fn eval(&self, objs: &[ObjectOpts]) -> Result<Vec<Event>, std::io::Error> {
|
|
||||||
if objs.is_empty() {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
if objs.len() != objs[0].num_versions {
|
|
||||||
return Err(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidInput,
|
|
||||||
format!("number of versions mismatch, expected {}, got {}", objs[0].num_versions, objs.len()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(self.eval_inner(objs, OffsetDateTime::now_utc()).await)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use rustfs_common::metrics::IlmAction;
|
|
||||||
use s3s::dto::{
|
|
||||||
BucketLifecycleConfiguration, ExpirationStatus, LifecycleExpiration, LifecycleRule, Transition, TransitionStorageClass,
|
|
||||||
};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::bucket::lifecycle::replication_sink::{ReplicationStatusType, VersionPurgeStatusType};
|
|
||||||
fn expired_marker_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
|
||||||
Arc::new(BucketLifecycleConfiguration {
|
|
||||||
expiry_updated_at: None,
|
|
||||||
rules: vec![LifecycleRule {
|
|
||||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
|
||||||
expiration: Some(LifecycleExpiration {
|
|
||||||
expired_object_delete_marker: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
abort_incomplete_multipart_upload: None,
|
|
||||||
del_marker_expiration: None,
|
|
||||||
filter: None,
|
|
||||||
id: Some("expired-marker".to_string()),
|
|
||||||
noncurrent_version_expiration: None,
|
|
||||||
noncurrent_version_transitions: None,
|
|
||||||
prefix: None,
|
|
||||||
transitions: None,
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn latest_expiration_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
|
||||||
Arc::new(BucketLifecycleConfiguration {
|
|
||||||
expiry_updated_at: None,
|
|
||||||
rules: vec![LifecycleRule {
|
|
||||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
|
||||||
expiration: Some(LifecycleExpiration {
|
|
||||||
days: Some(1),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
abort_incomplete_multipart_upload: None,
|
|
||||||
del_marker_expiration: None,
|
|
||||||
filter: None,
|
|
||||||
id: Some("expire-current".to_string()),
|
|
||||||
noncurrent_version_expiration: None,
|
|
||||||
noncurrent_version_transitions: None,
|
|
||||||
prefix: None,
|
|
||||||
transitions: None,
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn latest_transition_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
|
||||||
Arc::new(BucketLifecycleConfiguration {
|
|
||||||
expiry_updated_at: None,
|
|
||||||
rules: vec![LifecycleRule {
|
|
||||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
|
||||||
expiration: None,
|
|
||||||
abort_incomplete_multipart_upload: None,
|
|
||||||
del_marker_expiration: None,
|
|
||||||
filter: None,
|
|
||||||
id: Some("transition-current".to_string()),
|
|
||||||
noncurrent_version_expiration: None,
|
|
||||||
noncurrent_version_transitions: None,
|
|
||||||
prefix: None,
|
|
||||||
transitions: Some(vec![Transition {
|
|
||||||
days: Some(1),
|
|
||||||
date: None,
|
|
||||||
storage_class: Some(TransitionStorageClass::from_static("WARM")),
|
|
||||||
}]),
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn all_versions_expiration_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
|
||||||
Arc::new(BucketLifecycleConfiguration {
|
|
||||||
expiry_updated_at: None,
|
|
||||||
rules: vec![LifecycleRule {
|
|
||||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
|
||||||
expiration: Some(LifecycleExpiration {
|
|
||||||
days: Some(1),
|
|
||||||
expired_object_all_versions: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
abort_incomplete_multipart_upload: None,
|
|
||||||
del_marker_expiration: None,
|
|
||||||
filter: None,
|
|
||||||
id: Some("delete-all".to_string()),
|
|
||||||
noncurrent_version_expiration: None,
|
|
||||||
noncurrent_version_transitions: None,
|
|
||||||
prefix: None,
|
|
||||||
transitions: None,
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn object_opts(replication_status: ReplicationStatusType, version_purge_status: VersionPurgeStatusType) -> ObjectOpts {
|
|
||||||
ObjectOpts {
|
|
||||||
name: "logs/object".to_string(),
|
|
||||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).expect("valid fixed test timestamp")),
|
|
||||||
version_id: Some(Uuid::new_v4()),
|
|
||||||
is_latest: true,
|
|
||||||
delete_marker: true,
|
|
||||||
num_versions: 1,
|
|
||||||
replication_status,
|
|
||||||
version_purge_status,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_object_opts(replication_status: ReplicationStatusType) -> ObjectOpts {
|
|
||||||
ObjectOpts {
|
|
||||||
name: "logs/object".to_string(),
|
|
||||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).expect("valid fixed test timestamp")),
|
|
||||||
version_id: Some(Uuid::new_v4()),
|
|
||||||
is_latest: true,
|
|
||||||
num_versions: 1,
|
|
||||||
replication_status,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn versioned_object_opts(replication_status: ReplicationStatusType, is_latest: bool) -> ObjectOpts {
|
|
||||||
ObjectOpts {
|
|
||||||
num_versions: 2,
|
|
||||||
is_latest,
|
|
||||||
..current_object_opts(replication_status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_allows_expired_delete_marker_after_replication_completed() {
|
|
||||||
let evaluator = Evaluator::new(expired_marker_lifecycle());
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[object_opts(
|
|
||||||
ReplicationStatusType::Completed,
|
|
||||||
VersionPurgeStatusType::Complete,
|
|
||||||
)])
|
|
||||||
.await
|
|
||||||
.expect("completed replication should allow lifecycle evaluation");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::DeleteVersionAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_skips_expired_delete_marker_while_replication_pending() {
|
|
||||||
let evaluator = Evaluator::new(expired_marker_lifecycle());
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[object_opts(ReplicationStatusType::Pending, VersionPurgeStatusType::default())])
|
|
||||||
.await
|
|
||||||
.expect("pending replication should still return a lifecycle decision");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::NoneAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_skips_expired_delete_marker_while_version_purge_pending() {
|
|
||||||
let evaluator = Evaluator::new(expired_marker_lifecycle());
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[object_opts(ReplicationStatusType::Completed, VersionPurgeStatusType::Pending)])
|
|
||||||
.await
|
|
||||||
.expect("pending version purge should still return a lifecycle decision");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::NoneAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_skips_latest_expiration_while_replication_failed() {
|
|
||||||
let evaluator = Evaluator::new(latest_expiration_lifecycle());
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[current_object_opts(ReplicationStatusType::Failed)])
|
|
||||||
.await
|
|
||||||
.expect("failed replication should still return a lifecycle decision");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::NoneAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_allows_latest_expiration_after_replication_completed() {
|
|
||||||
let evaluator = Evaluator::new(latest_expiration_lifecycle());
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[current_object_opts(ReplicationStatusType::Completed)])
|
|
||||||
.await
|
|
||||||
.expect("completed replication should allow latest expiration");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::DeleteAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_skips_transition_while_replication_pending() {
|
|
||||||
let evaluator = Evaluator::new(latest_transition_lifecycle());
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[current_object_opts(ReplicationStatusType::Pending)])
|
|
||||||
.await
|
|
||||||
.expect("pending replication should still return a lifecycle decision");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::NoneAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_allows_transition_after_replication_completed() {
|
|
||||||
let evaluator = Evaluator::new(latest_transition_lifecycle());
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[current_object_opts(ReplicationStatusType::Completed)])
|
|
||||||
.await
|
|
||||||
.expect("completed replication should allow transition");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::TransitionAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_skips_delete_all_versions_when_any_version_replication_pending() {
|
|
||||||
let evaluator = Evaluator::new(all_versions_expiration_lifecycle());
|
|
||||||
let latest = versioned_object_opts(ReplicationStatusType::Completed, true);
|
|
||||||
let noncurrent = versioned_object_opts(ReplicationStatusType::Pending, false);
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[latest, noncurrent])
|
|
||||||
.await
|
|
||||||
.expect("pending noncurrent replication should still return lifecycle decisions");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::NoneAction);
|
|
||||||
assert_eq!(events[1].action, IlmAction::NoneAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn evaluator_allows_delete_all_versions_when_all_versions_replication_completed() {
|
|
||||||
let evaluator = Evaluator::new(all_versions_expiration_lifecycle());
|
|
||||||
let latest = versioned_object_opts(ReplicationStatusType::Completed, true);
|
|
||||||
let noncurrent = versioned_object_opts(ReplicationStatusType::Completed, false);
|
|
||||||
|
|
||||||
let events = evaluator
|
|
||||||
.eval(&[latest, noncurrent])
|
|
||||||
.await
|
|
||||||
.expect("completed replication should allow delete-all lifecycle decision");
|
|
||||||
|
|
||||||
assert_eq!(events[0].action, IlmAction::DeleteAllVersionsAction);
|
|
||||||
assert_eq!(events[1].action, IlmAction::NoneAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::bucket::object_lock::objectlock_sys::{self, ObjectLockBlockReason};
|
|||||||
use crate::object_api::ObjectInfo;
|
use crate::object_api::ObjectInfo;
|
||||||
|
|
||||||
pub(crate) fn is_object_locked_by_metadata(user_defined: &HashMap<String, String>, is_delete_marker: bool) -> bool {
|
pub(crate) fn is_object_locked_by_metadata(user_defined: &HashMap<String, String>, is_delete_marker: bool) -> bool {
|
||||||
objectlock_sys::is_object_locked_by_metadata(user_defined, is_delete_marker)
|
rustfs_lifecycle::object_lock::is_object_locked_by_metadata(user_defined, is_delete_marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn check_object_lock_for_deletion(
|
pub(crate) async fn check_object_lock_for_deletion(
|
||||||
|
|||||||
@@ -11,193 +11,5 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// 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 super::tagging_boundary;
|
|
||||||
use s3s::dto::{LifecycleRuleAndOperator, LifecycleRuleFilter, NoncurrentVersionTransition, Tag, Transition};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
const _ERR_TRANSITION_INVALID_DAYS: &str = "Days must be 0 or greater when used with Transition";
|
pub use rustfs_lifecycle::rule::{Filter, NoncurrentVersionTransitionOps, TransitionOps};
|
||||||
const _ERR_TRANSITION_INVALID_DATE: &str = "Date must be provided in ISO 8601 format";
|
|
||||||
const ERR_TRANSITION_INVALID: &str =
|
|
||||||
"Exactly one of Days (0 or greater) or Date (positive ISO 8601 format) should be present in Transition.";
|
|
||||||
const _ERR_TRANSITION_DATE_NOT_MIDNIGHT: &str = "'Date' must be at midnight GMT";
|
|
||||||
|
|
||||||
pub trait Filter {
|
|
||||||
fn test_tags(&self, user_tags: &str) -> bool;
|
|
||||||
fn by_size(&self, sz: i64) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Filter for LifecycleRuleFilter {
|
|
||||||
fn test_tags(&self, user_tags: &str) -> bool {
|
|
||||||
if !requires_tag_matching(self) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_tags = tagging_boundary::decode_tags_to_map(user_tags);
|
|
||||||
|
|
||||||
self.tag.as_ref().is_none_or(|tag| tag_matches(tag, &user_tags))
|
|
||||||
&& self.and.as_ref().is_none_or(|and| and_tags_match(and, &user_tags))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn by_size(&self, sz: i64) -> bool {
|
|
||||||
let sz = sz.max(0);
|
|
||||||
|
|
||||||
self.object_size_greater_than.is_none_or(|min| sz > min)
|
|
||||||
&& self.object_size_less_than.is_none_or(|max| sz < max)
|
|
||||||
&& self.and.as_ref().is_none_or(|and| and_size_matches(and, sz))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requires_tag_matching(filter: &LifecycleRuleFilter) -> bool {
|
|
||||||
filter.tag.is_some()
|
|
||||||
|| filter
|
|
||||||
.and
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|and| and.tags.as_ref())
|
|
||||||
.is_some_and(|tags| !tags.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tag_matches(tag: &Tag, user_tags: &std::collections::HashMap<String, String>) -> bool {
|
|
||||||
let Some(key) = tag.key.as_deref() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let Some(value) = tag.value.as_deref() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
user_tags.get(key).is_some_and(|actual| actual == value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn and_tags_match(and: &LifecycleRuleAndOperator, user_tags: &std::collections::HashMap<String, String>) -> bool {
|
|
||||||
and.tags
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|tags| tags.iter().all(|tag| tag_matches(tag, user_tags)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn and_size_matches(and: &LifecycleRuleAndOperator, sz: i64) -> bool {
|
|
||||||
and.object_size_greater_than.is_none_or(|min| sz > min) && and.object_size_less_than.is_none_or(|max| sz < max)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TransitionOps {
|
|
||||||
fn validate(&self) -> Result<(), std::io::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait NoncurrentVersionTransitionOps {
|
|
||||||
fn validate(&self) -> Result<(), std::io::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_midnight(date: OffsetDateTime) -> bool {
|
|
||||||
date.hour() == 0 && date.minute() == 0 && date.second() == 0 && date.nanosecond() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransitionOps for Transition {
|
|
||||||
fn validate(&self) -> Result<(), std::io::Error> {
|
|
||||||
if self
|
|
||||||
.storage_class
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|storage_class| storage_class.as_str().is_empty())
|
|
||||||
{
|
|
||||||
return Err(std::io::Error::other("ERR_XML_NOT_WELL_FORMED"));
|
|
||||||
}
|
|
||||||
if self.date.is_some() == self.days.is_some() {
|
|
||||||
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
|
||||||
}
|
|
||||||
if self.days.is_some_and(|days| days < 0) {
|
|
||||||
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
|
||||||
}
|
|
||||||
if let Some(date) = self.date.clone() {
|
|
||||||
let date = OffsetDateTime::from(date);
|
|
||||||
if date.unix_timestamp() <= 0 || !is_midnight(date) {
|
|
||||||
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NoncurrentVersionTransitionOps for NoncurrentVersionTransition {
|
|
||||||
fn validate(&self) -> Result<(), std::io::Error> {
|
|
||||||
if self
|
|
||||||
.storage_class
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|storage_class| storage_class.as_str().is_empty())
|
|
||||||
{
|
|
||||||
return Err(std::io::Error::other("ERR_XML_NOT_WELL_FORMED"));
|
|
||||||
}
|
|
||||||
if self.noncurrent_days.is_none() {
|
|
||||||
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
|
||||||
}
|
|
||||||
if self.noncurrent_days.is_some_and(|days| days < 0)
|
|
||||||
|| self.newer_noncurrent_versions.is_some_and(|versions| versions < 0)
|
|
||||||
{
|
|
||||||
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lifecycle_rule_filter_matches_single_tag() {
|
|
||||||
let filter = LifecycleRuleFilter {
|
|
||||||
tag: Some(Tag {
|
|
||||||
key: Some("env".to_string()),
|
|
||||||
value: Some("prod".to_string()),
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=storage"));
|
|
||||||
assert!(!<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=dev&team=storage"));
|
|
||||||
assert!(!<LifecycleRuleFilter as Filter>::test_tags(&filter, "team=storage"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lifecycle_rule_filter_matches_all_and_tags() {
|
|
||||||
let filter = LifecycleRuleFilter {
|
|
||||||
and: Some(LifecycleRuleAndOperator {
|
|
||||||
tags: Some(vec![
|
|
||||||
Tag {
|
|
||||||
key: Some("env".to_string()),
|
|
||||||
value: Some("prod".to_string()),
|
|
||||||
},
|
|
||||||
Tag {
|
|
||||||
key: Some("team".to_string()),
|
|
||||||
value: Some("storage".to_string()),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=storage"));
|
|
||||||
assert!(!<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=platform"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lifecycle_rule_filter_respects_size_bounds() {
|
|
||||||
let filter = LifecycleRuleFilter {
|
|
||||||
object_size_greater_than: Some(5),
|
|
||||||
object_size_less_than: Some(10),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(!filter.by_size(5));
|
|
||||||
assert!(filter.by_size(6));
|
|
||||||
assert!(!filter.by_size(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lifecycle_rule_filter_without_tag_constraints_accepts_any_tags() {
|
|
||||||
let filter = LifecycleRuleFilter {
|
|
||||||
object_size_greater_than: Some(5),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=storage"));
|
|
||||||
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "rustfs-lifecycle"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
description = "Lifecycle rule evaluation contracts for RustFS."
|
||||||
|
keywords = ["lifecycle", "storage", "rustfs", "Minio"]
|
||||||
|
categories = ["web-programming", "development-tools", "filesystem"]
|
||||||
|
documentation = "https://docs.rs/rustfs-lifecycle/latest/rustfs_lifecycle/"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait.workspace = true
|
||||||
|
rustfs-common.workspace = true
|
||||||
|
rustfs-config = { workspace = true, features = ["constants"] }
|
||||||
|
rustfs-replication.workspace = true
|
||||||
|
rustfs-storage-api.workspace = true
|
||||||
|
s3s.workspace = true
|
||||||
|
time.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
uuid = { workspace = true, features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serial_test.workspace = true
|
||||||
|
temp-env.workspace = true
|
||||||
|
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
doctest = false
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,448 @@
|
|||||||
|
// 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::sync::Arc;
|
||||||
|
|
||||||
|
use s3s::dto::{BucketLifecycleConfiguration, ObjectLockConfiguration, ObjectLockEnabled};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use rustfs_common::metrics::IlmAction;
|
||||||
|
use rustfs_replication::ReplicationStatusType;
|
||||||
|
|
||||||
|
use crate::object_lock;
|
||||||
|
use crate::{Event, Lifecycle, ObjectOpts};
|
||||||
|
|
||||||
|
const LOG_COMPONENT_ECSTORE: &str = "ecstore";
|
||||||
|
const LOG_SUBSYSTEM_LIFECYCLE: &str = "lifecycle";
|
||||||
|
const EVENT_LIFECYCLE_VERSION_SCAN_SKIPPED: &str = "lifecycle_version_scan_skipped";
|
||||||
|
|
||||||
|
/// Evaluator - evaluates lifecycle policy on objects for the given lifecycle
|
||||||
|
/// configuration and lock retention configuration.
|
||||||
|
pub struct Evaluator {
|
||||||
|
policy: Arc<BucketLifecycleConfiguration>,
|
||||||
|
lock_retention: Option<Arc<ObjectLockConfiguration>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Evaluator {
|
||||||
|
/// NewEvaluator - creates a new evaluator with the given lifecycle
|
||||||
|
pub fn new(policy: Arc<BucketLifecycleConfiguration>) -> Self {
|
||||||
|
Self {
|
||||||
|
policy,
|
||||||
|
lock_retention: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WithLockRetention - sets the lock retention configuration for the evaluator
|
||||||
|
pub fn with_lock_retention(mut self, lr: Option<Arc<ObjectLockConfiguration>>) -> Self {
|
||||||
|
self.lock_retention = lr;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WithReplicationConfig is retained for caller compatibility.
|
||||||
|
/// Lifecycle replication guards are evaluated from per-object replication state.
|
||||||
|
pub fn with_replication_config<T>(self, _rcfg: Option<Arc<T>>) -> Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IsPendingReplication checks if the object is pending replication.
|
||||||
|
pub fn is_pending_replication(&self, obj: &ObjectOpts) -> bool {
|
||||||
|
has_pending_lifecycle_replication(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn any_version_has_pending_replication(&self, objs: &[ObjectOpts]) -> bool {
|
||||||
|
objs.iter().any(|obj| self.is_pending_replication(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IsObjectLocked checks if it is appropriate to remove an
|
||||||
|
/// object according to locking configuration when this is lifecycle/bucket quota asking.
|
||||||
|
/// Uses the common `is_object_locked_by_metadata` function for consistency.
|
||||||
|
pub fn is_object_locked(&self, obj: &ObjectOpts) -> bool {
|
||||||
|
// First check if object lock is enabled for this bucket
|
||||||
|
if self.lock_retention.as_ref().is_none_or(|v| {
|
||||||
|
v.object_lock_enabled
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|v| v.as_str() != ObjectLockEnabled::ENABLED)
|
||||||
|
}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the common function to check if the object is locked
|
||||||
|
object_lock::is_object_locked_by_metadata(&obj.user_defined, obj.delete_marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// eval will return a lifecycle event for each object in objs for a given time.
|
||||||
|
async fn eval_inner(&self, objs: &[ObjectOpts], now: OffsetDateTime) -> Vec<Event> {
|
||||||
|
let mut events = vec![Event::default(); objs.len()];
|
||||||
|
let mut newer_noncurrent_versions = 0;
|
||||||
|
|
||||||
|
'top_loop: {
|
||||||
|
for (i, obj) in objs.iter().enumerate() {
|
||||||
|
let mut event = self.policy.eval_inner(obj, now, newer_noncurrent_versions).await;
|
||||||
|
if lifecycle_action_waits_for_replication(event.action) && self.is_pending_replication(obj) {
|
||||||
|
event = Event::default();
|
||||||
|
}
|
||||||
|
match event.action {
|
||||||
|
IlmAction::DeleteAllVersionsAction | IlmAction::DelMarkerDeleteAllVersionsAction => {
|
||||||
|
// Skip if bucket has object locking enabled; To prevent the
|
||||||
|
// possibility of violating an object retention on one of the
|
||||||
|
// noncurrent versions of this object.
|
||||||
|
if self.lock_retention.as_ref().is_some_and(|v| {
|
||||||
|
v.object_lock_enabled
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|v| v.as_str() == ObjectLockEnabled::ENABLED)
|
||||||
|
}) || self.any_version_has_pending_replication(objs)
|
||||||
|
{
|
||||||
|
event = Event::default();
|
||||||
|
} else {
|
||||||
|
// No need to evaluate remaining versions' lifecycle
|
||||||
|
// events after DeleteAllVersionsAction*
|
||||||
|
events[i] = event;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
event = EVENT_LIFECYCLE_VERSION_SCAN_SKIPPED,
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_LIFECYCLE,
|
||||||
|
reason = "delete_all_versions_action",
|
||||||
|
action = ?events[i].action,
|
||||||
|
"Skipped remaining lifecycle version scan"
|
||||||
|
);
|
||||||
|
|
||||||
|
break 'top_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IlmAction::DeleteVersionAction | IlmAction::DeleteRestoredVersionAction => {
|
||||||
|
// Defensive code, should never happen
|
||||||
|
if obj.version_id.is_none_or(|v| v.is_nil()) {
|
||||||
|
event.action = IlmAction::NoneAction;
|
||||||
|
}
|
||||||
|
if self.is_object_locked(obj) {
|
||||||
|
event = Event::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !obj.is_latest {
|
||||||
|
match event.action {
|
||||||
|
IlmAction::DeleteVersionAction => {
|
||||||
|
// this noncurrent version will be expired, nothing to add
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// this noncurrent version will be spared
|
||||||
|
newer_noncurrent_versions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events[i] = event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eval will return a lifecycle event for each object in objs
|
||||||
|
pub async fn eval(&self, objs: &[ObjectOpts]) -> Result<Vec<Event>, std::io::Error> {
|
||||||
|
if objs.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
if objs.len() != objs[0].num_versions {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
format!("number of versions mismatch, expected {}, got {}", objs[0].num_versions, objs.len()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(self.eval_inner(objs, OffsetDateTime::now_utc()).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_pending_version_purge(obj: &ObjectOpts) -> bool {
|
||||||
|
obj.version_purge_status.is_pending()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_pending_object_replication(obj: &ObjectOpts) -> bool {
|
||||||
|
replication_status_blocks_lifecycle(&obj.replication_status)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_pending_lifecycle_replication(obj: &ObjectOpts) -> bool {
|
||||||
|
has_pending_object_replication(obj) || has_pending_version_purge(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replication_status_blocks_lifecycle(status: &ReplicationStatusType) -> bool {
|
||||||
|
matches!(status, ReplicationStatusType::Pending | ReplicationStatusType::Failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lifecycle_action_waits_for_replication(action: IlmAction) -> bool {
|
||||||
|
matches!(
|
||||||
|
action,
|
||||||
|
IlmAction::DeleteAction
|
||||||
|
| IlmAction::DeleteVersionAction
|
||||||
|
| IlmAction::DeleteRestoredAction
|
||||||
|
| IlmAction::DeleteRestoredVersionAction
|
||||||
|
| IlmAction::DeleteAllVersionsAction
|
||||||
|
| IlmAction::DelMarkerDeleteAllVersionsAction
|
||||||
|
| IlmAction::TransitionAction
|
||||||
|
| IlmAction::TransitionVersionAction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rustfs_common::metrics::IlmAction;
|
||||||
|
use s3s::dto::{
|
||||||
|
BucketLifecycleConfiguration, ExpirationStatus, LifecycleExpiration, LifecycleRule, Transition, TransitionStorageClass,
|
||||||
|
};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use rustfs_replication::{ReplicationStatusType, VersionPurgeStatusType};
|
||||||
|
fn expired_marker_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
||||||
|
Arc::new(BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: None,
|
||||||
|
rules: vec![LifecycleRule {
|
||||||
|
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||||
|
expiration: Some(LifecycleExpiration {
|
||||||
|
expired_object_delete_marker: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
abort_incomplete_multipart_upload: None,
|
||||||
|
del_marker_expiration: None,
|
||||||
|
filter: None,
|
||||||
|
id: Some("expired-marker".to_string()),
|
||||||
|
noncurrent_version_expiration: None,
|
||||||
|
noncurrent_version_transitions: None,
|
||||||
|
prefix: None,
|
||||||
|
transitions: None,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latest_expiration_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
||||||
|
Arc::new(BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: None,
|
||||||
|
rules: vec![LifecycleRule {
|
||||||
|
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||||
|
expiration: Some(LifecycleExpiration {
|
||||||
|
days: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
abort_incomplete_multipart_upload: None,
|
||||||
|
del_marker_expiration: None,
|
||||||
|
filter: None,
|
||||||
|
id: Some("expire-current".to_string()),
|
||||||
|
noncurrent_version_expiration: None,
|
||||||
|
noncurrent_version_transitions: None,
|
||||||
|
prefix: None,
|
||||||
|
transitions: None,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latest_transition_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
||||||
|
Arc::new(BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: None,
|
||||||
|
rules: vec![LifecycleRule {
|
||||||
|
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||||
|
expiration: None,
|
||||||
|
abort_incomplete_multipart_upload: None,
|
||||||
|
del_marker_expiration: None,
|
||||||
|
filter: None,
|
||||||
|
id: Some("transition-current".to_string()),
|
||||||
|
noncurrent_version_expiration: None,
|
||||||
|
noncurrent_version_transitions: None,
|
||||||
|
prefix: None,
|
||||||
|
transitions: Some(vec![Transition {
|
||||||
|
days: Some(1),
|
||||||
|
date: None,
|
||||||
|
storage_class: Some(TransitionStorageClass::from_static("WARM")),
|
||||||
|
}]),
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_versions_expiration_lifecycle() -> Arc<BucketLifecycleConfiguration> {
|
||||||
|
Arc::new(BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: None,
|
||||||
|
rules: vec![LifecycleRule {
|
||||||
|
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||||
|
expiration: Some(LifecycleExpiration {
|
||||||
|
days: Some(1),
|
||||||
|
expired_object_all_versions: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
abort_incomplete_multipart_upload: None,
|
||||||
|
del_marker_expiration: None,
|
||||||
|
filter: None,
|
||||||
|
id: Some("delete-all".to_string()),
|
||||||
|
noncurrent_version_expiration: None,
|
||||||
|
noncurrent_version_transitions: None,
|
||||||
|
prefix: None,
|
||||||
|
transitions: None,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object_opts(replication_status: ReplicationStatusType, version_purge_status: VersionPurgeStatusType) -> ObjectOpts {
|
||||||
|
ObjectOpts {
|
||||||
|
name: "logs/object".to_string(),
|
||||||
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).expect("valid fixed test timestamp")),
|
||||||
|
version_id: Some(Uuid::new_v4()),
|
||||||
|
is_latest: true,
|
||||||
|
delete_marker: true,
|
||||||
|
num_versions: 1,
|
||||||
|
replication_status,
|
||||||
|
version_purge_status,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_object_opts(replication_status: ReplicationStatusType) -> ObjectOpts {
|
||||||
|
ObjectOpts {
|
||||||
|
name: "logs/object".to_string(),
|
||||||
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).expect("valid fixed test timestamp")),
|
||||||
|
version_id: Some(Uuid::new_v4()),
|
||||||
|
is_latest: true,
|
||||||
|
num_versions: 1,
|
||||||
|
replication_status,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn versioned_object_opts(replication_status: ReplicationStatusType, is_latest: bool) -> ObjectOpts {
|
||||||
|
ObjectOpts {
|
||||||
|
num_versions: 2,
|
||||||
|
is_latest,
|
||||||
|
..current_object_opts(replication_status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_allows_expired_delete_marker_after_replication_completed() {
|
||||||
|
let evaluator = Evaluator::new(expired_marker_lifecycle());
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[object_opts(
|
||||||
|
ReplicationStatusType::Completed,
|
||||||
|
VersionPurgeStatusType::Complete,
|
||||||
|
)])
|
||||||
|
.await
|
||||||
|
.expect("completed replication should allow lifecycle evaluation");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::DeleteVersionAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_skips_expired_delete_marker_while_replication_pending() {
|
||||||
|
let evaluator = Evaluator::new(expired_marker_lifecycle());
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[object_opts(ReplicationStatusType::Pending, VersionPurgeStatusType::default())])
|
||||||
|
.await
|
||||||
|
.expect("pending replication should still return a lifecycle decision");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::NoneAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_skips_expired_delete_marker_while_version_purge_pending() {
|
||||||
|
let evaluator = Evaluator::new(expired_marker_lifecycle());
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[object_opts(ReplicationStatusType::Completed, VersionPurgeStatusType::Pending)])
|
||||||
|
.await
|
||||||
|
.expect("pending version purge should still return a lifecycle decision");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::NoneAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_skips_latest_expiration_while_replication_failed() {
|
||||||
|
let evaluator = Evaluator::new(latest_expiration_lifecycle());
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[current_object_opts(ReplicationStatusType::Failed)])
|
||||||
|
.await
|
||||||
|
.expect("failed replication should still return a lifecycle decision");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::NoneAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_allows_latest_expiration_after_replication_completed() {
|
||||||
|
let evaluator = Evaluator::new(latest_expiration_lifecycle());
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[current_object_opts(ReplicationStatusType::Completed)])
|
||||||
|
.await
|
||||||
|
.expect("completed replication should allow latest expiration");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::DeleteAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_skips_transition_while_replication_pending() {
|
||||||
|
let evaluator = Evaluator::new(latest_transition_lifecycle());
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[current_object_opts(ReplicationStatusType::Pending)])
|
||||||
|
.await
|
||||||
|
.expect("pending replication should still return a lifecycle decision");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::NoneAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_allows_transition_after_replication_completed() {
|
||||||
|
let evaluator = Evaluator::new(latest_transition_lifecycle());
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[current_object_opts(ReplicationStatusType::Completed)])
|
||||||
|
.await
|
||||||
|
.expect("completed replication should allow transition");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::TransitionAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_skips_delete_all_versions_when_any_version_replication_pending() {
|
||||||
|
let evaluator = Evaluator::new(all_versions_expiration_lifecycle());
|
||||||
|
let latest = versioned_object_opts(ReplicationStatusType::Completed, true);
|
||||||
|
let noncurrent = versioned_object_opts(ReplicationStatusType::Pending, false);
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[latest, noncurrent])
|
||||||
|
.await
|
||||||
|
.expect("pending noncurrent replication should still return lifecycle decisions");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::NoneAction);
|
||||||
|
assert_eq!(events[1].action, IlmAction::NoneAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn evaluator_allows_delete_all_versions_when_all_versions_replication_completed() {
|
||||||
|
let evaluator = Evaluator::new(all_versions_expiration_lifecycle());
|
||||||
|
let latest = versioned_object_opts(ReplicationStatusType::Completed, true);
|
||||||
|
let noncurrent = versioned_object_opts(ReplicationStatusType::Completed, false);
|
||||||
|
|
||||||
|
let events = evaluator
|
||||||
|
.eval(&[latest, noncurrent])
|
||||||
|
.await
|
||||||
|
.expect("completed replication should allow delete-all lifecycle decision");
|
||||||
|
|
||||||
|
assert_eq!(events[0].action, IlmAction::DeleteAllVersionsAction);
|
||||||
|
assert_eq!(events[1].action, IlmAction::NoneAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
pub mod core;
|
||||||
|
pub mod evaluator;
|
||||||
|
pub mod object_lock;
|
||||||
|
pub mod rule;
|
||||||
|
mod tagging;
|
||||||
|
|
||||||
|
pub use core::*;
|
||||||
|
pub use evaluator::Evaluator;
|
||||||
|
pub use rustfs_common::metrics::IlmAction;
|
||||||
|
pub use rustfs_replication::{ReplicationStatusType, VersionPurgeStatusType};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// 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 s3s::dto::ObjectLockRetentionMode;
|
||||||
|
use s3s::header::{X_AMZ_OBJECT_LOCK_LEGAL_HOLD, X_AMZ_OBJECT_LOCK_MODE, X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE};
|
||||||
|
use time::{OffsetDateTime, format_description};
|
||||||
|
|
||||||
|
pub fn is_object_locked_by_metadata(user_defined: &HashMap<String, String>, is_delete_marker: bool) -> bool {
|
||||||
|
if is_delete_marker {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_defined
|
||||||
|
.get(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str())
|
||||||
|
.is_some_and(|value| value.eq_ignore_ascii_case("ON"))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(mode) = user_defined.get(X_AMZ_OBJECT_LOCK_MODE.as_str()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !is_retention_mode(mode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
user_defined
|
||||||
|
.get(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str())
|
||||||
|
.and_then(|value| OffsetDateTime::parse(value, &format_description::well_known::Iso8601::DEFAULT).ok())
|
||||||
|
.is_some_and(|retain_until| retain_until.unix_timestamp() > OffsetDateTime::now_utc().unix_timestamp())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_retention_mode(mode: &str) -> bool {
|
||||||
|
mode.eq_ignore_ascii_case(ObjectLockRetentionMode::COMPLIANCE)
|
||||||
|
|| mode.eq_ignore_ascii_case(ObjectLockRetentionMode::GOVERNANCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_object_locked_by_metadata_preserves_object_lock_parser_behavior() {
|
||||||
|
let mut user_defined = HashMap::new();
|
||||||
|
user_defined.insert(X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str().to_string(), "ON".to_string());
|
||||||
|
|
||||||
|
assert!(is_object_locked_by_metadata(&user_defined, false));
|
||||||
|
assert!(!is_object_locked_by_metadata(&user_defined, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// 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 s3s::dto::{LifecycleRuleAndOperator, LifecycleRuleFilter, NoncurrentVersionTransition, Tag, Transition};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::tagging;
|
||||||
|
|
||||||
|
const _ERR_TRANSITION_INVALID_DAYS: &str = "Days must be 0 or greater when used with Transition";
|
||||||
|
const _ERR_TRANSITION_INVALID_DATE: &str = "Date must be provided in ISO 8601 format";
|
||||||
|
const ERR_TRANSITION_INVALID: &str =
|
||||||
|
"Exactly one of Days (0 or greater) or Date (positive ISO 8601 format) should be present in Transition.";
|
||||||
|
const _ERR_TRANSITION_DATE_NOT_MIDNIGHT: &str = "'Date' must be at midnight GMT";
|
||||||
|
|
||||||
|
pub trait Filter {
|
||||||
|
fn test_tags(&self, user_tags: &str) -> bool;
|
||||||
|
fn by_size(&self, sz: i64) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for LifecycleRuleFilter {
|
||||||
|
fn test_tags(&self, user_tags: &str) -> bool {
|
||||||
|
if !requires_tag_matching(self) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_tags = tagging::decode_tags_to_map(user_tags);
|
||||||
|
|
||||||
|
self.tag.as_ref().is_none_or(|tag| tag_matches(tag, &user_tags))
|
||||||
|
&& self.and.as_ref().is_none_or(|and| and_tags_match(and, &user_tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn by_size(&self, sz: i64) -> bool {
|
||||||
|
let sz = sz.max(0);
|
||||||
|
|
||||||
|
self.object_size_greater_than.is_none_or(|min| sz > min)
|
||||||
|
&& self.object_size_less_than.is_none_or(|max| sz < max)
|
||||||
|
&& self.and.as_ref().is_none_or(|and| and_size_matches(and, sz))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requires_tag_matching(filter: &LifecycleRuleFilter) -> bool {
|
||||||
|
filter.tag.is_some()
|
||||||
|
|| filter
|
||||||
|
.and
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|and| and.tags.as_ref())
|
||||||
|
.is_some_and(|tags| !tags.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_matches(tag: &Tag, user_tags: &std::collections::HashMap<String, String>) -> bool {
|
||||||
|
let Some(key) = tag.key.as_deref() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(value) = tag.value.as_deref() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
user_tags.get(key).is_some_and(|actual| actual == value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn and_tags_match(and: &LifecycleRuleAndOperator, user_tags: &std::collections::HashMap<String, String>) -> bool {
|
||||||
|
and.tags
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|tags| tags.iter().all(|tag| tag_matches(tag, user_tags)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn and_size_matches(and: &LifecycleRuleAndOperator, sz: i64) -> bool {
|
||||||
|
and.object_size_greater_than.is_none_or(|min| sz > min) && and.object_size_less_than.is_none_or(|max| sz < max)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TransitionOps {
|
||||||
|
fn validate(&self) -> Result<(), std::io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NoncurrentVersionTransitionOps {
|
||||||
|
fn validate(&self) -> Result<(), std::io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_midnight(date: OffsetDateTime) -> bool {
|
||||||
|
date.hour() == 0 && date.minute() == 0 && date.second() == 0 && date.nanosecond() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransitionOps for Transition {
|
||||||
|
fn validate(&self) -> Result<(), std::io::Error> {
|
||||||
|
if self
|
||||||
|
.storage_class
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|storage_class| storage_class.as_str().is_empty())
|
||||||
|
{
|
||||||
|
return Err(std::io::Error::other("ERR_XML_NOT_WELL_FORMED"));
|
||||||
|
}
|
||||||
|
if self.date.is_some() == self.days.is_some() {
|
||||||
|
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
||||||
|
}
|
||||||
|
if self.days.is_some_and(|days| days < 0) {
|
||||||
|
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
||||||
|
}
|
||||||
|
if let Some(date) = self.date.clone() {
|
||||||
|
let date = OffsetDateTime::from(date);
|
||||||
|
if date.unix_timestamp() <= 0 || !is_midnight(date) {
|
||||||
|
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoncurrentVersionTransitionOps for NoncurrentVersionTransition {
|
||||||
|
fn validate(&self) -> Result<(), std::io::Error> {
|
||||||
|
if self
|
||||||
|
.storage_class
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|storage_class| storage_class.as_str().is_empty())
|
||||||
|
{
|
||||||
|
return Err(std::io::Error::other("ERR_XML_NOT_WELL_FORMED"));
|
||||||
|
}
|
||||||
|
if self.noncurrent_days.is_none() {
|
||||||
|
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
||||||
|
}
|
||||||
|
if self.noncurrent_days.is_some_and(|days| days < 0)
|
||||||
|
|| self.newer_noncurrent_versions.is_some_and(|versions| versions < 0)
|
||||||
|
{
|
||||||
|
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lifecycle_rule_filter_matches_single_tag() {
|
||||||
|
let filter = LifecycleRuleFilter {
|
||||||
|
tag: Some(Tag {
|
||||||
|
key: Some("env".to_string()),
|
||||||
|
value: Some("prod".to_string()),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=storage"));
|
||||||
|
assert!(!<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=dev&team=storage"));
|
||||||
|
assert!(!<LifecycleRuleFilter as Filter>::test_tags(&filter, "team=storage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lifecycle_rule_filter_matches_all_and_tags() {
|
||||||
|
let filter = LifecycleRuleFilter {
|
||||||
|
and: Some(LifecycleRuleAndOperator {
|
||||||
|
tags: Some(vec![
|
||||||
|
Tag {
|
||||||
|
key: Some("env".to_string()),
|
||||||
|
value: Some("prod".to_string()),
|
||||||
|
},
|
||||||
|
Tag {
|
||||||
|
key: Some("team".to_string()),
|
||||||
|
value: Some("storage".to_string()),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=storage"));
|
||||||
|
assert!(!<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=platform"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lifecycle_rule_filter_respects_size_bounds() {
|
||||||
|
let filter = LifecycleRuleFilter {
|
||||||
|
object_size_greater_than: Some(5),
|
||||||
|
object_size_less_than: Some(10),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!filter.by_size(5));
|
||||||
|
assert!(filter.by_size(6));
|
||||||
|
assert!(!filter.by_size(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lifecycle_rule_filter_without_tag_constraints_accepts_any_tags() {
|
||||||
|
let filter = LifecycleRuleFilter {
|
||||||
|
object_size_greater_than: Some(5),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, "env=prod&team=storage"));
|
||||||
|
assert!(<LifecycleRuleFilter as Filter>::test_tags(&filter, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// 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 url::form_urlencoded;
|
||||||
|
|
||||||
|
pub(crate) fn decode_tags_to_map(tags: &str) -> HashMap<String, String> {
|
||||||
|
let mut list = HashMap::new();
|
||||||
|
|
||||||
|
for (k, v) in form_urlencoded::parse(tags.as_bytes()) {
|
||||||
|
if k.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.insert(k.to_string(), v.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::decode_tags_to_map;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_tags_to_map_preserves_bucket_tagging_parser_behavior() {
|
||||||
|
let tags = decode_tags_to_map("env=prod&encoded=a%2Fb&=ignored");
|
||||||
|
|
||||||
|
assert_eq!(tags.get("env").map(String::as_str), Some("prod"));
|
||||||
|
assert_eq!(tags.get("encoded").map(String::as_str), Some("a/b"));
|
||||||
|
assert!(!tags.contains_key(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,9 +35,10 @@ use storage_api::owner::{
|
|||||||
ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule, ecstore_apply_transition_rule,
|
ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule, ecstore_apply_transition_rule,
|
||||||
ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config,
|
ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config,
|
||||||
ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_is_erasure, ecstore_is_erasure_sd,
|
ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_is_erasure, ecstore_is_erasure_sd,
|
||||||
ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_path2_bucket_object,
|
ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_object_opts_from_object_info,
|
||||||
ecstore_path2_bucket_object_with_base_path, ecstore_read_config, ecstore_replace_bucket_usage_memory_from_info,
|
ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path, ecstore_read_config,
|
||||||
ecstore_resolve_object_store_handle, ecstore_save_config, scanner_replication_config_for_lifecycle_eval,
|
ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle, ecstore_save_config,
|
||||||
|
scanner_replication_config_for_lifecycle_eval,
|
||||||
};
|
};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use storage_api::owner::{EcstoreDiskOption, EcstoreDiskStore, EcstoreEndpoint, ecstore_config_init, ecstore_new_disk};
|
use storage_api::owner::{EcstoreDiskOption, EcstoreDiskStore, EcstoreEndpoint, ecstore_config_init, ecstore_new_disk};
|
||||||
|
|||||||
@@ -795,7 +795,7 @@ impl ScannerItem {
|
|||||||
|
|
||||||
let object_opts = object_infos
|
let object_opts = object_infos
|
||||||
.iter()
|
.iter()
|
||||||
.map(ObjectOpts::from_object_info)
|
.map(crate::ecstore_object_opts_from_object_info)
|
||||||
.collect::<Vec<ObjectOpts>>();
|
.collect::<Vec<ObjectOpts>>();
|
||||||
|
|
||||||
let events = match Evaluator::new(lifecycle.clone())
|
let events = match Evaluator::new(lifecycle.clone())
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub(crate) use rustfs_ecstore::api::bucket::lifecycle::bucket_lifecycle_ops::{
|
|||||||
pub(crate) use rustfs_ecstore::api::bucket::lifecycle::evaluator::Evaluator as EcstoreEvaluator;
|
pub(crate) use rustfs_ecstore::api::bucket::lifecycle::evaluator::Evaluator as EcstoreEvaluator;
|
||||||
pub(crate) use rustfs_ecstore::api::bucket::lifecycle::lifecycle::{
|
pub(crate) use rustfs_ecstore::api::bucket::lifecycle::lifecycle::{
|
||||||
Event as EcstoreEvent, Lifecycle as EcstoreLifecycle, ObjectOpts as EcstoreObjectOpts,
|
Event as EcstoreEvent, Lifecycle as EcstoreLifecycle, ObjectOpts as EcstoreObjectOpts,
|
||||||
TRANSITION_COMPLETE as ECSTORE_TRANSITION_COMPLETE,
|
TRANSITION_COMPLETE as ECSTORE_TRANSITION_COMPLETE, object_opts_from_object_info as ecstore_object_opts_from_object_info,
|
||||||
};
|
};
|
||||||
pub(crate) use rustfs_ecstore::api::bucket::metadata_sys::{
|
pub(crate) use rustfs_ecstore::api::bucket::metadata_sys::{
|
||||||
get_lifecycle_config as ecstore_get_lifecycle_config, get_object_lock_config as ecstore_get_object_lock_config,
|
get_lifecycle_config as ecstore_get_lifecycle_config, get_object_lock_config as ecstore_get_object_lock_config,
|
||||||
@@ -96,9 +96,9 @@ pub(crate) mod owner {
|
|||||||
ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule, ecstore_apply_transition_rule, ecstore_expiry_state_handle,
|
ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule, ecstore_apply_transition_rule, ecstore_expiry_state_handle,
|
||||||
ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config, ecstore_get_object_lock_config,
|
ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config, ecstore_get_object_lock_config,
|
||||||
ecstore_get_replication_config, ecstore_is_erasure, ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket,
|
ecstore_get_replication_config, ecstore_is_erasure, ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket,
|
||||||
ecstore_list_path_raw, ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path, ecstore_read_config,
|
ecstore_list_path_raw, ecstore_object_opts_from_object_info, ecstore_path2_bucket_object,
|
||||||
ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle, ecstore_save_config,
|
ecstore_path2_bucket_object_with_base_path, ecstore_read_config, ecstore_replace_bucket_usage_memory_from_info,
|
||||||
scanner_replication_config_for_lifecycle_eval,
|
ecstore_resolve_object_store_handle, ecstore_save_config, scanner_replication_config_for_lifecycle_eval,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# ECStore Module Split Plan
|
# ECStore Module Split Plan
|
||||||
|
|
||||||
This plan records the remaining ECStore split work after the final audit
|
This plan records the remaining ECStore split work after the final audit
|
||||||
remediation pass. It is intentionally design-only: no crate movement should
|
remediation pass. Runtime movement must still wait until each candidate
|
||||||
start until each candidate boundary has explicit contracts, compatibility
|
boundary has explicit contracts, compatibility coverage, dependency evidence,
|
||||||
coverage, dependency evidence, and rollback steps.
|
and rollback steps.
|
||||||
|
|
||||||
## Current Shape
|
## Current Shape
|
||||||
|
|
||||||
| Area | Current owner | Size | Split status |
|
| Area | Current owner | Size | Split status |
|
||||||
|---|---|---:|---|
|
|---|---|---:|---|
|
||||||
| Bucket lifecycle | `crates/ecstore/src/bucket/lifecycle/` | 8,657 lines | Proposal only |
|
| Bucket lifecycle | `crates/lifecycle/` + `crates/ecstore/src/bucket/lifecycle/` | core contracts + ECStore runtime | Core contract extracted |
|
||||||
| Bucket replication | `crates/ecstore/src/bucket/replication/` | 8,730 lines | Proposal only |
|
| Bucket replication | `crates/ecstore/src/bucket/replication/` | 8,730 lines | Proposal only |
|
||||||
| Set disks | `crates/ecstore/src/set_disk/` | state carrier plus operation modules | Keep in ECStore |
|
| Set disks | `crates/ecstore/src/set_disk/` | state carrier plus operation modules | Keep in ECStore |
|
||||||
| Public ECStore facade | `crates/ecstore/src/api/mod.rs` | broad compatibility surface | Shrink only through guarded PRs |
|
| Public ECStore facade | `crates/ecstore/src/api/mod.rs` | broad compatibility surface | Shrink only through guarded PRs |
|
||||||
@@ -50,7 +50,10 @@ not a runtime split PR.
|
|||||||
|
|
||||||
## Lifecycle Candidate
|
## Lifecycle Candidate
|
||||||
|
|
||||||
`bucket/lifecycle` is not ready for a standalone crate yet.
|
`rustfs-lifecycle` now owns the pure lifecycle rule, event, evaluator, tag
|
||||||
|
filtering, object-lock metadata check, and expiry-time contracts. ECStore keeps
|
||||||
|
the object-store runtime, queues, tiering, audit/notification, metadata, and
|
||||||
|
replication scheduling adapters.
|
||||||
|
|
||||||
Current coupling:
|
Current coupling:
|
||||||
|
|
||||||
@@ -61,9 +64,20 @@ Current coupling:
|
|||||||
through the lifecycle metadata boundary;
|
through the lifecycle metadata boundary;
|
||||||
- lifecycle expiry schedules bucket replication delete work through the
|
- lifecycle expiry schedules bucket replication delete work through the
|
||||||
replication lifecycle bridge contract;
|
replication lifecycle bridge contract;
|
||||||
- lifecycle evaluation shares S3 DTOs, object metadata, object lock, replication
|
- lifecycle evaluation uses S3 DTOs and replication status contracts from the
|
||||||
config reads through lifecycle-local boundaries, scanner metrics,
|
independent `rustfs-lifecycle`/`rustfs-replication` crates, while ECStore maps
|
||||||
notification/audit side effects, and tier services.
|
`ObjectInfo` into lifecycle object options at the compatibility boundary;
|
||||||
|
- lifecycle runtime still coordinates scanner metrics, notification/audit side
|
||||||
|
effects, metadata access, replication delete scheduling, and tier services.
|
||||||
|
|
||||||
|
Current extracted contracts:
|
||||||
|
|
||||||
|
- `LifecycleCrateCoreIndependence`: lifecycle rule validation, filtering,
|
||||||
|
event evaluation, transition/expiration options, tag decoding, object-lock
|
||||||
|
metadata checks, and ILM expiry-time rounding live in `rustfs-lifecycle`.
|
||||||
|
`rustfs-lifecycle` must not import ECStore internals, file metadata, or
|
||||||
|
`rustfs-utils`; ECStore owns the `ObjectInfo` adapter in
|
||||||
|
`crates/ecstore/src/bucket/lifecycle/core.rs`.
|
||||||
|
|
||||||
Required contracts before crate movement:
|
Required contracts before crate movement:
|
||||||
|
|
||||||
@@ -79,12 +93,13 @@ Required contracts before crate movement:
|
|||||||
without depending on the replication implementation module.
|
without depending on the replication implementation module.
|
||||||
- `LifecycleAuditSink`: lifecycle audit and notification emission boundary.
|
- `LifecycleAuditSink`: lifecycle audit and notification emission boundary.
|
||||||
|
|
||||||
First safe PR:
|
Next safe PR:
|
||||||
|
|
||||||
- add a lifecycle extraction inventory section or module-level README;
|
- move one runtime-facing dependency behind a trait or adapter owned by
|
||||||
- list current ECStore/runtime dependencies and the target contract owner for
|
`rustfs-lifecycle` without changing queue, transition, or delete behavior;
|
||||||
each dependency;
|
- keep ECStore compatibility shims until scanner and RustFS app consumers stop
|
||||||
- add no code movement and no behavior changes.
|
depending on `rustfs_ecstore::api::bucket::lifecycle` paths;
|
||||||
|
- add focused tests for the moved contract and keep architecture guard coverage.
|
||||||
|
|
||||||
The module-level inventory lives in
|
The module-level inventory lives in
|
||||||
`crates/ecstore/src/bucket/lifecycle/README.md`.
|
`crates/ecstore/src/bucket/lifecycle/README.md`.
|
||||||
|
|||||||
@@ -2017,7 +2017,7 @@ async fn resolve_put_object_expiration(bucket: &str, obj_info: &ObjectInfo) -> O
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let obj_opts = lifecycle::ObjectOpts::from_object_info(obj_info);
|
let obj_opts = lifecycle::object_opts_from_object_info(obj_info);
|
||||||
let event = predict_lifecycle_expiration(&lifecycle_config, &obj_opts).await;
|
let event = predict_lifecycle_expiration(&lifecycle_config, &obj_opts).await;
|
||||||
debug!(
|
debug!(
|
||||||
bucket,
|
bucket,
|
||||||
|
|||||||
@@ -374,6 +374,9 @@ pub(crate) mod bucket {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) const TRANSITION_PENDING: &str =
|
pub(crate) const TRANSITION_PENDING: &str =
|
||||||
crate::storage::storage_api::ecstore_bucket::lifecycle::lifecycle::TRANSITION_PENDING;
|
crate::storage::storage_api::ecstore_bucket::lifecycle::lifecycle::TRANSITION_PENDING;
|
||||||
|
pub(crate) fn object_opts_from_object_info(obj_info: &crate::storage::storage_api::StorageObjectInfo) -> ObjectOpts {
|
||||||
|
crate::storage::storage_api::ecstore_bucket::lifecycle::lifecycle::object_opts_from_object_info(obj_info)
|
||||||
|
}
|
||||||
pub(crate) fn expected_expiry_time(mod_time: time::OffsetDateTime, days: i32) -> time::OffsetDateTime {
|
pub(crate) fn expected_expiry_time(mod_time: time::OffsetDateTime, days: i32) -> time::OffsetDateTime {
|
||||||
crate::storage::storage_api::ecstore_bucket::lifecycle::lifecycle::expected_expiry_time(mod_time, days)
|
crate::storage::storage_api::ecstore_bucket::lifecycle::lifecycle::expected_expiry_time(mod_time, days)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ require_source_contains "docs/architecture/ecstore-module-split-plan.md" "Runtim
|
|||||||
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "ReplicationCrateFileMetaIndependence" "ECStore split plan replication crate filemeta independence section"
|
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "ReplicationCrateFileMetaIndependence" "ECStore split plan replication crate filemeta independence section"
|
||||||
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "ReplicationCrateStorageApiIndependence" "ECStore split plan replication crate storage-api independence section"
|
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "ReplicationCrateStorageApiIndependence" "ECStore split plan replication crate storage-api independence section"
|
||||||
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "ReplicationCrateUtilsIndependence" "ECStore split plan replication crate utils independence section"
|
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "ReplicationCrateUtilsIndependence" "ECStore split plan replication crate utils independence section"
|
||||||
|
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "LifecycleCrateCoreIndependence" "ECStore split plan lifecycle crate core independence section"
|
||||||
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "StorageApiReplicationContracts" "ECStore split plan storage-api replication contract section"
|
require_source_contains "docs/architecture/ecstore-module-split-plan.md" "StorageApiReplicationContracts" "ECStore split plan storage-api replication contract section"
|
||||||
require_source_contains "docs/architecture/ecstore-api-facade-inventory.md" "## Facade Group Inventory" "ECStore facade inventory group section"
|
require_source_contains "docs/architecture/ecstore-api-facade-inventory.md" "## Facade Group Inventory" "ECStore facade inventory group section"
|
||||||
require_source_contains "docs/architecture/ecstore-api-facade-inventory.md" "## External Consumer Boundaries" "ECStore facade inventory consumer boundary section"
|
require_source_contains "docs/architecture/ecstore-api-facade-inventory.md" "## External Consumer Boundaries" "ECStore facade inventory consumer boundary section"
|
||||||
@@ -143,6 +144,7 @@ LIFECYCLE_TAGGING_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_tagging_bounda
|
|||||||
LIFECYCLE_OBJECT_LOCK_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_object_lock_boundary_bypass_hits.txt"
|
LIFECYCLE_OBJECT_LOCK_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_object_lock_boundary_bypass_hits.txt"
|
||||||
LIFECYCLE_REPLICATION_SINK_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_replication_sink_bypass_hits.txt"
|
LIFECYCLE_REPLICATION_SINK_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_replication_sink_bypass_hits.txt"
|
||||||
LIFECYCLE_REPLICATION_CRATE_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_replication_crate_bypass_hits.txt"
|
LIFECYCLE_REPLICATION_CRATE_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_replication_crate_bypass_hits.txt"
|
||||||
|
LIFECYCLE_CRATE_CORE_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_crate_core_bypass_hits.txt"
|
||||||
ECSTORE_REPLICATION_CONTRACT_BYPASS_HITS_FILE="${TMP_DIR}/ecstore_replication_contract_bypass_hits.txt"
|
ECSTORE_REPLICATION_CONTRACT_BYPASS_HITS_FILE="${TMP_DIR}/ecstore_replication_contract_bypass_hits.txt"
|
||||||
STORAGE_API_REPLICATION_CONTRACT_BYPASS_HITS_FILE="${TMP_DIR}/storage_api_replication_contract_bypass_hits.txt"
|
STORAGE_API_REPLICATION_CONTRACT_BYPASS_HITS_FILE="${TMP_DIR}/storage_api_replication_contract_bypass_hits.txt"
|
||||||
STORE_API_EXTERNAL_LIST_CONSUMER_HITS_FILE="${TMP_DIR}/store_api_external_list_consumer_hits.txt"
|
STORE_API_EXTERNAL_LIST_CONSUMER_HITS_FILE="${TMP_DIR}/store_api_external_list_consumer_hits.txt"
|
||||||
@@ -863,9 +865,9 @@ if [[ -s "$STORE_API_LIFECYCLE_HELPER_DEFINITION_HITS_FILE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
require_source_line \
|
require_source_line \
|
||||||
"crates/ecstore/src/bucket/lifecycle/core.rs" \
|
"crates/lifecycle/src/core.rs" \
|
||||||
"pub use crate::storage_api_contracts::lifecycle::ExpirationOptions;" \
|
"pub use rustfs_storage_api::ExpirationOptions;" \
|
||||||
"ECStore ExpirationOptions compatibility re-export"
|
"rustfs-lifecycle ExpirationOptions storage-api re-export"
|
||||||
require_source_line \
|
require_source_line \
|
||||||
"crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs" \
|
"crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs" \
|
||||||
"pub use crate::storage_api_contracts::lifecycle::TransitionedObject;" \
|
"pub use crate::storage_api_contracts::lifecycle::TransitionedObject;" \
|
||||||
@@ -969,6 +971,24 @@ if [[ -s "$LIFECYCLE_REPLICATION_CRATE_BYPASS_HITS_FILE" ]]; then
|
|||||||
report_failure "lifecycle replication status/state contracts must stay behind lifecycle replication_sink: $(paste -sd '; ' "$LIFECYCLE_REPLICATION_CRATE_BYPASS_HITS_FILE")"
|
report_failure "lifecycle replication status/state contracts must stay behind lifecycle replication_sink: $(paste -sd '; ' "$LIFECYCLE_REPLICATION_CRATE_BYPASS_HITS_FILE")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${ROOT_DIR}/crates/lifecycle" ]]; then
|
||||||
|
(
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
{
|
||||||
|
rg -n --with-filename 'rustfs_ecstore::|use\s+rustfs_ecstore\b|rustfs_filemeta::|use\s+rustfs_filemeta\b|rustfs_utils::|use\s+rustfs_utils\b' \
|
||||||
|
crates/lifecycle/Cargo.toml crates/lifecycle/src \
|
||||||
|
--glob '*.rs' || true
|
||||||
|
rg -n --with-filename 'crate::bucket::|crate::object_api|crate::storage_api_contracts' \
|
||||||
|
crates/lifecycle/src \
|
||||||
|
--glob '*.rs' || true
|
||||||
|
}
|
||||||
|
) >"$LIFECYCLE_CRATE_CORE_BYPASS_HITS_FILE"
|
||||||
|
|
||||||
|
if [[ -s "$LIFECYCLE_CRATE_CORE_BYPASS_HITS_FILE" ]]; then
|
||||||
|
report_failure "rustfs-lifecycle core contracts must not import ECStore/filemeta/utils internals: $(paste -sd '; ' "$LIFECYCLE_CRATE_CORE_BYPASS_HITS_FILE")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
(
|
(
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user