aboutsummaryrefslogtreecommitdiff
path: root/src/garage/cli_v2/bucket.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/garage/cli_v2/bucket.rs')
-rw-r--r--src/garage/cli_v2/bucket.rs523
1 files changed, 523 insertions, 0 deletions
diff --git a/src/garage/cli_v2/bucket.rs b/src/garage/cli_v2/bucket.rs
new file mode 100644
index 00000000..837ce783
--- /dev/null
+++ b/src/garage/cli_v2/bucket.rs
@@ -0,0 +1,523 @@
+//use bytesize::ByteSize;
+use format_table::format_table;
+
+use garage_util::error::*;
+
+use garage_api::admin::api::*;
+
+use crate::cli as cli_v1;
+use crate::cli::structs::*;
+use crate::cli_v2::*;
+
+impl Cli {
+ pub async fn cmd_bucket(&self, cmd: BucketOperation) -> Result<(), Error> {
+ match cmd {
+ BucketOperation::List => self.cmd_list_buckets().await,
+ BucketOperation::Info(query) => self.cmd_bucket_info(query).await,
+ BucketOperation::Create(query) => self.cmd_create_bucket(query).await,
+ BucketOperation::Delete(query) => self.cmd_delete_bucket(query).await,
+ BucketOperation::Alias(query) => self.cmd_alias_bucket(query).await,
+ BucketOperation::Unalias(query) => self.cmd_unalias_bucket(query).await,
+ BucketOperation::Allow(query) => self.cmd_bucket_allow(query).await,
+ BucketOperation::Deny(query) => self.cmd_bucket_deny(query).await,
+ BucketOperation::Website(query) => self.cmd_bucket_website(query).await,
+ BucketOperation::SetQuotas(query) => self.cmd_bucket_set_quotas(query).await,
+
+ // TODO
+ x => cli_v1::cmd_admin(
+ &self.admin_rpc_endpoint,
+ self.rpc_host,
+ AdminRpc::BucketOperation(x),
+ )
+ .await
+ .ok_or_message("old error"),
+ }
+ }
+
+ pub async fn cmd_list_buckets(&self) -> Result<(), Error> {
+ let buckets = self.api_request(ListBucketsRequest).await?;
+
+ println!("List of buckets:");
+
+ let mut table = vec![];
+ for bucket in buckets.0.iter() {
+ let local_aliases_n = match &bucket.local_aliases[..] {
+ [] => "".into(),
+ [alias] => format!("{}:{}", alias.access_key_id, alias.alias),
+ s => format!("[{} local aliases]", s.len()),
+ };
+
+ table.push(format!(
+ "\t{}\t{}\t{}",
+ bucket.global_aliases.join(","),
+ local_aliases_n,
+ bucket.id,
+ ));
+ }
+ format_table(table);
+
+ Ok(())
+ }
+
+ pub async fn cmd_bucket_info(&self, opt: BucketOpt) -> Result<(), Error> {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: None,
+ search: Some(opt.name),
+ })
+ .await?;
+
+ println!("Bucket: {}", bucket.id);
+
+ let size = bytesize::ByteSize::b(bucket.bytes as u64);
+ println!(
+ "\nSize: {} ({})",
+ size.to_string_as(true),
+ size.to_string_as(false)
+ );
+ println!("Objects: {}", bucket.objects);
+ println!(
+ "Unfinished uploads (multipart and non-multipart): {}",
+ bucket.unfinished_uploads,
+ );
+ println!(
+ "Unfinished multipart uploads: {}",
+ bucket.unfinished_multipart_uploads
+ );
+ let mpu_size = bytesize::ByteSize::b(bucket.unfinished_multipart_uploads as u64);
+ println!(
+ "Size of unfinished multipart uploads: {} ({})",
+ mpu_size.to_string_as(true),
+ mpu_size.to_string_as(false),
+ );
+
+ println!("\nWebsite access: {}", bucket.website_access);
+
+ if bucket.quotas.max_size.is_some() || bucket.quotas.max_objects.is_some() {
+ println!("\nQuotas:");
+ if let Some(ms) = bucket.quotas.max_size {
+ let ms = bytesize::ByteSize::b(ms);
+ println!(
+ " maximum size: {} ({})",
+ ms.to_string_as(true),
+ ms.to_string_as(false)
+ );
+ }
+ if let Some(mo) = bucket.quotas.max_objects {
+ println!(" maximum number of objects: {}", mo);
+ }
+ }
+
+ println!("\nGlobal aliases:");
+ for alias in bucket.global_aliases {
+ println!(" {}", alias);
+ }
+
+ println!("\nKey-specific aliases:");
+ let mut table = vec![];
+ for key in bucket.keys.iter() {
+ for alias in key.bucket_local_aliases.iter() {
+ table.push(format!("\t{} ({})\t{}", key.access_key_id, key.name, alias));
+ }
+ }
+ format_table(table);
+
+ println!("\nAuthorized keys:");
+ let mut table = vec![];
+ for key in bucket.keys.iter() {
+ if !(key.permissions.read || key.permissions.write || key.permissions.owner) {
+ continue;
+ }
+ let rflag = if key.permissions.read { "R" } else { " " };
+ let wflag = if key.permissions.write { "W" } else { " " };
+ let oflag = if key.permissions.owner { "O" } else { " " };
+ table.push(format!(
+ "\t{}{}{}\t{}\t{}",
+ rflag, wflag, oflag, key.access_key_id, key.name
+ ));
+ }
+ format_table(table);
+
+ Ok(())
+ }
+
+ pub async fn cmd_create_bucket(&self, opt: BucketOpt) -> Result<(), Error> {
+ self.api_request(CreateBucketRequest {
+ global_alias: Some(opt.name.clone()),
+ local_alias: None,
+ })
+ .await?;
+
+ println!("Bucket {} was created.", opt.name);
+
+ Ok(())
+ }
+
+ pub async fn cmd_delete_bucket(&self, opt: DeleteBucketOpt) -> Result<(), Error> {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: None,
+ search: Some(opt.name.clone()),
+ })
+ .await?;
+
+ // CLI-only checks: the bucket must not have other aliases
+ if bucket
+ .global_aliases
+ .iter()
+ .find(|a| **a != opt.name)
+ .is_some()
+ {
+ return Err(Error::Message(format!("Bucket {} still has other global aliases. Use `bucket unalias` to delete them one by one.", opt.name)));
+ }
+
+ if bucket
+ .keys
+ .iter()
+ .any(|k| !k.bucket_local_aliases.is_empty())
+ {
+ return Err(Error::Message(format!("Bucket {} still has other local aliases. Use `bucket unalias` to delete them one by one.", opt.name)));
+ }
+
+ if !opt.yes {
+ println!("About to delete bucket {}.", bucket.id);
+ return Err(Error::Message(
+ "Add --yes flag to really perform this operation".to_string(),
+ ));
+ }
+
+ self.api_request(DeleteBucketRequest {
+ id: bucket.id.clone(),
+ })
+ .await?;
+
+ println!("Bucket {} has been deleted.", bucket.id);
+
+ Ok(())
+ }
+
+ pub async fn cmd_alias_bucket(&self, opt: AliasBucketOpt) -> Result<(), Error> {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: None,
+ search: Some(opt.existing_bucket.clone()),
+ })
+ .await?;
+
+ if let Some(key_pat) = &opt.local {
+ let key = self
+ .api_request(GetKeyInfoRequest {
+ search: Some(key_pat.clone()),
+ id: None,
+ show_secret_key: false,
+ })
+ .await?;
+
+ self.api_request(AddBucketAliasRequest {
+ bucket_id: bucket.id.clone(),
+ alias: BucketAliasEnum::Local {
+ local_alias: opt.new_name.clone(),
+ access_key_id: key.access_key_id.clone(),
+ },
+ })
+ .await?;
+
+ println!(
+ "Alias {} now points to bucket {:.16} in namespace of key {}",
+ opt.new_name, bucket.id, key.access_key_id
+ )
+ } else {
+ self.api_request(AddBucketAliasRequest {
+ bucket_id: bucket.id.clone(),
+ alias: BucketAliasEnum::Global {
+ global_alias: opt.new_name.clone(),
+ },
+ })
+ .await?;
+
+ println!(
+ "Alias {} now points to bucket {:.16}",
+ opt.new_name, bucket.id
+ )
+ }
+
+ Ok(())
+ }
+
+ pub async fn cmd_unalias_bucket(&self, opt: UnaliasBucketOpt) -> Result<(), Error> {
+ if let Some(key_pat) = &opt.local {
+ let key = self
+ .api_request(GetKeyInfoRequest {
+ search: Some(key_pat.clone()),
+ id: None,
+ show_secret_key: false,
+ })
+ .await?;
+
+ let bucket = key
+ .buckets
+ .iter()
+ .find(|x| x.local_aliases.contains(&opt.name))
+ .ok_or_message(format!(
+ "No bucket called {} in namespace of key {}",
+ opt.name, key.access_key_id
+ ))?;
+
+ self.api_request(RemoveBucketAliasRequest {
+ bucket_id: bucket.id.clone(),
+ alias: BucketAliasEnum::Local {
+ access_key_id: key.access_key_id.clone(),
+ local_alias: opt.name.clone(),
+ },
+ })
+ .await?;
+
+ println!(
+ "Alias {} no longer points to bucket {:.16} in namespace of key {}",
+ &opt.name, bucket.id, key.access_key_id
+ )
+ } else {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: Some(opt.name.clone()),
+ search: None,
+ })
+ .await?;
+
+ self.api_request(RemoveBucketAliasRequest {
+ bucket_id: bucket.id.clone(),
+ alias: BucketAliasEnum::Global {
+ global_alias: opt.name.clone(),
+ },
+ })
+ .await?;
+
+ println!(
+ "Alias {} no longer points to bucket {:.16}",
+ opt.name, bucket.id
+ )
+ }
+
+ Ok(())
+ }
+
+ pub async fn cmd_bucket_allow(&self, opt: PermBucketOpt) -> Result<(), Error> {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: None,
+ search: Some(opt.bucket.clone()),
+ })
+ .await?;
+
+ let key = self
+ .api_request(GetKeyInfoRequest {
+ id: None,
+ search: Some(opt.key_pattern.clone()),
+ show_secret_key: false,
+ })
+ .await?;
+
+ self.api_request(AllowBucketKeyRequest(BucketKeyPermChangeRequest {
+ bucket_id: bucket.id.clone(),
+ access_key_id: key.access_key_id.clone(),
+ permissions: ApiBucketKeyPerm {
+ read: opt.read,
+ write: opt.write,
+ owner: opt.owner,
+ },
+ }))
+ .await?;
+
+ let new_bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: Some(bucket.id),
+ global_alias: None,
+ search: None,
+ })
+ .await?;
+
+ if let Some(new_key) = new_bucket
+ .keys
+ .iter()
+ .find(|k| k.access_key_id == key.access_key_id)
+ {
+ println!(
+ "New permissions for key {} on bucket {:.16}:\n read {}\n write {}\n owner {}",
+ key.access_key_id,
+ new_bucket.id,
+ new_key.permissions.read,
+ new_key.permissions.write,
+ new_key.permissions.owner
+ );
+ } else {
+ println!(
+ "Access key {} has no permissions on bucket {:.16}",
+ key.access_key_id, new_bucket.id
+ );
+ }
+
+ Ok(())
+ }
+
+ pub async fn cmd_bucket_deny(&self, opt: PermBucketOpt) -> Result<(), Error> {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: None,
+ search: Some(opt.bucket.clone()),
+ })
+ .await?;
+
+ let key = self
+ .api_request(GetKeyInfoRequest {
+ id: None,
+ search: Some(opt.key_pattern.clone()),
+ show_secret_key: false,
+ })
+ .await?;
+
+ self.api_request(DenyBucketKeyRequest(BucketKeyPermChangeRequest {
+ bucket_id: bucket.id.clone(),
+ access_key_id: key.access_key_id.clone(),
+ permissions: ApiBucketKeyPerm {
+ read: opt.read,
+ write: opt.write,
+ owner: opt.owner,
+ },
+ }))
+ .await?;
+
+ let new_bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: Some(bucket.id),
+ global_alias: None,
+ search: None,
+ })
+ .await?;
+
+ if let Some(new_key) = new_bucket
+ .keys
+ .iter()
+ .find(|k| k.access_key_id == key.access_key_id)
+ {
+ println!(
+ "New permissions for key {} on bucket {:.16}:\n read {}\n write {}\n owner {}",
+ key.access_key_id,
+ new_bucket.id,
+ new_key.permissions.read,
+ new_key.permissions.write,
+ new_key.permissions.owner
+ );
+ } else {
+ println!(
+ "Access key {} no longer has permissions on bucket {:.16}",
+ key.access_key_id, new_bucket.id
+ );
+ }
+
+ Ok(())
+ }
+
+ pub async fn cmd_bucket_website(&self, opt: WebsiteOpt) -> Result<(), Error> {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: None,
+ search: Some(opt.bucket.clone()),
+ })
+ .await?;
+
+ if !(opt.allow ^ opt.deny) {
+ return Err(Error::Message(
+ "You must specify exactly one flag, either --allow or --deny".to_string(),
+ ));
+ }
+
+ let wa = if opt.allow {
+ UpdateBucketWebsiteAccess {
+ enabled: true,
+ index_document: Some(opt.index_document.clone()),
+ error_document: opt
+ .error_document
+ .or(bucket.website_config.and_then(|x| x.error_document.clone())),
+ }
+ } else {
+ UpdateBucketWebsiteAccess {
+ enabled: false,
+ index_document: None,
+ error_document: None,
+ }
+ };
+
+ self.api_request(UpdateBucketRequest {
+ id: bucket.id,
+ body: UpdateBucketRequestBody {
+ website_access: Some(wa),
+ quotas: None,
+ },
+ })
+ .await?;
+
+ if opt.allow {
+ println!("Website access allowed for {}", &opt.bucket);
+ } else {
+ println!("Website access denied for {}", &opt.bucket);
+ }
+
+ Ok(())
+ }
+
+ pub async fn cmd_bucket_set_quotas(&self, opt: SetQuotasOpt) -> Result<(), Error> {
+ let bucket = self
+ .api_request(GetBucketInfoRequest {
+ id: None,
+ global_alias: None,
+ search: Some(opt.bucket.clone()),
+ })
+ .await?;
+
+ if opt.max_size.is_none() && opt.max_objects.is_none() {
+ return Err(Error::Message(
+ "You must specify either --max-size or --max-objects (or both) for this command to do something.".to_string(),
+ ));
+ }
+
+ let new_quotas = ApiBucketQuotas {
+ max_size: match opt.max_size.as_deref() {
+ Some("none") => None,
+ Some(v) => Some(
+ v.parse::<bytesize::ByteSize>()
+ .ok_or_message(format!("Invalid size specified: {}", v))?
+ .as_u64(),
+ ),
+ None => bucket.quotas.max_size,
+ },
+ max_objects: match opt.max_objects.as_deref() {
+ Some("none") => None,
+ Some(v) => Some(
+ v.parse::<u64>()
+ .ok_or_message(format!("Invalid number: {}", v))?,
+ ),
+ None => bucket.quotas.max_objects,
+ },
+ };
+
+ self.api_request(UpdateBucketRequest {
+ id: bucket.id.clone(),
+ body: UpdateBucketRequestBody {
+ website_access: None,
+ quotas: Some(new_quotas),
+ },
+ })
+ .await?;
+
+ println!("Quotas updated for bucket {:.16}", bucket.id);
+
+ Ok(())
+ }
+}