diff --git a/Cargo.lock b/Cargo.lock index c29465e..dd2651a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1351,6 +1351,16 @@ dependencies = [ "url", ] +[[package]] +name = "example-postgres" +version = "0.1.0" +dependencies = [ + "async-trait", + "harmony", + "serde", + "tokio", +] + [[package]] name = "example-rust" version = "0.1.0" @@ -4807,6 +4817,7 @@ dependencies = [ "bytes", "libc", "mio 1.0.4", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml new file mode 100644 index 0000000..cfb1867 --- /dev/null +++ b/examples/postgres/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "example-postgres" +version = "0.1.0" +edition = "2021" + +[dependencies] +harmony = { path = "../../harmony" } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +async-trait = "0.1.80" \ No newline at end of file diff --git a/examples/postgres/src/main.rs b/examples/postgres/src/main.rs new file mode 100644 index 0000000..132ec93 --- /dev/null +++ b/examples/postgres/src/main.rs @@ -0,0 +1,84 @@ +use async_trait::async_trait; +use harmony::{ + data::{PostgresDatabase, PostgresUser}, + interpret::InterpretError, + inventory::Inventory, + maestro::Maestro, + modules::postgres::PostgresScore, + topology::{PostgresServer, Topology}, +}; +use std::error::Error; + +#[derive(Debug, Clone)] +struct MockTopology; + +#[async_trait] +impl Topology for MockTopology { + fn name(&self) -> &str { + "MockTopology" + } + + async fn ensure_ready(&self) -> Result { + Ok(harmony::interpret::Outcome::new( + harmony::interpret::InterpretStatus::SUCCESS, + "Mock topology is always ready".to_string(), + )) + } +} + +#[async_trait] +impl PostgresServer for MockTopology { + async fn ensure_users_exist(&self, users: Vec) -> Result<(), InterpretError> { + println!("Ensuring users exist:"); + for user in users { + println!(" - {}: {}", user.name, user.password); + } + Ok(()) + } + + async fn ensure_databases_exist( + &self, + databases: Vec, + ) -> Result<(), InterpretError> { + println!("Ensuring databases exist:"); + for db in databases { + println!(" - {}: owner={}", db.name, db.owner); + } + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let users = vec![ + PostgresUser { + name: "admin".to_string(), + password: "password".to_string(), + }, + PostgresUser { + name: "user".to_string(), + password: "password".to_string(), + }, + ]; + + let databases = vec![ + PostgresDatabase { + name: "app_db".to_string(), + owner: "admin".to_string(), + }, + PostgresDatabase { + name: "user_db".to_string(), + owner: "user".to_string(), + }, + ]; + + let postgres_score = PostgresScore::new(users, databases); + + let inventory = Inventory::empty(); + let topology = MockTopology; + let maestro = Maestro::new(inventory, topology); + + maestro.interpret(Box::new(postgres_score)).await?; + + Ok(()) +} \ No newline at end of file diff --git a/harmony/src/domain/data/mod.rs b/harmony/src/domain/data/mod.rs index e122b20..e8add77 100644 --- a/harmony/src/domain/data/mod.rs +++ b/harmony/src/domain/data/mod.rs @@ -2,3 +2,6 @@ mod id; mod version; pub use id::*; pub use version::*; + +mod postgres; +pub use postgres::*; diff --git a/harmony/src/domain/data/postgres.rs b/harmony/src/domain/data/postgres.rs new file mode 100644 index 0000000..a3c505d --- /dev/null +++ b/harmony/src/domain/data/postgres.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostgresUser { + pub name: String, + pub password: String, // In a real scenario, this should be a secret type +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostgresDatabase { + pub name: String, + pub owner: String, +} diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index c89b163..16fbf4f 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -22,6 +22,7 @@ pub enum InterpretName { K3dInstallation, TenantInterpret, Application, + Postgres, } impl std::fmt::Display for InterpretName { @@ -39,6 +40,7 @@ impl std::fmt::Display for InterpretName { InterpretName::K3dInstallation => f.write_str("K3dInstallation"), InterpretName::TenantInterpret => f.write_str("Tenant"), InterpretName::Application => f.write_str("Application"), + InterpretName::Postgres => f.write_str("Postgres"), } } } diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 7d3830d..0b899cc 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -23,6 +23,9 @@ pub use network::*; use serde::Serialize; pub use tftp::*; +mod postgres; +pub use postgres::*; + mod helm_command; pub use helm_command::*; diff --git a/harmony/src/domain/topology/postgres.rs b/harmony/src/domain/topology/postgres.rs new file mode 100644 index 0000000..9fda172 --- /dev/null +++ b/harmony/src/domain/topology/postgres.rs @@ -0,0 +1,14 @@ +use crate::{ + data::{PostgresDatabase, PostgresUser}, + interpret::InterpretError, +}; +use async_trait::async_trait; + +#[async_trait] +pub trait PostgresServer { + async fn ensure_users_exist(&self, users: Vec) -> Result<(), InterpretError>; + async fn ensure_databases_exist( + &self, + databases: Vec, + ) -> Result<(), InterpretError>; +} diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index e9b6c52..05ff266 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -16,3 +16,4 @@ pub mod opnsense; pub mod prometheus; pub mod tenant; pub mod tftp; +pub mod postgres; diff --git a/harmony/src/modules/postgres.rs b/harmony/src/modules/postgres.rs new file mode 100644 index 0000000..d44e39c --- /dev/null +++ b/harmony/src/modules/postgres.rs @@ -0,0 +1,119 @@ +use async_trait::async_trait; +use derive_new::new; +use log::info; +use serde::{Deserialize, Serialize}; + +use crate::{ + data::{PostgresDatabase, PostgresUser, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{PostgresServer, Topology}, +}; + +#[derive(Debug, new, Clone, Serialize, Deserialize)] +pub struct PostgresScore { + users: Vec, + databases: Vec, +} + +impl Score for PostgresScore { + fn create_interpret(&self) -> Box> { + Box::new(PostgresInterpret::new(self.clone())) + } + + fn name(&self) -> String { + "PostgresScore".to_string() + } +} + +#[derive(Debug, Clone)] +pub struct PostgresInterpret { + score: PostgresScore, + version: Version, + status: InterpretStatus, +} + +impl PostgresInterpret { + pub fn new(score: PostgresScore) -> Self { + let version = Version::from("1.0.0").expect("Version should be valid"); + + Self { + version, + score, + status: InterpretStatus::QUEUED, + } + } + + async fn ensure_users_exist( + &self, + postgres_server: &P, + ) -> Result { + let users = &self.score.users; + postgres_server.ensure_users_exist(users.clone()).await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!( + "PostgresInterpret ensured {} users exist successfully", + users.len() + ), + )) + } + + async fn ensure_databases_exist( + &self, + postgres_server: &P, + ) -> Result { + let databases = &self.score.databases; + postgres_server + .ensure_databases_exist(databases.clone()) + .await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!( + "PostgresInterpret ensured {} databases exist successfully", + databases.len() + ), + )) + } +} + +#[async_trait] +impl Interpret for PostgresInterpret { + fn get_name(&self) -> InterpretName { + InterpretName::Postgres + } + + fn get_version(&self) -> crate::domain::data::Version { + self.version.clone() + } + + fn get_status(&self) -> InterpretStatus { + self.status.clone() + } + + fn get_children(&self) -> Vec { + todo!() + } + + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + info!( + "Executing {} on inventory {inventory:?})", + >::get_name(self) + ); + + self.ensure_users_exist(topology).await?; + self.ensure_databases_exist(topology).await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Postgres Interpret execution successful".to_string(), + )) + } +}