From 646c5e723e114a8e6be825e374592f6983626258 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 4 Mar 2026 07:16:25 -0500 Subject: [PATCH 1/5] feat: implementing node_health --- Cargo.lock | 13 ++ examples/node_health/Cargo.toml | 16 +++ examples/node_health/src/main.rs | 16 +++ harmony/src/domain/topology/k8s/mod.rs | 2 +- harmony/src/modules/k8s/resource.rs | 6 +- harmony/src/modules/mod.rs | 1 + harmony/src/modules/node_health/mod.rs | 173 +++++++++++++++++++++++++ 7 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 examples/node_health/Cargo.toml create mode 100644 examples/node_health/src/main.rs create mode 100644 harmony/src/modules/node_health/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6cc09c6e..6bf169cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1981,6 +1981,19 @@ dependencies = [ "url", ] +[[package]] +name = "example-node-health" +version = "0.1.0" +dependencies = [ + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_types", + "log", + "tokio", +] + [[package]] name = "example-ntfy" version = "0.1.0" diff --git a/examples/node_health/Cargo.toml b/examples/node_health/Cargo.toml new file mode 100644 index 00000000..93f6f20b --- /dev/null +++ b/examples/node_health/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "example-node-health" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] +harmony = { path = "../../harmony" } +harmony_cli = { path = "../../harmony_cli" } +harmony_types = { path = "../../harmony_types" } +tokio = { workspace = true } +harmony_macros = { path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } diff --git a/examples/node_health/src/main.rs b/examples/node_health/src/main.rs new file mode 100644 index 00000000..2704336b --- /dev/null +++ b/examples/node_health/src/main.rs @@ -0,0 +1,16 @@ +use harmony::{inventory::Inventory, modules::node_health::NodeHealthDeploymentScore, topology::K8sAnywhereTopology}; + + +#[tokio::main] +async fn main() { + let node_health = NodeHealthDeploymentScore {}; + + harmony_cli::run( + Inventory::autoload(), + K8sAnywhereTopology::from_env(), + vec![Box::new(node_health)], + None, + ) + .await + .unwrap(); +} diff --git a/harmony/src/domain/topology/k8s/mod.rs b/harmony/src/domain/topology/k8s/mod.rs index fba42e27..e60b4c25 100644 --- a/harmony/src/domain/topology/k8s/mod.rs +++ b/harmony/src/domain/topology/k8s/mod.rs @@ -983,7 +983,7 @@ impl K8sClient { pub async fn apply_many(&self, resource: &[K], ns: Option<&str>) -> Result, Error> where K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize, - ::Scope: ApplyStrategy, + // ::Scope: ApplyStrategy, ::DynamicType: Default, { let mut result = Vec::new(); diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index dde73390..83e10a75 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use k8s_openapi::NamespaceResourceScope; +use k8s_openapi::{NamespaceResourceScope, ResourceScope}; use kube::Resource; use log::info; use serde::{Serialize, de::DeserializeOwned}; @@ -29,7 +29,7 @@ impl K8sResourceScore { } impl< - K: Resource + K: Resource + std::fmt::Debug + Sync + DeserializeOwned @@ -61,7 +61,7 @@ pub struct K8sResourceInterpret { #[async_trait] impl< - K: Resource + K: Resource + Clone + std::fmt::Debug + DeserializeOwned diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 3fa69469..b8234951 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -22,3 +22,4 @@ pub mod prometheus; pub mod storage; pub mod tenant; pub mod tftp; +pub mod node_health; diff --git a/harmony/src/modules/node_health/mod.rs b/harmony/src/modules/node_health/mod.rs new file mode 100644 index 00000000..3bda30e1 --- /dev/null +++ b/harmony/src/modules/node_health/mod.rs @@ -0,0 +1,173 @@ +use async_trait::async_trait; +use harmony_types::id::Id; +use k8s_openapi::api::{ + core::v1::{Namespace, ServiceAccount}, + rbac::v1::{ClusterRole, ClusterRoleBinding, PolicyRule, Role, RoleBinding, RoleRef, Subject}, +}; +use kube::api::ObjectMeta; +use serde::Serialize; +use std::collections::BTreeMap; + +use crate::{ + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + modules::k8s::resource::K8sResourceScore, + score::Score, + topology::{K8sclient, Topology}, +}; + +#[derive(Clone, Debug, Serialize)] +pub struct NodeHealthDeploymentScore {} + +impl Score for NodeHealthDeploymentScore { + fn name(&self) -> String { + todo!() + } + + #[doc(hidden)] + fn create_interpret(&self) -> Box> { + Box::new(NodeHealthDeploymentInterpret {}) + } +} + +#[derive(Debug, Clone)] +pub struct NodeHealthDeploymentInterpret { + // config: NodeHealthDeploymentConfig, +} + +#[async_trait] +impl Interpret for NodeHealthDeploymentInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + let namespace_name = "harmony-node-healthcheck".to_string(); + + // Namespace + let mut labels = BTreeMap::new(); + labels.insert("name".to_string(), namespace_name.clone()); + + let namespace = Namespace { + metadata: ObjectMeta { + name: Some(namespace_name.clone()), + labels: Some(labels), + ..ObjectMeta::default() + }, + ..Namespace::default() + }; + + // ServiceAccount + let service_account_name = "node-healthcheck-sa".to_string(); + let service_account = ServiceAccount { + metadata: ObjectMeta { + name: Some(service_account_name.clone()), + namespace: Some(namespace_name.clone()), + ..ObjectMeta::default() + }, + ..ServiceAccount::default() + }; + + // ClusterRole + let cluster_role = ClusterRole { + metadata: ObjectMeta { + name: Some("node-healthcheck-role".to_string()), + ..ObjectMeta::default() + }, + rules: Some(vec![PolicyRule { + api_groups: Some(vec!["".to_string()]), + resources: Some(vec!["nodes".to_string()]), + verbs: vec!["get".to_string(), "list".to_string()], + ..PolicyRule::default() + }]), + ..ClusterRole::default() + }; + + // Role + let role = Role { + metadata: ObjectMeta { + name: Some("allow-hostnetwork-scc".to_string()), + namespace: Some(namespace_name.clone()), + ..ObjectMeta::default() + }, + rules: Some(vec![PolicyRule { + api_groups: Some(vec!["security.openshift.io".to_string()]), + resources: Some(vec!["securitycontextconstraints".to_string()]), + resource_names: Some(vec!["hostnetwork".to_string()]), + verbs: vec!["use".to_string()], + ..PolicyRule::default() + }]), + ..Role::default() + }; + + // RoleBinding + let role_binding = RoleBinding { + metadata: ObjectMeta { + name: Some("node-status-querier-scc-binding".to_string()), + namespace: Some(namespace_name.clone()), + ..ObjectMeta::default() + }, + subjects: Some(vec![Subject { + kind: "ServiceAccount".to_string(), + name: service_account_name.clone(), + namespace: Some(namespace_name.clone()), + ..Subject::default() + }]), + role_ref: RoleRef { + api_group: "rbac.authorization.k8s.io".to_string(), + kind: "Role".to_string(), + name: "allow-hostnetwork-scc".to_string(), + }, + }; + + // ClusterRoleBinding + let cluster_role_binding = ClusterRoleBinding { + metadata: ObjectMeta { + name: Some("read-nodes-binding".to_string()), + ..ObjectMeta::default() + }, + subjects: Some(vec![Subject { + kind: "ServiceAccount".to_string(), + name: service_account_name, + namespace: Some(namespace_name.clone()), + ..Subject::default() + }]), + role_ref: RoleRef { + api_group: "rbac.authorization.k8s.io".to_string(), + kind: "ClusterRole".to_string(), + name: "node-healthcheck-role".to_string(), + }, + }; + + K8sResourceScore::single(namespace, None).interpret(inventory, topology); + /* + K8sResourceScore::single(service_account, Some(namespace_name.clone())) + .interpret(inventory, topology); + K8sResourceScore::single(cluster_role, None).interpret(inventory, topology); + K8sResourceScore::single(role, Some(namespace_name.clone())) + .interpret(inventory, topology); + K8sResourceScore::single(role_binding, Some(namespace_name.clone())) + .interpret(inventory, topology); + K8sResourceScore::single(cluster_role_binding, None).interpret(inventory, topology); + */ + + Ok(Outcome::success("Harmony node health successfully deployed".to_string())) + } + + fn get_name(&self) -> InterpretName { + InterpretName::Custom("NodeHealthDeployment") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} -- 2.39.5 From a25ca86bdf809f8a5cb79b051a83f9dbb6215dc4 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 4 Mar 2026 08:21:08 -0500 Subject: [PATCH 2/5] wip: happy path is working --- examples/node_health/src/main.rs | 4 +- harmony/src/modules/node_health/mod.rs | 129 +++++++++++++++++++++---- 2 files changed, 111 insertions(+), 22 deletions(-) diff --git a/examples/node_health/src/main.rs b/examples/node_health/src/main.rs index 2704336b..9b524cfa 100644 --- a/examples/node_health/src/main.rs +++ b/examples/node_health/src/main.rs @@ -1,9 +1,9 @@ -use harmony::{inventory::Inventory, modules::node_health::NodeHealthDeploymentScore, topology::K8sAnywhereTopology}; +use harmony::{inventory::Inventory, modules::node_health::NodeHealthScore, topology::K8sAnywhereTopology}; #[tokio::main] async fn main() { - let node_health = NodeHealthDeploymentScore {}; + let node_health = NodeHealthScore {}; harmony_cli::run( Inventory::autoload(), diff --git a/harmony/src/modules/node_health/mod.rs b/harmony/src/modules/node_health/mod.rs index 3bda30e1..cdacefc3 100644 --- a/harmony/src/modules/node_health/mod.rs +++ b/harmony/src/modules/node_health/mod.rs @@ -1,9 +1,15 @@ use async_trait::async_trait; use harmony_types::id::Id; use k8s_openapi::api::{ - core::v1::{Namespace, ServiceAccount}, + apps::v1::{DaemonSet, DaemonSetSpec}, + core::v1::{ + Container, ContainerPort, EnvVar, EnvVarSource, Namespace, ObjectFieldSelector, PodSpec, + PodTemplateSpec, ResourceRequirements, ServiceAccount, Toleration, + }, rbac::v1::{ClusterRole, ClusterRoleBinding, PolicyRule, Role, RoleBinding, RoleRef, Subject}, }; +use k8s_openapi::apimachinery::pkg::api::resource::Quantity; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; use kube::api::ObjectMeta; use serde::Serialize; use std::collections::BTreeMap; @@ -18,26 +24,26 @@ use crate::{ }; #[derive(Clone, Debug, Serialize)] -pub struct NodeHealthDeploymentScore {} +pub struct NodeHealthScore {} -impl Score for NodeHealthDeploymentScore { +impl Score for NodeHealthScore { fn name(&self) -> String { - todo!() + format!("NodeHealthScore") } #[doc(hidden)] fn create_interpret(&self) -> Box> { - Box::new(NodeHealthDeploymentInterpret {}) + Box::new(NodeHealthInterpret {}) } } #[derive(Debug, Clone)] -pub struct NodeHealthDeploymentInterpret { +pub struct NodeHealthInterpret { // config: NodeHealthDeploymentConfig, } #[async_trait] -impl Interpret for NodeHealthDeploymentInterpret { +impl Interpret for NodeHealthInterpret { async fn execute( &self, inventory: &Inventory, @@ -129,7 +135,7 @@ impl Interpret for NodeHealthDeploymentInterpret { }, subjects: Some(vec![Subject { kind: "ServiceAccount".to_string(), - name: service_account_name, + name: service_account_name.clone(), namespace: Some(namespace_name.clone()), ..Subject::default() }]), @@ -140,19 +146,102 @@ impl Interpret for NodeHealthDeploymentInterpret { }, }; - K8sResourceScore::single(namespace, None).interpret(inventory, topology); - /* - K8sResourceScore::single(service_account, Some(namespace_name.clone())) - .interpret(inventory, topology); - K8sResourceScore::single(cluster_role, None).interpret(inventory, topology); - K8sResourceScore::single(role, Some(namespace_name.clone())) - .interpret(inventory, topology); - K8sResourceScore::single(role_binding, Some(namespace_name.clone())) - .interpret(inventory, topology); - K8sResourceScore::single(cluster_role_binding, None).interpret(inventory, topology); - */ + // DaemonSet + let mut daemonset_labels = BTreeMap::new(); + daemonset_labels.insert("app".to_string(), "node-healthcheck".to_string()); - Ok(Outcome::success("Harmony node health successfully deployed".to_string())) + let daemon_set = DaemonSet { + metadata: ObjectMeta { + name: Some("node-healthcheck".to_string()), + namespace: Some(namespace_name.clone()), + labels: Some(daemonset_labels.clone()), + ..ObjectMeta::default() + }, + spec: Some(DaemonSetSpec { + selector: LabelSelector { + match_labels: Some(daemonset_labels.clone()), + ..LabelSelector::default() + }, + template: PodTemplateSpec { + metadata: Some(ObjectMeta { + labels: Some(daemonset_labels), + ..ObjectMeta::default() + }), + spec: Some(PodSpec { + service_account_name: Some(service_account_name.clone()), + host_network: Some(true), + tolerations: Some(vec![Toleration { + operator: Some("Exists".to_string()), + ..Toleration::default() + }]), + containers: vec![Container { + name: "checker".to_string(), + image: Some( + "hub.nationtech.io/harmony/harmony-node-readiness-endpoint:latest" + .to_string(), + ), + env: Some(vec![EnvVar { + name: "NODE_NAME".to_string(), + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + field_path: "spec.nodeName".to_string(), + ..ObjectFieldSelector::default() + }), + ..EnvVarSource::default() + }), + ..EnvVar::default() + }]), + ports: Some(vec![ContainerPort { + container_port: 25001, + host_port: Some(25001), + name: Some("health-port".to_string()), + ..ContainerPort::default() + }]), + resources: Some(ResourceRequirements { + requests: Some({ + let mut requests = BTreeMap::new(); + requests.insert("cpu".to_string(), Quantity("10m".to_string())); + requests + .insert("memory".to_string(), Quantity("50Mi".to_string())); + requests + }), + ..ResourceRequirements::default() + }), + ..Container::default() + }], + ..PodSpec::default() + }), + }, + ..DaemonSetSpec::default() + }), + ..DaemonSet::default() + }; + + K8sResourceScore::single(namespace, None) + .interpret(inventory, topology) + .await?; + K8sResourceScore::single(service_account, Some(namespace_name.clone())) + .interpret(inventory, topology) + .await?; + K8sResourceScore::single(cluster_role, None) + .interpret(inventory, topology) + .await?; + K8sResourceScore::single(role, Some(namespace_name.clone())) + .interpret(inventory, topology) + .await?; + K8sResourceScore::single(role_binding, Some(namespace_name.clone())) + .interpret(inventory, topology) + .await?; + K8sResourceScore::single(cluster_role_binding, None) + .interpret(inventory, topology) + .await?; + K8sResourceScore::single(daemon_set, Some(namespace_name.clone())) + .interpret(inventory, topology) + .await?; + + Ok(Outcome::success( + "Harmony node health successfully deployed".to_string(), + )) } fn get_name(&self) -> InterpretName { -- 2.39.5 From d9357adad34037fc55158900a6c905048f0f4c67 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 4 Mar 2026 09:28:32 -0500 Subject: [PATCH 3/5] format code, fix interpert name --- Cargo.lock | 20 ++++++++++++++++++++ examples/node_health/src/main.rs | 5 +++-- harmony/src/modules/mod.rs | 2 +- harmony/src/modules/node_health/mod.rs | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bf169cc..551c619f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,6 +3700,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "json-prompt" +version = "0.1.0" +dependencies = [ + "brocade", + "cidr", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_secret", + "harmony_secret_derive", + "harmony_types", + "log", + "schemars 0.8.22", + "serde", + "tokio", + "url", +] + [[package]] name = "jsonpath-rust" version = "0.7.5" diff --git a/examples/node_health/src/main.rs b/examples/node_health/src/main.rs index 9b524cfa..6f5bb784 100644 --- a/examples/node_health/src/main.rs +++ b/examples/node_health/src/main.rs @@ -1,5 +1,6 @@ -use harmony::{inventory::Inventory, modules::node_health::NodeHealthScore, topology::K8sAnywhereTopology}; - +use harmony::{ + inventory::Inventory, modules::node_health::NodeHealthScore, topology::K8sAnywhereTopology, +}; #[tokio::main] async fn main() { diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index b8234951..074254db 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -15,6 +15,7 @@ pub mod load_balancer; pub mod monitoring; pub mod nats; pub mod network; +pub mod node_health; pub mod okd; pub mod opnsense; pub mod postgresql; @@ -22,4 +23,3 @@ pub mod prometheus; pub mod storage; pub mod tenant; pub mod tftp; -pub mod node_health; diff --git a/harmony/src/modules/node_health/mod.rs b/harmony/src/modules/node_health/mod.rs index cdacefc3..9d27e096 100644 --- a/harmony/src/modules/node_health/mod.rs +++ b/harmony/src/modules/node_health/mod.rs @@ -245,7 +245,7 @@ impl Interpret for NodeHealthInterpret { } fn get_name(&self) -> InterpretName { - InterpretName::Custom("NodeHealthDeployment") + InterpretName::Custom("NodeHealth") } fn get_version(&self) -> Version { -- 2.39.5 From 6bb33c58452c4534e73ec7f88606c238389cdd93 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 4 Mar 2026 09:29:49 -0500 Subject: [PATCH 4/5] remove useless comment --- harmony/src/domain/topology/k8s/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/harmony/src/domain/topology/k8s/mod.rs b/harmony/src/domain/topology/k8s/mod.rs index e60b4c25..f5171add 100644 --- a/harmony/src/domain/topology/k8s/mod.rs +++ b/harmony/src/domain/topology/k8s/mod.rs @@ -983,7 +983,6 @@ impl K8sClient { pub async fn apply_many(&self, resource: &[K], ns: Option<&str>) -> Result, Error> where K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize, - // ::Scope: ApplyStrategy, ::DynamicType: Default, { let mut result = Vec::new(); -- 2.39.5 From 20172a7801d5e22e814c219cb267a92e6228509f Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 4 Mar 2026 09:31:02 -0500 Subject: [PATCH 5/5] removing another useless commented line --- harmony/src/modules/node_health/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/harmony/src/modules/node_health/mod.rs b/harmony/src/modules/node_health/mod.rs index 9d27e096..5a6bcce7 100644 --- a/harmony/src/modules/node_health/mod.rs +++ b/harmony/src/modules/node_health/mod.rs @@ -38,9 +38,7 @@ impl Score for NodeHealthScore { } #[derive(Debug, Clone)] -pub struct NodeHealthInterpret { - // config: NodeHealthDeploymentConfig, -} +pub struct NodeHealthInterpret {} #[async_trait] impl Interpret for NodeHealthInterpret { -- 2.39.5