Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion metrics-cache/src/piggyback/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use std::sync::Arc;

use crate::host_settings::HostSettings;
use crate::piggyback::{AggregationHost, Meta, PiggybackHost};
use crate::section::writeable::{SectionError, WriteableSection};
use crate::section::{
node::KubeNodeInfoV1,
writeable::{SectionError, WriteableSection},
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention with the rest of the project, prefer to only group leaves of import trees:

Suggested change
use crate::section::{
node::KubeNodeInfoV1,
writeable::{SectionError, WriteableSection},
};
use crate::section::node::KubeNodeInfoV1;
use crate::section::writeable::{SectionError, WriteableSection};

But I know there are a few places lingering in the repo that break the convention.

(This would be nice to enforce with rustfmt but it's a nightly-only feature 😕)

use crate::snapshot::Snapshot;

pub struct Node<'a> {
Expand Down Expand Up @@ -52,6 +55,9 @@ impl PiggybackHost for Node<'_> {
fn emit(&self) -> Vec<Result<WriteableSection, SectionError>> {
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));
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray semicolon here

Suggested change
};
}

We have a few others in the repo, too, I just checked. Feel free to add this to Cargo.toml in the clippy section (as another PR) and fix the existing few occurrences, I'd appreciate it:

unnecessary_semicolon = "deny"

out.extend(self.aggregation_sections(&me));
out
}
Expand Down
1 change: 1 addition & 0 deletions metrics-cache/src/section/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod common;
pub mod cronjob;
pub mod namespace;
pub mod node;
pub mod performance;
pub mod pod;
pub mod pvc;
Expand Down
142 changes: 142 additions & 0 deletions metrics-cache/src/section/node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use k8s_openapi::api::core::v1::Node;
use serde::Serialize;
use std::collections::BTreeMap;

use crate::host_settings::HostSettings;
use crate::section::Section;
use crate::section::common::LabelRef;

/// One entry of a Node's `status.addresses`.
///
/// Not `k8s_openapi`'s `NodeAddress`: that one serializes as `type`, while
/// Checkmk expects `type_`.
#[derive(Serialize)]
pub(crate) struct NodeAddressRef<'a> {
address: &'a str,
type_: &'a str,
}

/// Node info. (`kube_node_info_v1`)
#[derive(Serialize)]
pub(crate) struct KubeNodeInfoV1<'a> {
pub architecture: &'a str,
pub kernel_version: &'a str,
pub os_image: &'a str,
pub operating_system: &'a str,
pub container_runtime_version: &'a str,
pub name: &'a str,
pub creation_timestamp: Option<f64>,
pub labels: BTreeMap<&'a str, LabelRef<'a>>,
/// Annotations filtered with user input.
///
/// After receiving the annotations from the Kubernetes API, we cannot
/// process all of them as HostLabels. FilteredAnnotations are those
/// annotations, which can be processed. This means that the annotations can
/// no longer be arbitrary json objects and that options from the
/// `Kubernetes` rule have been taken into account.
pub annotations: BTreeMap<&'a str, &'a str>,
pub addresses: Vec<NodeAddressRef<'a>>,
pub cluster: &'a str,
pub kubernetes_cluster_hostname: &'a str,
}

impl<'a> KubeNodeInfoV1<'a> {
pub fn from_node(node: &'a Node, settings: &'a HostSettings) -> Option<KubeNodeInfoV1<'a>> {
let status = node.status.as_ref()?;
let node_info = status.node_info.as_ref()?;
let node_section = KubeNodeInfoV1 {
architecture: &node_info.architecture,
kernel_version: &node_info.kernel_version,
os_image: &node_info.os_image,
operating_system: &node_info.operating_system,
container_runtime_version: &node_info.container_runtime_version,
name: node.metadata.name.as_deref()?,
creation_timestamp: node
.metadata
.creation_timestamp
.as_ref()
.map(|x| x.0.as_millisecond() as f64 / 1000.0),
labels: node
.metadata
.labels
.as_ref()
.map(LabelRef::from_map)
.unwrap_or_default(),
annotations: node
.metadata
.annotations
.as_ref()
.map(|x| settings.annotation_key_pattern.filter(x))
.unwrap_or_default(),
addresses: status
.addresses
.iter()
.flatten()
.map(|x| NodeAddressRef {
address: &x.address,
type_: &x.type_,
})
.collect(),
cluster: &settings.cluster_name,
kubernetes_cluster_hostname: &settings.cluster_host_name,
};
Some(node_section)
}
}
impl Section for KubeNodeInfoV1<'_> {
const NAME: &'static str = "kube_node_info_v1";
}

#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;

use crate::host_settings::AnnotationKeyPattern;
use crate::test_support::{host_settings, node, node_prefilled};

#[test]
fn kube_node_info_v1() {
let node = node_prefilled("worker-1");
let mut settings = host_settings();
insta::assert_json_snapshot!(KubeNodeInfoV1::from_node(&node, &settings));

// This pattern should only match one annotation
settings.annotation_key_pattern =
AnnotationKeyPattern::Pattern(Regex::new("^example").unwrap());
assert_eq!(
KubeNodeInfoV1::from_node(&node, &settings)
.unwrap()
.annotations
.len(),
1
);

// Ignore all annotations, emit 0 of them
settings.annotation_key_pattern = AnnotationKeyPattern::IgnoreAll;
assert_eq!(
KubeNodeInfoV1::from_node(&node, &settings)
.unwrap()
.annotations
.len(),
0
);

// Import all annotations (fixture default, captured by insta above, but let's be explicit)
settings.annotation_key_pattern = AnnotationKeyPattern::ImportAll;
assert_eq!(
KubeNodeInfoV1::from_node(&node, &settings)
.unwrap()
.annotations
.len(),
2
);
}

/// A Node without `status.nodeInfo` cannot fill the five mandatory fields
/// of the section, so we emit nothing rather than something unparseable.
#[test]
fn kube_node_info_v1_without_node_info() {
assert!(KubeNodeInfoV1::from_node(&node("worker-1"), &host_settings()).is_none());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
source: metrics-cache/src/section/node.rs
expression: "KubeNodeInfoV1::from_node(&node, &settings)"
---
{
"architecture": "amd64",
"kernel_version": "6.8.0-51-generic",
"os_image": "Ubuntu 22.04.5 LTS",
"operating_system": "linux",
"container_runtime_version": "containerd://1.7.24",
"name": "worker-1",
"creation_timestamp": 1786130565.0,
"labels": {
"kubernetes.io/arch": {
"name": "kubernetes.io/arch",
"value": "amd64"
},
"kubernetes.io/hostname": {
"name": "kubernetes.io/hostname",
"value": "worker-1"
}
},
"annotations": {
"checkmk.com/promote-to-host": "true",
"example.com/cool-animal": "monkeys"
},
"addresses": [
{
"address": "10.0.0.5",
"type_": "InternalIP"
},
{
"address": "worker-1",
"type_": "Hostname"
}
],
"cluster": "the-cluster",
"kubernetes_cluster_hostname": "cluster.host.tld"
}
42 changes: 40 additions & 2 deletions metrics-cache/src/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
use k8s_openapi::api::apps::v1::ReplicaSet;
use k8s_openapi::api::batch::v1::{CronJob, CronJobSpec, Job};
use k8s_openapi::api::core::v1::{
Container, Namespace, Node, PersistentVolumeClaim, PersistentVolumeClaimSpec,
PersistentVolumeClaimStatus, Pod, PodSpec, VolumeResourceRequirements,
Container, Namespace, Node, NodeAddress, NodeStatus, NodeSystemInfo, PersistentVolumeClaim,
PersistentVolumeClaimSpec, PersistentVolumeClaimStatus, Pod, PodSpec,
VolumeResourceRequirements,
};
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference, Time};
Expand Down Expand Up @@ -103,6 +104,43 @@ pub fn node(name: &str) -> Node {
}
}

pub fn node_prefilled(name: &str) -> Node {
let timestamp: Timestamp = "2026-08-07 15:22:45-04".parse().unwrap();
let mut node = node(name);
node.metadata.creation_timestamp = Some(Time(timestamp));
node.metadata.labels = Some(BTreeMap::from([
(s("kubernetes.io/hostname"), name.to_string()),
(s("kubernetes.io/arch"), s("amd64")),
]));
node.metadata.annotations = Some(BTreeMap::from([
(s("example.com/cool-animal"), s("monkeys")),
(s("checkmk.com/promote-to-host"), s("true")),
]));
node.status = Some(NodeStatus {
addresses: Some(vec![
NodeAddress {
address: s("10.0.0.5"),
type_: s("InternalIP"),
},
NodeAddress {
address: name.to_string(),
type_: s("Hostname"),
},
]),
node_info: Some(NodeSystemInfo {
architecture: s("amd64"),
container_runtime_version: s("containerd://1.7.24"),
kernel_version: s("6.8.0-51-generic"),
kubelet_version: s("v1.34.0"),
operating_system: s("linux"),
os_image: s("Ubuntu 22.04.5 LTS"),
..Default::default()
}),
..Default::default()
});
node
}

pub fn owner_ref(kind: &str, name: &str, uid: &str) -> OwnerReference {
OwnerReference {
kind: kind.into(),
Expand Down