harmony/adr/003-abstractions/main_context_prompt.md

10 KiB

Here is the current condenses architecture sample for Harmony's core abstractions

use std::process::Command;

pub trait Capability {}

pub trait CommandCapability: Capability {
    fn execute_command(&self, command: &str, args: &[&str]) -> 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 Score<T: Topology> {
    fn compile(&self) -> Result<Box<dyn Interpret<T>>, 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<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())
        }
    }
}

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<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", &["-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<String, String> {
        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<String>,
}

impl CommandScore {
    pub fn new(name: String, command: String, args: Vec<String>) -> Self {
        Self {
            name,
            command,
            args,
        }
    }
}

pub trait Interpret<T: Topology> {
    fn execute(&self, topology: &T) -> Result<String, String>;
}

struct CommandInterpret;

impl<T> Interpret<T> for CommandInterpret
where
    T: Topology + CommandCapability,
{
    fn execute(&self, topology: &T) -> Result<String, String> {
        todo!()
    }
}

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 {
        &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<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 compile(&self) -> Result<Box<(dyn Interpret<T> + 'static)>, String> {
        Ok(Box::new(K8sResourceInterpret {
            score: self.clone(),
        }))
    }

    fn name(&self) -> &str {
        &self.name
    }
}

pub struct Maestro<T: Topology> {
    topology: T,
    scores: Vec<Box<dyn Score<T>>>,
}


impl<T: Topology> Maestro<T> {
    pub fn new(topology: T) -> Self {
        Self {
            topology,
            scores: Vec::new(),
        }
    }

    pub fn register_score<S>(&mut self, score: S)
    where
        S: Score<T> + '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