diff --git a/Cargo.lock b/Cargo.lock index d6aa025..521d6d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1607,6 +1607,8 @@ dependencies = [ "clap", "reqwest", "rustls", + "serde", + "serde_json", "thiserror", "tokio", "tracing", diff --git a/charts/cmk-rustik/templates/metrics-cache/deployment.yaml b/charts/cmk-rustik/templates/metrics-cache/deployment.yaml index 17b5b0b..56d380e 100644 --- a/charts/cmk-rustik/templates/metrics-cache/deployment.yaml +++ b/charts/cmk-rustik/templates/metrics-cache/deployment.yaml @@ -55,6 +55,7 @@ spec: - --cluster-host-name={{ required "clusterHostName is required (it must exactly match the Checkmk host representing this cluster)" .Values.clusterHostName }} - --kubelet-stats-cache-ttl={{ .Values.metricsCache.kubeletStatsCacheTtl }} - --system-agent-cache-ttl={{ .Values.metricsCache.systemAgentCacheTtl }} + - --kubelet-health-cache-ttl={{ .Values.metricsCache.kubeletHealthCacheTtl }} {{- if .Values.hostLabels.importAllAnnotations }} {{- if .Values.hostLabels.importKeyPattern }} {{ fail "hostLabels.importKeyPattern should not be set while hostLabels.importAllAnnotations is true" }} diff --git a/charts/cmk-rustik/values.yaml b/charts/cmk-rustik/values.yaml index b7f43b4..0c5cfb6 100644 --- a/charts/cmk-rustik/values.yaml +++ b/charts/cmk-rustik/values.yaml @@ -103,6 +103,10 @@ metricsCache: # persisted in the cache before expiring. systemAgentCacheTtl: 120 + # kubeletHealthCacheTtl is how long (seconds) kubelet health entries are + # persisted in the cache before expiring. + kubeletHealthCacheTtl: 120 + # extraVolumes is volume definitions to add to the Pod in the Deployment. extraVolumes: [] # extraVolumeMounts is volume mounts to mount in the metrics-cache container. diff --git a/metrics-cache/src/cli_args.rs b/metrics-cache/src/cli_args.rs index 7d773d0..d8cb4a4 100644 --- a/metrics-cache/src/cli_args.rs +++ b/metrics-cache/src/cli_args.rs @@ -94,6 +94,14 @@ pub struct CliArgs { )] pub system_agent_cache_ttl: Duration, + /// How long (seconds) kubelet health entries are persisted in the cache + #[arg( + long, + value_parser = parse_duration_secs, + default_value = "120" + )] + pub kubelet_health_cache_ttl: Duration, + /// How verbose to log #[arg( short = 'l', @@ -418,6 +426,7 @@ mod tests { let args = parse(&[]).expect("minimal args should parse"); assert_eq!(args.kubelet_stats_cache_ttl, Duration::from_secs(120)); assert_eq!(args.system_agent_cache_ttl, Duration::from_secs(120)); + assert_eq!(args.kubelet_health_cache_ttl, Duration::from_secs(120)); } #[test] @@ -427,10 +436,13 @@ mod tests { "30", "--system-agent-cache-ttl", "60", + "--kubelet-health-cache-ttl", + "90", ]) .expect("cache TTL flags should parse"); assert_eq!(args.kubelet_stats_cache_ttl, Duration::from_secs(30)); assert_eq!(args.system_agent_cache_ttl, Duration::from_secs(60)); + assert_eq!(args.kubelet_health_cache_ttl, Duration::from_secs(90)); } #[test] diff --git a/metrics-cache/src/handlers/debug.rs b/metrics-cache/src/handlers/debug.rs index 9d5ce31..816784d 100644 --- a/metrics-cache/src/handlers/debug.rs +++ b/metrics-cache/src/handlers/debug.rs @@ -12,6 +12,7 @@ pub async fn get(State(state): State>) -> Result>, @@ -26,6 +26,22 @@ pub async fn kubelet_stats_summary( Json("ok".to_string()) } +pub async fn kubelet_health( + State(state): State>, + Path(node_name): Path, + Json(health): Json, +) -> Json { + let ingestion = MetricsFetcherIngestion { + received_at: Instant::now(), + payload: health, + }; + state + .kubelet_health_cache + .insert(node_name, Arc::new(ingestion)) + .await; + Json("ok".to_string()) +} + /// Store the raw output of a machine-level agent (currently only Linux's /// `check_mk_agent`) for a node as-is, keyed by node name. No /// parsing/validation is done here or by the caller. Kept as [`Bytes`] diff --git a/metrics-cache/src/handlers/mod.rs b/metrics-cache/src/handlers/mod.rs index 1b67476..464a850 100644 --- a/metrics-cache/src/handlers/mod.rs +++ b/metrics-cache/src/handlers/mod.rs @@ -36,6 +36,7 @@ pub fn ingest_app(state: AppState) -> Router { "/kubelet_stats_summary", post(ingest::kubelet_stats_summary), ) + .route("/kubelet_health/{node_name}", post(ingest::kubelet_health)) .route("/system_agent/{node_name}", post(ingest::system_agent)) .route_layer(middleware::from_fn_with_state( state.clone(), diff --git a/metrics-cache/src/ingest/kubelet_health.rs b/metrics-cache/src/ingest/kubelet_health.rs new file mode 100644 index 0000000..4fb73b5 --- /dev/null +++ b/metrics-cache/src/ingest/kubelet_health.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum KubeletHealth { + Response { status_code: u16, response: String }, + ConnectionError { message: String }, +} diff --git a/metrics-cache/src/ingest/mod.rs b/metrics-cache/src/ingest/mod.rs index 7b9e90b..2a3955e 100644 --- a/metrics-cache/src/ingest/mod.rs +++ b/metrics-cache/src/ingest/mod.rs @@ -1,6 +1,7 @@ use axum::body::Bytes; use std::time::Instant; +pub mod kubelet_health; pub mod kubelet_stats; pub mod reflectors; diff --git a/metrics-cache/src/piggyback/node.rs b/metrics-cache/src/piggyback/node.rs index 6a176b8..2b0845e 100644 --- a/metrics-cache/src/piggyback/node.rs +++ b/metrics-cache/src/piggyback/node.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use crate::host_settings::HostSettings; use crate::piggyback::{AggregationHost, Meta, PiggybackHost}; use crate::section::node::KubeNodeInfoV1; +use crate::section::node_kubelet::KubeNodeKubeletV1; use crate::section::writeable::{SectionError, WriteableSection}; use crate::snapshot::Snapshot; @@ -53,9 +54,16 @@ impl PiggybackHost for Node<'_> { fn emit(&self) -> Vec> { let me = self.meta.piggyback_hostname(&self.settings.cluster_name); let mut out = Vec::new(); + if let Some(kube_node_info_v1) = KubeNodeInfoV1::from_node(self.api, self.settings) { out.push(WriteableSection::of(&me, &kube_node_info_v1)); } + + if let Some(kube_node_kubelet_v1) = + KubeNodeKubeletV1::from_node(self.api, &self.snapshot.kubelet_health) + { + out.push(WriteableSection::of(&me, &kube_node_kubelet_v1)); + } if let Some(ingestion) = self.snapshot.system_agent_snapshot.get(self.meta.name) { out.push(Ok(WriteableSection::raw(&me, ingestion.payload.0.clone()))); } @@ -95,6 +103,7 @@ mod tests { let snapshot = Snapshot::new( state.stores, state.kubelet_stats_summary_cache, + state.kubelet_health_cache, state.system_agent_cache, ); let node = Node::new(&api, &snapshot, &host_settings).unwrap(); diff --git a/metrics-cache/src/push/mod.rs b/metrics-cache/src/push/mod.rs index d21cabd..d2a7013 100644 --- a/metrics-cache/src/push/mod.rs +++ b/metrics-cache/src/push/mod.rs @@ -48,6 +48,7 @@ async fn push_cycle( let snap = Snapshot::new( state.stores.clone(), state.kubelet_stats_summary_cache.clone(), + state.kubelet_health_cache.clone(), state.system_agent_cache.clone(), ); let sections = emit_all(&snap, &state.host_settings); diff --git a/metrics-cache/src/section/mod.rs b/metrics-cache/src/section/mod.rs index 861f4a6..a0ba04a 100644 --- a/metrics-cache/src/section/mod.rs +++ b/metrics-cache/src/section/mod.rs @@ -2,6 +2,7 @@ pub mod common; pub mod cronjob; pub mod namespace; pub mod node; +pub mod node_kubelet; pub mod performance; pub mod pod; pub mod pvc; diff --git a/metrics-cache/src/section/node_kubelet.rs b/metrics-cache/src/section/node_kubelet.rs new file mode 100644 index 0000000..2519971 --- /dev/null +++ b/metrics-cache/src/section/node_kubelet.rs @@ -0,0 +1,91 @@ +use k8s_openapi::api::core::v1::Node; +use serde::Serialize; + +use crate::ingest::kubelet_health::KubeletHealth; +use crate::section::Section; +use crate::snapshot::kubelet_health::KubeletHealths; + +/// Kubelet version and health. (`kube_node_kubelet_v1`) +#[derive(Serialize)] +pub(crate) struct KubeNodeKubeletV1<'a> { + pub version: &'a str, + pub health: &'a KubeletHealth, +} + +impl<'a> KubeNodeKubeletV1<'a> { + pub fn from_node( + node: &'a Node, + kubelet_healths: &'a KubeletHealths, + ) -> Option> { + let node_info = node.status.as_ref()?.node_info.as_ref()?; + let node_name = node.metadata.name.as_deref()?; + let health = kubelet_healths.get(node_name)?; + + Some(KubeNodeKubeletV1 { + version: &node_info.kubelet_version, + health, + }) + } +} + +impl Section for KubeNodeKubeletV1<'_> { + const NAME: &'static str = "kube_node_kubelet_v1"; +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Instant; + + use super::*; + + use crate::ingest::MetricsFetcherIngestion; + use crate::test_support::*; + + fn kubelet_healths(entries: &[(&str, KubeletHealth)]) -> KubeletHealths { + KubeletHealths { + by_node: entries + .iter() + .map(|(name, health)| { + let ingestion = MetricsFetcherIngestion { + received_at: Instant::now(), + payload: health.clone(), + }; + (name.to_string(), Arc::new(ingestion)) + }) + .collect(), + } + } + + #[test] + fn kube_node_kubelet_v1_response() { + let node = node_prefilled("node01"); + let healths = kubelet_healths(&[( + "node01", + KubeletHealth::Response { + status_code: 200, + response: "ok".to_string(), + }, + )]); + insta::assert_json_snapshot!(KubeNodeKubeletV1::from_node(&node, &healths)); + } + + #[test] + fn kube_node_kubelet_v1_connection_error() { + let node = node_prefilled("node01"); + let healths = kubelet_healths(&[( + "node01", + KubeletHealth::ConnectionError { + message: "connection refused".to_string(), + }, + )]); + insta::assert_json_snapshot!(KubeNodeKubeletV1::from_node(&node, &healths)); + } + + #[test] + fn kube_node_kubelet_v1_no_health_reported() { + let node = node_prefilled("node01"); + let healths = kubelet_healths(&[]); + assert!(KubeNodeKubeletV1::from_node(&node, &healths).is_none()); + } +} diff --git a/metrics-cache/src/section/snapshots/metrics_cache__section__node_kubelet__tests__kube_node_kubelet_v1_connection_error.snap b/metrics-cache/src/section/snapshots/metrics_cache__section__node_kubelet__tests__kube_node_kubelet_v1_connection_error.snap new file mode 100644 index 0000000..e39057e --- /dev/null +++ b/metrics-cache/src/section/snapshots/metrics_cache__section__node_kubelet__tests__kube_node_kubelet_v1_connection_error.snap @@ -0,0 +1,10 @@ +--- +source: metrics-cache/src/section/node_kubelet.rs +expression: "KubeNodeKubeletV1::from_node(&node, &healths)" +--- +{ + "version": "v1.34.0", + "health": { + "message": "connection refused" + } +} diff --git a/metrics-cache/src/section/snapshots/metrics_cache__section__node_kubelet__tests__kube_node_kubelet_v1_response.snap b/metrics-cache/src/section/snapshots/metrics_cache__section__node_kubelet__tests__kube_node_kubelet_v1_response.snap new file mode 100644 index 0000000..33d3ee9 --- /dev/null +++ b/metrics-cache/src/section/snapshots/metrics_cache__section__node_kubelet__tests__kube_node_kubelet_v1_response.snap @@ -0,0 +1,11 @@ +--- +source: metrics-cache/src/section/node_kubelet.rs +expression: "KubeNodeKubeletV1::from_node(&node, &healths)" +--- +{ + "version": "v1.34.0", + "health": { + "status_code": 200, + "response": "ok" + } +} diff --git a/metrics-cache/src/snapshot/kubelet_health.rs b/metrics-cache/src/snapshot/kubelet_health.rs new file mode 100644 index 0000000..483e303 --- /dev/null +++ b/metrics-cache/src/snapshot/kubelet_health.rs @@ -0,0 +1,30 @@ +use moka::future::Cache; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::ingest::MetricsFetcherIngestion; +use crate::ingest::kubelet_health::KubeletHealth; + +/// Kubelet `/healthz` results pushed by metrics-fetcher, indexed by node name. +#[derive(Debug)] +pub struct KubeletHealths { + pub by_node: HashMap>>, +} + +impl KubeletHealths { + pub fn from_cache(cache: &Cache>>) -> Self { + Self { + by_node: cache + .iter() + .map(|(name, ingestion)| (name.to_string(), ingestion)) + .collect(), + } + } + + /// Get the Kubelet `/healthz` result last reported for a node, if any. + pub fn get(&self, node_name: &str) -> Option<&KubeletHealth> { + self.by_node + .get(node_name) + .map(|ingestion| &ingestion.payload) + } +} diff --git a/metrics-cache/src/snapshot/mod.rs b/metrics-cache/src/snapshot/mod.rs index 7615441..02fa6f2 100644 --- a/metrics-cache/src/snapshot/mod.rs +++ b/metrics-cache/src/snapshot/mod.rs @@ -1,4 +1,5 @@ pub mod indexes; +pub mod kubelet_health; pub mod metric_tables; pub mod owner_graph; pub mod self_health; @@ -11,9 +12,11 @@ use std::time::Instant; use crate::ingest::MetricsFetcherIngestion; use crate::ingest::SystemAgentOutput; +use crate::ingest::kubelet_health::KubeletHealth; use crate::ingest::kubelet_stats::StatsSummary; use crate::ingest::reflectors::{FrozenStores, Stores}; use crate::snapshot::indexes::Indexes; +use crate::snapshot::kubelet_health::KubeletHealths; use crate::snapshot::metric_tables::MetricTables; use crate::snapshot::owner_graph::OwnerGraph; use crate::snapshot::self_health::SelfHealth; @@ -40,6 +43,7 @@ pub struct Snapshot { pub metrics: MetricTables, pub indexes: Indexes, pub self_health: SelfHealth, + pub kubelet_health: KubeletHealths, pub system_agent_snapshot: HashMap>>, } @@ -49,6 +53,7 @@ impl Snapshot { pub fn new( stores: Stores, kubelet_stats_summary_cache: Cache>>, + kubelet_health_cache: Cache>>, system_agent_cache: Cache>>, ) -> Self { let instant = Instant::now(); @@ -63,6 +68,7 @@ impl Snapshot { reflector_healths, &kubelet_stats_summary_cache, ); + let kubelet_health = KubeletHealths::from_cache(&kubelet_health_cache); let system_agent_snapshot: HashMap< String, Arc>, @@ -77,6 +83,7 @@ impl Snapshot { metrics, indexes, self_health, + kubelet_health, system_agent_snapshot, } } diff --git a/metrics-cache/src/state.rs b/metrics-cache/src/state.rs index 3110ce6..429b12b 100644 --- a/metrics-cache/src/state.rs +++ b/metrics-cache/src/state.rs @@ -10,6 +10,7 @@ use crate::error::Result; use crate::host_settings::{AlwaysEmitted, AnnotationKeyPattern, HostSettings}; use crate::ingest::MetricsFetcherIngestion; use crate::ingest::SystemAgentOutput; +use crate::ingest::kubelet_health::KubeletHealth; use crate::ingest::kubelet_stats::StatsSummary; use crate::ingest::reflectors::Stores; @@ -23,6 +24,7 @@ pub struct AppState { pub reader_allowlist: Vec, pub writer_allowlist: Vec, pub kubelet_stats_summary_cache: Cache>>, + pub kubelet_health_cache: Cache>>, pub system_agent_cache: Cache>>, pub host_settings: Arc, } @@ -50,6 +52,10 @@ impl AppState { .time_to_live(args.kubelet_stats_cache_ttl) .max_capacity(MAX_SUPPORTED_KUBERNETES_NODES) .build(), + kubelet_health_cache: Cache::builder() + .time_to_live(args.kubelet_health_cache_ttl) + .max_capacity(MAX_SUPPORTED_KUBERNETES_NODES) + .build(), system_agent_cache: Cache::builder() .time_to_live(args.system_agent_cache_ttl) .max_capacity(MAX_SUPPORTED_KUBERNETES_NODES) @@ -108,6 +114,10 @@ pub mod tests { .time_to_live(Duration::from_secs(120)) .max_capacity(10000) .build(), + kubelet_health_cache: Cache::builder() + .time_to_live(Duration::from_secs(120)) + .max_capacity(10000) + .build(), system_agent_cache: Cache::builder() .time_to_live(Duration::from_secs(120)) .max_capacity(10000) diff --git a/metrics-fetcher/Cargo.toml b/metrics-fetcher/Cargo.toml index de2fa86..4942aa4 100644 --- a/metrics-fetcher/Cargo.toml +++ b/metrics-fetcher/Cargo.toml @@ -13,6 +13,8 @@ bytes = "1.11.1" clap = { workspace = true } reqwest = { version = "0.13.3", features = ["blocking", "query", "json", "rustls-no-provider", "http2", "system-proxy"], default-features = false } rustls = { workspace = true } +serde = { workspace = true } +serde_json = "1.0.150" thiserror = { workspace = true } tokio = { workspace = true, features = ["process"] } tracing = { workspace = true } diff --git a/metrics-fetcher/src/kubelet_health.rs b/metrics-fetcher/src/kubelet_health.rs new file mode 100644 index 0000000..e1b6b99 --- /dev/null +++ b/metrics-fetcher/src/kubelet_health.rs @@ -0,0 +1,109 @@ +use reqwest::Client; +use serde::Serialize; +use std::sync::Arc; +use tracing::debug; + +use crate::cli_args::CliArgs; +use crate::error::{Error, Result}; +use crate::payload::Payload; +use crate::scraper::Scraper; + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub(crate) enum KubeletHealth { + Response { status_code: u16, response: String }, + ConnectionError { message: String }, +} + +pub(crate) struct KubeletHealthScraper { + scrape_client: Client, + relay_client: Client, + args: Arc, +} + +impl KubeletHealthScraper { + pub(crate) fn new(args: Arc, metrics_cache_client: Client) -> KubeletHealthScraper { + let scrape_client = Client::builder() + .danger_accept_invalid_certs(true) + .build() + .expect("Could not build scrape client for kubelet health"); + KubeletHealthScraper { + scrape_client, + relay_client: metrics_cache_client, + args, + } + } +} + +impl Scraper for KubeletHealthScraper { + fn relay_client(&self) -> Client { + self.relay_client.clone() + } + + fn args(&self) -> Arc { + self.args.clone() + } + + async fn scrape(&self) -> Result { + let node_ip = std::env::var("NODE_IP").map_err(|e| Error::EnvVar { + name: "NODE_IP".to_string(), + source: e, + })?; + let node_name = std::env::var("NODE_NAME").map_err(|e| Error::EnvVar { + name: "NODE_NAME".to_string(), + source: e, + })?; + let token = std::fs::read_to_string("/var/run/secrets/kubernetes.io/serviceaccount/token")?; + + debug!("fetching Kubelet /healthz"); + let health = match self + .scrape_client + .get(format!("https://{node_ip}:10250/healthz")) + .bearer_auth(token.trim()) + .send() + .await + { + Ok(response) => { + let status_code = response.status().as_u16(); + let response = response.text().await?; + KubeletHealth::Response { + status_code, + response, + } + } + Err(e) => KubeletHealth::ConnectionError { + message: e.to_string(), + }, + }; + + Ok(Payload::KubeletHealth { node_name, health }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kubelet_health_response_wire_shape() { + let health = KubeletHealth::Response { + status_code: 200, + response: "ok".to_string(), + }; + assert_eq!( + serde_json::to_string(&health).expect("KubeletHealth always serializes"), + r#"{"status_code":200,"response":"ok"}"# + ); + } + + #[test] + fn kubelet_health_connection_error_wire_shape() { + let health = KubeletHealth::ConnectionError { + message: "connection refused".to_string(), + }; + assert_eq!( + serde_json::to_string(&health).expect("KubeletHealth always serializes"), + r#"{"message":"connection refused"}"# + ); + } +} diff --git a/metrics-fetcher/src/main.rs b/metrics-fetcher/src/main.rs index 229f919..c73106e 100644 --- a/metrics-fetcher/src/main.rs +++ b/metrics-fetcher/src/main.rs @@ -1,5 +1,6 @@ mod cli_args; mod error; +mod kubelet_health; mod kubelet_stats_summary; mod linux_agent; mod payload; @@ -13,6 +14,7 @@ use std::sync::Arc; use tracing_subscriber::EnvFilter; use crate::cli_args::CliArgs; +use crate::kubelet_health::KubeletHealthScraper; use crate::kubelet_stats_summary::KubeletStatsSummaryScraper; use crate::linux_agent::LinuxAgentScraper; use crate::scraper::Scraper; @@ -45,11 +47,13 @@ async fn main() -> Result<()> { .build()?; let args = Arc::new(args); - let kubelet_stats_summary_scraper = - KubeletStatsSummaryScraper::new(args.clone(), metrics_cache_client.clone()); - let linux_agent_scraper = LinuxAgentScraper::new(args.clone(), metrics_cache_client); - let kubelet_scrape = tokio::spawn(kubelet_stats_summary_scraper.loop_push_scrape()); + let kubelet_health_scraper = + KubeletHealthScraper::new(args.clone(), metrics_cache_client.clone()); + let linux_agent_scraper = LinuxAgentScraper::new(args.clone(), metrics_cache_client.clone()); + let kubelet_stats_summary_scraper = KubeletStatsSummaryScraper::new(args, metrics_cache_client); + let kubelet_health_scrape = tokio::spawn(kubelet_health_scraper.loop_push_scrape()); let linux_agent_scrape = tokio::spawn(linux_agent_scraper.loop_push_scrape()); + let kubelet_scrape = tokio::spawn(kubelet_stats_summary_scraper.loop_push_scrape()); tokio::select! { res = kubelet_scrape => { @@ -60,5 +64,9 @@ async fn main() -> Result<()> { tracing::error!(error = ?res, "linux agent scrape loop exited unexpectedly"); anyhow::bail!("linux agent scrape loop terminated unexpectedly"); } + res = kubelet_health_scrape => { + tracing::error!(error = ?res, "kubelet health scrape loop exited unexpectedly"); + anyhow::bail!("kubelet health scrape loop terminated unexpectedly"); + } } } diff --git a/metrics-fetcher/src/payload.rs b/metrics-fetcher/src/payload.rs index 4c02d5d..9569085 100644 --- a/metrics-fetcher/src/payload.rs +++ b/metrics-fetcher/src/payload.rs @@ -4,17 +4,26 @@ use std::time::Duration; use tracing::{debug, trace, warn}; use crate::error::Result; +use crate::kubelet_health::KubeletHealth; #[derive(Debug)] pub(crate) enum Payload { KubeletStatsSummary(Bytes), - CheckmkLinuxAgent { node_name: String, body: Bytes }, + KubeletHealth { + node_name: String, + health: KubeletHealth, + }, + CheckmkLinuxAgent { + node_name: String, + body: Bytes, + }, } impl Payload { fn metrics_cache_endpoint(&self) -> String { match self { Self::KubeletStatsSummary(_) => "/kubelet_stats_summary".to_string(), + Self::KubeletHealth { node_name, .. } => format!("/kubelet_health/{node_name}"), Self::CheckmkLinuxAgent { node_name, .. } => format!("/system_agent/{node_name}"), } } @@ -22,6 +31,7 @@ impl Payload { fn content_type(&self) -> &'static str { match self { Self::KubeletStatsSummary(_) => "application/json", + Self::KubeletHealth { .. } => "application/json", // Not text/plain: a patched image's plugin can make check_mk_agent // output non-UTF-8, even non-textual, so we don't claim otherwise. Self::CheckmkLinuxAgent { .. } => "application/octet-stream", @@ -31,6 +41,9 @@ impl Payload { fn extract(&self) -> Bytes { match self { Self::KubeletStatsSummary(bytes) => bytes.clone(), + Self::KubeletHealth { health, .. } => { + Bytes::from(serde_json::to_vec(health).expect("KubeletHealth always serializes")) + } Self::CheckmkLinuxAgent { body, .. } => body.clone(), } }