mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 19:42:17 +00:00
fix(ecstore): honor lifecycle tag filters (#2264)
Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
@@ -21,7 +21,8 @@
|
|||||||
use rustfs_filemeta::{ReplicationStatusType, VersionPurgeStatusType};
|
use rustfs_filemeta::{ReplicationStatusType, VersionPurgeStatusType};
|
||||||
use s3s::dto::{
|
use s3s::dto::{
|
||||||
BucketLifecycleConfiguration, ExpirationStatus, LifecycleExpiration, LifecycleRule, LifecycleRuleAndOperator,
|
BucketLifecycleConfiguration, ExpirationStatus, LifecycleExpiration, LifecycleRule, LifecycleRuleAndOperator,
|
||||||
NoncurrentVersionTransition, ObjectLockConfiguration, ObjectLockEnabled, RestoreRequest, Transition, TransitionStorageClass,
|
LifecycleRuleFilter, NoncurrentVersionTransition, ObjectLockConfiguration, ObjectLockEnabled, RestoreRequest, Transition,
|
||||||
|
TransitionStorageClass,
|
||||||
};
|
};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -350,12 +351,15 @@ impl Lifecycle for BucketLifecycleConfiguration {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*if !rule.filter.test_tags(obj.user_tags) {
|
if let Some(filter) = rule.filter.as_ref() {
|
||||||
continue;
|
if !<LifecycleRuleFilter as crate::bucket::lifecycle::rule::Filter>::test_tags(filter, &obj.user_tags) {
|
||||||
}*/
|
continue;
|
||||||
//if !obj.delete_marker && !rule.filter.BySize(obj.size) {
|
}
|
||||||
if !obj.delete_marker && false {
|
if !obj.delete_marker
|
||||||
continue;
|
&& !<LifecycleRuleFilter as crate::bucket::lifecycle::rule::Filter>::by_size(filter, obj.size as i64)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rules.push(rule.clone());
|
rules.push(rule.clone());
|
||||||
}
|
}
|
||||||
@@ -1567,6 +1571,122 @@ mod tests {
|
|||||||
assert_eq!(not_matched.len(), 0);
|
assert_eq!(not_matched.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn filter_rules_respects_filter_tag() {
|
||||||
|
let filter = LifecycleRuleFilter {
|
||||||
|
tag: Some(s3s::dto::Tag {
|
||||||
|
key: Some("env".to_string()),
|
||||||
|
value: Some("prod".to_string()),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let lc = BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: None,
|
||||||
|
rules: vec![LifecycleRule {
|
||||||
|
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||||
|
expiration: Some(LifecycleExpiration {
|
||||||
|
days: Some(30),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
abort_incomplete_multipart_upload: None,
|
||||||
|
filter: Some(filter),
|
||||||
|
id: Some("rule-tag".to_string()),
|
||||||
|
noncurrent_version_expiration: None,
|
||||||
|
noncurrent_version_transitions: None,
|
||||||
|
prefix: None,
|
||||||
|
transitions: None,
|
||||||
|
del_marker_expiration: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let matched = lc
|
||||||
|
.filter_rules(&ObjectOpts {
|
||||||
|
name: "obj".to_string(),
|
||||||
|
user_tags: "env=prod&team=storage".to_string(),
|
||||||
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||||
|
is_latest: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(matched.len(), 1);
|
||||||
|
|
||||||
|
let not_matched = lc
|
||||||
|
.filter_rules(&ObjectOpts {
|
||||||
|
name: "obj".to_string(),
|
||||||
|
user_tags: "env=dev&team=storage".to_string(),
|
||||||
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||||
|
is_latest: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(not_matched.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn filter_rules_respects_filter_and_tags() {
|
||||||
|
let mut filter = LifecycleRuleFilter::default();
|
||||||
|
filter.and = Some(LifecycleRuleAndOperator {
|
||||||
|
tags: Some(vec![
|
||||||
|
s3s::dto::Tag {
|
||||||
|
key: Some("env".to_string()),
|
||||||
|
value: Some("prod".to_string()),
|
||||||
|
},
|
||||||
|
s3s::dto::Tag {
|
||||||
|
key: Some("team".to_string()),
|
||||||
|
value: Some("storage".to_string()),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let lc = BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: None,
|
||||||
|
rules: vec![LifecycleRule {
|
||||||
|
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||||
|
expiration: Some(LifecycleExpiration {
|
||||||
|
days: Some(30),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
abort_incomplete_multipart_upload: None,
|
||||||
|
filter: Some(filter),
|
||||||
|
id: Some("rule-and-tags".to_string()),
|
||||||
|
noncurrent_version_expiration: None,
|
||||||
|
noncurrent_version_transitions: None,
|
||||||
|
prefix: None,
|
||||||
|
transitions: None,
|
||||||
|
del_marker_expiration: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let matched = lc
|
||||||
|
.filter_rules(&ObjectOpts {
|
||||||
|
name: "obj".to_string(),
|
||||||
|
user_tags: "env=prod&team=storage".to_string(),
|
||||||
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||||
|
is_latest: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(matched.len(), 1);
|
||||||
|
|
||||||
|
let not_matched = lc
|
||||||
|
.filter_rules(&ObjectOpts {
|
||||||
|
name: "obj".to_string(),
|
||||||
|
user_tags: "env=prod&team=platform".to_string(),
|
||||||
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_000_000).unwrap()),
|
||||||
|
is_latest: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(not_matched.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn expired_object_delete_marker_requires_single_version() {
|
async fn expired_object_delete_marker_requires_single_version() {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#![allow(unused_imports)]
|
|
||||||
// Copyright 2024 RustFS Team
|
// Copyright 2024 RustFS Team
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -12,13 +11,8 @@
|
|||||||
// 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.
|
||||||
#![allow(unused_variables)]
|
use crate::bucket::tagging::decode_tags_to_map;
|
||||||
#![allow(unused_mut)]
|
use s3s::dto::{LifecycleRuleAndOperator, LifecycleRuleFilter, Tag, Transition};
|
||||||
#![allow(unused_assignments)]
|
|
||||||
#![allow(unused_must_use)]
|
|
||||||
#![allow(clippy::all)]
|
|
||||||
|
|
||||||
use s3s::dto::{LifecycleRuleFilter, Transition};
|
|
||||||
|
|
||||||
const _ERR_TRANSITION_INVALID_DAYS: &str = "Days must be 0 or greater when used with Transition";
|
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_DATE: &str = "Date must be provided in ISO 8601 format";
|
||||||
@@ -33,21 +27,62 @@ pub trait Filter {
|
|||||||
|
|
||||||
impl Filter for LifecycleRuleFilter {
|
impl Filter for LifecycleRuleFilter {
|
||||||
fn test_tags(&self, user_tags: &str) -> bool {
|
fn test_tags(&self, user_tags: &str) -> bool {
|
||||||
true
|
if !requires_tag_matching(self) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_tags = 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 {
|
fn by_size(&self, sz: i64) -> bool {
|
||||||
true
|
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 {
|
pub trait TransitionOps {
|
||||||
fn validate(&self) -> Result<(), std::io::Error>;
|
fn validate(&self) -> Result<(), std::io::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransitionOps for Transition {
|
impl TransitionOps for Transition {
|
||||||
fn validate(&self) -> Result<(), std::io::Error> {
|
fn validate(&self) -> Result<(), std::io::Error> {
|
||||||
if !self.date.is_none() && self.days.expect("err!") > 0 {
|
if self.date.is_some() && self.days.is_some_and(|d| d > 0) {
|
||||||
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
return Err(std::io::Error::other(ERR_TRANSITION_INVALID));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +97,65 @@ impl TransitionOps for Transition {
|
|||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn test_rule() {
|
fn lifecycle_rule_filter_matches_single_tag() {
|
||||||
//assert!(skip_access_checks(p.to_str().unwrap()));
|
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, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user