diff --git a/src/crd.rs b/src/crd.rs index e6374ec..0921e7d 100644 --- a/src/crd.rs +++ b/src/crd.rs @@ -14,7 +14,7 @@ pub struct MinioInstanceSpec { pub credentials: String, } -#[derive(Debug, Serialize, Deserialize, Default, Clone, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, Default, Copy, Clone, JsonSchema, PartialEq, Eq)] pub enum RetentionType { #[default] #[serde(rename_all = "lowercase")] @@ -23,7 +23,7 @@ pub enum RetentionType { Governance, } -#[derive(Debug, Serialize, Deserialize, Default, Clone, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, JsonSchema, PartialEq, Eq)] pub struct BucketRetention { pub validity: usize, pub r#type: RetentionType, diff --git a/src/minio.rs b/src/minio.rs index 5516352..00b1d90 100644 --- a/src/minio.rs +++ b/src/minio.rs @@ -4,7 +4,7 @@ use serde::de::DeserializeOwned; use serde::Deserialize; use crate::constants::{MC_EXE, SECRET_MINIO_BUCKET_ACCESS_LEN, SECRET_MINIO_BUCKET_SECRET_LEN}; -use crate::crd::{MinioBucketSpec, RetentionType}; +use crate::crd::{BucketRetention, MinioBucketSpec, RetentionType}; use crate::utils::rand_str; const MC_ALIAS_NAME: &str = "managedminioinst"; @@ -84,6 +84,13 @@ struct MinioQuota { pub quota: Option, } +#[derive(Debug, Clone, Deserialize)] +struct MinioRetentionResult { + pub enabled: Option, + pub mode: Option, + pub validity: Option, +} + impl BasicMinioResult { pub fn success(&self) -> bool { self.status == "success" @@ -206,7 +213,7 @@ impl MinioService { let bucket_name = format!("{}/{}", MC_ALIAS_NAME, b.name); let mut args = ["mb", bucket_name.as_str()].to_vec(); - if b.lock || b.retention.is_some() { + if b.lock { args.push("--with-lock"); } @@ -215,36 +222,15 @@ impl MinioService { return Err(MinioError::MakeBucketFailed.into()); } - self.bucket_set_versioning(&b.name, b.versioning).await?; + self.bucket_set_versioning(&b.name, b.versioning || b.lock) + .await?; self.bucket_set_anonymous_access(&b.name, b.anonymous_read_access) .await?; - - // Set quota, if requested self.bucket_set_quota(&b.name, b.quota).await?; - - // Set retention, if requested - if let Some(retention) = &b.retention { - let days = format!("{}d", retention.validity); - - let res = self - .exec_mc_cmd::(&[ - "retention", - "set", - "--default", - match retention.r#type { - RetentionType::Compliance => "compliance", - RetentionType::Governance => "governance", - }, - days.as_str(), - bucket_name.as_str(), - ]) + if b.lock { + self.bucket_set_default_retention(&b.name, b.retention) .await?; - - if res.get(0).map(|r| r.success()) != Some(true) { - return Err(MinioError::SetRetentionFailed.into()); - } } - Ok(()) } @@ -353,11 +339,84 @@ impl MinioService { .remove(0) .quota) } + + /// Set bucket default retention policy + pub async fn bucket_set_default_retention( + &self, + bucket_name: &str, + retention: Option, + ) -> anyhow::Result<()> { + let bucket_name = self.absolute_bucket_name(bucket_name); + let res = if let Some(retention) = &retention { + let days = format!("{}d", retention.validity); + + self.exec_mc_cmd::(&[ + "retention", + "set", + "--default", + match retention.r#type { + RetentionType::Compliance => "compliance", + RetentionType::Governance => "governance", + }, + days.as_str(), + bucket_name.as_str(), + ]) + .await? + } else { + self.exec_mc_cmd::(&[ + "retention", + "clear", + "--default", + bucket_name.as_str(), + ]) + .await? + }; + + if res.get(0).map(|r| r.success()) != Some(true) { + return Err(MinioError::SetRetentionFailed.into()); + } + + Ok(()) + } + + /// Get bucket default retention policy + pub async fn bucket_get_default_retention( + &self, + bucket: &str, + ) -> anyhow::Result> { + let bucket_name = self.absolute_bucket_name(bucket); + let res = self + .exec_mc_cmd::(&[ + "retention", + "info", + bucket_name.as_str(), + "--default", + ]) + .await? + .remove(0); + + if let (Some(mode), Some(validity), Some(enabled)) = (res.mode, res.validity, res.enabled) { + if enabled.to_lowercase().eq("enabled") { + return Ok(Some(BucketRetention { + validity: validity.to_lowercase().replace("days", "").parse()?, + r#type: match mode.to_lowercase().as_str() { + "governance" => RetentionType::Governance, + "compliance" => RetentionType::Compliance, + o => { + log::error!("Unknown retention type: {}", o); + return Ok(None); + } + }, + })); + } + } + Ok(None) + } } #[cfg(test)] mod test { - use crate::crd::MinioBucketSpec; + use crate::crd::{BucketRetention, MinioBucketSpec, RetentionType}; use crate::minio_test_server::MinioTestServer; const TEST_BUCKET_NAME: &str = "mybucket"; @@ -614,7 +673,7 @@ mod test { } #[tokio::test] - async fn bucket_without_qutoa() { + async fn bucket_without_quota() { let _ = env_logger::builder().is_test(true).try_init(); let srv = MinioTestServer::start().await.unwrap(); @@ -659,7 +718,7 @@ mod test { } #[tokio::test] - async fn bucket_with_qutoa() { + async fn bucket_with_quota() { let _ = env_logger::builder().is_test(true).try_init(); let srv = MinioTestServer::start().await.unwrap(); @@ -685,6 +744,123 @@ mod test { ); } - // TODO : with retention - // TODO : without retention + #[tokio::test] + async fn bucket_with_retention() { + let _ = env_logger::builder().is_test(true).try_init(); + + let srv = MinioTestServer::start().await.unwrap(); + let service = srv.as_service(); + service + .create_bucket(&MinioBucketSpec { + instance: "".to_string(), + name: TEST_BUCKET_NAME.to_string(), + secret: "".to_string(), + anonymous_read_access: false, + versioning: false, + quota: Some(42300), + lock: true, + retention: Some(BucketRetention { + validity: 10, + r#type: RetentionType::Governance, + }), + }) + .await + .unwrap(); + + assert!(service.bucket_exists(TEST_BUCKET_NAME).await.unwrap()); + assert_eq!( + service + .bucket_get_default_retention(TEST_BUCKET_NAME) + .await + .unwrap(), + Some(BucketRetention { + validity: 10, + r#type: RetentionType::Governance + }) + ); + + service + .bucket_set_default_retention(TEST_BUCKET_NAME, None) + .await + .unwrap(); + assert_eq!( + service + .bucket_get_default_retention(TEST_BUCKET_NAME) + .await + .unwrap(), + None + ); + + service + .bucket_set_default_retention( + TEST_BUCKET_NAME, + Some(BucketRetention { + validity: 42, + r#type: RetentionType::Compliance, + }), + ) + .await + .unwrap(); + assert_eq!( + service + .bucket_get_default_retention(TEST_BUCKET_NAME) + .await + .unwrap(), + Some(BucketRetention { + validity: 42, + r#type: RetentionType::Compliance + }) + ); + + service + .bucket_set_default_retention( + TEST_BUCKET_NAME, + Some(BucketRetention { + validity: 21, + r#type: RetentionType::Governance, + }), + ) + .await + .unwrap(); + assert_eq!( + service + .bucket_get_default_retention(TEST_BUCKET_NAME) + .await + .unwrap(), + Some(BucketRetention { + validity: 21, + r#type: RetentionType::Governance + }) + ); + } + + #[tokio::test] + async fn bucket_without_retention() { + let _ = env_logger::builder().is_test(true).try_init(); + + let srv = MinioTestServer::start().await.unwrap(); + let service = srv.as_service(); + service + .create_bucket(&MinioBucketSpec { + instance: "".to_string(), + name: TEST_BUCKET_NAME.to_string(), + secret: "".to_string(), + anonymous_read_access: false, + versioning: false, + quota: Some(42300), + lock: true, + retention: None, + }) + .await + .unwrap(); + + assert!(service.bucket_exists(TEST_BUCKET_NAME).await.unwrap()); + assert_eq!( + service + .bucket_get_default_retention(TEST_BUCKET_NAME) + .await + .unwrap(), + None + ); + } } diff --git a/src/minio_test_server.rs b/src/minio_test_server.rs index 5e8654a..29a655d 100644 --- a/src/minio_test_server.rs +++ b/src/minio_test_server.rs @@ -7,6 +7,7 @@ use crate::utils::rand_str; use rand::RngCore; use std::io::ErrorKind; use std::process::{Child, Command}; +use std::time::Duration; pub struct MinioTestServer { #[allow(dead_code)] @@ -62,6 +63,8 @@ impl MinioTestServer { } check_count += 1; + std::thread::sleep(Duration::from_millis(100)); + if instance.as_service().is_ready().await { break; } diff --git a/yaml/minio-bucket.yaml b/yaml/minio-bucket.yaml index 7a4795d..82b696b 100644 --- a/yaml/minio-bucket.yaml +++ b/yaml/minio-bucket.yaml @@ -50,7 +50,7 @@ spec: description: Limits the amount of data in the bucket, in bytes. By default it is unlimited example: 1000000000 lock: - description: Object locking prevent objects from being deleted. Will be considered as set to true when retention is defined. + description: Object locking prevent objects from being deleted. MUST be set to true when retention is defined. Cannot be changed. type: boolean default: false retention: