feat(orchestration): introduce Interpret trait and refactor score application
Refactor the orchestration process to use an `Interpret` trait instead of directly applying scores. This change introduces a more flexible and extensible design for executing commands associated with different types of topologies. The `CommandScore` and `K8sResourceScore` now implement this trait, providing a clear separation between score definition and execution logic. Update the `Maestro::orchestrate` method to compile scores into interpreters before executing them against their respective topologies.
This commit is contained in:
parent
3962238f0d
commit
d7897f29c4
@ -1,47 +1,31 @@
|
|||||||
|
use rand::Rng;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use rand::Rng; // Add rand dependency
|
|
||||||
|
|
||||||
// ===== Capability Traits =====
|
|
||||||
|
|
||||||
/// Base trait for all capabilities
|
|
||||||
pub trait Capability {}
|
pub trait Capability {}
|
||||||
|
|
||||||
/// Capability for executing shell commands on a host
|
|
||||||
pub trait CommandCapability: Capability {
|
pub trait CommandCapability: Capability {
|
||||||
fn execute_command(&self, command: &str, args: &[&str]) -> Result<String, String>;
|
fn execute_command(&self, command: &str, args: &[&str]) -> Result<String, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Capability for interacting with a Kubernetes cluster
|
|
||||||
pub trait KubernetesCapability: Capability {
|
pub trait KubernetesCapability: Capability {
|
||||||
fn apply_manifest(&self, manifest: &str) -> Result<(), String>;
|
fn apply_manifest(&self, manifest: &str) -> Result<(), String>;
|
||||||
fn get_resource(&self, resource_type: &str, name: &str) -> Result<String, String>;
|
fn get_resource(&self, resource_type: &str, name: &str) -> Result<String, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Topology Traits =====
|
|
||||||
|
|
||||||
/// Base trait for all topologies
|
|
||||||
pub trait Topology {
|
pub trait Topology {
|
||||||
// Base topology methods that don't depend on capabilities
|
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Score Traits =====
|
|
||||||
|
|
||||||
/// Generic Score trait with an associated Capability type
|
|
||||||
pub trait Score<T: Topology> {
|
pub trait Score<T: Topology> {
|
||||||
fn apply(&self, topology: &T) -> Result<(), String>;
|
fn compile(&self) -> Result<Box<dyn Interpret<T>>, String>;
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Concrete Topologies =====
|
|
||||||
|
|
||||||
/// A topology representing a Linux host
|
|
||||||
pub struct LinuxHostTopology {
|
pub struct LinuxHostTopology {
|
||||||
name: String,
|
name: String,
|
||||||
host: String,
|
host: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement the base Capability trait for LinuxHostTopology
|
|
||||||
impl Capability for LinuxHostTopology {}
|
impl Capability for LinuxHostTopology {}
|
||||||
|
|
||||||
impl LinuxHostTopology {
|
impl LinuxHostTopology {
|
||||||
@ -64,7 +48,7 @@ impl CommandCapability for LinuxHostTopology {
|
|||||||
.args(args)
|
.args(args)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
} else {
|
} else {
|
||||||
@ -73,19 +57,17 @@ impl CommandCapability for LinuxHostTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A topology representing a K3D Kubernetes cluster
|
|
||||||
pub struct K3DTopology {
|
pub struct K3DTopology {
|
||||||
name: String,
|
name: String,
|
||||||
linux_host: LinuxHostTopology,
|
linux_host: LinuxHostTopology,
|
||||||
cluster_name: String,
|
cluster_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement the base Capability trait for K3DTopology
|
|
||||||
impl Capability for K3DTopology {}
|
impl Capability for K3DTopology {}
|
||||||
|
|
||||||
impl K3DTopology {
|
impl K3DTopology {
|
||||||
pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self {
|
pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
linux_host,
|
linux_host,
|
||||||
cluster_name,
|
cluster_name,
|
||||||
@ -101,7 +83,6 @@ impl Topology for K3DTopology {
|
|||||||
|
|
||||||
impl CommandCapability for K3DTopology {
|
impl CommandCapability for K3DTopology {
|
||||||
fn execute_command(&self, command: &str, args: &[&str]) -> Result<String, String> {
|
fn execute_command(&self, command: &str, args: &[&str]) -> Result<String, String> {
|
||||||
// Delegate to the underlying Linux host
|
|
||||||
self.linux_host.execute_command(command, args)
|
self.linux_host.execute_command(command, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,39 +93,40 @@ impl KubernetesCapability for K3DTopology {
|
|||||||
// Write manifest to a temporary file
|
// Write manifest to a temporary file
|
||||||
//let temp_file = format!("/tmp/manifest-{}.yaml", rand::thread_rng().gen::<u32>());
|
//let temp_file = format!("/tmp/manifest-{}.yaml", rand::thread_rng().gen::<u32>());
|
||||||
let temp_file = format!("/tmp/manifest-TODO_RANDOM_NUMBER.yaml");
|
let temp_file = format!("/tmp/manifest-TODO_RANDOM_NUMBER.yaml");
|
||||||
|
|
||||||
// Use the linux_host directly to avoid capability trait bounds
|
// Use the linux_host directly to avoid capability trait bounds
|
||||||
self.linux_host.execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?;
|
self.linux_host
|
||||||
|
.execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?;
|
||||||
|
|
||||||
// Apply with kubectl
|
// Apply with kubectl
|
||||||
self.linux_host.execute_command(
|
self.linux_host.execute_command("kubectl", &[
|
||||||
"kubectl",
|
"--context",
|
||||||
&["--context", &format!("k3d-{}", self.cluster_name), "apply", "-f", &temp_file]
|
&format!("k3d-{}", self.cluster_name),
|
||||||
)?;
|
"apply",
|
||||||
|
"-f",
|
||||||
|
&temp_file,
|
||||||
|
])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_resource(&self, resource_type: &str, name: &str) -> Result<String, String> {
|
fn get_resource(&self, resource_type: &str, name: &str) -> Result<String, String> {
|
||||||
println!("Getting resource {}/{} from K3D cluster '{}'", resource_type, name, self.cluster_name);
|
println!(
|
||||||
self.linux_host.execute_command(
|
"Getting resource {}/{} from K3D cluster '{}'",
|
||||||
"kubectl",
|
resource_type, name, self.cluster_name
|
||||||
&[
|
);
|
||||||
"--context",
|
self.linux_host.execute_command("kubectl", &[
|
||||||
&format!("k3d-{}", self.cluster_name),
|
"--context",
|
||||||
"get",
|
&format!("k3d-{}", self.cluster_name),
|
||||||
resource_type,
|
"get",
|
||||||
name,
|
resource_type,
|
||||||
"-o",
|
name,
|
||||||
"yaml",
|
"-o",
|
||||||
]
|
"yaml",
|
||||||
)
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Concrete Scores =====
|
|
||||||
|
|
||||||
/// A score that executes commands on a topology
|
|
||||||
pub struct CommandScore {
|
pub struct CommandScore {
|
||||||
name: String,
|
name: String,
|
||||||
command: String,
|
command: String,
|
||||||
@ -153,19 +135,35 @@ pub struct CommandScore {
|
|||||||
|
|
||||||
impl CommandScore {
|
impl CommandScore {
|
||||||
pub fn new(name: String, command: String, args: Vec<String>) -> Self {
|
pub fn new(name: String, command: String, args: Vec<String>) -> Self {
|
||||||
Self { name, command, args }
|
Self {
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Score<T> for CommandScore
|
pub trait Interpret<T: Topology> {
|
||||||
where
|
fn execute(&self, topology: &T) -> Result<String, String>;
|
||||||
T: Topology + CommandCapability
|
}
|
||||||
|
|
||||||
|
struct CommandInterpret;
|
||||||
|
|
||||||
|
impl<T> Interpret<T> for CommandInterpret
|
||||||
|
where
|
||||||
|
T: Topology + CommandCapability,
|
||||||
{
|
{
|
||||||
fn apply(&self, topology: &T) -> Result<(), String> {
|
fn execute(&self, topology: &T) -> Result<String, String> {
|
||||||
println!("Applying CommandScore '{}' to topology '{}'", self.name, topology.name());
|
todo!()
|
||||||
let args_refs: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect();
|
}
|
||||||
topology.execute_command(&self.command, &args_refs)?;
|
}
|
||||||
Ok(())
|
|
||||||
|
impl<T> Score<T> for CommandScore
|
||||||
|
where
|
||||||
|
T: Topology + CommandCapability,
|
||||||
|
{
|
||||||
|
fn compile(&self) -> Result<Box<dyn Interpret<T>>, String> {
|
||||||
|
Ok(Box::new(CommandInterpret {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
@ -173,7 +171,8 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A score that applies Kubernetes resources to a topology
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct K8sResourceScore {
|
pub struct K8sResourceScore {
|
||||||
name: String,
|
name: String,
|
||||||
manifest: String,
|
manifest: String,
|
||||||
@ -185,13 +184,24 @@ impl K8sResourceScore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Score<T> for K8sResourceScore
|
struct K8sResourceInterpret {
|
||||||
where
|
score: K8sResourceScore,
|
||||||
T: Topology + KubernetesCapability
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + KubernetesCapability> Interpret<T> for K8sResourceInterpret {
|
||||||
|
fn execute(&self, topology: &T) -> Result<String, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Score<T> for K8sResourceScore
|
||||||
|
where
|
||||||
|
T: Topology + KubernetesCapability,
|
||||||
{
|
{
|
||||||
fn apply(&self, topology: &T) -> Result<(), String> {
|
fn compile(&self) -> Result<Box<(dyn Interpret<T> + 'static)>, String> {
|
||||||
println!("Applying K8sResourceScore '{}' to topology '{}'", self.name, topology.name());
|
Ok(Box::new(K8sResourceInterpret {
|
||||||
topology.apply_manifest(&self.manifest)
|
score: self.clone(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
@ -199,35 +209,11 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Maestro Orchestrator =====
|
|
||||||
|
|
||||||
/// Type-safe orchestrator that enforces capability requirements at compile time
|
|
||||||
pub struct Maestro<T: Topology> {
|
pub struct Maestro<T: Topology> {
|
||||||
topology: T,
|
topology: T,
|
||||||
scores: Vec<Box<dyn ScoreWrapper<T>>>,
|
scores: Vec<Box<dyn Score<T>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait object wrapper that hides the specific Score type but preserves its
|
|
||||||
/// capability requirements
|
|
||||||
trait ScoreWrapper<T: Topology> {
|
|
||||||
fn apply(&self, topology: &T) -> Result<(), String>;
|
|
||||||
fn name(&self) -> &str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation of ScoreWrapper for any Score that works with topology T
|
|
||||||
impl<T, S> ScoreWrapper<T> for S
|
|
||||||
where
|
|
||||||
T: Topology,
|
|
||||||
S: Score<T> + 'static
|
|
||||||
{
|
|
||||||
fn apply(&self, topology: &T) -> Result<(), String> {
|
|
||||||
<S as Score<T>>::apply(self, topology)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
<S as Score<T>>::name(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology> Maestro<T> {
|
impl<T: Topology> Maestro<T> {
|
||||||
pub fn new(topology: T) -> Self {
|
pub fn new(topology: T) -> Self {
|
||||||
@ -237,70 +223,66 @@ impl<T: Topology> Maestro<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a score that is compatible with this topology's capabilities
|
pub fn register_score<S>(&mut self, score: S)
|
||||||
pub fn register_score<S>(&mut self, score: S)
|
where
|
||||||
where
|
S: Score<T> + 'static,
|
||||||
S: Score<T> + 'static
|
|
||||||
{
|
{
|
||||||
println!("Registering score '{}' for topology '{}'", score.name(), self.topology.name());
|
println!(
|
||||||
|
"Registering score '{}' for topology '{}'",
|
||||||
|
score.name(),
|
||||||
|
self.topology.name()
|
||||||
|
);
|
||||||
self.scores.push(Box::new(score));
|
self.scores.push(Box::new(score));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply all registered scores to the topology
|
|
||||||
pub fn orchestrate(&self) -> Result<(), String> {
|
pub fn orchestrate(&self) -> Result<(), String> {
|
||||||
println!("Orchestrating topology '{}'", self.topology.name());
|
println!("Orchestrating topology '{}'", self.topology.name());
|
||||||
for score in &self.scores {
|
for score in &self.scores {
|
||||||
score.apply(&self.topology)?;
|
let interpret = score.compile()?;
|
||||||
|
interpret.execute(&self.topology)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Example Usage =====
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Create a Linux host topology
|
let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string());
|
||||||
let linux_host = LinuxHostTopology::new(
|
|
||||||
"dev-machine".to_string(),
|
|
||||||
"localhost".to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a maestro for the Linux host
|
|
||||||
let mut linux_maestro = Maestro::new(linux_host);
|
let mut linux_maestro = Maestro::new(linux_host);
|
||||||
|
|
||||||
// Register a command score that works with any topology having CommandCapability
|
|
||||||
linux_maestro.register_score(CommandScore::new(
|
linux_maestro.register_score(CommandScore::new(
|
||||||
"check-disk".to_string(),
|
"check-disk".to_string(),
|
||||||
"df".to_string(),
|
"df".to_string(),
|
||||||
vec!["-h".to_string()]
|
vec!["-h".to_string()],
|
||||||
));
|
));
|
||||||
|
linux_maestro.orchestrate().unwrap();
|
||||||
|
|
||||||
// This would fail to compile if we tried to register a K8sResourceScore
|
// This would fail to compile if we tried to register a K8sResourceScore
|
||||||
// because LinuxHostTopology doesn't implement KubernetesCapability
|
// because LinuxHostTopology doesn't implement KubernetesCapability
|
||||||
// linux_maestro.register_score(K8sResourceScore::new(...));
|
//linux_maestro.register_score(K8sResourceScore::new(
|
||||||
|
// "...".to_string(),
|
||||||
|
// "...".to_string(),
|
||||||
|
//));
|
||||||
|
|
||||||
// Create a K3D topology which has both Command and Kubernetes capabilities
|
// Create a K3D topology which has both Command and Kubernetes capabilities
|
||||||
let k3d_host = LinuxHostTopology::new(
|
let k3d_host = LinuxHostTopology::new("k3d-host".to_string(), "localhost".to_string());
|
||||||
"k3d-host".to_string(),
|
|
||||||
"localhost".to_string()
|
|
||||||
);
|
|
||||||
|
|
||||||
let k3d_topology = K3DTopology::new(
|
let k3d_topology = K3DTopology::new(
|
||||||
"dev-cluster".to_string(),
|
"dev-cluster".to_string(),
|
||||||
k3d_host,
|
k3d_host,
|
||||||
"devcluster".to_string()
|
"devcluster".to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a maestro for the K3D topology
|
// Create a maestro for the K3D topology
|
||||||
let mut k3d_maestro = Maestro::new(k3d_topology);
|
let mut k3d_maestro = Maestro::new(k3d_topology);
|
||||||
|
|
||||||
// We can register both command scores and kubernetes scores
|
// We can register both command scores and kubernetes scores
|
||||||
k3d_maestro.register_score(CommandScore::new(
|
k3d_maestro.register_score(CommandScore::new(
|
||||||
"check-nodes".to_string(),
|
"check-nodes".to_string(),
|
||||||
"kubectl".to_string(),
|
"kubectl".to_string(),
|
||||||
vec!["get".to_string(), "nodes".to_string()]
|
vec!["get".to_string(), "nodes".to_string()],
|
||||||
));
|
));
|
||||||
|
|
||||||
k3d_maestro.register_score(K8sResourceScore::new(
|
k3d_maestro.register_score(K8sResourceScore::new(
|
||||||
"deploy-nginx".to_string(),
|
"deploy-nginx".to_string(),
|
||||||
r#"
|
r#"
|
||||||
@ -323,9 +305,10 @@ fn main() {
|
|||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
"#.to_string()
|
"#
|
||||||
|
.to_string(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Orchestrate both topologies
|
// Orchestrate both topologies
|
||||||
linux_maestro.orchestrate().unwrap();
|
linux_maestro.orchestrate().unwrap();
|
||||||
k3d_maestro.orchestrate().unwrap();
|
k3d_maestro.orchestrate().unwrap();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user