diff --git a/adr/003-infrastructure-abstractions.md b/adr/003-infrastructure-abstractions.md index 3d01531..5785bd4 100644 --- a/adr/003-infrastructure-abstractions.md +++ b/adr/003-infrastructure-abstractions.md @@ -1,12 +1,18 @@ -**Architecture Decision Record: Harmony Infrastructure Abstractions** +## Architecture Decision Record: Core Harmony Infrastructure Abstractions -**Status**: Proposed +## Status -**Context**: Harmony is an infrastructure orchestrator written in pure Rust, aiming to provide real portability of automation across different cloud providers and infrastructure setups. To achieve this, we need to define infrastructure abstractions that are provider-agnostic and flexible enough to accommodate various use cases. +Proposed -**Decision**: We will define our infrastructure abstractions using a domain-driven approach, focusing on the core logic of Harmony. These abstractions will only include the absolutely required elements for a specific resource, without referencing specific providers or implementations. +## Context -**Example: Database Abstraction** +Harmony is an infrastructure orchestrator written in pure Rust, aiming to provide real portability of automation across different cloud providers and infrastructure setups. To achieve this, we need to define infrastructure abstractions that are provider-agnostic and flexible enough to accommodate various use cases. + +## Decision + +We will define our infrastructure abstractions using a domain-driven approach, focusing on the core logic of Harmony. These abstractions will only include the absolutely required elements for a specific resource, without referencing specific providers or implementations. + +### Example: Database Abstraction To deploy a database to any cloud provider, we define an abstraction that includes essential elements such as: ```rust diff --git a/adr/005-interactive-project.md b/adr/005-interactive-project.md index 9b5abc5..01e9794 100644 --- a/adr/005-interactive-project.md +++ b/adr/005-interactive-project.md @@ -2,7 +2,7 @@ ## Status -Draft +Proposal ## Context @@ -31,30 +31,10 @@ This ADR will outline the approach taken to go from a LAMP project to be standal ## Decision -# Option 1 : Score spec -To simplify onboarding of existing projects, we decided to integrate with Score Spec for the following reasons : +# Custom Rust DSL -- It is a CNCF project, which helps a lot with adoption and community building -- It already supports important targets for us including docker-compose and k8s -- It provides a way to define the application's infrastructure at the correct level of abstraction for us to deploy it anywhere -- that is the goal of the score-spec project -- Once we evolve, we can simply have a score compatible provider that allows any project with a score spec to be deployed on the harmony stack -- Score was built with enterprise use-cases in mind : Humanitec platform engineering customers - - -## Consequences - -### Positive - -- Score Community is growing, using harmony will be very easy for them - -### Negative - -- Score is not that big yet, and mostly used by Humanitec's clients (I guess), which is a hard to penetrate environment - -# Option 2 : Custom Rust DSL - -We decided to develop a rust based DSL. Even though this means people will be afraid of "Rust", we believe the numerous advantages are worth the risk. +We decided to develop a rust based DSL. Even though this means people might be "afraid of Rust", we believe the numerous advantages are worth the risk. The main selection criterias are : @@ -76,3 +56,25 @@ The main selection criterias are : - Lack of an existing community and ecosystem, which could slow down adoption. - Increased maintenance overhead as the DSL needs to be updated and supported internally. +## Alternatives considered + +### Score spec + +We considered integrating with the score-spec project : https://github.com/score-spec/spec + +The idea was to benefit from an existing community and ecosystem. The motivations to consider score were the following : + +- It is a CNCF project, which helps a lot with adoption and community building +- It already supports important targets for us including docker-compose and k8s +- It provides a way to define the application's infrastructure at the correct level of abstraction for us to deploy it anywhere -- that is the goal of the score-spec project +- Once we evolve, we can simply have a score compatible provider that allows any project with a score spec to be deployed on the harmony stack +- Score was built with enterprise use-cases in mind : Humanitec platform engineering customers + + +Positive Consequences + +- Score Community is growing, using harmony will be very easy for them + +Negative Consequences + +- Score is not that big yet, and mostly used by Humanitec's clients (I guess), which is a hard to penetrate environment diff --git a/adr/core-abstractions/main_context_prompt.md b/adr/core-abstractions/main_context_prompt.md new file mode 100644 index 0000000..4b1e54e --- /dev/null +++ b/adr/core-abstractions/main_context_prompt.md @@ -0,0 +1,360 @@ + +# Here is the current condenses architecture sample for Harmony's core abstractions + +```rust +use std::process::Command; + +pub trait Capability {} + +pub trait CommandCapability: Capability { + fn execute_command(&self, command: &str, args: &[&str]) -> Result; +} + +pub trait KubernetesCapability: Capability { + fn apply_manifest(&self, manifest: &str) -> Result<(), String>; + fn get_resource(&self, resource_type: &str, name: &str) -> Result; +} + +pub trait Topology { + fn name(&self) -> &str; +} + +pub trait Score { + fn compile(&self) -> Result>, String>; + fn name(&self) -> &str; +} + +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: &[&str]) -> Result { + 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()) + } + } +} + +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: &[&str]) -> Result { + 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", &["-c", &format!("cat > {}", temp_file)])?; + + // Apply with kubectl + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "apply", + "-f", + &temp_file, + ])?; + + Ok(()) + } + + fn get_resource(&self, resource_type: &str, name: &str) -> Result { + println!( + "Getting resource {}/{} from K3D cluster '{}'", + resource_type, name, self.cluster_name + ); + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "get", + resource_type, + name, + "-o", + "yaml", + ]) + } +} + +pub struct CommandScore { + name: String, + command: String, + args: Vec, +} + +impl CommandScore { + pub fn new(name: String, command: String, args: Vec) -> Self { + Self { + name, + command, + args, + } + } +} + +pub trait Interpret { + fn execute(&self, topology: &T) -> Result; +} + +struct CommandInterpret; + +impl Interpret for CommandInterpret +where + T: Topology + CommandCapability, +{ + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for CommandScore +where + T: Topology + CommandCapability, +{ + fn compile(&self) -> Result>, String> { + Ok(Box::new(CommandInterpret {})) + } + + fn name(&self) -> &str { + &self.name + } +} + + +#[derive(Clone)] +pub struct K8sResourceScore { + name: String, + manifest: String, +} + +impl K8sResourceScore { + pub fn new(name: String, manifest: String) -> Self { + Self { name, manifest } + } +} + +struct K8sResourceInterpret { + score: K8sResourceScore, +} + +impl Interpret for K8sResourceInterpret { + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for K8sResourceScore +where + T: Topology + KubernetesCapability, +{ + fn compile(&self) -> Result + 'static)>, String> { + Ok(Box::new(K8sResourceInterpret { + score: self.clone(), + })) + } + + fn name(&self) -> &str { + &self.name + } +} + +pub struct Maestro { + topology: T, + scores: Vec>>, +} + + +impl Maestro { + pub fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + pub fn register_score(&mut self, score: S) + where + S: Score + 'static, + { + println!( + "Registering score '{}' for topology '{}'", + score.name(), + self.topology.name() + ); + self.scores.push(Box::new(score)); + } + + pub fn orchestrate(&self) -> Result<(), String> { + println!("Orchestrating topology '{}'", self.topology.name()); + for score in &self.scores { + let interpret = score.compile()?; + interpret.execute(&self.topology)?; + } + Ok(()) + } +} + +fn main() { + let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string()); + + let mut linux_maestro = Maestro::new(linux_host); + + linux_maestro.register_score(CommandScore::new( + "check-disk".to_string(), + "df".to_string(), + vec!["-h".to_string()], + )); + linux_maestro.orchestrate().unwrap(); + + // This would fail to compile if we tried to register a K8sResourceScore + // because LinuxHostTopology doesn't implement KubernetesCapability + //linux_maestro.register_score(K8sResourceScore::new( + // "...".to_string(), + // "...".to_string(), + //)); + + // Create a K3D topology which has both Command and Kubernetes capabilities + 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(), + ); + + // Create a maestro for the K3D topology + let mut k3d_maestro = Maestro::new(k3d_topology); + + // We can register both command scores and kubernetes scores + k3d_maestro.register_score(CommandScore::new( + "check-nodes".to_string(), + "kubectl".to_string(), + vec!["get".to_string(), "nodes".to_string()], + )); + + k3d_maestro.register_score(K8sResourceScore::new( + "deploy-nginx".to_string(), + r#" + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 + "# + .to_string(), + )); + + // Orchestrate both topologies + linux_maestro.orchestrate().unwrap(); + k3d_maestro.orchestrate().unwrap(); +} +``` + + +## Technical take + +The key insight is that we might not need a complex TypeMap or runtime capability checking. Instead, we should leverage Rust's trait system to express capability requirements directly in the type system. + +By clarifying the problem and focusing on type-level solutions rather than runtime checks, we can likely arrive at a simpler, more robust design that leverages the strengths of Rust's type system. + +## Philosophical Shifts + +1. **From Runtime to Compile-Time**: Move capability checking from runtime to compile-time. + +2. **From Objects to Functions**: Think of scores less as objects and more as functions that transform topologies. + +3. **From Homogeneous to Heterogeneous API**: Embrace different API paths for different capability combinations rather than trying to force everything through a single interface. + +4. **From Complex to Simple**: Focus on making common cases simple, even if it means less abstraction for uncommon cases. + +## High level concepts + +The high level concepts so far has evolved towards this definition. + +Topology -> Has -> Capabilities +Score -> Defines -> Work to be done / desired state +Interpret -> Requires -> Capabilities to execute a Score +Maestro -> Enforces -> Compatibility (through the type system at compile time) + +## Why Harmony + +The compile time safety is paramount here. Harmony's main goal is to make the entire software delivery pipeline robust. Current IaC tools are very hard to work with, require complex setups to test and debug real code. + +Leveraging Rust's compiler allows us to shift left a lot of the complexities and frustration that comes with using tools like Ansible that is Yaml based and quickly becomes brittle at scale. Or Terraform, when running a `terraform plan` makes you think everything is correct only to fail horribly when confidently launching `terraform apply` and leaving you with tens or hundreds of resources to clean manually. + +Of course, this requires a significant effort to get to the point where we have actually implemented all the logic. + +But using Rust and a Type Driven Design approach, we believe we are providing a much more robust foundation for our customer's and user's deployments anywhere. + +Also, having the full power of a mature programming language like Rust enables organizations and the community to customize their deployment any way they want, build upon it in a reliable way that has been evolved and proven over decades of enterprise dependency management, API definitions, etc. + +=== + +Given all this c diff --git a/examples/topology/Cargo.toml b/adr/core-abstractions/topology/Cargo.toml similarity index 100% rename from examples/topology/Cargo.toml rename to adr/core-abstractions/topology/Cargo.toml diff --git a/examples/topology/src/main.rs b/adr/core-abstractions/topology/src/main.rs similarity index 100% rename from examples/topology/src/main.rs rename to adr/core-abstractions/topology/src/main.rs diff --git a/examples/topology/src/main_claude37_2.rs b/adr/core-abstractions/topology/src/main_claude37_2.rs similarity index 100% rename from examples/topology/src/main_claude37_2.rs rename to adr/core-abstractions/topology/src/main_claude37_2.rs diff --git a/examples/topology/src/main_claudev1.rs b/adr/core-abstractions/topology/src/main_claudev1.rs similarity index 100% rename from examples/topology/src/main_claudev1.rs rename to adr/core-abstractions/topology/src/main_claudev1.rs diff --git a/examples/topology/src/main_gemini25pro.rs b/adr/core-abstractions/topology/src/main_gemini25pro.rs similarity index 100% rename from examples/topology/src/main_gemini25pro.rs rename to adr/core-abstractions/topology/src/main_gemini25pro.rs diff --git a/examples/topology/src/main_geminifail.rs b/adr/core-abstractions/topology/src/main_geminifail.rs similarity index 100% rename from examples/topology/src/main_geminifail.rs rename to adr/core-abstractions/topology/src/main_geminifail.rs diff --git a/examples/topology/src/main_right.rs b/adr/core-abstractions/topology/src/main_right.rs similarity index 100% rename from examples/topology/src/main_right.rs rename to adr/core-abstractions/topology/src/main_right.rs diff --git a/examples/topology/src/main_v1.rs b/adr/core-abstractions/topology/src/main_v1.rs similarity index 100% rename from examples/topology/src/main_v1.rs rename to adr/core-abstractions/topology/src/main_v1.rs diff --git a/adr/core-abstractions/topology2/Cargo.toml b/adr/core-abstractions/topology2/Cargo.toml new file mode 100644 index 0000000..ef57a6d --- /dev/null +++ b/adr/core-abstractions/topology2/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "example-topology2" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] diff --git a/adr/core-abstractions/topology2/src/main.rs b/adr/core-abstractions/topology2/src/main.rs new file mode 100644 index 0000000..313afa1 --- /dev/null +++ b/adr/core-abstractions/topology2/src/main.rs @@ -0,0 +1,183 @@ +// Clean capability-based design using type parameters + +trait Capability {} + +trait K8sCapability: Capability { + fn deploy_k8s_resource(&self, resource_yaml: &str); + fn execute_kubectl(&self, command: &str) -> String; +} + +trait LinuxCapability: Capability { + fn execute_command(&self, command: &str, args: &[&str]); + fn download_file(&self, url: &str, destination: &str) -> Result<(), String>; +} + +trait LoadBalancerCapability: Capability { + fn configure_load_balancer(&self, services: &[&str], port: u16); + fn get_load_balancer_status(&self) -> String; +} + +// Score trait with capability type parameter +trait Score { + fn execute(&self, capability: &C) -> String; +} + +// Topology implementations with marker trait +trait Topology {} + +struct K3DTopology {} +impl Topology for K3DTopology {} +impl Capability for K3DTopology {} +impl K8sCapability for K3DTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + todo!() + } + + fn execute_kubectl(&self, command: &str) -> String { + todo!() + } + // Implementation... +} + +struct LinuxTopology {} +impl Topology for LinuxTopology {} +impl Capability for LinuxTopology {} +impl LinuxCapability for LinuxTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + todo!() + } + // Implementation... +} + +struct OKDHaClusterTopology {} +impl Topology for OKDHaClusterTopology {} +impl Capability for OKDHaClusterTopology {} +impl K8sCapability for OKDHaClusterTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + todo!() + } + + fn execute_kubectl(&self, command: &str) -> String { + todo!() + } + // Implementation... +} +impl LinuxCapability for OKDHaClusterTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + todo!() + } + // Implementation... +} +impl LoadBalancerCapability for OKDHaClusterTopology { + fn configure_load_balancer(&self, services: &[&str], port: u16) { + todo!() + } + + fn get_load_balancer_status(&self) -> String { + todo!() + } + // Implementation... +} + +// Score implementations +struct LAMPScore {} +impl Score for LAMPScore { + fn execute(&self, capability: &dyn K8sCapability) -> String { + todo!() + // Implementation... + } +} + +struct BinaryScore {} +impl Score for BinaryScore { + fn execute(&self, capability: &dyn LinuxCapability) -> String { + todo!() + // Implementation... + } +} + +struct LoadBalancerScore {} +impl Score for LoadBalancerScore { + fn execute(&self, capability: &dyn LoadBalancerCapability) -> String { + todo!() + // Implementation... + } +} + +// Generic Maestro +struct Maestro { + topology: T, + scores: Vec String>>, +} + +impl Maestro { + fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + fn interpret_all(&mut self) -> Vec { + self.scores.iter_mut() + .map(|score| score(&self.topology)) + .collect() + } +} + +// Capability-specific extensions +impl Maestro { + fn register_k8s_score + 'static>(&mut self, score: S) { + let score_box = Box::new(move |topology: &T| { + score.execute(topology as &dyn K8sCapability) + }); + self.scores.push(score_box); + } +} + +impl Maestro { + fn register_linux_score + 'static>(&mut self, score: S) { + let score_box = Box::new(move |topology: &T| { + score.execute(topology as &dyn LinuxCapability) + }); + self.scores.push(score_box); + } +} + +impl Maestro { + fn register_lb_score + 'static>(&mut self, score: S) { + let score_box = Box::new(move |topology: &T| { + score.execute(topology as &dyn LoadBalancerCapability) + }); + self.scores.push(score_box); + } +} + +fn main() { + // Example usage + let k3d = K3DTopology {}; + let mut k3d_maestro = Maestro::new(k3d); + + // These will compile because K3D implements K8sCapability + k3d_maestro.register_k8s_score(LAMPScore {}); + + // This would not compile because K3D doesn't implement LoadBalancerCapability + // k3d_maestro.register_lb_score(LoadBalancerScore {}); + + let linux = LinuxTopology {}; + let mut linux_maestro = Maestro::new(linux); + + // This will compile because Linux implements LinuxCapability + linux_maestro.register_linux_score(BinaryScore {}); + + // This would not compile because Linux doesn't implement K8sCapability + // linux_maestro.register_k8s_score(LAMPScore {}); +} diff --git a/adr/core-abstractions/topology2/src/main_capabilities.rs b/adr/core-abstractions/topology2/src/main_capabilities.rs new file mode 100644 index 0000000..499069a --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_capabilities.rs @@ -0,0 +1,324 @@ +fn main() { + // Create various topologies + let okd_topology = OKDHaClusterTopology::new(); + let k3d_topology = K3DTopology::new(); + let linux_topology = LinuxTopology::new(); + + // Create scores + let lamp_score = LAMPScore::new("MySQL 8.0", "PHP 8.1", "Apache 2.4"); + let binary_score = BinaryScore::new("https://example.com/binary", vec!["--arg1", "--arg2"]); + let load_balancer_score = LoadBalancerScore::new(vec!["service1", "service2"], 80); + + // Example 1: Running LAMP stack on OKD + println!("\n=== Deploying LAMP stack on OKD cluster ==="); + lamp_score.execute(&okd_topology); + + // Example 2: Running LAMP stack on K3D + println!("\n=== Deploying LAMP stack on K3D cluster ==="); + lamp_score.execute(&k3d_topology); + + // Example 3: Running binary on Linux host + println!("\n=== Running binary on Linux host ==="); + binary_score.execute(&linux_topology); + + // Example 4: Running binary on OKD (which can also run Linux commands) + println!("\n=== Running binary on OKD host ==="); + binary_score.execute(&okd_topology); + + // Example 5: Load balancer configuration on OKD + println!("\n=== Configuring load balancer on OKD ==="); + load_balancer_score.execute(&okd_topology); + + // The following would not compile: + // load_balancer_score.execute(&k3d_topology); // K3D doesn't implement LoadBalancerCapability + // lamp_score.execute(&linux_topology); // Linux doesn't implement K8sCapability +} + +// Base Topology trait +trait Topology { + fn name(&self) -> &str; +} + +// Define capabilities +trait K8sCapability { + fn deploy_k8s_resource(&self, resource_yaml: &str); + fn execute_kubectl(&self, command: &str) -> String; +} + +trait OKDCapability: K8sCapability { + fn execute_oc(&self, command: &str) -> String; +} + +trait LinuxCapability { + fn execute_command(&self, command: &str, args: &[&str]) -> String; + fn download_file(&self, url: &str, destination: &str) -> Result<(), String>; +} + +trait LoadBalancerCapability { + fn configure_load_balancer(&self, services: &[&str], port: u16); + fn get_load_balancer_status(&self) -> String; +} + +trait FirewallCapability { + fn open_port(&self, port: u16, protocol: &str); + fn close_port(&self, port: u16, protocol: &str); +} + +trait RouterCapability { + fn configure_route(&self, service: &str, hostname: &str); +} + +// Topology implementations +struct OKDHaClusterTopology { + cluster_name: String, +} + +impl OKDHaClusterTopology { + fn new() -> Self { + Self { + cluster_name: "okd-ha-cluster".to_string(), + } + } +} + +impl Topology for OKDHaClusterTopology { + fn name(&self) -> &str { + &self.cluster_name + } +} + +impl K8sCapability for OKDHaClusterTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on OKD cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on OKD cluster: {}", command); + "kubectl command output".to_string() + } +} + +impl OKDCapability for OKDHaClusterTopology { + fn execute_oc(&self, command: &str) -> String { + println!("Executing oc command on OKD cluster: {}", command); + "oc command output".to_string() + } +} + +impl LinuxCapability for OKDHaClusterTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> String { + println!( + "Executing command '{}' with args {:?} on OKD node", + command, args + ); + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!( + "Downloading file from {} to {} on OKD node", + url, destination + ); + Ok(()) + } +} + +impl LoadBalancerCapability for OKDHaClusterTopology { + fn configure_load_balancer(&self, services: &[&str], port: u16) { + println!( + "Configuring load balancer for services {:?} on port {} in OKD", + services, port + ); + } + + fn get_load_balancer_status(&self) -> String { + "OKD Load Balancer: HEALTHY".to_string() + } +} + +impl FirewallCapability for OKDHaClusterTopology { + fn open_port(&self, port: u16, protocol: &str) { + println!( + "Opening port {} with protocol {} on OKD firewall", + port, protocol + ); + } + + fn close_port(&self, port: u16, protocol: &str) { + println!( + "Closing port {} with protocol {} on OKD firewall", + port, protocol + ); + } +} + +impl RouterCapability for OKDHaClusterTopology { + fn configure_route(&self, service: &str, hostname: &str) { + println!( + "Configuring route for service {} with hostname {} on OKD", + service, hostname + ); + } +} + +struct K3DTopology { + cluster_name: String, +} + +impl K3DTopology { + fn new() -> Self { + Self { + cluster_name: "k3d-local".to_string(), + } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.cluster_name + } +} + +impl K8sCapability for K3DTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on K3D cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on K3D cluster: {}", command); + "kubectl command output from K3D".to_string() + } +} + +struct LinuxTopology { + hostname: String, +} + +impl LinuxTopology { + fn new() -> Self { + Self { + hostname: "linux-host".to_string(), + } + } +} + +impl Topology for LinuxTopology { + fn name(&self) -> &str { + &self.hostname + } +} + +impl LinuxCapability for LinuxTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> String { + println!( + "Executing command '{}' with args {:?} on Linux host", + command, args + ); + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!( + "Downloading file from {} to {} on Linux host", + url, destination + ); + Ok(()) + } +} + +// Score implementations +struct LAMPScore { + mysql_version: String, + php_version: String, + apache_version: String, +} + +impl LAMPScore { + fn new(mysql_version: &str, php_version: &str, apache_version: &str) -> Self { + Self { + mysql_version: mysql_version.to_string(), + php_version: php_version.to_string(), + apache_version: apache_version.to_string(), + } + } + + fn execute(&self, topology: &T) { + // Deploy MySQL + topology.deploy_k8s_resource("mysql-deployment.yaml"); + + // Deploy PHP + topology.deploy_k8s_resource("php-deployment.yaml"); + + // Deploy Apache + topology.deploy_k8s_resource("apache-deployment.yaml"); + + // Create service + topology.deploy_k8s_resource("lamp-service.yaml"); + + // Check deployment + let status = topology.execute_kubectl("get pods -l app=lamp"); + println!("LAMP deployment status: {}", status); + } +} + +struct BinaryScore { + url: String, + args: Vec, +} + +impl BinaryScore { + fn new(url: &str, args: Vec<&str>) -> Self { + Self { + url: url.to_string(), + args: args.iter().map(|s| s.to_string()).collect(), + } + } + + fn execute(&self, topology: &T) { + let destination = "/tmp/binary"; + + match topology.download_file(&self.url, destination) { + Ok(_) => { + println!("Binary downloaded successfully"); + + // Convert args to slice of &str + let args: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); + + // Execute the binary + topology.execute_command(destination, &args); + println!("Binary execution completed"); + } + Err(e) => { + println!("Failed to download binary: {}", e); + } + } + } +} + +struct LoadBalancerScore { + services: Vec, + port: u16, +} + +impl LoadBalancerScore { + fn new(services: Vec<&str>, port: u16) -> Self { + Self { + services: services.iter().map(|s| s.to_string()).collect(), + port, + } + } + + fn execute(&self, topology: &T) { + println!("Configuring load balancer for services"); + + // Convert services to slice of &str + let services: Vec<&str> = self.services.iter().map(|s| s.as_str()).collect(); + + // Configure load balancer + topology.configure_load_balancer(&services, self.port); + + // Check status + let status = topology.get_load_balancer_status(); + println!("Load balancer status: {}", status); + } +} diff --git a/adr/core-abstractions/topology2/src/main_v1.rs b/adr/core-abstractions/topology2/src/main_v1.rs new file mode 100644 index 0000000..0771470 --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_v1.rs @@ -0,0 +1,34 @@ +fn main() {} + +trait Topology {} + +struct DummyTopology {} + +impl Topology for DummyTopology {} + +impl Topology for LampTopology {} + +struct LampTopology {} + +struct Maestro { + topology: Box, +} + +trait Score { + type Topology: Topology; + fn execute(&self, topology: &Self::Topology); +} + +struct K8sScore {} +impl Score for K8sScore { + type Topology = LampTopology; + fn execute(&self, topology: &Box) { + todo!() + } +} + +impl Maestro { + pub fn execute(&self, score: Box>) { + score.execute(&self.topology); + } +} diff --git a/adr/core-abstractions/topology2/src/main_v2.rs b/adr/core-abstractions/topology2/src/main_v2.rs new file mode 100644 index 0000000..865d0dd --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_v2.rs @@ -0,0 +1,76 @@ +fn main() { + // Example usage + let lamp_topology = LampTopology {}; + let k8s_score = K8sScore {}; + let docker_topology = DockerTopology{}; + + // Type-safe execution + let maestro = Maestro::new(Box::new(docker_topology)); + maestro.execute(&k8s_score); // This will work + + // This would fail at compile time if we tried: + // let dummy_topology = DummyTopology {}; + // let maestro = Maestro::new(Box::new(dummy_topology)); + // maestro.execute(&k8s_score); // Error: expected LampTopology, found DummyTopology +} + +// Base trait for all topologies +trait Topology { + // Common topology methods could go here + fn topology_type(&self) -> &str; +} + +struct DummyTopology {} +impl Topology for DummyTopology { + fn topology_type(&self) -> &str { "Dummy" } +} + +struct LampTopology {} +impl Topology for LampTopology { + fn topology_type(&self) -> &str { "LAMP" } +} + +struct DockerTopology {} + +impl Topology for DockerTopology { + fn topology_type(&self) -> &str { + todo!("DockerTopology") + } +} + +// The Score trait with an associated type for the required topology +trait Score { + type RequiredTopology: Topology + ?Sized; + fn execute(&self, topology: &Self::RequiredTopology); + fn score_type(&self) -> &str; +} + +// A score that requires LampTopology +struct K8sScore {} +impl Score for K8sScore { + type RequiredTopology = DockerTopology; + + fn execute(&self, topology: &Self::RequiredTopology) { + println!("Executing K8sScore on {} topology", topology.topology_type()); + // Implementation details... + } + + fn score_type(&self) -> &str { "K8s" } +} + +// A generic maestro that can work with any topology type +struct Maestro { + topology: Box, +} + +impl Maestro { + pub fn new(topology: Box) -> Self { + Maestro { topology } + } + + // Execute a score that requires this specific topology type + pub fn execute>(&self, score: &S) { + println!("Maestro executing {} score", score.score_type()); + score.execute(&*self.topology); + } +} diff --git a/adr/core-abstractions/topology2/src/main_v4.rs b/adr/core-abstractions/topology2/src/main_v4.rs new file mode 100644 index 0000000..8f8d004 --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_v4.rs @@ -0,0 +1,360 @@ +fn main() { + // Create topologies + let okd_topology = OKDHaClusterTopology::new(); + let k3d_topology = K3DTopology::new(); + let linux_topology = LinuxTopology::new(); + + // Create scores - boxing them as trait objects for dynamic dispatch + let scores: Vec> = vec![ + Box::new(LAMPScore::new("MySQL 8.0", "PHP 8.1", "Apache 2.4")), + Box::new(BinaryScore::new("https://example.com/binary", vec!["--arg1", "--arg2"])), + Box::new(LoadBalancerScore::new(vec!["service1", "service2"], 80)), + ]; + + // Running scores on OKD topology (which has all capabilities) + println!("\n=== Running all scores on OKD HA Cluster ==="); + for score in &scores { + match score.execute(&okd_topology) { + Ok(result) => println!("Score executed successfully: {}", result), + Err(e) => println!("Failed to execute score: {}", e), + } + } + + // Running scores on K3D topology (only has K8s capability) + println!("\n=== Running scores on K3D Cluster ==="); + for score in &scores { + match score.execute(&k3d_topology) { + Ok(result) => println!("Score executed successfully: {}", result), + Err(e) => println!("Failed to execute score: {}", e), + } + } + + // Running scores on Linux topology (only has Linux capability) + println!("\n=== Running scores on Linux Host ==="); + for score in &scores { + match score.execute(&linux_topology) { + Ok(result) => println!("Score executed successfully: {}", result), + Err(e) => println!("Failed to execute score: {}", e), + } + } +} + +// Base Topology trait +trait Topology: Any { + fn name(&self) -> &str; + + // This method allows us to get type information at runtime + fn as_any(&self) -> &dyn Any; +} + +// Use Any trait for runtime type checking +use std::any::Any; + +// Define capabilities +trait K8sCapability { + fn deploy_k8s_resource(&self, resource_yaml: &str); + fn execute_kubectl(&self, command: &str) -> String; +} + +trait OKDCapability: K8sCapability { + fn execute_oc(&self, command: &str) -> String; +} + +trait LinuxCapability { + fn execute_command(&self, command: &str, args: &[&str]); + fn download_file(&self, url: &str, destination: &str) -> Result<(), String>; +} + +trait LoadBalancerCapability { + fn configure_load_balancer(&self, services: &[&str], port: u16); + fn get_load_balancer_status(&self) -> String; +} + +// Base Score trait with dynamic dispatch +trait Score { + // Generic execute method that takes any topology + fn execute(&self, topology: &dyn Topology) -> Result; + + // Optional method to get score type for better error messages + fn score_type(&self) -> &str; +} + +// Topology implementations +struct OKDHaClusterTopology { + cluster_name: String, +} + +impl OKDHaClusterTopology { + fn new() -> Self { + Self { cluster_name: "okd-ha-cluster".to_string() } + } +} + +impl Topology for OKDHaClusterTopology { + fn name(&self) -> &str { + &self.cluster_name + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl K8sCapability for OKDHaClusterTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on OKD cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on OKD cluster: {}", command); + "kubectl command output".to_string() + } +} + +impl OKDCapability for OKDHaClusterTopology { + fn execute_oc(&self, command: &str) -> String { + println!("Executing oc command on OKD cluster: {}", command); + "oc command output".to_string() + } +} + +impl LinuxCapability for OKDHaClusterTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + println!("Executing command '{}' with args {:?} on OKD node", command, args); + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!("Downloading file from {} to {} on OKD node", url, destination); + Ok(()) + } +} + +impl LoadBalancerCapability for OKDHaClusterTopology { + fn configure_load_balancer(&self, services: &[&str], port: u16) { + println!("Configuring load balancer for services {:?} on port {} in OKD", services, port); + } + + fn get_load_balancer_status(&self) -> String { + "OKD Load Balancer: HEALTHY".to_string() + } +} + +struct K3DTopology { + cluster_name: String, +} + +impl K3DTopology { + fn new() -> Self { + Self { cluster_name: "k3d-local".to_string() } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.cluster_name + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl K8sCapability for K3DTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on K3D cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on K3D cluster: {}", command); + "kubectl command output from K3D".to_string() + } +} + +struct LinuxTopology { + hostname: String, +} + +impl LinuxTopology { + fn new() -> Self { + Self { hostname: "linux-host".to_string() } + } +} + +impl Topology for LinuxTopology { + fn name(&self) -> &str { + &self.hostname + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl LinuxCapability for LinuxTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + println!("Executing command '{}' with args {:?} on Linux host", command, args); + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!("Downloading file from {} to {} on Linux host", url, destination); + Ok(()) + } +} + +// Score implementations using dynamic capability checks +struct LAMPScore { + mysql_version: String, + php_version: String, + apache_version: String, +} + +impl LAMPScore { + fn new(mysql_version: &str, php_version: &str, apache_version: &str) -> Self { + Self { + mysql_version: mysql_version.to_string(), + php_version: php_version.to_string(), + apache_version: apache_version.to_string(), + } + } + + // Helper method for typesafe execution + fn execute_with_k8s(&self, topology: &dyn K8sCapability) -> String { + println!("Deploying LAMP stack with MySQL {}, PHP {}, Apache {}", + self.mysql_version, self.php_version, self.apache_version); + + // Deploy MySQL + topology.deploy_k8s_resource("mysql-deployment.yaml"); + + // Deploy PHP + topology.deploy_k8s_resource("php-deployment.yaml"); + + // Deploy Apache + topology.deploy_k8s_resource("apache-deployment.yaml"); + + // Create service + topology.deploy_k8s_resource("lamp-service.yaml"); + + // Check deployment + let status = topology.execute_kubectl("get pods -l app=lamp"); + format!("LAMP deployment status: {}", status) + } +} + +impl Score for LAMPScore { + fn execute(&self, topology: &dyn Topology) -> Result { + // Try to downcast to K8sCapability + if let Some(k8s_topology) = topology.as_any().downcast_ref::() { + Ok(self.execute_with_k8s(k8s_topology)) + } else if let Some(k8s_topology) = topology.as_any().downcast_ref::() { + Ok(self.execute_with_k8s(k8s_topology)) + } else { + Err(format!("LAMPScore requires K8sCapability but topology {} doesn't provide it", + topology.name())) + } + } + + fn score_type(&self) -> &str { + "LAMP" + } +} + +struct BinaryScore { + url: String, + args: Vec, +} + +impl BinaryScore { + fn new(url: &str, args: Vec<&str>) -> Self { + Self { + url: url.to_string(), + args: args.iter().map(|s| s.to_string()).collect(), + } + } + + // Helper method for typesafe execution + fn execute_with_linux(&self, topology: &dyn LinuxCapability) -> Result { + let destination = "/tmp/binary"; + + // Download the binary + println!("Preparing to run binary from {}", self.url); + + match topology.download_file(&self.url, destination) { + Ok(_) => { + println!("Binary downloaded successfully"); + + // Convert args to slice of &str + let args: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); + + // Execute the binary + topology.execute_command(destination, &args); + Ok("Binary execution completed successfully".to_string()) + }, + Err(e) => { + Err(format!("Failed to download binary: {}", e)) + } + } + } +} + +impl Score for BinaryScore { + fn execute(&self, topology: &dyn Topology) -> Result { + // Try to downcast to LinuxCapability + if let Some(linux_topology) = topology.as_any().downcast_ref::() { + self.execute_with_linux(linux_topology) + } else if let Some(linux_topology) = topology.as_any().downcast_ref::() { + self.execute_with_linux(linux_topology) + } else { + Err(format!("BinaryScore requires LinuxCapability but topology {} doesn't provide it", + topology.name())) + } + } + + fn score_type(&self) -> &str { + "Binary" + } +} + +struct LoadBalancerScore { + services: Vec, + port: u16, +} + +impl LoadBalancerScore { + fn new(services: Vec<&str>, port: u16) -> Self { + Self { + services: services.iter().map(|s| s.to_string()).collect(), + port, + } + } + + // Helper method for typesafe execution + fn execute_with_lb(&self, topology: &dyn LoadBalancerCapability) -> String { + println!("Configuring load balancer for services"); + + // Convert services to slice of &str + let services: Vec<&str> = self.services.iter().map(|s| s.as_str()).collect(); + + // Configure load balancer + topology.configure_load_balancer(&services, self.port); + + // Check status + let status = topology.get_load_balancer_status(); + format!("Load balancer configured successfully. Status: {}", status) + } +} + +impl Score for LoadBalancerScore { + fn execute(&self, topology: &dyn Topology) -> Result { + // Only OKDHaClusterTopology implements LoadBalancerCapability + if let Some(lb_topology) = topology.as_any().downcast_ref::() { + Ok(self.execute_with_lb(lb_topology)) + } else { + Err(format!("LoadBalancerScore requires LoadBalancerCapability but topology {} doesn't provide it", + topology.name())) + } + } + + fn score_type(&self) -> &str { + "LoadBalancer" + } +}