forked from NationTech/harmony
chore: Move ADR helper files into folders with their corresponding ADR number
This commit is contained in:
369
adr/003-abstractions/topology/src/main_gemini25pro.rs
Normal file
369
adr/003-abstractions/topology/src/main_gemini25pro.rs
Normal file
@@ -0,0 +1,369 @@
|
||||
// Import necessary items (though for this example, few are needed beyond std)
|
||||
use std::fmt;
|
||||
|
||||
// --- Error Handling ---
|
||||
// A simple error type for demonstration purposes. In a real app, use `thiserror` or `anyhow`.
|
||||
#[derive(Debug)]
|
||||
enum OrchestrationError {
|
||||
CommandFailed(String),
|
||||
KubeClientError(String),
|
||||
TopologySetupFailed(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for OrchestrationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
OrchestrationError::CommandFailed(e) => write!(f, "Command execution failed: {}", e),
|
||||
OrchestrationError::KubeClientError(e) => write!(f, "Kubernetes client error: {}", e),
|
||||
OrchestrationError::TopologySetupFailed(e) => write!(f, "Topology setup failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OrchestrationError {}
|
||||
|
||||
// Define a common Result type
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
// --- 1. Capability Specification (as Traits) ---
|
||||
|
||||
/// Capability trait representing the ability to run Linux commands.
|
||||
/// This follows the "Parse, Don't Validate" idea implicitly - if you have an object
|
||||
/// implementing this, you know you *can* run commands, no need to check later.
|
||||
trait LinuxOperations {
|
||||
fn run_command(&self, command: &str) -> Result<String>;
|
||||
}
|
||||
|
||||
/// A mock Kubernetes client trait for demonstration.
|
||||
trait KubeClient {
|
||||
fn apply_manifest(&self, manifest: &str) -> Result<()>;
|
||||
fn get_pods(&self, namespace: &str) -> Result<Vec<String>>;
|
||||
}
|
||||
|
||||
/// Mock implementation of a KubeClient.
|
||||
struct MockKubeClient {
|
||||
cluster_name: String,
|
||||
}
|
||||
|
||||
impl KubeClient for MockKubeClient {
|
||||
fn apply_manifest(&self, manifest: &str) -> Result<()> {
|
||||
println!(
|
||||
"[{}] Applying Kubernetes manifest:\n---\n{}\n---",
|
||||
self.cluster_name, manifest
|
||||
);
|
||||
// Simulate success or failure
|
||||
if manifest.contains("invalid") {
|
||||
Err(Box::new(OrchestrationError::KubeClientError(
|
||||
"Invalid manifest content".into(),
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn get_pods(&self, namespace: &str) -> Result<Vec<String>> {
|
||||
println!(
|
||||
"[{}] Getting pods in namespace '{}'",
|
||||
self.cluster_name, namespace
|
||||
);
|
||||
Ok(vec![
|
||||
format!("pod-a-12345-{}-{}", namespace, self.cluster_name),
|
||||
format!("pod-b-67890-{}-{}", namespace, self.cluster_name),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Capability trait representing access to a Kubernetes cluster.
|
||||
/// This follows Rust Embedded WG's "Zero-Cost Abstractions" - the trait itself
|
||||
/// adds no runtime overhead, only compile-time structure.
|
||||
trait KubernetesCluster {
|
||||
// Provides access to a Kubernetes client instance.
|
||||
// Using `impl Trait` in return position for flexibility.
|
||||
fn get_kube_client(&self) -> Result<impl KubeClient>;
|
||||
}
|
||||
|
||||
// --- 2. Topology Implementations ---
|
||||
// Topologies implement the capabilities they provide.
|
||||
|
||||
/// Represents a basic Linux host.
|
||||
#[derive(Debug, Clone)]
|
||||
struct LinuxHostTopology {
|
||||
hostname: String,
|
||||
// In a real scenario: SSH connection details, etc.
|
||||
}
|
||||
|
||||
impl LinuxHostTopology {
|
||||
fn new(hostname: &str) -> Self {
|
||||
println!("Initializing LinuxHostTopology for {}", hostname);
|
||||
Self {
|
||||
hostname: hostname.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LinuxHostTopology provides LinuxOperations capability.
|
||||
impl LinuxOperations for LinuxHostTopology {
|
||||
fn run_command(&self, command: &str) -> Result<String> {
|
||||
println!("[{}] Running command: '{}'", self.hostname, command);
|
||||
// Simulate command execution (e.g., via SSH)
|
||||
if command.starts_with("fail") {
|
||||
Err(Box::new(OrchestrationError::CommandFailed(format!(
|
||||
"Command '{}' failed",
|
||||
command
|
||||
))))
|
||||
} else {
|
||||
Ok(format!("Output of '{}' on {}", command, self.hostname))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a K3D (Kubernetes in Docker) cluster running on a host.
|
||||
#[derive(Debug, Clone)]
|
||||
struct K3DTopology {
|
||||
cluster_name: String,
|
||||
host_os: String, // Example: might implicitly run commands on the underlying host
|
||||
// In a real scenario: Kubeconfig path, Docker client, etc.
|
||||
}
|
||||
|
||||
impl K3DTopology {
|
||||
fn new(cluster_name: &str) -> Self {
|
||||
println!("Initializing K3DTopology for cluster {}", cluster_name);
|
||||
Self {
|
||||
cluster_name: cluster_name.to_string(),
|
||||
host_os: "Linux".to_string(), // Assume k3d runs on Linux for this example
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// K3DTopology provides KubernetesCluster capability.
|
||||
impl KubernetesCluster for K3DTopology {
|
||||
fn get_kube_client(&self) -> Result<impl KubeClient> {
|
||||
println!("[{}] Creating mock Kubernetes client", self.cluster_name);
|
||||
// In a real scenario, this would initialize a client using kubeconfig etc.
|
||||
Ok(MockKubeClient {
|
||||
cluster_name: self.cluster_name.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// K3DTopology *also* provides LinuxOperations (e.g., for running commands inside nodes or on the host managing k3d).
|
||||
impl LinuxOperations for K3DTopology {
|
||||
fn run_command(&self, command: &str) -> Result<String> {
|
||||
println!(
|
||||
"[{} on {} host] Running command: '{}'",
|
||||
self.cluster_name, self.host_os, command
|
||||
);
|
||||
// Simulate command execution (maybe `docker exec` or similar)
|
||||
if command.starts_with("fail") {
|
||||
Err(Box::new(OrchestrationError::CommandFailed(format!(
|
||||
"Command '{}' failed within k3d context",
|
||||
command
|
||||
))))
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Output of '{}' within k3d cluster {}",
|
||||
command, self.cluster_name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Score Implementations ---
|
||||
// Scores require capabilities via trait bounds on their execution logic.
|
||||
|
||||
/// Base trait for identifying scores. Could be empty or hold metadata.
|
||||
trait Score {
|
||||
fn name(&self) -> &'static str;
|
||||
// We don't put execute here, as its signature depends on required capabilities.
|
||||
}
|
||||
|
||||
/// A score that runs a shell command on a Linux host.
|
||||
#[derive(Debug)]
|
||||
struct CommandScore {
|
||||
command: String,
|
||||
}
|
||||
|
||||
impl Score for CommandScore {
|
||||
fn name(&self) -> &'static str {
|
||||
"CommandScore"
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandScore {
|
||||
fn new(command: &str) -> Self {
|
||||
Self {
|
||||
command: command.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute method is generic over T, but requires T implements LinuxOperations.
|
||||
/// This follows the "Scores as Polymorphic Functions" idea.
|
||||
fn execute<T: LinuxOperations + ?Sized>(&self, topology: &T) -> Result<()> {
|
||||
println!("Executing Score: {}", Score::name(self));
|
||||
let output = topology.run_command(&self.command)?;
|
||||
println!("Command Score Output: {}", output);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A score that applies a Kubernetes resource manifest.
|
||||
#[derive(Debug)]
|
||||
struct K8sResourceScore {
|
||||
manifest_path: String, // Path or content
|
||||
}
|
||||
|
||||
impl Score for K8sResourceScore {
|
||||
fn name(&self) -> &'static str {
|
||||
"K8sResourceScore"
|
||||
}
|
||||
}
|
||||
|
||||
impl K8sResourceScore {
|
||||
fn new(manifest_path: &str) -> Self {
|
||||
Self {
|
||||
manifest_path: manifest_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute method requires T implements KubernetesCluster.
|
||||
fn execute<T: KubernetesCluster + ?Sized>(&self, topology: &T) -> Result<()> {
|
||||
println!("Executing Score: {}", Score::name(self));
|
||||
let client = topology.get_kube_client()?;
|
||||
let manifest_content = format!(
|
||||
"apiVersion: v1\nkind: Pod\nmetadata:\n name: my-pod-from-{}",
|
||||
self.manifest_path
|
||||
); // Simulate reading file
|
||||
client.apply_manifest(&manifest_content)?;
|
||||
println!(
|
||||
"K8s Resource Score applied manifest: {}",
|
||||
self.manifest_path
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Maestro (The Orchestrator) ---
|
||||
|
||||
// This version of Maestro uses a helper trait (`ScoreRunner`) to enable
|
||||
// storing heterogeneous scores while preserving compile-time checks.
|
||||
|
||||
/// A helper trait to erase the specific capability requirements *after*
|
||||
/// the compiler has verified them, allowing storage in a Vec.
|
||||
/// The verification happens in the blanket impls below.
|
||||
trait ScoreRunner<T> {
|
||||
// T is the concrete Topology type
|
||||
fn run(&self, topology: &T) -> Result<()>;
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
// Blanket implementation: A CommandScore can be run on any Topology T
|
||||
// *if and only if* T implements LinuxOperations.
|
||||
// The compiler checks this bound when `add_score` is called.
|
||||
impl<T: LinuxOperations> ScoreRunner<T> for CommandScore {
|
||||
fn run(&self, topology: &T) -> Result<()> {
|
||||
self.execute(topology) // Call the capability-specific execute method
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
Score::name(self)
|
||||
}
|
||||
}
|
||||
|
||||
// Blanket implementation: A K8sResourceScore can be run on any Topology T
|
||||
// *if and only if* T implements KubernetesCluster.
|
||||
impl<T: KubernetesCluster> ScoreRunner<T> for K8sResourceScore {
|
||||
fn run(&self, topology: &T) -> Result<()> {
|
||||
self.execute(topology) // Call the capability-specific execute method
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
Score::name(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The Maestro orchestrator, strongly typed to a specific Topology `T`.
|
||||
struct Maestro<T> {
|
||||
topology: T,
|
||||
// Stores type-erased runners, but addition is type-safe.
|
||||
scores: Vec<Box<dyn ScoreRunner<T>>>,
|
||||
}
|
||||
|
||||
impl<T> Maestro<T> {
|
||||
/// Creates a new Maestro instance bound to a specific topology.
|
||||
fn new(topology: T) -> Self {
|
||||
println!("Maestro initialized.");
|
||||
Maestro {
|
||||
topology,
|
||||
scores: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a score to the Maestro.
|
||||
/// **Compile-time check happens here!**
|
||||
/// The `S: ScoreRunner<T>` bound ensures that the score `S` provides an
|
||||
/// implementation of `ScoreRunner` *for the specific topology type `T`*.
|
||||
/// The blanket impls above ensure this is only possible if `T` has the
|
||||
/// required capabilities for `S`.
|
||||
/// This directly follows the "Theoretical Example: The Compiler as an Ally".
|
||||
fn add_score<S>(&mut self, score: S)
|
||||
where
|
||||
S: Score + ScoreRunner<T> + 'static, // S must be runnable on *this* T
|
||||
{
|
||||
println!("Registering score: {}", Score::name(&score));
|
||||
self.scores.push(Box::new(score));
|
||||
}
|
||||
|
||||
/// Runs all registered scores sequentially on the topology.
|
||||
fn run_all(&self) -> Vec<Result<()>> {
|
||||
println!("\n--- Running all scores ---");
|
||||
self.scores
|
||||
.iter()
|
||||
.map(|score_runner| {
|
||||
println!("---");
|
||||
let result = score_runner.run(&self.topology);
|
||||
match &result {
|
||||
Ok(_) => println!("Score '{}' completed successfully.", score_runner.name()),
|
||||
Err(e) => eprintln!("Score '{}' failed: {}", score_runner.name(), e),
|
||||
}
|
||||
result
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5. Example Usage ---
|
||||
|
||||
fn main() {
|
||||
println!("=== Scenario 1: Linux Host Topology ===");
|
||||
let linux_host = LinuxHostTopology::new("server1.example.com");
|
||||
let mut maestro_linux = Maestro::new(linux_host);
|
||||
|
||||
// Add scores compatible with LinuxHostTopology (which has LinuxOperations)
|
||||
maestro_linux.add_score(CommandScore::new("uname -a"));
|
||||
maestro_linux.add_score(CommandScore::new("ls -l /tmp"));
|
||||
|
||||
// *** Compile-time Error Example ***
|
||||
// Try adding a score that requires KubernetesCluster capability.
|
||||
// This line WILL NOT COMPILE because LinuxHostTopology does not implement KubernetesCluster,
|
||||
// therefore K8sResourceScore does not implement ScoreRunner<LinuxHostTopology>.
|
||||
// maestro_linux.add_score(K8sResourceScore::new("my-app.yaml"));
|
||||
// Uncomment the line above to see the compiler error! The error message will
|
||||
// likely point to the `ScoreRunner<LinuxHostTopology>` bound not being satisfied
|
||||
// for `K8sResourceScore`.
|
||||
|
||||
let results_linux = maestro_linux.run_all();
|
||||
println!("\nLinux Host Results: {:?}", results_linux);
|
||||
|
||||
println!("\n=== Scenario 2: K3D Topology ===");
|
||||
let k3d_cluster = K3DTopology::new("dev-cluster");
|
||||
let mut maestro_k3d = Maestro::new(k3d_cluster);
|
||||
|
||||
// Add scores compatible with K3DTopology (which has LinuxOperations AND KubernetesCluster)
|
||||
maestro_k3d.add_score(CommandScore::new("pwd")); // Uses LinuxOperations
|
||||
maestro_k3d.add_score(K8sResourceScore::new("nginx-deployment.yaml")); // Uses KubernetesCluster
|
||||
maestro_k3d.add_score(K8sResourceScore::new("invalid-service.yaml")); // Test error case
|
||||
maestro_k3d.add_score(CommandScore::new("fail please")); // Test error case
|
||||
|
||||
let results_k3d = maestro_k3d.run_all();
|
||||
println!("\nK3D Cluster Results: {:?}", results_k3d);
|
||||
|
||||
println!("\n=== Compile-Time Safety Demonstrated ===");
|
||||
println!("(Check the commented-out line in the code for the compile error example)");
|
||||
}
|
||||
Reference in New Issue
Block a user