forked from NationTech/harmony
feat: Introduce Topology Trait for Compile-Time Safe Score Binding
Introduce the `Topology` trait to ensure that `Maestro` can compile-time safely bind compatible `Scores` and `Topologies`. This refactoring includes updating `HarmonyTuiEvent`, `ScoreListWidget`, and related structures to work with generic `Topology` types, enhancing type safety and modularity.
This commit is contained in:
parent
f7dc15cbf0
commit
fc718f11cf
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -823,7 +823,6 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"harmony",
|
"harmony",
|
||||||
"harmony_macros",
|
"harmony_macros",
|
||||||
"harmony_tui",
|
|
||||||
"harmony_types",
|
"harmony_types",
|
||||||
"log",
|
"log",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -860,6 +859,17 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "example-topology"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "example-topology2"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "example-tui"
|
name = "example-tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
harmony = { path = "../../harmony" }
|
harmony = { path = "../../harmony" }
|
||||||
harmony_tui = { path = "../../harmony_tui" }
|
#harmony_tui = { path = "../../harmony_tui" }
|
||||||
harmony_types = { path = "../../harmony_types" }
|
harmony_types = { path = "../../harmony_types" }
|
||||||
cidr = { workspace = true }
|
cidr = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@ -2,7 +2,8 @@ use harmony::{
|
|||||||
data::Version,
|
data::Version,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::lamp::{LAMPConfig, LAMPScore},
|
modules::lamp::{LAMPConfig, LAMPScore},
|
||||||
topology::Url,
|
score::Score,
|
||||||
|
topology::{HAClusterTopology, Topology, Url},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -17,8 +18,12 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Maestro::load_from_env()
|
Maestro::<HAClusterTopology>::load_from_env()
|
||||||
.interpret(Box::new(lamp_stack))
|
.interpret(Box::new(lamp_stack))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clone_score<T: Topology, S: Score<T> + Clone + 'static>(score: S) -> Box<S> {
|
||||||
|
Box::new(score.clone())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::{dummy::{ErrorScore, PanicScore, SuccessScore}, k8s::deployment::K8sDeploymentScore},
|
modules::{
|
||||||
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
|
k8s::deployment::K8sDeploymentScore,
|
||||||
|
},
|
||||||
topology::HAClusterTopology,
|
topology::HAClusterTopology,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -56,10 +56,7 @@ impl<T: Topology> Maestro<T> {
|
|||||||
score_mut.append(&mut scores);
|
score_mut.append(&mut scores);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn interpret<S>(&self, score: S) -> Result<Outcome, InterpretError>
|
pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> {
|
||||||
where
|
|
||||||
S: Score<T>,
|
|
||||||
{
|
|
||||||
info!("Running score {score:?}");
|
info!("Running score {score:?}");
|
||||||
let interpret = score.create_interpret();
|
let interpret = score.create_interpret();
|
||||||
info!("Launching interpret {interpret:?}");
|
info!("Launching interpret {interpret:?}");
|
||||||
|
|||||||
@ -1,6 +1,21 @@
|
|||||||
use super::{interpret::Interpret, topology::Topology};
|
use super::{interpret::Interpret, topology::Topology};
|
||||||
|
|
||||||
pub trait Score<T: Topology>: std::fmt::Debug + Send + Sync {
|
pub trait Score<T: Topology>: std::fmt::Debug + Send + Sync + CloneBoxScore<T> {
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait CloneBoxScore<T: Topology> {
|
||||||
|
fn clone_box(&self) -> Box<dyn Score<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<S, T> CloneBoxScore<T> for S
|
||||||
|
where
|
||||||
|
T: Topology,
|
||||||
|
S: Score<T> + Clone + 'static,
|
||||||
|
{
|
||||||
|
fn clone_box(&self) -> Box<dyn Score<T>> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -99,7 +99,10 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret {
|
|||||||
inventory: &Inventory,
|
inventory: &Inventory,
|
||||||
topology: &T,
|
topology: &T,
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
info!("Executing {} on inventory {inventory:?}", <DnsInterpret as Interpret<T>>::get_name(self));
|
info!(
|
||||||
|
"Executing {} on inventory {inventory:?}",
|
||||||
|
<DnsInterpret as Interpret<T>>::get_name(self)
|
||||||
|
);
|
||||||
|
|
||||||
self.serve_dhcp_entries(inventory, topology).await?;
|
self.serve_dhcp_entries(inventory, topology).await?;
|
||||||
self.ensure_hosts_registered(topology).await?;
|
self.ensure_hosts_registered(topology).await?;
|
||||||
|
|||||||
@ -10,12 +10,12 @@ use widget::{help::HelpWidget, score::ScoreListWidget};
|
|||||||
use std::{panic, sync::Arc, time::Duration};
|
use std::{panic, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind};
|
use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind};
|
||||||
use harmony::{maestro::Maestro, score::Score};
|
use harmony::{maestro::Maestro, score::Score, topology::Topology};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
self, Frame,
|
self, Frame,
|
||||||
layout::{Constraint, Layout, Position},
|
layout::{Constraint, Layout, Position},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
widgets::{Block, Borders, ListItem},
|
widgets::{Block, Borders},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod tui {
|
pub mod tui {
|
||||||
@ -43,23 +43,25 @@ pub mod tui {
|
|||||||
/// init(maestro).await.unwrap();
|
/// init(maestro).await.unwrap();
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn init(maestro: Maestro) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn init<T: Topology + std::fmt::Debug + Send + Sync + 'static>(
|
||||||
|
maestro: Maestro<T>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
HarmonyTUI::new(maestro).init().await
|
HarmonyTUI::new(maestro).init().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HarmonyTUI {
|
pub struct HarmonyTUI<T: Topology> {
|
||||||
score: ScoreListWidget,
|
score: ScoreListWidget<T>,
|
||||||
should_quit: bool,
|
should_quit: bool,
|
||||||
tui_state: TuiWidgetState,
|
tui_state: TuiWidgetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum HarmonyTuiEvent {
|
enum HarmonyTuiEvent<T: Topology> {
|
||||||
LaunchScore(ScoreItem),
|
LaunchScore(Box<dyn Score<T>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HarmonyTUI {
|
impl<T: Topology + std::fmt::Debug + Send + Sync + 'static> HarmonyTUI<T> {
|
||||||
pub fn new(maestro: Maestro) -> Self {
|
pub fn new(maestro: Maestro<T>) -> Self {
|
||||||
let maestro = Arc::new(maestro);
|
let maestro = Arc::new(maestro);
|
||||||
let (_handle, sender) = Self::start_channel(maestro.clone());
|
let (_handle, sender) = Self::start_channel(maestro.clone());
|
||||||
let score = ScoreListWidget::new(Self::scores_list(&maestro), sender);
|
let score = ScoreListWidget::new(Self::scores_list(&maestro), sender);
|
||||||
@ -72,9 +74,12 @@ impl HarmonyTUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_channel(
|
fn start_channel(
|
||||||
maestro: Arc<Maestro>,
|
maestro: Arc<Maestro<T>>,
|
||||||
) -> (tokio::task::JoinHandle<()>, mpsc::Sender<HarmonyTuiEvent>) {
|
) -> (
|
||||||
let (sender, mut receiver) = mpsc::channel::<HarmonyTuiEvent>(32);
|
tokio::task::JoinHandle<()>,
|
||||||
|
mpsc::Sender<HarmonyTuiEvent<T>>,
|
||||||
|
) {
|
||||||
|
let (sender, mut receiver) = mpsc::channel::<HarmonyTuiEvent<T>>(32);
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
info!("Starting message channel receiver loop");
|
info!("Starting message channel receiver loop");
|
||||||
while let Some(event) = receiver.recv().await {
|
while let Some(event) = receiver.recv().await {
|
||||||
@ -84,8 +89,7 @@ impl HarmonyTUI {
|
|||||||
let maestro = maestro.clone();
|
let maestro = maestro.clone();
|
||||||
|
|
||||||
let joinhandle_result =
|
let joinhandle_result =
|
||||||
tokio::spawn(async move { maestro.interpret(score_item.0).await })
|
tokio::spawn(async move { maestro.interpret(score_item).await }).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
match joinhandle_result {
|
match joinhandle_result {
|
||||||
Ok(interpretation_result) => match interpretation_result {
|
Ok(interpretation_result) => match interpretation_result {
|
||||||
@ -163,13 +167,10 @@ impl HarmonyTUI {
|
|||||||
frame.render_widget(tui_logger, output_area)
|
frame.render_widget(tui_logger, output_area)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scores_list(maestro: &Maestro) -> Vec<ScoreItem> {
|
fn scores_list(maestro: &Maestro<T>) -> Vec<Box<dyn Score<T>>> {
|
||||||
let scores = maestro.scores();
|
let scores = maestro.scores();
|
||||||
let scores_read = scores.read().expect("Should be able to read scores");
|
let scores_read = scores.read().expect("Should be able to read scores");
|
||||||
scores_read
|
scores_read.iter().map(|s| s.clone_box()).collect()
|
||||||
.iter()
|
|
||||||
.map(|s| ScoreItem(s.clone_box()))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_event(&mut self, event: &Event) {
|
async fn handle_event(&mut self, event: &Event) {
|
||||||
@ -189,18 +190,3 @@ impl HarmonyTUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ScoreItem(Box<dyn Score>);
|
|
||||||
|
|
||||||
impl ScoreItem {
|
|
||||||
pub fn clone(&self) -> Self {
|
|
||||||
Self(self.0.clone_box())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<ListItem<'_>> for &ScoreItem {
|
|
||||||
fn into(self) -> ListItem<'static> {
|
|
||||||
ListItem::new(self.0.name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crossterm::event::{Event, KeyCode, KeyEventKind};
|
use crossterm::event::{Event, KeyCode, KeyEventKind};
|
||||||
|
use harmony::{score::Score, topology::Topology};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame
|
||||||
layout::Rect,
|
|
||||||
style::{Style, Stylize},
|
|
||||||
widgets::{List, ListState, StatefulWidget, Widget},
|
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::{HarmonyTuiEvent, ScoreItem};
|
use crate::HarmonyTuiEvent;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ExecutionState {
|
enum ExecutionState {
|
||||||
@ -20,22 +18,22 @@ enum ExecutionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Execution {
|
struct Execution<T: Topology> {
|
||||||
state: ExecutionState,
|
state: ExecutionState,
|
||||||
score: ScoreItem,
|
score: Box<dyn Score<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct ScoreListWidget {
|
pub(crate) struct ScoreListWidget<T: Topology> {
|
||||||
list_state: Arc<RwLock<ListState>>,
|
list_state: Arc<RwLock<ListState>>,
|
||||||
scores: Vec<ScoreItem>,
|
scores: Vec<Box<dyn Score<T>>>,
|
||||||
execution: Option<Execution>,
|
execution: Option<Execution<T>>,
|
||||||
execution_history: Vec<Execution>,
|
execution_history: Vec<Execution<T>>,
|
||||||
sender: mpsc::Sender<HarmonyTuiEvent>,
|
sender: mpsc::Sender<HarmonyTuiEvent<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScoreListWidget {
|
impl<T: Topology + std::fmt::Debug> ScoreListWidget<T> {
|
||||||
pub(crate) fn new(scores: Vec<ScoreItem>, sender: mpsc::Sender<HarmonyTuiEvent>) -> Self {
|
pub(crate) fn new(scores: Vec<Box<dyn Score<T>>>, sender: mpsc::Sender<HarmonyTuiEvent<T>>) -> Self {
|
||||||
let mut list_state = ListState::default();
|
let mut list_state = ListState::default();
|
||||||
list_state.select_first();
|
list_state.select_first();
|
||||||
let list_state = Arc::new(RwLock::new(list_state));
|
let list_state = Arc::new(RwLock::new(list_state));
|
||||||
@ -58,9 +56,9 @@ impl ScoreListWidget {
|
|||||||
|
|
||||||
self.execution = Some(Execution {
|
self.execution = Some(Execution {
|
||||||
state: ExecutionState::INITIATED,
|
state: ExecutionState::INITIATED,
|
||||||
score: score.clone(),
|
score: score.clone_box(),
|
||||||
});
|
});
|
||||||
info!("{:#?}\n\nConfirm Execution (Press y/n)", score.0);
|
info!("{:#?}\n\nConfirm Execution (Press y/n)", score);
|
||||||
} else {
|
} else {
|
||||||
warn!("No Score selected, nothing to launch");
|
warn!("No Score selected, nothing to launch");
|
||||||
}
|
}
|
||||||
@ -94,7 +92,7 @@ impl ScoreListWidget {
|
|||||||
execution.state = ExecutionState::RUNNING;
|
execution.state = ExecutionState::RUNNING;
|
||||||
info!("Launch execution {:?}", execution);
|
info!("Launch execution {:?}", execution);
|
||||||
self.sender
|
self.sender
|
||||||
.send(HarmonyTuiEvent::LaunchScore(execution.score.clone()))
|
.send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box()))
|
||||||
.await
|
.await
|
||||||
.expect("Should be able to send message");
|
.expect("Should be able to send message");
|
||||||
}
|
}
|
||||||
@ -123,16 +121,22 @@ impl ScoreListWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for &ScoreListWidget {
|
impl<T: Topology> Widget for &ScoreListWidget<T> {
|
||||||
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let mut list_state = self.list_state.write().unwrap();
|
let mut list_state = self.list_state.write().unwrap();
|
||||||
let list = List::new(&self.scores)
|
let scores_items: Vec<ListItem<'_>> = self.scores.iter().map(score_to_list_item).collect();
|
||||||
|
let list = List::new(scores_items)
|
||||||
.highlight_style(Style::new().bold().italic())
|
.highlight_style(Style::new().bold().italic())
|
||||||
.highlight_symbol("🠊 ");
|
.highlight_symbol("🠊 ");
|
||||||
|
|
||||||
StatefulWidget::render(list, area, buf, &mut list_state)
|
StatefulWidget::render(list, area, buf, &mut list_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn score_to_list_item<'a, T: Topology>(score: &'a Box<dyn Score<T>>) -> ListItem<'a> {
|
||||||
|
ListItem::new(score.name())
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user