493 lines
16 KiB
Rust
493 lines
16 KiB
Rust
use std::any::Any;
|
|
use std::fmt::Debug;
|
|
use std::process::Command;
|
|
pub trait Capability {}
|
|
|
|
pub trait CommandCapability: Capability {
|
|
fn execute_command(&self, command: &str, args: &Vec<String>) -> Result<String, String>;
|
|
}
|
|
|
|
pub trait KubernetesCapability: Capability {
|
|
fn apply_manifest(&self, manifest: &str) -> Result<(), String>;
|
|
fn get_resource(&self, resource_type: &str, name: &str) -> Result<String, String>;
|
|
}
|
|
|
|
pub trait Topology {
|
|
fn name(&self) -> &str;
|
|
}
|
|
|
|
pub trait Interpret<T: Topology> {
|
|
fn execute(&self, topology: &T) -> Result<String, String>;
|
|
}
|
|
|
|
// --- Score Definition Structs (Concrete) ---
|
|
// CommandScore struct remains the same
|
|
#[derive(Debug, Clone)] // Added Debug/Clone for easier handling
|
|
pub struct CommandScore {
|
|
name: String,
|
|
command: String,
|
|
args: Vec<String>,
|
|
}
|
|
|
|
impl CommandScore {
|
|
pub fn new(name: String, command: String, args: Vec<String>) -> Self {
|
|
Self { name, command, args }
|
|
}
|
|
}
|
|
|
|
// K8sResourceScore struct remains the same
|
|
#[derive(Debug, Clone)]
|
|
pub struct K8sResourceScore {
|
|
name: String,
|
|
manifest: String,
|
|
}
|
|
|
|
impl K8sResourceScore {
|
|
pub fn new(name: String, manifest: String) -> Self {
|
|
Self { name, manifest }
|
|
}
|
|
}
|
|
|
|
|
|
// --- Metadata / Base Score Trait (Non-Generic) ---
|
|
// Trait for common info and enabling downcasting later if needed
|
|
pub trait ScoreDefinition: Debug + Send + Sync {
|
|
fn name(&self) -> &str;
|
|
// Method to allow downcasting
|
|
fn as_any(&self) -> &dyn Any;
|
|
// Optional: Could add methods for description, parameters etc.
|
|
// fn description(&self) -> &str;
|
|
|
|
// Optional but potentially useful: A way to clone the definition
|
|
fn box_clone(&self) -> Box<dyn ScoreDefinition>;
|
|
}
|
|
|
|
// Implement Clone for Box<dyn ScoreDefinition>
|
|
impl Clone for Box<dyn ScoreDefinition> {
|
|
fn clone(&self) -> Self {
|
|
self.box_clone()
|
|
}
|
|
}
|
|
|
|
// Implement ScoreDefinition for your concrete score types
|
|
impl ScoreDefinition for CommandScore {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
fn box_clone(&self) -> Box<dyn ScoreDefinition> {
|
|
Box::new(self.clone())
|
|
}
|
|
}
|
|
|
|
impl ScoreDefinition for K8sResourceScore {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
fn box_clone(&self) -> Box<dyn ScoreDefinition> {
|
|
Box::new(self.clone())
|
|
}
|
|
}
|
|
|
|
|
|
// --- Score Compatibility Trait (Generic over T) ---
|
|
// This remains largely the same, ensuring compile-time checks
|
|
pub trait Score<T: Topology>: ScoreDefinition {
|
|
// No need for name() here, it's in ScoreDefinition
|
|
fn compile(&self) -> Result<Box<dyn Interpret<T>>, String>;
|
|
}
|
|
|
|
// --- Implementations of Score<T> (Crucial Link) ---
|
|
|
|
// CommandScore implements Score<T> for any T with CommandCapability
|
|
impl<T> Score<T> for CommandScore
|
|
where
|
|
T: Topology + CommandCapability + 'static, // Added 'static bound often needed for Box<dyn>
|
|
// Self: ScoreDefinition // This bound is implicit now
|
|
{
|
|
fn compile(&self) -> Result<Box<dyn Interpret<T>>, String> {
|
|
// Pass necessary data from self to CommandInterpret
|
|
Ok(Box::new(CommandInterpret {
|
|
command: self.command.clone(),
|
|
args: self.args.clone(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
// K8sResourceScore implements Score<T> for any T with KubernetesCapability
|
|
impl<T> Score<T> for K8sResourceScore
|
|
where
|
|
T: Topology + KubernetesCapability + 'static,
|
|
// Self: ScoreDefinition
|
|
{
|
|
fn compile(&self) -> Result<Box<dyn Interpret<T>>, String> {
|
|
Ok(Box::new(K8sResourceInterpret {
|
|
manifest: self.manifest.clone(), // Pass needed data
|
|
}))
|
|
}
|
|
}
|
|
|
|
|
|
// --- Interpret Implementations ---
|
|
// Need to hold the actual data now
|
|
|
|
struct CommandInterpret {
|
|
command: String,
|
|
args: Vec<String>, // Or owned Strings if lifetime is tricky
|
|
}
|
|
|
|
impl<'a, T> Interpret<T> for CommandInterpret
|
|
where
|
|
T: Topology + CommandCapability,
|
|
{
|
|
fn execute(&self, topology: &T) -> Result<String, String> {
|
|
// Now uses data stored in self
|
|
topology.execute_command(&self.command, &self.args)
|
|
}
|
|
}
|
|
|
|
struct K8sResourceInterpret {
|
|
manifest: String,
|
|
}
|
|
|
|
impl<T: Topology + KubernetesCapability> Interpret<T> for K8sResourceInterpret {
|
|
fn execute(&self, topology: &T) -> Result<String, String> {
|
|
topology.apply_manifest(&self.manifest)?;
|
|
// apply_manifest returns Result<(), String>, adapt if needed
|
|
Ok(format!("Applied manifest for {}", topology.name())) // Example success message
|
|
}
|
|
}
|
|
|
|
// --- Maestro ---
|
|
// Maestro remains almost identical, leveraging the Score<T> bound
|
|
pub struct Maestro<T: Topology> {
|
|
topology: T,
|
|
// Stores Score<T> trait objects, ensuring compatibility
|
|
scores: Vec<Box<dyn Score<T>>>,
|
|
}
|
|
|
|
impl<T: Topology + 'static> Maestro<T> { // Often need T: 'static here
|
|
pub fn new(topology: T) -> Self {
|
|
Self {
|
|
topology,
|
|
scores: Vec::new(),
|
|
}
|
|
}
|
|
|
|
// This method signature is key - it takes a concrete S
|
|
// and the compiler checks if S implements Score<T>
|
|
pub fn register_score<S>(&mut self, score: S) -> Result<(), String>
|
|
where
|
|
S: Score<T> + ScoreDefinition + Clone + 'static, // Ensure S is a Score for *this* T
|
|
// We might need S: Clone if we want to store Box::new(score)
|
|
// Alternatively, accept Box<dyn ScoreDefinition> and try to downcast/wrap
|
|
{
|
|
println!(
|
|
"Registering score '{}' for topology '{}'",
|
|
score.name(),
|
|
self.topology.name()
|
|
);
|
|
// The compiler has already guaranteed that S implements Score<T>
|
|
// We need to box it as dyn Score<T>
|
|
self.scores.push(Box::new(score));
|
|
Ok(())
|
|
}
|
|
|
|
// Alternative registration if you have Box<dyn ScoreDefinition>
|
|
pub fn register_score_definition(&mut self, score_def: Box<dyn ScoreDefinition>) -> Result<(), String>
|
|
where
|
|
T: Topology + CommandCapability + KubernetesCapability + 'static, // Example: list all needed caps here, or use generics + downcasting
|
|
{
|
|
println!(
|
|
"Attempting to register score '{}' for topology '{}'",
|
|
score_def.name(),
|
|
self.topology.name()
|
|
);
|
|
|
|
// Downcast to check concrete type and then check compatibility
|
|
if let Some(cs) = score_def.as_any().downcast_ref::<CommandScore>() {
|
|
// Check if T satisfies CommandScore's requirements (CommandCapability)
|
|
// This check is somewhat manual or needs restructuring if we avoid listing all caps
|
|
// A simpler way is to just try to create the Box<dyn Score<T>>
|
|
let boxed_score: Box<dyn Score<T>> = Box::new(cs.clone()); // This relies on the blanket impls
|
|
self.scores.push(boxed_score);
|
|
Ok(())
|
|
} else if let Some(ks) = score_def.as_any().downcast_ref::<K8sResourceScore>() {
|
|
// Check if T satisfies K8sResourceScore's requirements (KubernetesCapability)
|
|
let boxed_score: Box<dyn Score<T>> = Box::new(ks.clone());
|
|
self.scores.push(boxed_score);
|
|
Ok(())
|
|
} else {
|
|
Err(format!("Score '{}' is of an unknown type or incompatible", score_def.name()))
|
|
}
|
|
// This downcasting approach in Maestro slightly undermines the full compile-time
|
|
// check unless designed carefully. The generic `register_score<S: Score<T>>` is safer.
|
|
}
|
|
|
|
|
|
pub fn orchestrate(&self) -> Result<(), String> {
|
|
println!("Orchestrating topology '{}'", self.topology.name());
|
|
for score in &self.scores {
|
|
println!("Compiling score '{}'", score.name()); // Use name() from ScoreDefinition
|
|
let interpret = score.compile()?;
|
|
println!("Executing score '{}'", score.name());
|
|
interpret.execute(&self.topology)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// --- TUI Example ---
|
|
struct ScoreItem {
|
|
// Holds the definition/metadata, NOT the Score<T> trait object
|
|
definition: Box<dyn ScoreDefinition>,
|
|
}
|
|
|
|
struct HarmonyTui {
|
|
// List of available score *definitions*
|
|
available_scores: Vec<ScoreItem>,
|
|
// Example: Maybe maps topology names to Maestros
|
|
// maestros: HashMap<String, Box<dyn Any>>, // Storing Maestros generically is another challenge!
|
|
}
|
|
|
|
impl HarmonyTui {
|
|
fn new() -> Self {
|
|
HarmonyTui { available_scores: vec![] }
|
|
}
|
|
|
|
fn add_available_score(&mut self, score_def: Box<dyn ScoreDefinition>) {
|
|
self.available_scores.push(ScoreItem { definition: score_def });
|
|
}
|
|
|
|
fn display_scores(&self) {
|
|
println!("Available Scores:");
|
|
for (i, item) in self.available_scores.iter().enumerate() {
|
|
println!("{}: {}", i, item.definition.name());
|
|
}
|
|
}
|
|
|
|
fn execute_score(&self, score: ScoreItem) {
|
|
score.definition.
|
|
|
|
}
|
|
|
|
// Example: Function to add a selected score to a specific Maestro
|
|
// This function would need access to the Maestros and handle the types
|
|
fn add_selected_score_to_maestro<T>(
|
|
&self,
|
|
score_index: usize,
|
|
maestro: &mut Maestro<T>
|
|
) -> Result<(), String>
|
|
where
|
|
T: Topology + CommandCapability + KubernetesCapability + 'static, // Adjust bounds as needed
|
|
{
|
|
let score_item = self.available_scores.get(score_index)
|
|
.ok_or("Invalid score index")?;
|
|
|
|
// We have Box<dyn ScoreDefinition>, need to add to Maestro<T>
|
|
// Easiest is to downcast and call the generic register_score
|
|
|
|
if let Some(cs) = score_item.definition.as_any().downcast_ref::<CommandScore>() {
|
|
// Compiler checks if CommandScore: Score<T> via register_score's bound
|
|
maestro.register_score(cs.clone())?;
|
|
Ok(())
|
|
} else if let Some(ks) = score_item.definition.as_any().downcast_ref::<K8sResourceScore>() {
|
|
// Compiler checks if K8sResourceScore: Score<T> via register_score's bound
|
|
maestro.register_score(ks.clone())?;
|
|
Ok(())
|
|
} else {
|
|
Err(format!("Cannot add score '{}': Unknown type or check Maestro compatibility", score_item.definition.name()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct K3DTopology {
|
|
name: String,
|
|
linux_host: LinuxHostTopology,
|
|
cluster_name: String,
|
|
}
|
|
|
|
impl Capability for K3DTopology {}
|
|
|
|
impl K3DTopology {
|
|
pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self {
|
|
Self {
|
|
name,
|
|
linux_host,
|
|
cluster_name,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Topology for K3DTopology {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
}
|
|
|
|
impl CommandCapability for K3DTopology {
|
|
fn execute_command(&self, command: &str, args: &Vec<String>) -> Result<String, String> {
|
|
self.linux_host.execute_command(command, args)
|
|
}
|
|
}
|
|
|
|
impl KubernetesCapability for K3DTopology {
|
|
fn apply_manifest(&self, manifest: &str) -> Result<(), String> {
|
|
println!("Applying manifest to K3D cluster '{}'", self.cluster_name);
|
|
// Write manifest to a temporary file
|
|
let temp_file = format!("/tmp/manifest-harmony-temp.yaml");
|
|
|
|
// Use the linux_host directly to avoid capability trait bounds
|
|
self.linux_host
|
|
.execute_command("bash", &Vec::from(["-c".to_string(), format!("cat > {}", temp_file)]))?;
|
|
|
|
// Apply with kubectl
|
|
self.linux_host.execute_command("kubectl", &Vec::from([
|
|
"--context".to_string(),
|
|
format!("k3d-{}", self.cluster_name),
|
|
"apply".to_string(),
|
|
"-f".to_string(),
|
|
temp_file.to_string(),
|
|
]))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_resource(&self, resource_type: &str, name: &str) -> Result<String, String> {
|
|
println!(
|
|
"Getting resource {}/{} from K3D cluster '{}'",
|
|
resource_type, name, self.cluster_name
|
|
);
|
|
self.linux_host.execute_command("kubectl", &Vec::from([
|
|
"--context".to_string(),
|
|
format!("k3d-{}", self.cluster_name),
|
|
"get".to_string(),
|
|
resource_type.to_string(),
|
|
name.to_string(),
|
|
"-o".to_string(),
|
|
"yaml".to_string(),
|
|
]))
|
|
}
|
|
}
|
|
|
|
|
|
pub struct LinuxHostTopology {
|
|
name: String,
|
|
host: String,
|
|
}
|
|
impl Capability for LinuxHostTopology {}
|
|
|
|
impl LinuxHostTopology {
|
|
pub fn new(name: String, host: String) -> Self {
|
|
Self { name, host }
|
|
}
|
|
}
|
|
|
|
impl Topology for LinuxHostTopology {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
}
|
|
|
|
impl CommandCapability for LinuxHostTopology {
|
|
fn execute_command(&self, command: &str, args: &Vec<String>) -> Result<String, String> {
|
|
println!("Executing on {}: {} {:?}", self.host, command, args);
|
|
// In a real implementation, this would SSH to the host and execute the command
|
|
let output = Command::new(command)
|
|
.args(args)
|
|
.output()
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
if output.status.success() {
|
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
|
} else {
|
|
Err(String::from_utf8_lossy(&output.stderr).to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// --- Main Function Adapated ---
|
|
fn main() {
|
|
// --- Linux Host ---
|
|
let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string());
|
|
let mut linux_maestro = Maestro::new(linux_host);
|
|
|
|
let df_score = CommandScore::new(
|
|
"check-disk".to_string(),
|
|
"df".to_string(),
|
|
vec!["-h".to_string()],
|
|
);
|
|
|
|
// Registration uses the generic method, compiler checks CommandScore: Score<LinuxHostTopology>
|
|
linux_maestro.register_score(df_score.clone()).unwrap(); // clone needed if df_score used later
|
|
|
|
// --- K3D Host ---
|
|
let k3d_host = LinuxHostTopology::new("k3d-host".to_string(), "localhost".to_string());
|
|
let k3d_topology = K3DTopology::new(
|
|
"dev-cluster".to_string(),
|
|
k3d_host,
|
|
"devcluster".to_string(),
|
|
);
|
|
let mut k3d_maestro = Maestro::new(k3d_topology);
|
|
|
|
let nodes_score = CommandScore::new(
|
|
"check-nodes".to_string(),
|
|
"kubectl".to_string(),
|
|
vec!["get".to_string(), "nodes".to_string()],
|
|
);
|
|
let nginx_score = K8sResourceScore::new(
|
|
"deploy-nginx".to_string(),
|
|
// ... manifest string ...
|
|
r#"..."#.to_string(),
|
|
);
|
|
|
|
// Compiler checks CommandScore: Score<K3DTopology>
|
|
k3d_maestro.register_score(nodes_score.clone()).unwrap();
|
|
// Compiler checks K8sResourceScore: Score<K3DTopology>
|
|
k3d_maestro.register_score(nginx_score.clone()).unwrap();
|
|
|
|
|
|
// --- TUI Example Usage ---
|
|
let mut tui = HarmonyTui::new();
|
|
// Add score *definitions* to the TUI
|
|
tui.add_available_score(Box::new(df_score));
|
|
tui.add_available_score(Box::new(nodes_score));
|
|
tui.add_available_score(Box::new(nginx_score));
|
|
|
|
tui.display_scores();
|
|
|
|
// Simulate user selecting score 0 (check-disk) and adding to linux_maestro
|
|
match tui.add_selected_score_to_maestro(0, &mut linux_maestro) {
|
|
Ok(_) => println!("Successfully registered check-disk to linux_maestro via TUI selection"),
|
|
Err(e) => println!("Failed: {}", e), // Should succeed
|
|
}
|
|
|
|
// Simulate user selecting score 2 (deploy-nginx) and adding to linux_maestro
|
|
match tui.add_selected_score_to_maestro(2, &mut linux_maestro) {
|
|
Ok(_) => println!("Successfully registered deploy-nginx to linux_maestro via TUI selection"), // Should fail!
|
|
Err(e) => println!("Correctly failed to add deploy-nginx to linux_maestro: {}", e),
|
|
// The failure happens inside add_selected_score_to_maestro because the
|
|
// maestro.register_score(ks.clone()) call fails the trait bound check
|
|
// K8sResourceScore: Score<LinuxHostTopology> is false.
|
|
}
|
|
|
|
// Simulate user selecting score 2 (deploy-nginx) and adding to k3d_maestro
|
|
match tui.add_selected_score_to_maestro(2, &mut k3d_maestro) {
|
|
Ok(_) => println!("Successfully registered deploy-nginx to k3d_maestro via TUI selection"), // Should succeed
|
|
Err(e) => println!("Failed: {}", e),
|
|
}
|
|
|
|
// --- Orchestration ---
|
|
println!("\n--- Orchestrating Linux Maestro ---");
|
|
linux_maestro.orchestrate().unwrap();
|
|
println!("\n--- Orchestrating K3D Maestro ---");
|
|
k3d_maestro.orchestrate().unwrap();
|
|
}
|