aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2023-08-29 18:22:03 +0200
committerAlex Auvolat <alex@adnab.me>2023-08-29 18:22:03 +0200
commitabf011c2906d04200bb39d7bc82f7ed973215500 (patch)
treed881ca50b64d229198b8e9da552d70392d26706b
parent8041d9a8274619b9a7cb66735ed560bcfba16078 (diff)
downloadgarage-abf011c2906d04200bb39d7bc82f7ed973215500.tar.gz
garage-abf011c2906d04200bb39d7bc82f7ed973215500.zip
lifecycle: implement validation into garage's internal data structure
-rw-r--r--src/api/s3/lifecycle.rs200
-rw-r--r--src/model/bucket_table.rs2
2 files changed, 178 insertions, 24 deletions
diff --git a/src/api/s3/lifecycle.rs b/src/api/s3/lifecycle.rs
index cb0cc83a..48265870 100644
--- a/src/api/s3/lifecycle.rs
+++ b/src/api/s3/lifecycle.rs
@@ -22,13 +22,7 @@ pub async fn handle_get_lifecycle(bucket: &Bucket) -> Result<Response<Body>, Err
.ok_or_internal_error("Bucket should not be deleted at this point")?;
if let Some(lifecycle) = param.lifecycle_config.get() {
- let wc = LifecycleConfiguration {
- xmlns: (),
- lifecycle_rules: lifecycle
- .iter()
- .map(LifecycleRule::from_garage_lifecycle_rule)
- .collect::<Vec<_>>(),
- };
+ let wc = LifecycleConfiguration::from_garage_lifecycle_config(lifecycle);
let xml = to_xml_with_header(&wc)?;
Ok(Response::builder()
.status(StatusCode::OK)
@@ -81,9 +75,10 @@ pub async fn handle_put_lifecycle(
let conf: LifecycleConfiguration = from_reader(&body as &[u8])?;
- param
- .lifecycle_config
- .update(Some(conf.validate_into_garage_lifecycle_config()?));
+ let config = conf
+ .validate_into_garage_lifecycle_config()
+ .ok_or_bad_request("Invalid lifecycle configuration")?;
+ param.lifecycle_config.update(Some(config));
garage.bucket_table.insert(&bucket).await?;
Ok(Response::builder()
@@ -109,7 +104,7 @@ pub struct LifecycleRule {
#[serde(rename = "Status")]
pub status: Value,
#[serde(rename = "Filter", default)]
- pub filter: Filter,
+ pub filter: Option<Filter>,
#[serde(rename = "Expiration", default)]
pub expiration: Option<Expiration>,
#[serde(rename = "AbortIncompleteMultipartUpload", default)]
@@ -139,11 +134,13 @@ pub struct Expiration {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct AbortIncompleteMpu {
#[serde(rename = "DaysAfterInitiation")]
- pub days: Option<IntValue>,
+ pub days: IntValue,
}
impl LifecycleConfiguration {
- pub fn validate_into_garage_lifecycle_config(self) -> Result<Vec<GarageLifecycleRule>, Error> {
+ pub fn validate_into_garage_lifecycle_config(
+ self,
+ ) -> Result<Vec<GarageLifecycleRule>, &'static str> {
let mut ret = vec![];
for rule in self.lifecycle_rules {
ret.push(rule.validate_into_garage_lifecycle_rule()?);
@@ -163,12 +160,136 @@ impl LifecycleConfiguration {
}
impl LifecycleRule {
- pub fn validate_into_garage_lifecycle_rule(self) -> Result<GarageLifecycleRule, Error> {
- todo!()
+ pub fn validate_into_garage_lifecycle_rule(self) -> Result<GarageLifecycleRule, &'static str> {
+ let enabled = match self.status.0.as_str() {
+ "Enabled" => true,
+ "Disabled" => false,
+ _ => return Err("invalid value for <Status>"),
+ };
+
+ let filter = self
+ .filter
+ .map(Filter::validate_into_garage_lifecycle_filter)
+ .transpose()?
+ .unwrap_or_default();
+
+ let abort_incomplete_mpu_days = self.abort_incomplete_mpu.map(|x| x.days.0 as usize);
+
+ let expiration = self
+ .expiration
+ .map(Expiration::validate_into_garage_lifecycle_expiration)
+ .transpose()?;
+
+ Ok(GarageLifecycleRule {
+ id: self.id.map(|x| x.0),
+ enabled,
+ filter,
+ abort_incomplete_mpu_days,
+ expiration,
+ })
}
pub fn from_garage_lifecycle_rule(rule: &GarageLifecycleRule) -> Self {
- todo!()
+ Self {
+ id: rule.id.as_deref().map(Value::from),
+ status: if rule.enabled {
+ Value::from("Enabled")
+ } else {
+ Value::from("Disabled")
+ },
+ filter: Filter::from_garage_lifecycle_filter(&rule.filter),
+ abort_incomplete_mpu: rule
+ .abort_incomplete_mpu_days
+ .map(|days| AbortIncompleteMpu {
+ days: IntValue(days as i64),
+ }),
+ expiration: rule
+ .expiration
+ .as_ref()
+ .map(Expiration::from_garage_lifecycle_expiration),
+ }
+ }
+}
+
+impl Filter {
+ pub fn count(&self) -> i32 {
+ fn count<T>(x: &Option<T>) -> i32 {
+ x.as_ref().map(|_| 1).unwrap_or(0)
+ }
+ count(&self.prefix) + count(&self.size_gt) + count(&self.size_lt)
+ }
+
+ pub fn validate_into_garage_lifecycle_filter(
+ self,
+ ) -> Result<GarageLifecycleFilter, &'static str> {
+ if self.count() > 0 && self.and.is_some() {
+ Err("Filter tag cannot contain both <And> and another condition")
+ } else if let Some(and) = self.and {
+ if and.and.is_some() {
+ return Err("Nested <And> tags");
+ }
+ Ok(and.internal_into_garage_lifecycle_filter())
+ } else if self.count() > 1 {
+ Err("Multiple Filter conditions must be wrapped in an <And> tag")
+ } else {
+ Ok(self.internal_into_garage_lifecycle_filter())
+ }
+ }
+
+ fn internal_into_garage_lifecycle_filter(self) -> GarageLifecycleFilter {
+ GarageLifecycleFilter {
+ prefix: self.prefix.map(|x| x.0),
+ size_gt: self.size_gt.map(|x| x.0 as usize),
+ size_lt: self.size_lt.map(|x| x.0 as usize),
+ }
+ }
+
+ pub fn from_garage_lifecycle_filter(rule: &GarageLifecycleFilter) -> Option<Self> {
+ let filter = Filter {
+ and: None,
+ prefix: rule.prefix.as_deref().map(Value::from),
+ size_gt: rule.size_gt.map(|x| IntValue(x as i64)),
+ size_lt: rule.size_lt.map(|x| IntValue(x as i64)),
+ };
+ match filter.count() {
+ 0 => None,
+ 1 => Some(filter),
+ _ => Some(Filter {
+ and: Some(Box::new(filter)),
+ ..Default::default()
+ }),
+ }
+ }
+}
+
+impl Expiration {
+ pub fn validate_into_garage_lifecycle_expiration(
+ self,
+ ) -> Result<GarageLifecycleExpiration, &'static str> {
+ match (self.days, self.at_date) {
+ (Some(_), Some(_)) => Err("cannot have both <Days> and <Date> in <Expiration>"),
+ (None, None) => Err("<Expiration> must contain either <Days> or <Date>"),
+ (Some(days), None) => Ok(GarageLifecycleExpiration::AfterDays(days.0 as usize)),
+ (None, Some(date)) => {
+ if date.0.parse::<chrono::NaiveDate>().is_err() {
+ return Err("Invalid expiration <Date>");
+ }
+ Ok(GarageLifecycleExpiration::AtDate(date.0))
+ }
+ }
+ }
+
+ pub fn from_garage_lifecycle_expiration(exp: &GarageLifecycleExpiration) -> Self {
+ match exp {
+ GarageLifecycleExpiration::AfterDays(days) => Expiration {
+ days: Some(IntValue(*days as i64)),
+ at_date: None,
+ },
+ GarageLifecycleExpiration::AtDate(days) => Expiration {
+ days: None,
+ at_date: Some(Value::from(days.as_str())),
+ },
+ }
}
}
@@ -213,26 +334,24 @@ mod tests {
LifecycleRule {
id: Some("id1".into()),
status: "Enabled".into(),
- filter: Filter {
+ filter: Some(Filter {
prefix: Some("documents/".into()),
..Default::default()
- },
- expiration: None,
- abort_incomplete_mpu: Some(AbortIncompleteMpu {
- days: Some(IntValue(7)),
}),
+ expiration: None,
+ abort_incomplete_mpu: Some(AbortIncompleteMpu { days: IntValue(7) }),
},
LifecycleRule {
id: Some("id2".into()),
status: "Enabled".into(),
- filter: Filter {
+ filter: Some(Filter {
and: Some(Box::new(Filter {
prefix: Some("logs/".into()),
size_gt: Some(IntValue(1000000)),
..Default::default()
})),
..Default::default()
- },
+ }),
expiration: Some(Expiration {
days: Some(IntValue(365)),
at_date: None,
@@ -251,6 +370,41 @@ mod tests {
let cleanup = |c: &str| c.replace(char::is_whitespace, "");
assert_eq!(cleanup(message), cleanup(&message2));
+ // Check validation
+ let validated = ref_value
+ .validate_into_garage_lifecycle_config()
+ .ok_or_bad_request("invalid xml config")?;
+
+ let ref_config = vec![
+ GarageLifecycleRule {
+ id: Some("id1".into()),
+ enabled: true,
+ filter: GarageLifecycleFilter {
+ prefix: Some("documents/".into()),
+ ..Default::default()
+ },
+ expiration: None,
+ abort_incomplete_mpu_days: Some(7),
+ },
+ GarageLifecycleRule {
+ id: Some("id2".into()),
+ enabled: true,
+ filter: GarageLifecycleFilter {
+ prefix: Some("logs/".into()),
+ size_gt: Some(1000000),
+ ..Default::default()
+ },
+ expiration: Some(GarageLifecycleExpiration::AfterDays(365)),
+ abort_incomplete_mpu_days: None,
+ },
+ ];
+ assert_eq!(validated, ref_config);
+
+ let message3 = to_xml_with_header(&LifecycleConfiguration::from_garage_lifecycle_config(
+ &validated,
+ ))?;
+ assert_eq!(cleanup(message), cleanup(&message3));
+
Ok(())
}
}
diff --git a/src/model/bucket_table.rs b/src/model/bucket_table.rs
index dc4e4509..fed20e05 100644
--- a/src/model/bucket_table.rs
+++ b/src/model/bucket_table.rs
@@ -90,7 +90,7 @@ mod v08 {
/// A lifecycle filter is a set of conditions that must all be true.
/// For each condition, if it is None, it is not verified (always true),
/// and if it is Some(x), then it is verified for value x
- #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
+ #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Default)]
pub struct LifecycleFilter {
/// If Some(x), object key has to start with prefix x
pub prefix: Option<String>,