From cb4382fbb510d495c6cfe08d134e39abe6003a0d Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 30 Aug 2025 16:02:49 -0400 Subject: [PATCH] feat: Inventory PhysicalHost persistence with sqlx and local sqlite db --- ...a7934b0a7fac04a4fe4b2a354f0443d630990.json | 32 ++ ...93d65a237d31d24b74a29c6a8d8420d255ab8.json | 12 + Cargo.lock | 343 +++++++++++++++++- Cargo.toml | 2 + harmony/Cargo.toml | 4 +- harmony/src/domain/data/id.rs | 14 + harmony/src/domain/hardware/mod.rs | 21 +- harmony/src/domain/inventory/mod.rs | 3 + harmony/src/domain/inventory/repository.rs | 25 ++ harmony/src/infra/inventory/mod.rs | 1 + harmony/src/infra/inventory/sqlite.rs | 69 ++++ harmony/src/infra/mod.rs | 2 + harmony/src/infra/sqlx.rs | 36 ++ 13 files changed, 560 insertions(+), 4 deletions(-) create mode 100644 .sqlx/query-934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990.json create mode 100644 .sqlx/query-f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8.json create mode 100644 harmony/src/domain/inventory/repository.rs create mode 100644 harmony/src/infra/inventory/mod.rs create mode 100644 harmony/src/infra/inventory/sqlite.rs create mode 100644 harmony/src/infra/sqlx.rs diff --git a/.sqlx/query-934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990.json b/.sqlx/query-934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990.json new file mode 100644 index 0000000..2d1f1ba --- /dev/null +++ b/.sqlx/query-934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT id, version_id, data as \"data: Json\" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "version_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "data: Json", + "ordinal": 2, + "type_info": "Null" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "934035c7ca6e064815393e4e049a7934b0a7fac04a4fe4b2a354f0443d630990" +} diff --git a/.sqlx/query-f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8.json b/.sqlx/query-f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8.json new file mode 100644 index 0000000..7df288a --- /dev/null +++ b/.sqlx/query-f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO physical_hosts (id, version_id, data) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "f10f615ee42129ffa293e46f2f893d65a237d31d24b74a29c6a8d8420d255ab8" +} diff --git a/Cargo.lock b/Cargo.lock index 9120acb..a33b177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,15 @@ dependencies = [ "syn 2.0.105", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1052,6 +1061,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1089,6 +1113,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1409,6 +1442,12 @@ dependencies = [ "syn 2.0.105", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dyn-clone" version = "1.0.19" @@ -1471,6 +1510,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -1586,6 +1628,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.4.0" @@ -1984,6 +2037,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -2241,11 +2305,13 @@ dependencies = [ "serde_with", "serde_yaml", "similar", + "sqlx", "strum 0.27.1", "tar", "temp-dir", "temp-file", "tempfile", + "thiserror 2.0.14", "tokio", "tokio-util", "url", @@ -2391,6 +2457,15 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.4", +] + [[package]] name = "headers" version = "0.4.1" @@ -2960,7 +3035,7 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "infisical" version = "0.0.2" -source = "git+https://github.com/jggc/rust-sdk.git?branch=patch-1#5a8509ef5483a5798c5d1a7f7ebeb5ba5b783253" +source = "git+https://github.com/jggc/rust-sdk.git?branch=patch-1#30d820194d29491411bd14f6c2e18ec500bb0b14" dependencies = [ "base64 0.22.1", "reqwest 0.12.20", @@ -3333,6 +3408,17 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3429,6 +3515,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "md5" version = "0.7.0" @@ -5280,6 +5376,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "snafu" @@ -5352,6 +5451,194 @@ dependencies = [ "der", ] +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.4", + "hashlink", + "indexmap 2.10.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.14", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.105", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.105", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.9.1", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.14", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.9.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.14", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.14", + "tracing", + "url", +] + [[package]] name = "ssh-cipher" version = "0.2.0" @@ -5415,6 +5702,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -6018,12 +6316,33 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -6147,6 +6466,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -6186,6 +6511,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -6306,6 +6637,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.2.8" diff --git a/Cargo.toml b/Cargo.toml index 81de5e2..aec04b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,3 +65,5 @@ directories = "6.0.0" thiserror = "2.0.14" serde = { version = "1.0.209", features = ["derive", "rc"] } serde_json = "1.0.127" +askama = "0.14" +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] } diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 56cce84..26e5f71 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -65,10 +65,12 @@ kube-derive = "1.1.0" bollard.workspace = true tar.workspace = true base64.workspace = true +thiserror.workspace = true once_cell = "1.21.3" harmony_inventory_agent = { path = "../harmony_inventory_agent" } harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" } -askama = "0.14.0" +askama.workspace = true +sqlx.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/src/domain/data/id.rs b/harmony/src/domain/data/id.rs index 5a17ae0..98cf1b9 100644 --- a/harmony/src/domain/data/id.rs +++ b/harmony/src/domain/data/id.rs @@ -24,6 +24,14 @@ pub struct Id { value: String, } +impl Id { + pub fn empty() -> Self { + Id { + value: String::new(), + } + } +} + impl FromStr for Id { type Err = (); @@ -34,6 +42,12 @@ impl FromStr for Id { } } +impl From for Id { + fn from(value: String) -> Self { + Self { value } + } +} + impl std::fmt::Display for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.value) diff --git a/harmony/src/domain/hardware/mod.rs b/harmony/src/domain/hardware/mod.rs index 399f313..d8d63df 100644 --- a/harmony/src/domain/hardware/mod.rs +++ b/harmony/src/domain/hardware/mod.rs @@ -1,8 +1,8 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use derive_new::new; use harmony_types::net::MacAddress; -use serde::{Serialize, Serializer, ser::SerializeStruct}; +use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct}; use serde_value::Value; pub type HostGroup = Vec; @@ -11,6 +11,7 @@ pub type FirewallGroup = Vec; #[derive(Debug, Clone)] pub struct PhysicalHost { + pub id: Id, pub category: HostCategory, pub network: Vec, pub management: Arc, @@ -23,6 +24,7 @@ pub struct PhysicalHost { impl PhysicalHost { pub fn empty(category: HostCategory) -> Self { Self { + id: Id::empty(), category, network: vec![], storage: vec![], @@ -128,6 +130,15 @@ impl Serialize for PhysicalHost { } } +impl<'de> Deserialize<'de> for PhysicalHost { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + todo!() + } +} + #[derive(new, Serialize)] pub struct ManualManagementInterface; @@ -187,6 +198,8 @@ pub struct NetworkInterface { #[cfg(test)] use harmony_macros::mac_address; + +use crate::data::Id; #[cfg(test)] impl NetworkInterface { pub fn dummy() -> Self { @@ -304,6 +317,7 @@ mod tests { fn test_serialize_physical_host_with_hp_ilo() { // Create a PhysicalHost with HP iLO management let host = PhysicalHost { + id: Id::empty(), category: HostCategory::Server, network: vec![NetworkInterface::dummy()], management: Arc::new(MockHPIlo { @@ -341,6 +355,7 @@ mod tests { fn test_serialize_physical_host_with_dell_idrac() { // Create a PhysicalHost with Dell iDRAC management let host = PhysicalHost { + id: Id::empty(), category: HostCategory::Server, network: vec![NetworkInterface::dummy()], management: Arc::new(MockDellIdrac { @@ -375,6 +390,7 @@ mod tests { fn test_different_management_implementations_produce_valid_json() { // Create hosts with different management implementations let host1 = PhysicalHost { + id: Id::empty(), category: HostCategory::Server, network: vec![], management: Arc::new(MockHPIlo { @@ -390,6 +406,7 @@ mod tests { }; let host2 = PhysicalHost { + id: Id::empty(), category: HostCategory::Server, network: vec![], management: Arc::new(MockDellIdrac { diff --git a/harmony/src/domain/inventory/mod.rs b/harmony/src/domain/inventory/mod.rs index 851a150..2760e12 100644 --- a/harmony/src/domain/inventory/mod.rs +++ b/harmony/src/domain/inventory/mod.rs @@ -1,3 +1,6 @@ +mod repository; +pub use repository::*; + #[derive(Debug, new, Clone)] pub struct InventoryFilter { target: Vec, diff --git a/harmony/src/domain/inventory/repository.rs b/harmony/src/domain/inventory/repository.rs new file mode 100644 index 0000000..e4e02a9 --- /dev/null +++ b/harmony/src/domain/inventory/repository.rs @@ -0,0 +1,25 @@ +use async_trait::async_trait; + +use crate::hardware::PhysicalHost; + +/// Errors that can occur within the repository layer. +#[derive(thiserror::Error, Debug)] +pub enum RepoError { + #[error("Database query failed: {0}")] + QueryFailed(String), + #[error("Data serialization failed: {0}")] + Serialization(String), + #[error("Data deserialization failed: {0}")] + Deserialization(String), + #[error("Could not connect to the database: {0}")] + ConnectionFailed(String), +} + +// --- Trait and Implementation --- + +/// Defines the contract for inventory persistence. +#[async_trait] +pub trait InventoryRepository: Send + Sync + 'static { + async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>; + async fn get_latest_by_id(&self, host_id: &str) -> Result, RepoError>; +} diff --git a/harmony/src/infra/inventory/mod.rs b/harmony/src/infra/inventory/mod.rs new file mode 100644 index 0000000..8e0367d --- /dev/null +++ b/harmony/src/infra/inventory/mod.rs @@ -0,0 +1 @@ +mod sqlite; diff --git a/harmony/src/infra/inventory/sqlite.rs b/harmony/src/infra/inventory/sqlite.rs new file mode 100644 index 0000000..03fae14 --- /dev/null +++ b/harmony/src/infra/inventory/sqlite.rs @@ -0,0 +1,69 @@ +use crate::{ + data::Id, + hardware::PhysicalHost, + inventory::{InventoryRepository, RepoError}, +}; +use async_trait::async_trait; +use log::{info, warn}; +use sqlx::{Pool, Sqlite, SqlitePool}; + +/// A thread-safe, connection-pooled repository using SQLite. +#[derive(Debug)] +pub struct SqliteInventoryRepository { + pool: Pool, +} + +impl SqliteInventoryRepository { + pub async fn new(database_url: &str) -> Result { + let pool = SqlitePool::connect(database_url) + .await + .map_err(|e| RepoError::ConnectionFailed(e.to_string()))?; + + todo!("make sure migrations are up to date"); + info!( + "SQLite inventory repository initialized at '{}'", + database_url, + ); + Ok(Self { pool }) + } +} + +#[async_trait] +impl InventoryRepository for SqliteInventoryRepository { + async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError> { + let data = serde_json::to_vec(host).map_err(|e| RepoError::Serialization(e.to_string()))?; + + let id = Id::default().to_string(); + let host_id = host.id.to_string(); + + sqlx::query!( + "INSERT INTO physical_hosts (id, version_id, data) VALUES (?, ?, ?)", + host_id, + id, + data, + ) + .execute(&self.pool) + .await?; + + info!("Saved new inventory version for host '{}'", host.id); + Ok(()) + } + + async fn get_latest_by_id(&self, host_id: &str) -> Result, RepoError> { + let row = sqlx::query_as!( + DbHost, + r#"SELECT id, version_id, data as "data: Json" FROM physical_hosts WHERE id = ? ORDER BY version_id DESC LIMIT 1"#, + host_id + ) + .fetch_optional(&self.pool) + .await?; + todo!() + } +} + +use sqlx::types::Json; +struct DbHost { + data: Json, + id: Id, + version_id: Id, +} diff --git a/harmony/src/infra/mod.rs b/harmony/src/infra/mod.rs index b3a94ea..c05c7b6 100644 --- a/harmony/src/infra/mod.rs +++ b/harmony/src/infra/mod.rs @@ -1,4 +1,6 @@ pub mod executors; pub mod hp_ilo; pub mod intel_amt; +pub mod inventory; pub mod opnsense; +mod sqlx; diff --git a/harmony/src/infra/sqlx.rs b/harmony/src/infra/sqlx.rs new file mode 100644 index 0000000..00472b2 --- /dev/null +++ b/harmony/src/infra/sqlx.rs @@ -0,0 +1,36 @@ +use crate::inventory::RepoError; + +impl From for RepoError { + fn from(value: sqlx::Error) -> Self { + match value { + sqlx::Error::Configuration(_) + | sqlx::Error::Io(_) + | sqlx::Error::Tls(_) + | sqlx::Error::Protocol(_) + | sqlx::Error::PoolTimedOut + | sqlx::Error::PoolClosed + | sqlx::Error::WorkerCrashed => RepoError::ConnectionFailed(value.to_string()), + sqlx::Error::InvalidArgument(_) + | sqlx::Error::Database(_) + | sqlx::Error::RowNotFound + | sqlx::Error::TypeNotFound { .. } + | sqlx::Error::ColumnIndexOutOfBounds { .. } + | sqlx::Error::ColumnNotFound(_) + | sqlx::Error::AnyDriverError(_) + | sqlx::Error::Migrate(_) + | sqlx::Error::InvalidSavePointStatement + | sqlx::Error::BeginFailed => RepoError::QueryFailed(value.to_string()), + sqlx::Error::Encode(_) => RepoError::Serialization(value.to_string()), + sqlx::Error::Decode(_) | sqlx::Error::ColumnDecode { .. } => { + RepoError::Deserialization(value.to_string()) + } + _ => RepoError::QueryFailed(value.to_string()), + } + } +} + +impl From for RepoError { + fn from(value: serde_json::Error) -> Self { + RepoError::Serialization(value.to_string()) + } +}