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());
}
}