Files
harmony/docs/guides/writing-a-topology.md
Jean-Gabriel Gill-Couture 64582caa64
Some checks failed
Run Check Script / check (pull_request) Failing after 10s
docs: Major rehaul of documentation
2026-03-19 22:38:55 -04:00

4.3 KiB

Writing a Topology

A Topology models your infrastructure environment and exposes Capability traits that Scores use to interact with it. Where a Score declares what you want, a Topology exposes what it can do.

The Minimum Implementation

At minimum, a Topology needs:

use async_trait::async_trait;
use harmony::{
    topology::{PreparationError, PreparationOutcome, Topology},
};

#[derive(Debug, Clone)]
pub struct MyTopology {
    pub name: String,
}

#[async_trait]
impl Topology for MyTopology {
    fn name(&self) -> &str {
        "MyTopology"
    }

    async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
        // Verify the infrastructure is accessible and ready
        Ok(PreparationOutcome::Success { details: "ready".to_string() })
    }
}

Implementing Capabilities

Scores express dependencies on Capabilities through trait bounds. For example, if your Topology should support Scores that deploy Helm charts, implement HelmCommand:

use std::process::Command;
use harmony::topology::HelmCommand;

impl HelmCommand for MyTopology {
    fn get_helm_command(&self) -> Command {
        let mut cmd = Command::new("helm");
        if let Some(kubeconfig) = &self.kubeconfig {
            cmd.arg("--kubeconfig").arg(kubeconfig);
        }
        cmd
    }
}

For Scores that need a Kubernetes client, implement K8sclient:

use std::sync::Arc;
use harmony_k8s::K8sClient;
use harmony::topology::K8sclient;

#[async_trait]
impl K8sclient for MyTopology {
    async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
        let client = if let Some(kubeconfig) = &self.kubeconfig {
            K8sClient::from_kubeconfig(kubeconfig).await?
        } else {
            K8sClient::try_default().await?
        };
        Ok(Arc::new(client))
    }
}

Loading Topology from Environment

For flexibility, implement from_env() to read configuration from environment variables:

impl MyTopology {
    pub fn from_env() -> Self {
        Self {
            name: std::env::var("MY_TOPOLOGY_NAME")
                .unwrap_or_else(|_| "default".to_string()),
            kubeconfig: std::env::var("KUBECONFIG").ok(),
        }
    }
}

This pattern lets operators switch between environments without recompiling:

export KUBECONFIG=/path/to/prod-cluster.kubeconfig
cargo run --example my_example

Complete Example: K8sAnywhereTopology

The K8sAnywhereTopology is the most commonly used Topology and handles both local (K3D) and remote Kubernetes clusters:

pub struct K8sAnywhereTopology {
    pub k8s_state: Arc<OnceCell<K8sState>>,
    pub tenant_manager: Arc<OnceCell<TenantManager>>,
    pub config: Arc<K8sAnywhereConfig>,
}

#[async_trait]
impl Topology for K8sAnywhereTopology {
    fn name(&self) -> &str {
        "K8sAnywhereTopology"
    }

    async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
        // 1. If autoinstall is enabled and no cluster exists, provision K3D
        // 2. Verify kubectl connectivity
        // 3. Optionally wait for cluster operators to be ready
        Ok(PreparationOutcome::Success { details: "cluster ready".to_string() })
    }
}

Key Patterns

Lazy Initialization

Use OnceCell for expensive resources like Kubernetes clients:

pub struct K8sAnywhereTopology {
    k8s_state: Arc<OnceCell<K8sState>>,
}

Multi-Target Topologies

For Scores that span multiple clusters (like NATS supercluster), implement MultiTargetTopology:

pub trait MultiTargetTopology: Topology {
    fn current_target(&self) -> &str;
    fn set_target(&mut self, target: &str);
}

Composing Topologies

Complex topologies combine multiple infrastructure components:

pub struct HAClusterTopology {
    pub router: Arc<dyn Router>,
    pub load_balancer: Arc<dyn LoadBalancer>,
    pub firewall: Arc<dyn Firewall>,
    pub dhcp_server: Arc<dyn DhcpServer>,
    pub dns_server: Arc<dyn DnsServer>,
    pub kubeconfig: Option<String>,
    // ...
}

Testing Your Topology

Test Topologies in isolation by implementing them against mock infrastructure:

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_topology_ensure_ready() {
        let topo = MyTopology::from_env();
        let result = topo.ensure_ready().await;
        assert!(result.is_ok());
    }
}