4.1 KiB
Adding Capabilities
Capabilities are trait methods that a Topology exposes to Scores. They are the "how" — the specific APIs and features that let a Score translate intent into infrastructure actions.
How Capabilities Work
When a Score declares it needs certain Capabilities:
impl<T: Topology + K8sclient + HelmCommand> Score<T> for MyScore {
// ...
}
The compiler verifies that the target Topology implements both K8sclient and HelmCommand. If it doesn't, compilation fails. This is the compile-time safety check that prevents invalid configurations from reaching production.
Built-in Capabilities
Harmony provides a set of standard Capabilities:
| Capability | What it provides |
|---|---|
K8sclient |
A Kubernetes API client |
HelmCommand |
A configured helm CLI invocation |
TlsRouter |
TLS certificate management |
NetworkManager |
Host network configuration |
SwitchClient |
Network switch configuration |
CertificateManagement |
Certificate issuance via cert-manager |
Implementing a Capability
Capabilities are implemented as trait methods on your Topology:
use std::sync::Arc;
use harmony_k8s::K8sClient;
use harmony::topology::K8sclient;
pub struct MyTopology {
kubeconfig: Option<String>,
}
#[async_trait]
impl K8sclient for MyTopology {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
let client = match &self.kubeconfig {
Some(path) => K8sClient::from_kubeconfig(path).await?,
None => K8sClient::try_default().await?,
};
Ok(Arc::new(client))
}
}
Adding a Custom Capability
For specialized infrastructure needs, add your own Capability as a trait:
use async_trait::async_trait;
use crate::executors::ExecutorError;
/// A capability for configuring network switches
#[async_trait]
pub trait SwitchClient: Send + Sync {
async fn configure_port(
&self,
switch: &str,
port: &str,
vlan: u16,
) -> Result<(), ExecutorError>;
async fn configure_port_channel(
&self,
switch: &str,
name: &str,
ports: &[&str],
) -> Result<(), ExecutorError>;
}
Then implement it on your Topology:
use harmony_infra::brocade::BrocadeClient;
pub struct MyTopology {
switch_client: Arc<dyn SwitchClient>,
}
impl SwitchClient for MyTopology {
async fn configure_port(&self, switch: &str, port: &str, vlan: u16) -> Result<(), ExecutorError> {
self.switch_client.configure_port(switch, port, vlan).await
}
async fn configure_port_channel(&self, switch: &str, name: &str, ports: &[&str]) -> Result<(), ExecutorError> {
self.switch_client.configure_port_channel(switch, name, ports).await
}
}
Now Scores that need SwitchClient can run on MyTopology.
Capability Composition
Topologies often compose multiple Capabilities to support complex Scores:
pub struct HAClusterTopology {
pub kubeconfig: Option<String>,
pub router: Arc<dyn Router>,
pub load_balancer: Arc<dyn LoadBalancer>,
pub switch_client: Arc<dyn SwitchClient>,
pub dhcp_server: Arc<dyn DhcpServer>,
pub dns_server: Arc<dyn DnsServer>,
// ...
}
impl K8sclient for HAClusterTopology { ... }
impl HelmCommand for HAClusterTopology { ... }
impl SwitchClient for HAClusterTopology { ... }
impl DhcpServer for HAClusterTopology { ... }
impl DnsServer for HAClusterTopology { ... }
impl Router for HAClusterTopology { ... }
impl LoadBalancer for HAClusterTopology { ... }
A Score that needs all of these can run on HAClusterTopology because the Topology provides all of them.
Best Practices
- Keep Capabilities focused — one Capability per concern (Kubernetes client, Helm, switch config)
- Return meaningful errors — use specific error types so Scores can handle failures appropriately
- Make Capabilities optional where sensible — not every Topology needs every Capability; use
Option<T>or a separate trait for optional features - Document preconditions — if a Capability requires the infrastructure to be in a specific state, document it in the trait doc comments