Structure the Harmony core to rely on a DAG for declaring & executing Scores
This commit is contained in:
commit
a351fd1228
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
### Rust ###
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
### rust-analyzer ###
|
||||||
|
# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules)
|
||||||
|
rust-project.json
|
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = [ "examples/harmony","harmony-core", "harmony-derive"]
|
13
examples/harmony/Cargo.toml
Normal file
13
examples/harmony/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "example-harmony"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
harmony-core = { version = "0.1.0", path = "../../harmony-core" }
|
||||||
|
harmony-derive = { version = "0.1.0", path = "../../harmony-derive" }
|
||||||
|
inquire = "0.7.5"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.143"
|
||||||
|
tokio = "1.47.1"
|
801
examples/harmony/src/main.rs
Normal file
801
examples/harmony/src/main.rs
Normal file
@ -0,0 +1,801 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use harmony_core::{
|
||||||
|
Dependency, ExecutionContext, Id, Interpret, InterpretError, InterpretOutcome, Inventory,
|
||||||
|
LinkedValue, Maestro, Score, Topology, dependency,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
trait Ingress {
|
||||||
|
fn get_domain(&self, service: &str) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait DhcpServer {
|
||||||
|
fn get_gateway_ip(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait LoadBalancer {
|
||||||
|
fn get_domain_name(&self) -> String;
|
||||||
|
fn get_load_balancer_ip(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct HaClusterTopology;
|
||||||
|
|
||||||
|
impl Topology for HaClusterTopology {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"HaCluster"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ingress for HaClusterTopology {
|
||||||
|
fn get_domain(&self, service: &str) -> String {
|
||||||
|
format!("https://{service}.domain.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DhcpServer for HaClusterTopology {
|
||||||
|
fn get_gateway_ip(&self) -> String {
|
||||||
|
"1.1.1.1".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadBalancer for HaClusterTopology {
|
||||||
|
fn get_domain_name(&self) -> String {
|
||||||
|
"domain.com".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_load_balancer_ip(&self) -> String {
|
||||||
|
"192.168.1.1".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct IngressScore {
|
||||||
|
service: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for IngressScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("IngressScore[{}]", self.service))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + Ingress> Score<T> for IngressScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id(format!("ingress-{}", self.service))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!("IngressScore[{}]", self.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(IngressInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IngressInterpret {
|
||||||
|
score: IngressScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + Ingress> Interpret<T> for IngressInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
let domain = topology.get_domain(&self.score.service);
|
||||||
|
context.insert(Id("domain".into()), &domain);
|
||||||
|
|
||||||
|
println!("Ingress domain: {domain}");
|
||||||
|
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ApplicationScore {
|
||||||
|
name: String,
|
||||||
|
domain: LinkedValue<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ApplicationScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("ApplicationScore[{}]", self.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationScore {
|
||||||
|
fn new(name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.clone(),
|
||||||
|
domain: LinkedValue::ContextKey(Id("ingress.domain".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + Ingress> Score<T> for ApplicationScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id(format!("application-{}", self.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!("ApplicationScore[{}]", self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![dependency!(
|
||||||
|
Id("ingress".into()),
|
||||||
|
IngressScore {
|
||||||
|
service: self.name.clone()
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(ApplicationInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ApplicationInterpret {
|
||||||
|
score: ApplicationScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + Ingress> Interpret<T> for ApplicationInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
let domain = self.score.domain.resolve(&context)?;
|
||||||
|
|
||||||
|
println!("{}: {}", self.score.name, domain);
|
||||||
|
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct DhcpServerScore {}
|
||||||
|
|
||||||
|
impl Display for DhcpServerScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("DhcpServerScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + DhcpServer> Score<T> for DhcpServerScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("dhcp-server".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"DhcpServerScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(DhcpServerInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DhcpServerInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + DhcpServer> Interpret<T> for DhcpServerInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
let gateway_ip = topology.get_gateway_ip();
|
||||||
|
context.insert(Id("gateway-ip".into()), gateway_ip);
|
||||||
|
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct DhcpScore {
|
||||||
|
host_binding: Vec<String>,
|
||||||
|
domain: Option<String>,
|
||||||
|
next_server: LinkedValue<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DhcpScore {
|
||||||
|
fn new(host_binding: Vec<String>, domain: Option<String>, next_server: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
host_binding,
|
||||||
|
domain,
|
||||||
|
next_server: match next_server {
|
||||||
|
Some(next_server) => LinkedValue::Value(next_server),
|
||||||
|
None => LinkedValue::ContextKey(Id("dhcp-server.gateway-ip".into())),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DhcpScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("DhcpScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + DhcpServer> Score<T> for DhcpScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("dhcp".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"DhcpScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
let mut dependencies: Vec<Dependency<T>> = vec![];
|
||||||
|
|
||||||
|
if let LinkedValue::ContextKey(_) = &self.next_server {
|
||||||
|
dependencies.push(dependency!(Id("dhcp-server".into()), DhcpServerScore {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(DhcpInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DhcpInterpret {
|
||||||
|
score: DhcpScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for DhcpInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
let next_server = self.score.next_server.resolve(&context)?;
|
||||||
|
println!(
|
||||||
|
"Configuring DHCP [\n - next_server: {:?}\n - host_binding: {:?}\n - domain: {:?}\n]",
|
||||||
|
next_server, self.score.host_binding, self.score.domain
|
||||||
|
);
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct OkdIpxeScore {
|
||||||
|
kickstart_filename: String,
|
||||||
|
harmony_inventory_agent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for OkdIpxeScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("OkdIpxeScore[{}]", self.kickstart_filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + DhcpServer> Score<T> for OkdIpxeScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("okd-ipxe".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!("OkdIpxeScore[{}]", self.kickstart_filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![
|
||||||
|
dependency!(Id("dhcp".into()), DhcpScore::new(vec![], None, None)),
|
||||||
|
dependency!(
|
||||||
|
Id("tftp".into()),
|
||||||
|
TftpScore {
|
||||||
|
files_to_serve: "./data/pxe/okd/tftpboot/".into(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(OkdIpxeInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OkdIpxeInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for OkdIpxeInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
_context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct TftpScore {
|
||||||
|
files_to_serve: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TftpScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("TftpScore[{}]", self.files_to_serve))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> Score<T> for TftpScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("TftpScore".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"TftpScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(TftpInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TftpInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for TftpInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
_context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct OkdInstallationScore {}
|
||||||
|
|
||||||
|
impl Display for OkdInstallationScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("OkdInstallationScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + LoadBalancer> Score<T> for OkdInstallationScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("okd-installation".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"OkdInstallationScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![
|
||||||
|
dependency!(Id("prepare".into()), OkdInstallationPrepareScore::new()),
|
||||||
|
dependency!(Id("bootstrap".into()), OkdInstallationBootstrapScore {}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(OkdInstallationInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OkdInstallationInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + LoadBalancer> Interpret<T> for OkdInstallationInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
_context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct PhysicalHost {
|
||||||
|
id: Id,
|
||||||
|
category: HostCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PhysicalHost {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("Host[{}, {:?}]", self.id, self.category))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum HostRole {
|
||||||
|
Bootstrap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
enum HostCategory {
|
||||||
|
Server,
|
||||||
|
Firewall,
|
||||||
|
Switch,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct OkdInstallationPrepareScore {
|
||||||
|
bootstrap_host: LinkedValue<PhysicalHost>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OkdInstallationPrepareScore {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
bootstrap_host: LinkedValue::ContextKey(Id("discover-host.host".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for OkdInstallationPrepareScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("OkdInstallationPrepareScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + LoadBalancer> Score<T> for OkdInstallationPrepareScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("okd-installation-prepare".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"OkdInstallationPrepareScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![
|
||||||
|
dependency!(Id("opnsense-hosts".into()), OPNsenseHostsScore {}),
|
||||||
|
dependency!(
|
||||||
|
Id("discover-host".into()),
|
||||||
|
DiscoverHostForRoleScore::new(HostRole::Bootstrap)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(OkdInstallationPrepareInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OkdInstallationPrepareInterpret {
|
||||||
|
score: OkdInstallationPrepareScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + LoadBalancer> Interpret<T> for OkdInstallationPrepareInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
let bootstrap_host = self.score.bootstrap_host.resolve(&context)?;
|
||||||
|
println!("Bootstrap host: {}", bootstrap_host);
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct DiscoverHostForRoleScore {
|
||||||
|
role: HostRole,
|
||||||
|
hosts: LinkedValue<Vec<PhysicalHost>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscoverHostForRoleScore {
|
||||||
|
fn new(role: HostRole) -> Self {
|
||||||
|
Self {
|
||||||
|
role,
|
||||||
|
hosts: LinkedValue::ContextKey(Id("available-hosts.hosts".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DiscoverHostForRoleScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("DiscoverHostForRoleScore[{:?}]", self.role))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> Score<T> for DiscoverHostForRoleScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id(format!("discover-host-{:?}", self.role))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!("DiscoverHostForRoleScore[{:?}]", self.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![dependency!(
|
||||||
|
Id("available-hosts".into()),
|
||||||
|
DiscoverAvailableHostsScore {}
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(DiscoverHostForRoleInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DiscoverHostForRoleInterpret {
|
||||||
|
score: DiscoverHostForRoleScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for DiscoverHostForRoleInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
let hosts = self.score.hosts.resolve(&context)?;
|
||||||
|
let host = hosts.first().unwrap();
|
||||||
|
context.insert(Id("host".into()), host);
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct DiscoverAvailableHostsScore {}
|
||||||
|
|
||||||
|
impl Display for DiscoverAvailableHostsScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("DiscoverAvailableHostsScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> Score<T> for DiscoverAvailableHostsScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("discover-available-hosts".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"DiscoverAvailableHostsScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(DiscoverAvailableHostsInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DiscoverAvailableHostsInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for DiscoverAvailableHostsInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
context.insert(
|
||||||
|
Id("hosts".into()),
|
||||||
|
vec![PhysicalHost {
|
||||||
|
id: Id("host-1".into()),
|
||||||
|
category: HostCategory::Server,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct OPNsenseHostsScore {}
|
||||||
|
|
||||||
|
impl Display for OPNsenseHostsScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("OPNsenseHostsScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + LoadBalancer> Score<T> for OPNsenseHostsScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("opnsense-hosts".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"OPNsenseHostsScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(OPNsenseHostsInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OPNsenseHostsInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + LoadBalancer> Interpret<T> for OPNsenseHostsInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
_context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
let cluster_domain = &topology.get_domain_name();
|
||||||
|
let load_balancer_ip = &topology.get_load_balancer_ip();
|
||||||
|
|
||||||
|
inquire::Confirm::new(&format!(
|
||||||
|
"Set hostnames manually in your opnsense dnsmasq config :
|
||||||
|
*.apps.{cluster_domain} -> {load_balancer_ip}
|
||||||
|
api.{cluster_domain} -> {load_balancer_ip}
|
||||||
|
api-int.{cluster_domain} -> {load_balancer_ip}
|
||||||
|
|
||||||
|
When you can dig them, confirm to continue.
|
||||||
|
"
|
||||||
|
))
|
||||||
|
.prompt()
|
||||||
|
.expect("Prompt error");
|
||||||
|
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct OkdInstallationBootstrapScore {}
|
||||||
|
|
||||||
|
impl Display for OkdInstallationBootstrapScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("OkdInstallationBootstrapScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> Score<T> for OkdInstallationBootstrapScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("okd-installation-bootstrap".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"OkdInstallationBootstrapScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(OkdInstallationBootstrapInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OkdInstallationBootstrapInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for OkdInstallationBootstrapInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
_context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ProvisionHighAvailabilityClusterScore {}
|
||||||
|
|
||||||
|
impl Display for ProvisionHighAvailabilityClusterScore {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("ProvisionHighAvailabilityClusterScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + DhcpServer + LoadBalancer> Score<T> for ProvisionHighAvailabilityClusterScore {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id("provision-high-availability-cluster".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"ProvisionHighAvailabilityClusterScore".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>> {
|
||||||
|
vec![
|
||||||
|
dependency!(
|
||||||
|
Id("okd-ipxe".into()),
|
||||||
|
OkdIpxeScore {
|
||||||
|
kickstart_filename: "inventory.kickstart".into(),
|
||||||
|
harmony_inventory_agent: "harmony_inventory_agent".into(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
dependency!(Id("okd-installation".into()), OkdInstallationScore {}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(ProvisionHighAvailabilityClusterInterpret {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ProvisionHighAvailabilityClusterInterpret {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology> Interpret<T> for ProvisionHighAvailabilityClusterInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
_topology: &T,
|
||||||
|
_context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError> {
|
||||||
|
Ok(InterpretOutcome {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), InterpretError> {
|
||||||
|
let inventory = Inventory::autoload();
|
||||||
|
let topology = HaClusterTopology;
|
||||||
|
|
||||||
|
let maestro = Maestro::init(
|
||||||
|
inventory,
|
||||||
|
topology,
|
||||||
|
vec![Box::new(ProvisionHighAvailabilityClusterScore {})],
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = maestro.plan();
|
||||||
|
maestro.execute().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
11
harmony-core/Cargo.toml
Normal file
11
harmony-core/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "harmony-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
petgraph = "0.8.2"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.143"
|
||||||
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
319
harmony-core/src/lib.rs
Normal file
319
harmony-core/src/lib.rs
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use petgraph::{
|
||||||
|
algo,
|
||||||
|
dot::Dot,
|
||||||
|
graph::{DiGraph, NodeIndex},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Id(pub String);
|
||||||
|
|
||||||
|
impl Default for Id {
|
||||||
|
fn default() -> Self {
|
||||||
|
Id("default_id".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Id {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(self.0.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum LinkedValue<T> {
|
||||||
|
Value(T),
|
||||||
|
ContextKey(Id),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> LinkedValue<T>
|
||||||
|
where
|
||||||
|
T: Clone + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
pub fn resolve(&self, context: &ExecutionContext) -> Result<T, InterpretError> {
|
||||||
|
match self {
|
||||||
|
LinkedValue::Value(v) => Ok(v.clone()),
|
||||||
|
LinkedValue::ContextKey(key_id) => {
|
||||||
|
context.get::<T>(key_id).ok_or_else(|| InterpretError {
|
||||||
|
msg: format!("Value for key '{}' not found in context.", key_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Inventory;
|
||||||
|
|
||||||
|
impl Inventory {
|
||||||
|
pub fn autoload() -> Self {
|
||||||
|
Inventory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Topology: Send + Sync {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct InterpretOutcome {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InterpretError {
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Dependency<T>(pub Id, pub Box<dyn Score<T>>);
|
||||||
|
|
||||||
|
impl<T: Topology> Clone for Dependency<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone(), self.1.clone_box())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> Display for Dependency<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("{}", self.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! dependency {
|
||||||
|
($id:expr, $score:expr) => {
|
||||||
|
Dependency($id, Box::new($score))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Score<T: Topology>: Display + Send + Sync + CloneBoxScore<T> {
|
||||||
|
fn id(&self) -> Id;
|
||||||
|
fn name(&self) -> String;
|
||||||
|
fn depends_on(&self) -> Vec<Dependency<T>>;
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CloneBoxScore<T: Topology> {
|
||||||
|
fn clone_box(&self) -> Box<dyn Score<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, T> CloneBoxScore<T> for S
|
||||||
|
where
|
||||||
|
T: Topology,
|
||||||
|
S: Score<T> + Clone + 'static,
|
||||||
|
{
|
||||||
|
fn clone_box(&self) -> Box<dyn Score<T>> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Interpret<T>: Debug + Send {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
context: ExecutionContext,
|
||||||
|
) -> Result<InterpretOutcome, InterpretError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Context {
|
||||||
|
data: std::collections::HashMap<Id, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn get<T: DeserializeOwned>(&self, id: &Id) -> Option<T> {
|
||||||
|
self.data
|
||||||
|
.get(id)
|
||||||
|
.and_then(|value| serde_json::from_value(value.clone()).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert<T: Serialize>(&mut self, id: Id, data: T) {
|
||||||
|
if let Ok(value) = serde_json::to_value(data) {
|
||||||
|
self.data.insert(id, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExecutionContext {
|
||||||
|
id: Id,
|
||||||
|
context: Arc<Mutex<Context>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExecutionContext {
|
||||||
|
fn wrap<T: Topology>(context: Arc<Mutex<Context>>, dependency: Dependency<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dependency.0,
|
||||||
|
context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<T: DeserializeOwned>(&self, id: &Id) -> Option<T> {
|
||||||
|
let key = self.map_key(id);
|
||||||
|
|
||||||
|
let context = self.context.lock().unwrap();
|
||||||
|
context
|
||||||
|
.get(&key)
|
||||||
|
.and_then(|value: serde_json::Value| serde_json::from_value(value.clone()).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert<T: Serialize>(&self, id: Id, data: T) {
|
||||||
|
let key = self.map_key(&id);
|
||||||
|
|
||||||
|
let mut context = self.context.lock().unwrap();
|
||||||
|
context.insert(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_key(&self, id: &Id) -> Id {
|
||||||
|
Id(format!("{}.{}", self.id.0, id.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Maestro<T: Topology> {
|
||||||
|
inventory: Inventory,
|
||||||
|
topology: T,
|
||||||
|
scores: DiGraph<Dependency<T>, &'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> Maestro<T> {
|
||||||
|
pub fn init(inventory: Inventory, topology: T, scores: Vec<Box<dyn Score<T>>>) -> Self {
|
||||||
|
let scores = build_dag(scores).expect("Invalid scores DAG");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inventory,
|
||||||
|
topology,
|
||||||
|
scores,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plan(&self) -> Result<(), String> {
|
||||||
|
let dot = Dot::new(&self.scores);
|
||||||
|
println!("{}", dot);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(&self) -> Result<(), InterpretError> {
|
||||||
|
println!("Executing scores based on dependencies:");
|
||||||
|
let context = Arc::new(Mutex::new(Context::default()));
|
||||||
|
|
||||||
|
// Use petgraph's topological sort to get a guaranteed order.
|
||||||
|
let mut sorted_nodes = match algo::toposort(&self.scores, None) {
|
||||||
|
Ok(nodes) => nodes,
|
||||||
|
Err(e) => {
|
||||||
|
let cycle_node_id = &self.scores.node_weight(e.node_id()).unwrap().0;
|
||||||
|
return Err(InterpretError {
|
||||||
|
msg: format!(
|
||||||
|
"A cycle was detected in the graph, involving score ID: {}",
|
||||||
|
cycle_node_id
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sorted_nodes.reverse();
|
||||||
|
|
||||||
|
for node_index in sorted_nodes {
|
||||||
|
let dependency = self.scores.node_weight(node_index).ok_or(InterpretError {
|
||||||
|
msg: "Failed to get score from graph node.".into(),
|
||||||
|
})?;
|
||||||
|
let execution_context = ExecutionContext::wrap(context.clone(), dependency.clone());
|
||||||
|
|
||||||
|
println!(" - Executing {}", dependency.0);
|
||||||
|
|
||||||
|
let interpret = dependency.1.create_interpret();
|
||||||
|
interpret
|
||||||
|
.execute(&self.inventory, &self.topology, execution_context)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a Directed Acyclic Graph (DAG) from a list of scores.
|
||||||
|
///
|
||||||
|
/// This function uses the `petgraph` library to perform cycle validation.
|
||||||
|
/// It correctly handles scores that are only present as dependencies,
|
||||||
|
/// ensuring all nodes in the graph are discovered and validated.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `scores` - A vector of `Box<dyn Score>` objects. These are the starting
|
||||||
|
/// nodes for the DAG. The function will recursively find all
|
||||||
|
/// transitive dependencies.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(DiGraph<Box<dyn Score<'a, T>>, ()>)` - The final DAG, represented as a
|
||||||
|
/// `petgraph::DiGraph`, if no cycles were found. The node weights are the `Box<dyn Score>` objects.
|
||||||
|
/// * `Err(String)` - An error message if a cycle or duplicate ID was detected.
|
||||||
|
fn build_dag<T: Topology>(
|
||||||
|
scores: Vec<Box<dyn Score<T>>>,
|
||||||
|
) -> Result<DiGraph<Dependency<T>, &'static str>, String> {
|
||||||
|
// A map to link each score's ID to its petgraph NodeIndex.
|
||||||
|
let mut id_to_node_index: HashMap<Id, NodeIndex> = HashMap::new();
|
||||||
|
|
||||||
|
// The directed graph. The node weights are the score objects.
|
||||||
|
let mut graph: DiGraph<Dependency<T>, &str> = DiGraph::new();
|
||||||
|
|
||||||
|
// A queue for processing new nodes and their dependencies.
|
||||||
|
let mut processing_queue: Vec<NodeIndex> = Vec::new();
|
||||||
|
|
||||||
|
// First, add all initial scores to the graph and the queue.
|
||||||
|
for score in scores {
|
||||||
|
let id = score.id();
|
||||||
|
let dependency = Dependency(id.clone(), score.clone_box());
|
||||||
|
|
||||||
|
// Check for duplicate root scores.
|
||||||
|
if id_to_node_index.contains_key(&id) {
|
||||||
|
return Err(format!("Duplicate score ID found: {}", id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let node_index = graph.add_node(dependency);
|
||||||
|
id_to_node_index.insert(id, node_index);
|
||||||
|
processing_queue.push(node_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, process the queue to build the full graph.
|
||||||
|
while let Some(current_node_index) = processing_queue.pop() {
|
||||||
|
let current_node = graph.node_weight(current_node_index).unwrap();
|
||||||
|
let current_id = ¤t_node.0.clone();
|
||||||
|
|
||||||
|
let dependencies = current_node.1.depends_on();
|
||||||
|
|
||||||
|
for dependency in dependencies {
|
||||||
|
let dep_id = Id(format!("{}.{}", current_id, &dependency.0));
|
||||||
|
let dependency = Dependency(dep_id.clone(), dependency.1.clone_box());
|
||||||
|
|
||||||
|
let dep_node_index = match id_to_node_index.get(&dep_id) {
|
||||||
|
Some(index) => *index,
|
||||||
|
None => {
|
||||||
|
// This is a new dependency, add it to the graph.
|
||||||
|
let new_node_index = graph.add_node(dependency.clone());
|
||||||
|
id_to_node_index.insert(dep_id.clone(), new_node_index);
|
||||||
|
processing_queue.push(new_node_index);
|
||||||
|
new_node_index
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add an edge from the current score to its dependency.
|
||||||
|
graph.add_edge(current_node_index, dep_node_index, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use petgraph's `toposort` to validate the DAG.
|
||||||
|
match algo::toposort(&graph, None) {
|
||||||
|
Ok(_) => Ok(graph),
|
||||||
|
Err(e) => {
|
||||||
|
let cycle_node_weight = graph
|
||||||
|
.node_weight(e.node_id())
|
||||||
|
.ok_or_else(|| "Cycle detected, but cycle node not found".to_string())?;
|
||||||
|
Err(format!(
|
||||||
|
"Cycle detected in dependencies, involving score with ID: {}",
|
||||||
|
cycle_node_weight.0
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
harmony-derive/Cargo.toml
Normal file
11
harmony-derive/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "harmony-derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0.40"
|
||||||
|
syn = "2.0.106"
|
58
harmony-derive/src/lib.rs
Normal file
58
harmony-derive/src/lib.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{Attribute, Ident, ItemStruct, parse_macro_input};
|
||||||
|
|
||||||
|
// This was an attempt to get a derive macro to simplify implementing new scores
|
||||||
|
//
|
||||||
|
// The goal was to get a syntax similar to this:
|
||||||
|
// #[derive(Score)]
|
||||||
|
// #[interpret(MyInterpret)]
|
||||||
|
// #[dependencies(OtherScore("foo"), OtherScore("bar"), ExtraScore("baz"))]
|
||||||
|
// struct MyScore {
|
||||||
|
// foo: LinkedValue<String>,
|
||||||
|
// bar: LinkedValue<String>,
|
||||||
|
// baz: LinkedValue<Vec<u32>>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[proc_macro_derive(Score, attributes(interpret))]
|
||||||
|
pub fn score_derive(input: TokenStream) -> TokenStream {
|
||||||
|
let input_struct = parse_macro_input!(input as ItemStruct);
|
||||||
|
let score_name = &input_struct.ident;
|
||||||
|
|
||||||
|
let interpret_name = get_interpret_name(&input_struct.attrs);
|
||||||
|
|
||||||
|
let expanded = quote! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct #interpret_name {
|
||||||
|
score: #score_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> crate::Score<T> for #score_name {
|
||||||
|
fn create_interpret(&self) -> Box<dyn crate::Interpret<T>> {
|
||||||
|
Box::new(#interpret_name {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_interpret_name(attrs: &Vec<Attribute>) -> Ident {
|
||||||
|
for attr in attrs {
|
||||||
|
if attr.path().is_ident("interpret") {
|
||||||
|
let nested_meta = attr
|
||||||
|
.parse_args_with(
|
||||||
|
syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
if let Some(ident) = nested_meta.first() {
|
||||||
|
return ident.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a default or panic if the attribute is not found
|
||||||
|
Ident::new("DefaultInterpret", proc_macro::Span::call_site().into())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user