working on ntfy score

This commit is contained in:
tahahawa 2025-06-27 02:28:44 -04:00
parent 80e209d333
commit 6a29969c7f
14 changed files with 797 additions and 231 deletions

729
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -24,19 +24,31 @@ log = "0.4"
env_logger = "0.11" env_logger = "0.11"
derive-new = "0.7" derive-new = "0.7"
async-trait = "0.1" async-trait = "0.1"
tokio = { version = "1.40", features = ["io-std", "fs", "macros", "rt-multi-thread"] } tokio = { version = "1.40", features = [
"io-std",
"fs",
"macros",
"rt-multi-thread",
] }
cidr = { features = ["serde"], version = "0.2" } cidr = { features = ["serde"], version = "0.2" }
russh = "0.45" russh = "0.45"
russh-keys = "0.45" russh-keys = "0.45"
rand = "0.8" rand = "0.8"
url = "2.5" url = "2.5"
kube = "0.98" kube = { version = "0.98", features = [
"config",
"client",
"runtime",
"rustls-tls",
"ws",
"jsonpatch",
] }
k8s-openapi = { version = "0.24", features = ["v1_30"] } k8s-openapi = { version = "0.24", features = ["v1_30"] }
serde_yaml = "0.9" serde_yaml = "0.9"
serde-value = "0.7" serde-value = "0.7"
http = "1.2" http = "1.2"
inquire = "0.7" inquire = "0.7"
convert_case = "0.8" convert_case = "0.8"
chrono = "0.4" chrono = "0.4"
similar = "2" similar = "2"
uuid = { version = "1.11", features = [ "v4", "fast-rng", "macro-diagnostics" ] } uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] }

View File

@ -15,7 +15,7 @@ log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }
url = { workspace = true } url = { workspace = true }
kube = "0.98.0" kube = "0.98.0"
k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] } k8s-openapi = { version = "0.25.0", features = ["v1_30"] }
http = "1.2.0" http = "1.2.0"
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
inquire.workspace = true inquire.workspace = true

12
examples/ntfy/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "example-ntfy"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
[dependencies]
harmony = { version = "0.1.0", path = "../../harmony" }
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
tokio.workspace = true
url.workspace = true

43
examples/ntfy/src/main.rs Normal file
View File

@ -0,0 +1,43 @@
use std::{collections::HashMap, str::FromStr};
use harmony::{
inventory::Inventory,
maestro::Maestro,
modules::helm::chart::{HelmChartScore, HelmRepository, NonBlankString},
topology::K8sAnywhereTopology,
};
#[tokio::main]
async fn main() {
let mut ntfy_overrides: HashMap<NonBlankString, String> = HashMap::new();
ntfy_overrides.insert(
NonBlankString::from_str("image.tag").unwrap(),
"v2.12.0".to_string(),
);
let ntfy_chart = HelmChartScore {
namespace: Some(NonBlankString::from_str("monitoring").unwrap()),
release_name: NonBlankString::from_str("ntfy").unwrap(),
chart_name: NonBlankString::from_str("sarab97/ntfy").unwrap(),
chart_version: Some(NonBlankString::from_str("0.1.7").unwrap()),
values_overrides: Some(ntfy_overrides),
values_yaml: None,
create_namespace: true,
install_only: false,
repository: Some(HelmRepository::new(
"sarab97".to_string(),
url::Url::parse("https://charts.sarabsingh.com").unwrap(),
true,
)),
};
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
)
.await
.unwrap();
maestro.register_all(vec![Box::new(ntfy_chart)]);
harmony_cli::init(maestro, None).await.unwrap();
}

View File

@ -54,3 +54,4 @@ fqdn = { version = "0.4.6", features = [
temp-dir = "0.1.14" temp-dir = "0.1.14"
dyn-clone = "1.0.19" dyn-clone = "1.0.19"
similar.workspace = true similar.workspace = true
futures-util = "0.3.31"

View File

@ -1,14 +1,28 @@
use derive_new::new; use derive_new::new;
use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope}; use futures_util::TryStreamExt;
use k8s_openapi::{
ClusterResourceScope, NamespaceResourceScope,
api::{apps::v1::Deployment, core::v1::Pod},
};
use kube::runtime::conditions;
use kube::runtime::wait::{Condition, await_condition};
use kube::{ use kube::{
Api, Client, Config, Error, Resource, Client, Config, Error, Resource,
api::{Patch, PatchParams}, api::{
Api, AttachParams, AttachedProcess, DeleteParams, ListParams, Patch, PatchParams,
PostParams, ResourceExt, WatchEvent, WatchParams,
},
config::{KubeConfigOptions, Kubeconfig}, config::{KubeConfigOptions, Kubeconfig},
core::ErrorResponse, core::ErrorResponse,
runtime::{
WatchStreamExt, metadata_watcher,
reflector::Lookup,
watcher::{self, watch_object},
},
}; };
use log::{debug, error, trace}; use log::{debug, error, trace};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use similar::TextDiff; use similar::{DiffableStr, TextDiff};
#[derive(new)] #[derive(new)]
pub struct K8sClient { pub struct K8sClient {
@ -22,6 +36,65 @@ impl K8sClient {
}) })
} }
pub async fn wait_until_deployment_ready(
&self,
name: String,
namespace: Option<&str>,
) -> Result<(), Error> {
let api: Api<Deployment>;
if let Some(ns) = namespace {
api = Api::namespaced(self.client.clone(), ns);
} else {
api = Api::default_namespaced(self.client.clone());
}
// need to upgrade to latest kube-rs version https://docs.rs/kube-runtime/latest/kube_runtime/wait/conditions/fn.is_deployment_completed.html
let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed());
let _ = tokio::time::timeout(std::time::Duration::from_secs(300), establish).await?;
Ok(())
}
pub async fn exec_pod(
&self,
name: String,
namespace: Option<&str>,
command: Vec<String>,
) -> Result<(), String> {
let api: Api<Pod>;
if let Some(ns) = namespace {
api = Api::namespaced(self.client.clone(), ns);
} else {
api = Api::default_namespaced(self.client.clone());
}
let pod_list = api
.list(&ListParams::default().labels(format!("app.kubernetes.io/name={name}").as_str()))
.await
.expect("couldn't get list of pods");
if pod_list.items.len() > 1 {
return Err("too many pods".into());
} else {
api.exec(
pod_list
.items
.first()
.expect("couldn't get pod")
.name()
.expect("couldn't get pod name")
.into_owned()
.as_str(),
command,
&AttachParams::default(),
)
.await;
}
Ok(())
}
/// Apply a resource in namespace /// Apply a resource in namespace
/// ///
/// See `kubectl apply` for more information on the expected behavior of this function /// See `kubectl apply` for more information on the expected behavior of this function

View File

@ -1,9 +1,6 @@
use serde::Serialize; use serde::Serialize;
use crate::modules::monitoring::{ use crate::modules::monitoring::kube_prometheus::types::{AlertManagerAdditionalPromRules, AlertManagerChannelConfig};
alert_rule::prometheus_alert_rule::AlertManagerRuleGroup,
kube_prometheus::types::{AlertManagerAdditionalPromRules, AlertManagerChannelConfig},
};
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct KubePrometheusConfig { pub struct KubePrometheusConfig {

View File

@ -1,3 +1,4 @@
pub mod alert_channel; pub mod alert_channel;
pub mod alert_rule; pub mod alert_rule;
pub mod kube_prometheus; pub mod kube_prometheus;
pub mod ntfy;

View File

@ -0,0 +1,6 @@
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct NtfyConfig {
pub namespace: String,
}

View File

@ -0,0 +1,2 @@
pub mod config;
pub mod ntfy_helm_chart;

View File

@ -0,0 +1,91 @@
use non_blank_string_rs::NonBlankString;
use std::{
str::FromStr,
sync::{Arc, Mutex},
};
use crate::modules::{
helm::chart::{HelmChartScore, HelmRepository},
monitoring::ntfy::helm::config::NtfyConfig,
};
pub fn ntfy_helm_chart_score(config: Arc<Mutex<NtfyConfig>>) -> HelmChartScore {
let config = config.lock().unwrap();
let values = format!(
r#"
replicaCount: 1
image:
repository: binwiederhier/ntfy
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "v2.12.0"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
# annotations:
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
# name: ""
service:
type: ClusterIP
port: 80
ingress:
enabled: false
# annotations:
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: ntfy.host.com
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
autoscaling:
enabled: false
config:
enabled: true
data:
# base-url: "https://ntfy.something.com"
auth-file: "/var/cache/ntfy/user.db"
auth-default-access: "deny-all"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
behind-proxy: true
# web-root: "disable"
enable-signup: false
enable-login: "true"
persistence:
enabled: true
size: 200Mi
"#,
);
HelmChartScore {
namespace: Some(NonBlankString::from_str(&config.namespace).unwrap()),
release_name: NonBlankString::from_str("ntfy").unwrap(),
chart_name: NonBlankString::from_str("sarab97/ntfy").unwrap(),
chart_version: Some(NonBlankString::from_str("0.1.7").unwrap()),
values_overrides: None,
values_yaml: Some(values.to_string()),
create_namespace: true,
install_only: false,
repository: Some(HelmRepository::new(
"sarab97".to_string(),
url::Url::parse("https://charts.sarabsingh.com").unwrap(),
true,
)),
}
}

View File

@ -0,0 +1,2 @@
pub mod helm;
pub mod ntfy;

View File

@ -0,0 +1,33 @@
use std::sync::{Arc, Mutex};
use crate::{
interpret::{InterpretError, Outcome},
inventory::Inventory,
modules::monitoring::ntfy::helm::{config::NtfyConfig, ntfy_helm_chart::ntfy_helm_chart_score},
score::Score,
topology::{HelmCommand, K8sclient, Topology},
};
pub struct Ntfy {
pub config: Arc<Mutex<NtfyConfig>>,
}
impl Ntfy {
async fn install_ntfy<T: Topology + HelmCommand + K8sclient + Send + Sync>(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let result = ntfy_helm_chart_score(self.config.clone())
.create_interpret()
.execute(inventory, topology)
.await;
let client = topology.k8s_client().await.expect("couldn't get k8s client");
client.wait_until_deployment_ready("ntfy", self.config.get_mut().expect("couldn't get config").namespace);
client.
result
}
}