4.6 KiB
Writing a Score
A Score declares what you want to achieve. It is decoupled from how it is achieved — that logic lives in an Interpret.
The Pattern
A Score consists of two parts:
- A struct — holds the configuration for your desired state
- A
Score<T>implementation — returns anInterpretthat knows how to execute
An Interpret contains the actual execution logic and connects your Score to the capabilities exposed by a Topology.
Example: A Simple Score
Here's a simplified version of NtfyScore from the ntfy module:
use async_trait::async_trait;
use harmony::{
interpret::{Interpret, InterpretError, Outcome},
inventory::Inventory,
score::Score,
topology::{HelmCommand, K8sclient, Topology},
};
/// MyScore declares "I want to install the ntfy server"
#[derive(Debug, Clone)]
pub struct MyScore {
pub namespace: String,
pub host: String,
}
impl<T: Topology + HelmCommand + K8sclient> Score<T> for MyScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(MyInterpret { score: self.clone() })
}
fn name(&self) -> String {
"ntfy [MyScore]".into()
}
}
/// MyInterpret knows _how_ to install ntfy using the Topology's capabilities
#[derive(Debug)]
pub struct MyInterpret {
pub score: MyScore,
}
#[async_trait]
impl<T: Topology + HelmCommand + K8sclient> Interpret<T> for MyInterpret {
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
// 1. Get a Kubernetes client from the Topology
let client = topology.k8s_client().await?;
// 2. Use Helm to install the ntfy chart
// (via topology's HelmCommand capability)
// 3. Wait for the deployment to be ready
client
.wait_until_deployment_ready("ntfy", Some(&self.score.namespace), None)
.await?;
Ok(Outcome::success("ntfy installed".to_string()))
}
}
The Compile-Time Safety Check
The generic Score<T> trait is bounded by T: Topology. This means the compiler enforces that your Score only runs on Topologies that expose the capabilities your Interpret needs:
// This only compiles if K8sAnywhereTopology (or any T)
// implements HelmCommand and K8sclient
impl<T: Topology + HelmCommand + K8sclient> Score<T> for MyScore { ... }
If you try to run this Score against a Topology that doesn't expose HelmCommand, you get a compile error — before any code runs.
Using Your Score
Once defined, your Score integrates with the Harmony CLI:
use harmony::{
inventory::Inventory,
topology::K8sAnywhereTopology,
};
#[tokio::main]
async fn main() {
let my_score = MyScore {
namespace: "monitoring".to_string(),
host: "ntfy.example.com".to_string(),
};
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(my_score)],
None,
)
.await
.unwrap();
}
Key Patterns
Composing Scores
Scores can include other Scores via features:
let app = ApplicationScore {
features: vec![
Box::new(PackagingDeployment { application: app.clone() }),
Box::new(Monitoring { application: app.clone(), alert_receiver: vec![] }),
],
application: app,
};
Reusing Interpret Logic
Many Scores delegate to shared Interpret implementations. For example, HelmChartScore provides a reusable Interpret for any Helm-based deployment. Your Score can wrap it:
impl<T: Topology + HelmCommand> Score<T> for MyScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(HelmChartInterpret { /* your config */ })
}
}
Accessing Topology Capabilities
Your Interpret accesses infrastructure through Capabilities exposed by the Topology:
// Via the Topology trait directly
let k8s_client = topology.k8s_client().await?;
let helm = topology.get_helm_command();
// Or via Capability traits
impl<T: Topology + K8sclient> Interpret<T> for MyInterpret {
async fn execute(...) {
let client = topology.k8s_client().await?;
// use client...
}
}
Best Practices
- Keep Scores focused — one Score per concern (deployment, monitoring, networking)
- Use
..Default::default()for optional fields so callers only need to specify what they care about - Return
Outcome— useOutcome::success,Outcome::failure, orOutcome::success_with_detailsto communicate results clearly - Handle errors gracefully — return meaningful
InterpretErrormessages that help operators debug issues