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

177 lines
4.3 KiB
Markdown

# 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:
```rust
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`:
```rust
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`:
```rust
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:
```rust
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:
```bash
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:
```rust
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:
```rust
pub struct K8sAnywhereTopology {
k8s_state: Arc<OnceCell<K8sState>>,
}
```
### Multi-Target Topologies
For Scores that span multiple clusters (like NATS supercluster), implement `MultiTargetTopology`:
```rust
pub trait MultiTargetTopology: Topology {
fn current_target(&self) -> &str;
fn set_target(&mut self, target: &str);
}
```
### Composing Topologies
Complex topologies combine multiple infrastructure components:
```rust
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:
```rust
#[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());
}
}
```