mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-28 09:08:58 +00:00
fix(lifecycle): expire sole delete markers by days (#4974)
This commit is contained in:
@@ -148,7 +148,7 @@ impl RuleValidate for LifecycleRule {
|
||||
return Err(std::io::Error::other(ERR_LIFECYCLE_EXPIRED_OBJECT_DELETE_MARKER_WITH_TAGS));
|
||||
}
|
||||
if let Some(expiration) = &self.expiration
|
||||
&& expiration.expired_object_delete_marker.is_some_and(|v| v)
|
||||
&& expiration.expired_object_delete_marker.is_some()
|
||||
&& (expiration.days.is_some() || expiration.date.is_some())
|
||||
{
|
||||
return Err(std::io::Error::other(ERR_LIFECYCLE_INVALID_EXPIRED_OBJECT_DELETE_MARKER));
|
||||
@@ -549,6 +549,19 @@ impl Lifecycle for BucketLifecycleConfiguration {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(days) = rule.expiration.as_ref().and_then(|expiration| expiration.days) {
|
||||
let due = expected_expiry_time(mod_time, days);
|
||||
if now.unix_timestamp() >= due.unix_timestamp() {
|
||||
events.push(Event {
|
||||
action: IlmAction::DeleteVersionAction,
|
||||
rule_id: rule.id.clone().unwrap_or_default(),
|
||||
due: Some(due),
|
||||
noncurrent_days: 0,
|
||||
newer_noncurrent_versions: 0,
|
||||
storage_class: "".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
// DelMarkerExpiration: expire delete marker after N days from mod_time
|
||||
if obj.delete_marker
|
||||
&& let Some(ref dme) = rule.del_marker_expiration
|
||||
@@ -2604,6 +2617,97 @@ mod tests {
|
||||
assert_eq!(event.due, Some(now));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn expiration_days_deletes_only_expired_delete_marker_when_due() {
|
||||
let base_time = OffsetDateTime::from_unix_timestamp(1_000_000).unwrap();
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
expiry_updated_at: None,
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
days: Some(3),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: None,
|
||||
id: Some("rule-expiration-days".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
del_marker_expiration: None,
|
||||
}],
|
||||
};
|
||||
let only_delete_marker = ObjectOpts {
|
||||
name: "obj".to_string(),
|
||||
mod_time: Some(base_time),
|
||||
is_latest: true,
|
||||
delete_marker: true,
|
||||
num_versions: 1,
|
||||
version_id: Some(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
};
|
||||
let due = expected_expiry_time(base_time, 3);
|
||||
|
||||
let before_due = lc.eval_inner(&only_delete_marker, due - Duration::seconds(1), 0).await;
|
||||
assert_eq!(before_due.action, IlmAction::NoneAction);
|
||||
|
||||
let at_due = lc.eval_inner(&only_delete_marker, due, 0).await;
|
||||
assert_eq!(at_due.action, IlmAction::DeleteVersionAction);
|
||||
assert_eq!(at_due.rule_id, "rule-expiration-days");
|
||||
assert_eq!(at_due.due, Some(due));
|
||||
|
||||
let marker_with_older_version = ObjectOpts {
|
||||
num_versions: 2,
|
||||
..only_delete_marker
|
||||
};
|
||||
let not_expired = lc
|
||||
.eval_inner(&marker_with_older_version, base_time + Duration::days(4), 0)
|
||||
.await;
|
||||
assert_eq!(not_expired.action, IlmAction::NoneAction);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn expiration_days_uses_earliest_due_rule_for_expired_delete_marker() {
|
||||
let base_time = OffsetDateTime::from_unix_timestamp(1_000_000).unwrap();
|
||||
let make_rule = |id: &str, days| LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
days: Some(days),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: None,
|
||||
id: Some(id.to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
del_marker_expiration: None,
|
||||
};
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
expiry_updated_at: None,
|
||||
rules: vec![make_rule("rule-30-days", 30), make_rule("rule-3-days", 3)],
|
||||
};
|
||||
let opts = ObjectOpts {
|
||||
name: "obj".to_string(),
|
||||
mod_time: Some(base_time),
|
||||
is_latest: true,
|
||||
delete_marker: true,
|
||||
num_versions: 1,
|
||||
version_id: Some(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let event = lc.eval_inner(&opts, base_time + Duration::days(31), 0).await;
|
||||
|
||||
assert_eq!(event.action, IlmAction::DeleteVersionAction);
|
||||
assert_eq!(event.rule_id, "rule-3-days");
|
||||
assert_eq!(event.due, Some(expected_expiry_time(base_time, 3)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_rejects_expired_object_delete_marker_with_date() {
|
||||
let future_date = OffsetDateTime::from_unix_timestamp(86_400 * 10).expect("valid midnight UTC test timestamp");
|
||||
@@ -2659,6 +2763,61 @@ mod tests {
|
||||
assert_eq!(err.to_string(), ERR_LIFECYCLE_INVALID_EXPIRED_OBJECT_DELETE_MARKER);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_rejects_false_expired_object_delete_marker_with_date() {
|
||||
let date = OffsetDateTime::from_unix_timestamp(86_400 * 10).expect("valid midnight UTC test timestamp");
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
expiry_updated_at: None,
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
date: Some(date.into()),
|
||||
expired_object_delete_marker: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: None,
|
||||
id: Some("rule-date-false-del-marker".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
del_marker_expiration: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let err = lc.validate(&ObjectLockConfiguration::default()).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), ERR_LIFECYCLE_INVALID_EXPIRED_OBJECT_DELETE_MARKER);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_rejects_false_expired_object_delete_marker_with_days() {
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
expiry_updated_at: None,
|
||||
rules: vec![LifecycleRule {
|
||||
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||
expiration: Some(LifecycleExpiration {
|
||||
days: Some(1),
|
||||
expired_object_delete_marker: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
abort_incomplete_multipart_upload: None,
|
||||
filter: None,
|
||||
id: Some("rule-days-false-del-marker".to_string()),
|
||||
noncurrent_version_expiration: None,
|
||||
noncurrent_version_transitions: None,
|
||||
prefix: None,
|
||||
transitions: None,
|
||||
del_marker_expiration: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let err = lc.validate(&ObjectLockConfiguration::default()).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), ERR_LIFECYCLE_INVALID_EXPIRED_OBJECT_DELETE_MARKER);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_rejects_expired_object_delete_marker_with_tag_filter() {
|
||||
let lc = BucketLifecycleConfiguration {
|
||||
|
||||
@@ -368,6 +368,28 @@ mod tests {
|
||||
assert_eq!(events[0].action, IlmAction::NoneAction);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn evaluator_skips_expiration_days_delete_marker_while_replication_pending() {
|
||||
let evaluator = Evaluator::new(latest_expiration_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);
|
||||
|
||||
let events = evaluator
|
||||
.eval(&[object_opts(
|
||||
ReplicationStatusType::Completed,
|
||||
VersionPurgeStatusType::Complete,
|
||||
)])
|
||||
.await
|
||||
.expect("completed replication should allow delete-marker expiration");
|
||||
|
||||
assert_eq!(events[0].action, IlmAction::DeleteVersionAction);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn evaluator_skips_latest_expiration_while_replication_failed() {
|
||||
let evaluator = Evaluator::new(latest_expiration_lifecycle());
|
||||
|
||||
Reference in New Issue
Block a user