Some checks failed
Run Check Script / check (pull_request) Failing after 10s
177 lines
4.3 KiB
Markdown
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());
|
|
}
|
|
}
|
|
```
|