diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 256c759..2bea72d 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -1,9 +1,9 @@ -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; -use log::info; +use log::{info, warn}; use super::{ - interpret::{InterpretError, Outcome}, + interpret::{InterpretError, InterpretStatus, Outcome}, inventory::Inventory, score::Score, topology::Topology, @@ -15,6 +15,7 @@ pub struct Maestro { inventory: Inventory, topology: T, scores: Arc>>, + topology_preparation_result: Mutex>, } impl Maestro { @@ -23,9 +24,28 @@ impl Maestro { inventory, topology, scores: Arc::new(RwLock::new(Vec::new())), + topology_preparation_result: None.into(), } } + /// Ensures the associated Topology is ready for operations. + /// Delegates the readiness check and potential setup actions to the Topology. + pub async fn prepare_topology(&self) -> Result { + info!("Ensuring topology '{}' is ready...", self.topology.name()); + let outcome = self.topology.ensure_ready().await?; + info!( + "Topology '{}' readiness check complete: {}", + self.topology.name(), + outcome.status + ); + + self.topology_preparation_result + .lock() + .unwrap() + .replace(outcome.clone()); + Ok(outcome) + } + // Load the inventory and inventory from environment. // This function is able to discover the context that it is running in, such as k8s clusters, aws cloud, linux host, etc. // When the HARMONY_TOPOLOGY environment variable is not set, it will default to install k3s @@ -47,16 +67,31 @@ impl Maestro { } } - pub fn start(&mut self) { - info!("Starting Maestro"); - } - pub fn register_all(&mut self, mut scores: ScoreVec) { let mut score_mut = self.scores.write().expect("Should acquire lock"); score_mut.append(&mut scores); } + fn is_topology_initialized(&self) -> bool { + let result = self.topology_preparation_result.lock().unwrap(); + if let Some(outcome) = result.as_ref() { + match outcome.status { + InterpretStatus::SUCCESS => return true, + _ => return false, + } + } else { + false + } + } + pub async fn interpret(&self, score: Box>) -> Result { + if !self.is_topology_initialized() { + warn!( + "Launching interpret for score {} but Topology {} is not fully initialized!", + score.name(), + self.topology.name(), + ); + } info!("Running score {score:?}"); let interpret = score.create_interpret(); info!("Launching interpret {interpret:?}"); diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index fa0b7fe..16525f5 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -5,6 +5,7 @@ mod load_balancer; pub mod openshift; mod router; mod tftp; +use async_trait::async_trait; pub use ha_cluster::*; pub use load_balancer::*; pub use router::*; @@ -17,8 +18,38 @@ pub use tftp::*; use std::net::IpAddr; -pub trait Topology { +use super::interpret::{InterpretError, Outcome}; + +/// Represents a logical view of an infrastructure environment providing specific capabilities. +/// +/// A Topology acts as a self-contained "package" responsible for managing access +/// to its underlying resources and ensuring they are in a ready state before use. +/// It defines the contract for the capabilities it provides through implemented +/// capability traits (e.g., `HasK8sCapability`, `HasDnsServer`). +#[async_trait] +pub trait Topology: Send + Sync { + /// Returns a unique identifier or name for this specific topology instance. + /// This helps differentiate between multiple instances of potentially the same type. fn name(&self) -> &str; + + /// Ensures that the topology and its required underlying components or services + /// are ready to provide their declared capabilities. + /// + /// Implementations of this method MUST be idempotent. Subsequent calls after a + /// successful readiness check should ideally be cheap NO-OPs. + /// + /// This method encapsulates the logic for: + /// 1. **Checking Current State:** Assessing if the required resources/services are already running and configured. + /// 2. **Discovery:** Identifying the runtime environment (e.g., local Docker, AWS, existing cluster). + /// 3. **Initialization/Bootstrapping:** Performing necessary setup actions if not already ready. This might involve: + /// * Making API calls. + /// * Running external commands (e.g., `k3d`, `docker`). + /// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic. + /// + /// # Returns + /// - `Ok(Outcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context. + /// - `Err(TopologyError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments. + async fn ensure_ready(&self) -> Result; } pub type IpAddress = IpAddr;