feat: Postgresql score based on the postgres capability now. true infrastructure abstraction!
Some checks failed
Run Check Script / check (pull_request) Failing after 33s

This commit is contained in:
2025-12-16 23:35:52 -05:00
parent b0383454f0
commit 440e684b35
14 changed files with 244 additions and 104 deletions

View File

@@ -56,7 +56,7 @@ enum ExecutionMode {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct BrocadeInfo { pub struct BrocadeInfo {
os: BrocadeOs, os: BrocadeOs,
version: String, _version: String,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -263,7 +263,7 @@ async fn get_brocade_info(session: &mut BrocadeSession) -> Result<BrocadeInfo, E
return Ok(BrocadeInfo { return Ok(BrocadeInfo {
os: BrocadeOs::NetworkOperatingSystem, os: BrocadeOs::NetworkOperatingSystem,
version, _version: version,
}); });
} else if output.contains("ICX") { } else if output.contains("ICX") {
let re = Regex::new(r"(?m)^\s*SW: Version\s*(?P<version>[a-zA-Z0-9.\-]+)") let re = Regex::new(r"(?m)^\s*SW: Version\s*(?P<version>[a-zA-Z0-9.\-]+)")
@@ -276,7 +276,7 @@ async fn get_brocade_info(session: &mut BrocadeSession) -> Result<BrocadeInfo, E
return Ok(BrocadeInfo { return Ok(BrocadeInfo {
os: BrocadeOs::FastIron, os: BrocadeOs::FastIron,
version, _version: version,
}); });
} }

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, str::FromStr}; use std::str::FromStr;
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,

View File

@@ -1,14 +1,18 @@
use harmony::{ use harmony::{
inventory::Inventory, modules::postgresql::PostgreSQLScore, topology::K8sAnywhereTopology, inventory::Inventory,
modules::postgresql::{PostgreSQLScore, capability::PostgreSQLConfig},
topology::K8sAnywhereTopology,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let postgresql = PostgreSQLScore { let postgresql = PostgreSQLScore {
name: "harmony-postgres-example".to_string(), // Override default name config: PostgreSQLConfig {
namespace: "harmony-postgres-example".to_string(), cluster_name: "harmony-postgres-example".to_string(), // Override default name
..Default::default() // Use harmony defaults, they are based on CNPG's default values : namespace: "harmony-postgres-example".to_string(),
// "default" namespace, 1 instance, 1Gi storage ..Default::default() // Use harmony defaults, they are based on CNPG's default values :
// "default" namespace, 1 instance, 1Gi storage
},
}; };
harmony_cli::run( harmony_cli::run(

View File

@@ -1,17 +1,22 @@
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::postgresql::{PostgreSQLConnectionScore, PostgreSQLScore, PublicPostgreSQLScore}, modules::postgresql::{
K8sPostgreSQLScore, PostgreSQLConnectionScore, PublicPostgreSQLScore,
capability::PostgreSQLConfig,
},
topology::K8sAnywhereTopology, topology::K8sAnywhereTopology,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let postgres = PublicPostgreSQLScore { let postgres = PublicPostgreSQLScore {
postgres_score: PostgreSQLScore { postgres_score: K8sPostgreSQLScore {
name: "harmony-postgres-example".to_string(), // Override default name config: PostgreSQLConfig {
namespace: "harmony-public-postgres".to_string(), cluster_name: "harmony-postgres-example".to_string(), // Override default name
..Default::default() // Use harmony defaults, they are based on CNPG's default values : namespace: "harmony-public-postgres".to_string(),
// "default" namespace, 1 instance, 1Gi storage ..Default::default() // Use harmony defaults, they are based on CNPG's default values :
// 1 instance, 1Gi storage
},
}, },
hostname: "postgrestest.sto1.nationtech.io".to_string(), hostname: "postgrestest.sto1.nationtech.io".to_string(),
}; };

View File

@@ -1,16 +1,27 @@
use async_trait::async_trait; use async_trait::async_trait;
use crate::{ use crate::{
modules::postgresql::capability::{ interpret::Outcome,
PostgreSQL, PostgreSQLConfig, PostgreSQLEndpoint, ReplicationCerts, inventory::Inventory,
modules::postgresql::{
K8sPostgreSQLScore,
capability::{PostgreSQL, PostgreSQLConfig, PostgreSQLEndpoint, ReplicationCerts},
}, },
score::Score,
topology::K8sAnywhereTopology, topology::K8sAnywhereTopology,
}; };
#[async_trait] #[async_trait]
impl PostgreSQL for K8sAnywhereTopology { impl PostgreSQL for K8sAnywhereTopology {
async fn deploy(&self, config: &PostgreSQLConfig) -> Result<String, String> { async fn deploy(&self, config: &PostgreSQLConfig) -> Result<String, String> {
todo!() K8sPostgreSQLScore {
config: config.clone(),
}
.interpret(&Inventory::empty(), self)
.await
.map_err(|e| format!("Failed to deploy k8s postgresql : {e}"))?;
Ok(config.cluster_name.clone())
} }
/// Extracts PostgreSQL-specific replication certs (PEM format) from a deployed primary cluster. /// Extracts PostgreSQL-specific replication certs (PEM format) from a deployed primary cluster.

View File

@@ -1,11 +1,9 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_macros::hurl; use harmony_macros::hurl;
use kube::{Api, api::GroupVersionKind}; use kube::api::GroupVersionKind;
use log::{debug, warn};
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde::Serialize; use serde::Serialize;
use serde::de::DeserializeOwned; use std::{str::FromStr, sync::Arc};
use std::{process::Command, str::FromStr, sync::Arc};
use crate::{ use crate::{
data::Version, data::Version,
@@ -13,10 +11,7 @@ use crate::{
inventory::Inventory, inventory::Inventory,
modules::helm::chart::{HelmChartScore, HelmRepository}, modules::helm::chart::{HelmChartScore, HelmRepository},
score::Score, score::Score,
topology::{ topology::{HelmCommand, K8sclient, Topology, ingress::Ingress, k8s::K8sClient},
HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, ingress::Ingress,
k8s::K8sClient,
},
}; };
use harmony_types::id::Id; use harmony_types::id::Id;

View File

@@ -30,6 +30,21 @@ pub struct PostgreSQLConfig {
pub instances: u32, pub instances: u32,
pub storage_size: StorageSize, pub storage_size: StorageSize,
pub role: PostgreSQLClusterRole, pub role: PostgreSQLClusterRole,
/// **Note :** on OpenShfit based clusters, the namespace `default` has security
/// settings incompatible with the default CNPG behavior.
pub namespace: String,
}
impl Default for PostgreSQLConfig {
fn default() -> Self {
Self {
cluster_name: "harmony-pg".to_string(),
instances: 1,
storage_size: StorageSize::gi(1),
role: PostgreSQLClusterRole::Primary,
namespace: "harmony".to_string(),
}
}
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]

View File

@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
)] )]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ClusterSpec { pub struct ClusterSpec {
pub instances: i32, pub instances: u32,
pub image_name: Option<String>, pub image_name: Option<String>,
pub storage: Storage, pub storage: Storage,
pub bootstrap: Bootstrap, pub bootstrap: Bootstrap,

View File

@@ -3,6 +3,7 @@ use log::debug;
use log::info; use log::info;
use std::collections::HashMap; use std::collections::HashMap;
use crate::interpret::Outcome;
use crate::{ use crate::{
modules::postgresql::capability::{ modules::postgresql::capability::{
BootstrapConfig, BootstrapStrategy, ExternalClusterConfig, PostgreSQL, BootstrapConfig, BootstrapStrategy, ExternalClusterConfig, PostgreSQL,
@@ -25,6 +26,7 @@ impl<T: PostgreSQL> PostgreSQL for FailoverTopology<T> {
instances: config.instances, instances: config.instances,
storage_size: config.storage_size.clone(), storage_size: config.storage_size.clone(),
role: PostgreSQLClusterRole::Primary, role: PostgreSQLClusterRole::Primary,
namespace: config.namespace.clone(),
}; };
info!( info!(
@@ -91,6 +93,7 @@ impl<T: PostgreSQL> PostgreSQL for FailoverTopology<T> {
instances: config.instances, instances: config.instances,
storage_size: config.storage_size.clone(), storage_size: config.storage_size.clone(),
role: PostgreSQLClusterRole::Replica(replica_cluster_config), role: PostgreSQLClusterRole::Replica(replica_cluster_config),
namespace: config.namespace.clone(),
}; };
info!( info!(
@@ -102,7 +105,7 @@ impl<T: PostgreSQL> PostgreSQL for FailoverTopology<T> {
info!( info!(
"Replica cluster '{}' deployed successfully; failover topology '{}' ready", "Replica cluster '{}' deployed successfully; failover topology '{}' ready",
replica_config.cluster_name, config.cluster_name replica_config.cluster_name, replica_config.cluster_name
); );
Ok(primary_cluster_name) Ok(primary_cluster_name)

View File

@@ -1,8 +1,8 @@
pub mod capability; pub mod capability;
mod score; mod score_k8s;
mod score_connect; mod score_connect;
pub use score_connect::*; pub use score_connect::*;
pub use score::*; pub use score_k8s::*;
mod score_public; mod score_public;
pub use score_public::*; pub use score_public::*;
@@ -10,4 +10,7 @@ pub mod failover;
mod operator; mod operator;
pub use operator::*; pub use operator::*;
mod score;
pub use score::*;
pub mod cnpg; pub mod cnpg;

View File

@@ -1,51 +1,42 @@
use async_trait::async_trait;
use harmony_types::id::Id;
use serde::Serialize; use serde::Serialize;
use crate::interpret::Interpret; use crate::data::Version;
use crate::modules::k8s::resource::K8sResourceScore; use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome};
use crate::modules::postgresql::cnpg::{Bootstrap, Cluster, ClusterSpec, Initdb, Storage}; use crate::inventory::Inventory;
use crate::modules::postgresql::capability::{PostgreSQL, PostgreSQLConfig};
use crate::score::Score; use crate::score::Score;
use crate::topology::{K8sclient, Topology}; use crate::topology::Topology;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
/// Deploys an opinionated, highly available PostgreSQL cluster managed by CNPG. /// High-level, infrastructure-agnostic PostgreSQL deployment score.
/// ///
/// # Goals /// Delegates to the Topology's PostgreSQL capability implementation,
/// - Production-ready Postgres HA (3 instances), persistent storage, app DB. /// allowing flexibility in deployment strategy (k8s/CNPG, cloud-managed, etc.).
/// ///
/// # Usage /// # Usage
/// ``` /// ```
/// use harmony::modules::postgresql::PostgreSQLScore; /// use harmony::modules::postgresql::PostgreSQLScore;
/// let score = PostgreSQLScore::new("my-app-ns"); /// let score = PostgreSQLScore::new("harmony");
/// ``` /// ```
/// ///
/// # Limitations (Happy Path) /// # Design
/// - Requires CNPG operator installed (use CloudNativePgOperatorScore). /// - PostgreSQLScore: High-level, relies on Topology's PostgreSQL implementation
/// - No backups, monitoring, extensions configured. /// - Topology implements PostgreSQL capability (decoupled from score)
/// - K8s topologies use K8sPostgreSQLScore internally for CNPG deployment
/// ///
/// TODO : refactor this to declare a clean dependency on cnpg operator. Then cnpg operator will /// This layered approach gives users choice:
/// self-deploy either using operatorhub or helm chart depending on k8s flavor. This is cnpg /// - Use PostgreSQLScore for portability across topologies
/// specific behavior /// - Use K8sPostgreSQLScore directly for k8s-specific control
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct PostgreSQLScore { pub struct PostgreSQLScore {
pub name: String, pub config: PostgreSQLConfig,
/// **Note :** on OpenShfit based clusters, the namespace `default` has security
/// settings incompatible with the default CNPG behavior.
pub namespace: String,
pub instances: i32,
pub storage_size: String,
pub image_name: Option<String>,
} }
impl Default for PostgreSQLScore { impl Default for PostgreSQLScore {
fn default() -> Self { fn default() -> Self {
Self { Self {
name: "harmony-pg".to_string(), config: PostgreSQLConfig::default(),
// We are using the namespace harmony by default since some clusters (openshift family)
// have incompatible configuration of the default namespace with cnpg
namespace: "harmony".to_string(),
instances: 1,
storage_size: "1Gi".to_string(),
image_name: None, // This lets cnpg use its default image
} }
} }
} }
@@ -53,41 +44,63 @@ impl Default for PostgreSQLScore {
impl PostgreSQLScore { impl PostgreSQLScore {
pub fn new(namespace: &str) -> Self { pub fn new(namespace: &str) -> Self {
Self { Self {
namespace: namespace.to_string(), config: PostgreSQLConfig {
..Default::default() namespace: namespace.to_string(),
..Default::default()
},
} }
} }
} }
impl<T: Topology + K8sclient> Score<T> for PostgreSQLScore { impl<T: Topology + PostgreSQL + Send + Sync> Score<T> for PostgreSQLScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let metadata = ObjectMeta { Box::new(PostgreSQLInterpret {
name: Some(self.name.clone()), config: self.config.clone(),
namespace: Some(self.namespace.clone()), })
..ObjectMeta::default()
};
let spec = ClusterSpec {
instances: self.instances,
image_name: self.image_name.clone(),
storage: Storage {
size: self.storage_size.clone(),
},
bootstrap: Bootstrap {
initdb: Initdb {
database: "app".to_string(),
owner: "app".to_string(),
},
},
..ClusterSpec::default()
};
let cluster = Cluster { metadata, spec };
K8sResourceScore::single(cluster, Some(self.namespace.clone())).create_interpret()
} }
fn name(&self) -> String { fn name(&self) -> String {
format!("PostgreSQLScore({})", self.namespace) format!(
"PostgreSQLScore({}:{})",
self.config.namespace, self.config.cluster_name
)
}
}
/// Interpret implementation that delegates to Topology's PostgreSQL capability.
#[derive(Debug, Clone)]
struct PostgreSQLInterpret {
config: PostgreSQLConfig,
}
#[async_trait]
impl<T: Topology + PostgreSQL + Send + Sync> Interpret<T> for PostgreSQLInterpret {
fn get_name(&self) -> InterpretName {
InterpretName::Custom("PostgreSQLInterpret")
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
async fn execute(&self, _inventory: &Inventory, topo: &T) -> Result<Outcome, InterpretError> {
// Delegate to topology's PostgreSQL capability
let cluster_name = topo
.deploy(&self.config)
.await
.map_err(|e| InterpretError::new(e))?;
Ok(Outcome::success(format!(
"PostgreSQL cluster '{}' deployed in namespace '{}'",
cluster_name, self.config.namespace
)))
} }
} }

View File

@@ -0,0 +1,80 @@
use serde::Serialize;
use crate::interpret::Interpret;
use crate::modules::k8s::resource::K8sResourceScore;
use crate::modules::postgresql::capability::PostgreSQLConfig;
use crate::modules::postgresql::cnpg::{Bootstrap, Cluster, ClusterSpec, Initdb, Storage};
use crate::score::Score;
use crate::topology::{K8sclient, Topology};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
/// Deploys an opinionated, highly available PostgreSQL cluster managed by CNPG.
///
/// # Usage
/// ```
/// use harmony::modules::postgresql::PostgreSQLScore;
/// let score = PostgreSQLScore::new("my-app-ns");
/// ```
///
/// # Limitations (Happy Path)
/// - Requires CNPG operator installed (use CloudNativePgOperatorScore).
/// - No backups, monitoring, extensions configured.
///
/// TODO : refactor this to declare a clean dependency on cnpg operator. Then cnpg operator will
/// self-deploy either using operatorhub or helm chart depending on k8s flavor. This is cnpg
/// specific behavior
#[derive(Debug, Clone, Serialize)]
pub struct K8sPostgreSQLScore {
pub config: PostgreSQLConfig,
}
impl Default for K8sPostgreSQLScore {
fn default() -> Self {
Self {
config: PostgreSQLConfig::default(),
}
}
}
impl K8sPostgreSQLScore {
pub fn new(namespace: &str) -> Self {
Self {
config: PostgreSQLConfig {
namespace: namespace.to_string(),
..Default::default()
},
}
}
}
impl<T: Topology + K8sclient> Score<T> for K8sPostgreSQLScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let metadata = ObjectMeta {
name: Some(self.config.cluster_name.clone()),
namespace: Some(self.config.namespace.clone()),
..ObjectMeta::default()
};
let spec = ClusterSpec {
instances: self.config.instances,
storage: Storage {
size: self.config.storage_size.to_string(),
},
bootstrap: Bootstrap {
initdb: Initdb {
database: "app".to_string(),
owner: "app".to_string(),
},
},
..ClusterSpec::default()
};
let cluster = Cluster { metadata, spec };
K8sResourceScore::single(cluster, Some(self.config.namespace.clone())).create_interpret()
}
fn name(&self) -> String {
format!("PostgreSQLScore({})", self.config.namespace)
}
}

View File

@@ -6,7 +6,7 @@ use crate::data::Version;
use crate::domain::topology::router::{TlsRoute, TlsRouter}; use crate::domain::topology::router::{TlsRoute, TlsRouter};
use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome};
use crate::inventory::Inventory; use crate::inventory::Inventory;
use crate::modules::postgresql::PostgreSQLScore; use crate::modules::postgresql::K8sPostgreSQLScore;
use crate::score::Score; use crate::score::Score;
use crate::topology::{K8sclient, Topology}; use crate::topology::{K8sclient, Topology};
@@ -23,7 +23,7 @@ use crate::topology::{K8sclient, Topology};
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct PublicPostgreSQLScore { pub struct PublicPostgreSQLScore {
/// Inner non-public Postgres cluster config. /// Inner non-public Postgres cluster config.
pub postgres_score: PostgreSQLScore, pub postgres_score: K8sPostgreSQLScore,
/// Public hostname for RW TLS passthrough (port 443 → cluster-rw:5432). /// Public hostname for RW TLS passthrough (port 443 → cluster-rw:5432).
pub hostname: String, pub hostname: String,
} }
@@ -31,7 +31,7 @@ pub struct PublicPostgreSQLScore {
impl PublicPostgreSQLScore { impl PublicPostgreSQLScore {
pub fn new(namespace: &str, hostname: &str) -> Self { pub fn new(namespace: &str, hostname: &str) -> Self {
Self { Self {
postgres_score: PostgreSQLScore::new(namespace), postgres_score: K8sPostgreSQLScore::new(namespace),
hostname: hostname.to_string(), hostname: hostname.to_string(),
} }
} }
@@ -39,9 +39,9 @@ impl PublicPostgreSQLScore {
impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Score<T> for PublicPostgreSQLScore { impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Score<T> for PublicPostgreSQLScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let rw_backend = format!("{}-rw", self.postgres_score.name); let rw_backend = format!("{}-rw", self.postgres_score.config.cluster_name);
let tls_route = TlsRoute { let tls_route = TlsRoute {
namespace: self.postgres_score.namespace.clone(), namespace: self.postgres_score.config.namespace.clone(),
hostname: self.hostname.clone(), hostname: self.hostname.clone(),
backend: rw_backend, backend: rw_backend,
target_port: 5432, target_port: 5432,
@@ -56,7 +56,7 @@ impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Score<T> for PublicPostg
fn name(&self) -> String { fn name(&self) -> String {
format!( format!(
"PublicPostgreSQLScore({}:{})", "PublicPostgreSQLScore({}:{})",
self.postgres_score.namespace, self.hostname self.postgres_score.config.namespace, self.hostname
) )
} }
} }
@@ -64,7 +64,7 @@ impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Score<T> for PublicPostg
/// Custom interpret: deploy Postgres then install public TLS route. /// Custom interpret: deploy Postgres then install public TLS route.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct PublicPostgreSQLInterpret { struct PublicPostgreSQLInterpret {
postgres_score: PostgreSQLScore, postgres_score: K8sPostgreSQLScore,
tls_route: TlsRoute, tls_route: TlsRoute,
} }
@@ -93,7 +93,7 @@ impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Interpret<T> for PublicP
Ok(Outcome::success(format!( Ok(Outcome::success(format!(
"Public CNPG cluster '{}' deployed with TLS passthrough route '{}'", "Public CNPG cluster '{}' deployed with TLS passthrough route '{}'",
self.postgres_score.name.clone(), self.postgres_score.config.cluster_name.clone(),
self.tls_route.hostname self.tls_route.hostname
))) )))
} }

View File

@@ -5,6 +5,8 @@ use std::fmt;
pub struct StorageSize { pub struct StorageSize {
size_bytes: u64, size_bytes: u64,
#[serde(skip)] #[serde(skip)]
display_value: Option<u64>,
#[serde(skip)]
display_suffix: Option<String>, display_suffix: Option<String>,
} }
@@ -12,6 +14,7 @@ impl StorageSize {
pub fn new(size_bytes: u64) -> Self { pub fn new(size_bytes: u64) -> Self {
Self { Self {
size_bytes, size_bytes,
display_value: None,
display_suffix: None, display_suffix: None,
} }
} }
@@ -19,6 +22,7 @@ impl StorageSize {
pub fn b(size: u64) -> Self { pub fn b(size: u64) -> Self {
Self { Self {
size_bytes: size, size_bytes: size,
display_value: Some(size),
display_suffix: Some("B".to_string()), display_suffix: Some("B".to_string()),
} }
} }
@@ -26,6 +30,7 @@ impl StorageSize {
pub fn kb(size: u64) -> Self { pub fn kb(size: u64) -> Self {
Self { Self {
size_bytes: size * 1024, size_bytes: size * 1024,
display_value: Some(size),
display_suffix: Some("KB".to_string()), display_suffix: Some("KB".to_string()),
} }
} }
@@ -33,6 +38,7 @@ impl StorageSize {
pub fn mb(size: u64) -> Self { pub fn mb(size: u64) -> Self {
Self { Self {
size_bytes: size * 1024 * 1024, size_bytes: size * 1024 * 1024,
display_value: Some(size),
display_suffix: Some("MB".to_string()), display_suffix: Some("MB".to_string()),
} }
} }
@@ -40,6 +46,7 @@ impl StorageSize {
pub fn gb(size: u64) -> Self { pub fn gb(size: u64) -> Self {
Self { Self {
size_bytes: size * 1024 * 1024 * 1024, size_bytes: size * 1024 * 1024 * 1024,
display_value: Some(size),
display_suffix: Some("GB".to_string()), display_suffix: Some("GB".to_string()),
} }
} }
@@ -47,13 +54,15 @@ impl StorageSize {
pub fn gi(size: u64) -> Self { pub fn gi(size: u64) -> Self {
Self { Self {
size_bytes: size * 1024 * 1024 * 1024, size_bytes: size * 1024 * 1024 * 1024,
display_suffix: Some("GiB".to_string()), display_value: Some(size),
display_suffix: Some("Gi".to_string()),
} }
} }
pub fn tb(size: u64) -> Self { pub fn tb(size: u64) -> Self {
Self { Self {
size_bytes: size * 1024 * 1024 * 1024 * 1024, size_bytes: size * 1024 * 1024 * 1024 * 1024,
display_value: Some(size),
display_suffix: Some("TB".to_string()), display_suffix: Some("TB".to_string()),
} }
} }
@@ -61,6 +70,7 @@ impl StorageSize {
pub fn ti(size: u64) -> Self { pub fn ti(size: u64) -> Self {
Self { Self {
size_bytes: size * 1024 * 1024 * 1024 * 1024, size_bytes: size * 1024 * 1024 * 1024 * 1024,
display_value: Some(size),
display_suffix: Some("TiB".to_string()), display_suffix: Some("TiB".to_string()),
} }
} }
@@ -73,7 +83,8 @@ impl StorageSize {
impl fmt::Display for StorageSize { impl fmt::Display for StorageSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(suffix) = &self.display_suffix { if let Some(suffix) = &self.display_suffix {
write!(f, "{}{}", self.size_bytes, suffix) let value = self.display_value.unwrap_or(self.size_bytes);
write!(f, "{}{}", value, suffix)
} else { } else {
write!(f, "{}B", self.size_bytes) write!(f, "{}B", self.size_bytes)
} }
@@ -95,42 +106,42 @@ mod tests {
fn test_kilobytes() { fn test_kilobytes() {
let size = StorageSize::kb(2); let size = StorageSize::kb(2);
assert_eq!(size.bytes(), 2048); assert_eq!(size.bytes(), 2048);
assert_eq!(size.to_string(), "2048KB"); assert_eq!(size.to_string(), "2KB");
} }
#[test] #[test]
fn test_megabytes() { fn test_megabytes() {
let size = StorageSize::mb(3); let size = StorageSize::mb(3);
assert_eq!(size.bytes(), 3 * 1024 * 1024); assert_eq!(size.bytes(), 3 * 1024 * 1024);
assert_eq!(size.to_string(), "3145728MB"); assert_eq!(size.to_string(), "3MB");
} }
#[test] #[test]
fn test_gigabytes() { fn test_gigabytes() {
let size = StorageSize::gb(4); let size = StorageSize::gb(4);
assert_eq!(size.bytes(), 4 * 1024 * 1024 * 1024); assert_eq!(size.bytes(), 4 * 1024 * 1024 * 1024);
assert_eq!(size.to_string(), "4294967296GB"); assert_eq!(size.to_string(), "4GB");
} }
#[test] #[test]
fn test_gibibytes() { fn test_gibibytes() {
let size = StorageSize::gi(1); let size = StorageSize::gi(1);
assert_eq!(size.bytes(), 1024 * 1024 * 1024); assert_eq!(size.bytes(), 1024 * 1024 * 1024);
assert_eq!(size.to_string(), "1073741824GiB"); assert_eq!(size.to_string(), "1Gi");
} }
#[test] #[test]
fn test_terabytes() { fn test_terabytes() {
let size = StorageSize::tb(5); let size = StorageSize::tb(5);
assert_eq!(size.bytes(), 5 * 1024 * 1024 * 1024 * 1024); assert_eq!(size.bytes(), 5 * 1024 * 1024 * 1024 * 1024);
assert_eq!(size.to_string(), "5497558138880TB"); assert_eq!(size.to_string(), "5TB");
} }
#[test] #[test]
fn test_tebibytes() { fn test_tebibytes() {
let size = StorageSize::ti(1); let size = StorageSize::ti(1);
assert_eq!(size.bytes(), 1024 * 1024 * 1024 * 1024); assert_eq!(size.bytes(), 1024 * 1024 * 1024 * 1024);
assert_eq!(size.to_string(), "1099511627776TiB"); assert_eq!(size.to_string(), "1Ti");
} }
#[test] #[test]
@@ -155,6 +166,6 @@ mod tests {
fn test_ord() { fn test_ord() {
let one_gb = StorageSize::gb(1); let one_gb = StorageSize::gb(1);
let one_gi = StorageSize::gi(1); let one_gi = StorageSize::gi(1);
assert!(one_gb < one_gi); // 1GB = 1000MB, 1GiB = 1024MB assert!(one_gb < one_gi); // 1GB = 1000MB, 1Gi = 1024MB
} }
} }