From e26a5f80973f240df105df18febb08b1a3f88bdf Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 23 Sep 2025 17:43:26 +0200 Subject: [PATCH] feat: Refetch object-storage resources Refetch the resources (account/container) on create/set operations to return the same what the show command would return. Drop the orphan types that were used before the structtable was extended. Since object-store commands still rely on one of those types reimplement it with the new interface streamlining (at least partially) the output processing. --- openstack_cli/src/common.rs | 294 ++---------------- .../src/object_store/v1/account/set.rs | 59 +++- .../src/object_store/v1/account/show.rs | 22 +- .../src/object_store/v1/container/create.rs | 70 ++++- .../src/object_store/v1/container/set.rs | 68 +++- .../src/object_store/v1/container/show.rs | 22 +- .../src/object_store/v1/object/show.rs | 22 +- openstack_cli/src/tracing_stats.rs | 2 +- 8 files changed, 215 insertions(+), 344 deletions(-) diff --git a/openstack_cli/src/common.rs b/openstack_cli/src/common.rs index 14dd36574..bd4d27469 100644 --- a/openstack_cli/src/common.rs +++ b/openstack_cli/src/common.rs @@ -15,11 +15,10 @@ //! Common helpers use crate::error::OpenStackCliError; -use serde::{Deserialize, Deserializer, Serialize, de::Visitor}; +use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::error::Error; -use std::fmt; use std::io::IsTerminal; use indicatif::{ProgressBar, ProgressStyle}; @@ -30,270 +29,25 @@ use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use tokio_util::io::InspectReader; use openstack_sdk::types::BoxedAsyncRead; - -/// Newtype for the `Vec` -#[derive(Deserialize, Default, Debug, Clone, Serialize)] -pub struct VecString(pub Vec); -impl fmt::Display for VecString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0.join(",")) - } -} - -/// Newtype for the `Vec` -#[derive(Deserialize, Default, Debug, Clone, Serialize)] -pub struct VecValue(pub Vec); -impl fmt::Display for VecValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - self.0 - .iter() - .map(|v| serde_json::to_string(v).unwrap_or("!SERIALIZE_ERROR!".to_string())) - .collect::>() - .join(",") - ) - } -} - -impl From> for VecValue { - fn from(item: Vec) -> Self { - VecValue(item) - } -} -impl From<&Vec> for VecValue { - fn from(item: &Vec) -> Self { - VecValue(item.clone()) - } -} +use structable::{StructTable, StructTableOptions}; /// Newtype for the `HashMap` #[derive(Deserialize, Default, Debug, Clone, Serialize)] -pub struct HashMapStringString(HashMap); - -impl From> for HashMapStringString { - fn from(data: HashMap) -> Self { - HashMapStringString(data.clone()) - } -} - -// And here's the display logic. -impl fmt::Display for HashMapStringString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - self.0 - .iter() - .map(|v| format!("{}={}", v.0, v.1)) - .collect::>() - .join("\n") - ) - } -} - -/// Newtype for the `Option>` -#[derive(Deserialize, Default, Debug, Clone, Serialize)] -pub struct OptionHashMapStringString(Option>); -impl fmt::Display for OptionHashMapStringString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.0 { - Some(ref data) => write!( - f, - "{}", - data.iter() - .map(|v| format!("{}={}", v.0, v.1)) - .collect::>() - .join(",") - ), - None => write!(f, ""), - } - } -} +pub struct HashMapStringString(pub HashMap); -/// Newtype for the `Option>>` -#[derive(Deserialize, Default, Debug, Clone, Serialize)] -pub struct OptionVecHashMapStringString(Option>>); -impl fmt::Display for OptionVecHashMapStringString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.0 { - Some(ref data) => write!( - f, - "{}", - data.iter() - .map(|v| v - .iter() - .map(|d| format!("{}={}", d.0, d.1)) - .collect::>() - .join(",")) - .collect::>() - .join(",") - ), - None => write!(f, ""), - } +impl StructTable for HashMapStringString { + fn instance_headers( + &self, + _options: &O, + ) -> Option<::std::vec::Vec<::std::string::String>> { + Some(self.0.keys().map(Into::into).collect()) } -} - -#[derive(Deserialize, Default, Debug, Clone, Serialize)] -pub struct VecHashMapStringString(Vec>); -impl fmt::Display for VecHashMapStringString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - self.0 - .iter() - .map(|v| v - .iter() - .map(|d| format!("{}={}", d.0, d.1)) - .collect::>() - .join(",")) - .collect::>() - .join(",") - ) - } -} -/// IntString (Integer or Integer as string) -#[derive(Clone, Debug, Serialize)] -#[serde(transparent)] -pub struct IntString(u64); -impl fmt::Display for IntString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} -impl<'de> Deserialize<'de> for IntString { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct MyVisitor; - - impl Visitor<'_> for MyVisitor { - type Value = IntString; - - fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.write_str("integer or string") - } - - fn visit_u64(self, val: u64) -> Result - where - E: serde::de::Error, - { - Ok(IntString(val)) - } - - fn visit_str(self, val: &str) -> Result - where - E: serde::de::Error, - { - match val.parse::() { - Ok(val) => self.visit_u64(val), - Err(_) => Ok(IntString(0)), - } - } - } - - deserializer.deserialize_any(MyVisitor) - } -} - -/// NumString (Any number or number as string) -#[derive(Clone, Debug, Serialize)] -#[serde(transparent)] -pub struct NumString(f64); -impl fmt::Display for NumString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} -impl<'de> Deserialize<'de> for NumString { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct MyVisitor; - - impl Visitor<'_> for MyVisitor { - type Value = NumString; - - fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.write_str("number or string") - } - - fn visit_u64(self, val: u64) -> Result - where - E: serde::de::Error, - { - Ok(NumString(val as f64)) - } - - fn visit_f64(self, val: f64) -> Result - where - E: serde::de::Error, - { - Ok(NumString(val)) - } - - fn visit_str(self, val: &str) -> Result - where - E: serde::de::Error, - { - match val.parse::() { - Ok(val) => self.visit_f64(val), - Err(_) => Ok(NumString(0.0)), - } - } - } - - deserializer.deserialize_any(MyVisitor) - } -} - -/// BoolString (Boolean or boolean as string) -#[derive(Clone, Debug, Serialize)] -#[serde(transparent)] -pub struct BoolString(bool); -impl fmt::Display for BoolString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} -impl<'de> Deserialize<'de> for BoolString { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct MyVisitor; - - impl Visitor<'_> for MyVisitor { - type Value = BoolString; - - fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.write_str("boolean or string") - } - - fn visit_bool(self, val: bool) -> Result - where - E: serde::de::Error, - { - Ok(BoolString(val)) - } - - fn visit_str(self, val: &str) -> Result - where - E: serde::de::Error, - { - match val.parse::() { - Ok(val) => self.visit_bool(val), - Err(_) => Ok(BoolString(false)), - } - } - } - - deserializer.deserialize_any(MyVisitor) + fn data( + &self, + _options: &O, + ) -> ::std::vec::Vec> { + self.0.values().map(|x| Some(x.into())).collect() } } @@ -445,16 +199,16 @@ pub(crate) async fn build_upload_asyncread( } } -#[derive(Debug, PartialEq, PartialOrd)] -pub(crate) struct ServiceApiVersion(pub u8, pub u8); - -impl TryFrom for ServiceApiVersion { - type Error = (); - fn try_from(ver: String) -> Result { - let parts: Vec = ver.split('.').flat_map(|v| v.parse::()).collect(); - Ok(ServiceApiVersion(parts[0], parts[1])) - } -} +// #[derive(Debug, PartialEq, PartialOrd)] +// pub(crate) struct ServiceApiVersion(pub u8, pub u8); +// +// impl TryFrom for ServiceApiVersion { +// type Error = (); +// fn try_from(ver: String) -> Result { +// let parts: Vec = ver.split('.').flat_map(|v| v.parse::()).collect(); +// Ok(ServiceApiVersion(parts[0], parts[1])) +// } +// } #[cfg(test)] mod tests { diff --git a/openstack_cli/src/object_store/v1/account/set.rs b/openstack_cli/src/object_store/v1/account/set.rs index d29d1a8e9..eac29cda8 100644 --- a/openstack_cli/src/object_store/v1/account/set.rs +++ b/openstack_cli/src/object_store/v1/account/set.rs @@ -30,7 +30,9 @@ use bytes::Bytes; use clap::Args; use http::Response; use http::{HeaderName, HeaderValue}; +use regex::Regex; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use tracing::info; use crate::Cli; @@ -44,8 +46,10 @@ use openstack_sdk::{ types::{ApiVersion, ServiceType}, }; +use crate::common::HashMapStringString; use crate::common::parse_key_val; use openstack_sdk::api::RawQueryAsync; +use openstack_sdk::api::object_store::v1::account::head::Request as GetRequest; use openstack_sdk::api::object_store::v1::account::set::Request; /// Creates, updates, or deletes account metadata. @@ -111,10 +115,59 @@ impl AccountCommand { .build() .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; let _rsp: Response = ep.raw_query_async(client).await?; - let data = Account {}; - // Maybe output some headers metadata - op.output_human::(&data)?; + // Refetch the container with the actual data + let mut ep_builder = GetRequest::builder(); + if let Some(account) = account { + ep_builder.account(account); + } + // Set query parameters + // Set body parameters + let ep = ep_builder + .build() + .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; + let rsp: Response = ep.raw_query_async(client).await?; + let mut metadata: HashMap = HashMap::new(); + let headers = rsp.headers(); + + let regexes: Vec = vec![ + Regex::new(r"(?i)X-Account-Meta-\.*").unwrap(), + Regex::new(r"(?i)X-Account-Storage-Policy\.*Bytes-Used").unwrap(), + Regex::new(r"(?i)X-Account-Storage-Policy\.*Container-Count").unwrap(), + Regex::new(r"(?i)X-Account-Storage-Policy\.*Object-Count").unwrap(), + ]; + + for (hdr, val) in headers.iter() { + if [ + "x-account-meta-temp-url-key", + "x-account-meta-temp-url-key-2", + "x-timestamp", + "x-account-bytes-used", + "x-account-container-count", + "x-account-object-count", + "x-account-meta-quota-bytes", + "x-account-access-control", + ] + .contains(&hdr.as_str()) + { + metadata.insert( + hdr.to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } else if !regexes.is_empty() { + for rex in regexes.iter() { + if rex.is_match(hdr.as_str()) { + metadata.insert( + hdr.to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } + } + } + } + let data = HashMapStringString(metadata); + + op.output_single::(serde_json::to_value(&data)?)?; op.show_command_hint()?; Ok(()) } diff --git a/openstack_cli/src/object_store/v1/account/show.rs b/openstack_cli/src/object_store/v1/account/show.rs index 96e927f03..bb837b0d0 100644 --- a/openstack_cli/src/object_store/v1/account/show.rs +++ b/openstack_cli/src/object_store/v1/account/show.rs @@ -21,14 +21,13 @@ use bytes::Bytes; use clap::Args; use http::Response; - -use serde::{Deserialize, Serialize}; +use regex::Regex; +use std::collections::HashMap; use tracing::info; use crate::Cli; use crate::OpenStackCliError; use crate::output::OutputProcessor; -use structable::{StructTable, StructTableOptions}; use openstack_sdk::{ AsyncOpenStack, @@ -39,8 +38,6 @@ use openstack_sdk::{ use crate::common::HashMapStringString; use openstack_sdk::api::RawQueryAsync; use openstack_sdk::api::object_store::v1::account::head::Request; -use regex::Regex; -use std::collections::HashMap; /// Shows metadata for an account. /// Because the storage system can store large amounts of data, take care when @@ -51,13 +48,6 @@ use std::collections::HashMap; #[derive(Args, Clone, Debug)] pub struct AccountCommand {} -/// Account -#[derive(Deserialize, Debug, Clone, Serialize, StructTable)] -pub struct Account { - #[structable(title = "metadata")] - metadata: HashMapStringString, -} - impl AccountCommand { /// Perform command action pub async fn take_action( @@ -129,11 +119,9 @@ impl AccountCommand { } } } - let data = Account { - metadata: metadata.into(), - }; - // Maybe output some headers metadata - op.output_human::(&data)?; + let data = HashMapStringString(metadata); + + op.output_single::(serde_json::to_value(&data)?)?; op.show_command_hint()?; Ok(()) } diff --git a/openstack_cli/src/object_store/v1/container/create.rs b/openstack_cli/src/object_store/v1/container/create.rs index c5e4fa350..e524dd99e 100644 --- a/openstack_cli/src/object_store/v1/container/create.rs +++ b/openstack_cli/src/object_store/v1/container/create.rs @@ -19,14 +19,13 @@ use bytes::Bytes; use clap::Args; use http::Response; - -use serde::{Deserialize, Serialize}; +use regex::Regex; +use std::collections::HashMap; use tracing::info; use crate::Cli; use crate::OpenStackCliError; use crate::output::OutputProcessor; -use structable::{StructTable, StructTableOptions}; use openstack_sdk::{ AsyncOpenStack, @@ -34,8 +33,10 @@ use openstack_sdk::{ types::{ApiVersion, ServiceType}, }; +use crate::common::HashMapStringString; use openstack_sdk::api::RawQueryAsync; use openstack_sdk::api::object_store::v1::container::create::Request; +use openstack_sdk::api::object_store::v1::container::head::Request as GetRequest; /// Creates a container. /// You do not need to check whether a container already exists before issuing @@ -54,10 +55,6 @@ pub struct ContainerCommand { container: String, } -/// Container -#[derive(Deserialize, Debug, Clone, Serialize, StructTable)] -pub struct Container {} - impl ContainerCommand { /// Perform command action pub async fn take_action( @@ -92,9 +89,62 @@ impl ContainerCommand { .build() .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; let _rsp: Response = ep.raw_query_async(client).await?; - let data = Container {}; - // Maybe output some headers metadata - op.output_human::(&data)?; + + let mut ep_builder = GetRequest::builder(); + if let Some(account) = account { + ep_builder.account(account); + } + ep_builder.container(&self.container); + // Set query parameters + // Set body parameters + let ep = ep_builder + .build() + .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; + let rsp: Response = ep.raw_query_async(client).await?; + + let mut metadata: HashMap = HashMap::new(); + let headers = rsp.headers(); + + let regexes: Vec = vec![Regex::new(r"(?i)X-Container-Meta-\.*").unwrap()]; + + for (hdr, val) in headers.iter() { + if [ + "x-timestamp", + "x-container-bytes-used", + "x-container-object-count", + "accept-ranges", + "x-container-meta-temp-url-key", + "x-container-meta-temp-url-key-2", + "x-container-meta-quota-count", + "x-container-meta-quota-bytes", + "x-storage-policy", + "x-container-read", + "x-container-write", + "x-container-sync-key", + "x-container-sync-to", + "x-versions-location", + "x-history-location", + ] + .contains(&hdr.as_str()) + { + metadata.insert( + hdr.to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } else if !regexes.is_empty() { + for rex in regexes.iter() { + if rex.is_match(hdr.as_str()) { + metadata.insert( + hdr.to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } + } + } + } + let data = HashMapStringString(metadata); + + op.output_single::(serde_json::to_value(&data)?)?; op.show_command_hint()?; Ok(()) } diff --git a/openstack_cli/src/object_store/v1/container/set.rs b/openstack_cli/src/object_store/v1/container/set.rs index 988f7827f..7f2f55687 100644 --- a/openstack_cli/src/object_store/v1/container/set.rs +++ b/openstack_cli/src/object_store/v1/container/set.rs @@ -17,13 +17,13 @@ use bytes::Bytes; use clap::Args; use http::Response; use http::{HeaderName, HeaderValue}; -use serde::{Deserialize, Serialize}; +use regex::Regex; +use std::collections::HashMap; use tracing::info; use crate::Cli; use crate::OpenStackCliError; use crate::output::OutputProcessor; -use structable::{StructTable, StructTableOptions}; use openstack_sdk::{ AsyncOpenStack, @@ -31,8 +31,10 @@ use openstack_sdk::{ types::{ApiVersion, ServiceType}, }; +use crate::common::HashMapStringString; use crate::common::parse_key_val; use openstack_sdk::api::RawQueryAsync; +use openstack_sdk::api::object_store::v1::container::head::Request as GetRequest; use openstack_sdk::api::object_store::v1::container::set::Request; /// Creates, updates, or deletes custom metadata for a container. @@ -53,10 +55,6 @@ pub struct ContainerCommand { property: Vec<(String, String)>, } -/// Container -#[derive(Deserialize, Debug, Clone, Serialize, StructTable)] -pub struct Container {} - impl ContainerCommand { /// Perform command action pub async fn take_action( @@ -97,9 +95,61 @@ impl ContainerCommand { .build() .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; let _rsp: Response = ep.raw_query_async(client).await?; - let data = Container {}; - // Maybe output some headers metadata - op.output_human::(&data)?; + + // Refetch the container with the actual data + let mut ep_builder = GetRequest::builder(); + if let Some(account) = account { + ep_builder.account(account); + } + ep_builder.container(&self.container); + let ep = ep_builder + .build() + .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; + let rsp: Response = ep.raw_query_async(client).await?; + + let mut metadata: HashMap = HashMap::new(); + let headers = rsp.headers(); + + let regexes: Vec = vec![Regex::new(r"(?i)X-Container-Meta-\.*").unwrap()]; + + for (hdr, val) in headers.iter() { + if [ + "x-timestamp", + "x-container-bytes-used", + "x-container-object-count", + "accept-ranges", + "x-container-meta-temp-url-key", + "x-container-meta-temp-url-key-2", + "x-container-meta-quota-count", + "x-container-meta-quota-bytes", + "x-storage-policy", + "x-container-read", + "x-container-write", + "x-container-sync-key", + "x-container-sync-to", + "x-versions-location", + "x-history-location", + ] + .contains(&hdr.as_str()) + { + metadata.insert( + hdr.to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } else if !regexes.is_empty() { + for rex in regexes.iter() { + if rex.is_match(hdr.as_str()) { + metadata.insert( + hdr.to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } + } + } + } + let data = HashMapStringString(metadata); + + op.output_single::(serde_json::to_value(&data)?)?; op.show_command_hint()?; Ok(()) } diff --git a/openstack_cli/src/object_store/v1/container/show.rs b/openstack_cli/src/object_store/v1/container/show.rs index 8038d87bc..0dc22c603 100644 --- a/openstack_cli/src/object_store/v1/container/show.rs +++ b/openstack_cli/src/object_store/v1/container/show.rs @@ -17,14 +17,13 @@ use bytes::Bytes; use clap::Args; use http::Response; - -use serde::{Deserialize, Serialize}; +use regex::Regex; +use std::collections::HashMap; use tracing::info; use crate::Cli; use crate::OpenStackCliError; use crate::output::OutputProcessor; -use structable::{StructTable, StructTableOptions}; use openstack_sdk::{ AsyncOpenStack, @@ -35,8 +34,6 @@ use openstack_sdk::{ use crate::common::HashMapStringString; use openstack_sdk::api::RawQueryAsync; use openstack_sdk::api::object_store::v1::container::head::Request; -use regex::Regex; -use std::collections::HashMap; /// Shows container metadata, including the number of objects and the total /// bytes of all objects stored in the container. @@ -53,13 +50,6 @@ pub struct ContainerCommand { container: String, } -/// Container -#[derive(Deserialize, Debug, Clone, Serialize, StructTable)] -pub struct Container { - #[structable(title = "metadata")] - metadata: HashMapStringString, -} - impl ContainerCommand { /// Perform command action pub async fn take_action( @@ -134,11 +124,9 @@ impl ContainerCommand { } } } - let data = Container { - metadata: metadata.into(), - }; - // Maybe output some headers metadata - op.output_human::(&data)?; + let data = HashMapStringString(metadata); + + op.output_single::(serde_json::to_value(&data)?)?; op.show_command_hint()?; Ok(()) } diff --git a/openstack_cli/src/object_store/v1/object/show.rs b/openstack_cli/src/object_store/v1/object/show.rs index ea7c99620..2d3b89838 100644 --- a/openstack_cli/src/object_store/v1/object/show.rs +++ b/openstack_cli/src/object_store/v1/object/show.rs @@ -16,14 +16,13 @@ use bytes::Bytes; use clap::Args; use http::Response; - -use serde::{Deserialize, Serialize}; +use regex::Regex; +use std::collections::HashMap; use tracing::info; use crate::Cli; use crate::OpenStackCliError; use crate::output::OutputProcessor; -use structable::{StructTable, StructTableOptions}; use openstack_sdk::{ AsyncOpenStack, @@ -34,8 +33,6 @@ use openstack_sdk::{ use crate::common::HashMapStringString; use openstack_sdk::api::RawQueryAsync; use openstack_sdk::api::object_store::v1::object::head::Request; -use regex::Regex; -use std::collections::HashMap; /// Shows object metadata. #[derive(Args, Clone, Debug)] @@ -89,13 +86,6 @@ pub struct ObjectCommand { symlink: Option, } -/// Object -#[derive(Deserialize, Debug, Clone, Serialize, StructTable)] -pub struct Object { - #[structable(title = "metadata")] - metadata: HashMapStringString, -} - impl ObjectCommand { /// Perform command action pub async fn take_action( @@ -185,11 +175,9 @@ impl ObjectCommand { } } } - let data = Object { - metadata: metadata.into(), - }; - // Maybe output some headers metadata - op.output_human::(&data)?; + let data = HashMapStringString(metadata); + + op.output_single::(serde_json::to_value(&data)?)?; op.show_command_hint()?; Ok(()) } diff --git a/openstack_cli/src/tracing_stats.rs b/openstack_cli/src/tracing_stats.rs index b6ee59d24..6c8d902b8 100644 --- a/openstack_cli/src/tracing_stats.rs +++ b/openstack_cli/src/tracing_stats.rs @@ -94,7 +94,7 @@ impl Visit for HttpRequest { _ => {} }; } - fn record_debug(&mut self, _: &Field, _: &dyn (core::fmt::Debug)) {} + fn record_debug(&mut self, _: &Field, _: &dyn core::fmt::Debug) {} } impl Layer for RequestTracingCollector