diff --git a/Cargo.lock b/Cargo.lock index 6cc09c6e..551c619f 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" @@ -3687,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/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..6f5bb784 --- /dev/null +++ b/examples/node_health/src/main.rs @@ -0,0 +1,17 @@ +use harmony::{ + inventory::Inventory, modules::node_health::NodeHealthScore, topology::K8sAnywhereTopology, +}; + +#[tokio::main] +async fn main() { + let node_health = NodeHealthScore {}; + + 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..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(); 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..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; diff --git a/harmony/src/modules/node_health/mod.rs b/harmony/src/modules/node_health/mod.rs new file mode 100644 index 00000000..5a6bcce7 --- /dev/null +++ b/harmony/src/modules/node_health/mod.rs @@ -0,0 +1,260 @@ +use async_trait::async_trait; +use harmony_types::id::Id; +use k8s_openapi::api::{ + 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; + +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 NodeHealthScore {} + +impl Score for NodeHealthScore { + fn name(&self) -> String { + format!("NodeHealthScore") + } + + #[doc(hidden)] + fn create_interpret(&self) -> Box> { + Box::new(NodeHealthInterpret {}) + } +} + +#[derive(Debug, Clone)] +pub struct NodeHealthInterpret {} + +#[async_trait] +impl Interpret for NodeHealthInterpret { + 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.clone(), + 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(), + }, + }; + + // DaemonSet + let mut daemonset_labels = BTreeMap::new(); + daemonset_labels.insert("app".to_string(), "node-healthcheck".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 { + InterpretName::Custom("NodeHealth") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +}