Files
harmony/docs/guides/adding-capabilities.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.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