Skip to content

Commit b052b7c

Browse files
committed
review: address relrod's feedback on linux agent ingestion
- linux_agent_cache stores MetricsFetcherIngestion<Bytes> instead of a bare String/Arc<String>, matching kubelet_stats_summary_cache and giving self-health reporting a received_at timestamp to use later - switch the ingest handler and cache to Bytes throughout, since check_mk_agent plugin output isn't guaranteed to be valid UTF-8; drop the false "charset=utf-8" claim on the content-type header sent by metrics-fetcher - simplify NODE_NAME lookup and agent-timeout handling in metrics-fetcher's LinuxAgentScraper per suggested diffs - fix metrics-fetcher main() silently exiting 0 if either scrape loop panics, by select!+log+bail on the join handles like metrics-cache's main() does - move the linux_agent ingest router tests into handlers/mod.rs, since they exercise routing/middleware rather than the handler itself; add a manual, fail-closed Default for PullAgentMiddlewareConfig instead of an ad-hoc no_pull_agent() test helper - drop the payload.rs unit tests that only asserted match arms
1 parent ce73049 commit b052b7c

6 files changed

Lines changed: 110 additions & 131 deletions

File tree

metrics-cache/src/auth/pull_agent.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ pub struct PullAgentMiddlewareConfig {
3131
pub shared_secret: Option<String>,
3232
}
3333

34+
impl Default for PullAgentMiddlewareConfig {
35+
/// Defaults closed: auth enabled with no configured secret, so
36+
/// [`authorized`] rejects every request. This is only meant as a
37+
/// placeholder for tests that don't exercise pull-agent auth at all; if it
38+
/// were ever reached in real code, it fails safe instead of leaving the
39+
/// endpoint open.
40+
fn default() -> Self {
41+
Self {
42+
auth_enabled: true,
43+
shared_secret: None,
44+
}
45+
}
46+
}
47+
3448
/// Perform authentication, specifically for pull-agent endpoints.
3549
///
3650
/// If `config.shared_secret` is `None` or empty, we _REJECT_ all requests.
Lines changed: 10 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use axum::body::Bytes;
12
use axum::extract::Path;
23
use axum::{Json, extract::State};
34
use std::sync::Arc;
@@ -25,99 +26,21 @@ pub async fn kubelet_stats_summary(
2526
}
2627

2728
/// Store the raw check_mk_agent output for a node, verbatim, keyed by node
28-
/// name. No parsing/validation is done here or by the caller.
29+
/// name. No parsing/validation is done here or by the caller. Kept as
30+
/// [`Bytes`] rather than [`String`] since agent plugins are not guaranteed to
31+
/// produce valid UTF-8.
2932
pub async fn linux_agent(
3033
State(state): State<AppState<impl TokenValidator>>,
3134
Path(node_name): Path<String>,
32-
body: String,
35+
body: Bytes,
3336
) -> Json<String> {
37+
let ingestion = MetricsFetcherIngestion {
38+
received_at: Instant::now(),
39+
payload: body,
40+
};
3441
state
3542
.linux_agent_cache
36-
.insert(node_name, Arc::new(body))
43+
.insert(node_name, Arc::new(ingestion))
3744
.await;
3845
Json("ok".to_string())
3946
}
40-
41-
#[cfg(test)]
42-
mod tests {
43-
use axum::body::Body;
44-
use axum::http::{Request, StatusCode};
45-
use k8s_openapi::api::authentication::v1::{TokenReview, TokenReviewStatus, UserInfo};
46-
use tower::ServiceExt;
47-
48-
use crate::auth::pull_agent::PullAgentMiddlewareConfig;
49-
use crate::handlers::app;
50-
use crate::state::tests::{MockValidator, test_app_state_with_validator};
51-
52-
fn no_pull_agent() -> PullAgentMiddlewareConfig {
53-
PullAgentMiddlewareConfig {
54-
auth_enabled: false,
55-
shared_secret: None,
56-
}
57-
}
58-
59-
fn authenticated_review(username: &str) -> TokenReview {
60-
TokenReview {
61-
status: Some(TokenReviewStatus {
62-
authenticated: Some(true),
63-
user: Some(UserInfo {
64-
username: Some(username.to_string()),
65-
..Default::default()
66-
}),
67-
..Default::default()
68-
}),
69-
..Default::default()
70-
}
71-
}
72-
73-
#[tokio::test]
74-
async fn linux_agent_ingest_populates_cache_and_returns_ok() {
75-
let state = test_app_state_with_validator(MockValidator {
76-
response: Ok(authenticated_review(
77-
"system:serviceaccount:test-ns:test-writer",
78-
)),
79-
});
80-
let cache = state.linux_agent_cache.clone();
81-
let app = app(state, no_pull_agent());
82-
83-
let resp = app
84-
.oneshot(
85-
Request::builder()
86-
.method("POST")
87-
.uri("/ingest/linux_agent/node-1")
88-
.header("Authorization", "Bearer test-token")
89-
.body(Body::from("<<<check_mk>>>\nVersion: 2.5.0\n"))
90-
.unwrap(),
91-
)
92-
.await
93-
.unwrap();
94-
95-
assert_eq!(resp.status(), StatusCode::OK);
96-
cache.run_pending_tasks().await;
97-
assert_eq!(
98-
cache.get("node-1").await.map(|v| (*v).clone()),
99-
Some("<<<check_mk>>>\nVersion: 2.5.0\n".to_string())
100-
);
101-
}
102-
103-
#[tokio::test]
104-
async fn linux_agent_ingest_requires_auth() {
105-
let state = test_app_state_with_validator(MockValidator {
106-
response: Ok(TokenReview::default()),
107-
});
108-
let app = app(state, no_pull_agent());
109-
110-
let resp = app
111-
.oneshot(
112-
Request::builder()
113-
.method("POST")
114-
.uri("/ingest/linux_agent/node-1")
115-
.body(Body::from("<<<check_mk>>>\n"))
116-
.unwrap(),
117-
)
118-
.await
119-
.unwrap();
120-
121-
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
122-
}
123-
}

metrics-cache/src/handlers/mod.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,77 @@ pub fn ingest_app<V: TokenValidator>(state: AppState<V>) -> Router {
4747
.layer(tower_http::compression::CompressionLayer::new())
4848
.with_state(state)
4949
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use axum::body::{Body, Bytes};
54+
use axum::http::{Request, StatusCode};
55+
use k8s_openapi::api::authentication::v1::{TokenReview, TokenReviewStatus, UserInfo};
56+
use tower::ServiceExt;
57+
58+
use super::*;
59+
use crate::state::tests::{MockValidator, test_app_state, test_app_state_with_validator};
60+
61+
fn authenticated_review(username: &str) -> TokenReview {
62+
TokenReview {
63+
status: Some(TokenReviewStatus {
64+
authenticated: Some(true),
65+
user: Some(UserInfo {
66+
username: Some(username.to_string()),
67+
..Default::default()
68+
}),
69+
..Default::default()
70+
}),
71+
..Default::default()
72+
}
73+
}
74+
75+
#[tokio::test]
76+
async fn linux_agent_ingest_populates_cache_and_returns_ok() {
77+
let state = test_app_state_with_validator(MockValidator {
78+
response: Ok(authenticated_review(
79+
"system:serviceaccount:test-ns:test-writer",
80+
)),
81+
});
82+
let cache = state.linux_agent_cache.clone();
83+
let app = ingest_app(state);
84+
85+
let resp = app
86+
.oneshot(
87+
Request::builder()
88+
.method("POST")
89+
.uri("/ingest/linux_agent/node-1")
90+
.header("Authorization", "Bearer test-token")
91+
.body(Body::from("<<<check_mk>>>\nVersion: 2.5.0\n"))
92+
.unwrap(),
93+
)
94+
.await
95+
.unwrap();
96+
97+
assert_eq!(resp.status(), StatusCode::OK);
98+
cache.run_pending_tasks().await;
99+
assert_eq!(
100+
cache.get("node-1").await.map(|v| v.payload.clone()),
101+
Some(Bytes::from_static(b"<<<check_mk>>>\nVersion: 2.5.0\n"))
102+
);
103+
}
104+
105+
#[tokio::test]
106+
async fn linux_agent_ingest_requires_auth() {
107+
let state = test_app_state();
108+
let app = ingest_app(state);
109+
110+
let resp = app
111+
.oneshot(
112+
Request::builder()
113+
.method("POST")
114+
.uri("/ingest/linux_agent/node-1")
115+
.body(Body::from("<<<check_mk>>>\n"))
116+
.unwrap(),
117+
)
118+
.await
119+
.unwrap();
120+
121+
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
122+
}
123+
}

metrics-cache/src/state.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use axum::body::Bytes;
12
use kube::Client;
23
use moka::future::Cache;
34
use std::sync::Arc;
@@ -22,7 +23,7 @@ pub struct AppState<V: TokenValidator> {
2223
pub reader_allowlist: Vec<String>,
2324
pub writer_allowlist: Vec<String>,
2425
pub kubelet_stats_summary_cache: Cache<String, Arc<MetricsFetcherIngestion<StatsSummary>>>,
25-
pub linux_agent_cache: Cache<String, Arc<String>>,
26+
pub linux_agent_cache: Cache<String, Arc<MetricsFetcherIngestion<Bytes>>>,
2627
pub host_settings: Arc<HostSettings>,
2728
}
2829

metrics-fetcher/src/linux_agent.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,10 @@ impl Scraper for LinuxAgentScraper {
4242
/// We do not parse the output or perform any calculations on it here,
4343
/// leaving these tasks for metrics-cache (and later, Checkmk itself) to do.
4444
async fn scrape(&self) -> Result<Payload> {
45-
let node_name = match std::env::var("NODE_NAME") {
46-
Ok(val) => val,
47-
Err(err) => {
48-
return Err(Error::EnvVar {
49-
name: "NODE_NAME".to_string(),
50-
source: err,
51-
});
52-
}
53-
};
45+
let node_name = std::env::var("NODE_NAME").map_err(|e| Error::EnvVar {
46+
name: "NODE_NAME".to_string(),
47+
source: e,
48+
})?;
5449

5550
debug!("running check_mk_agent");
5651
let child = Command::new(AGENT_PATH)
@@ -61,10 +56,9 @@ impl Scraper for LinuxAgentScraper {
6156
.kill_on_drop(true)
6257
.spawn()?;
6358

64-
let output = match timeout(AGENT_TIMEOUT, child.wait_with_output()).await {
65-
Ok(result) => result?,
66-
Err(_elapsed) => return Err(Error::AgentTimeout(AGENT_TIMEOUT)),
67-
};
59+
let output = timeout(AGENT_TIMEOUT, child.wait_with_output())
60+
.await
61+
.map_err(|_| Error::AgentTimeout(AGENT_TIMEOUT))??;
6862

6963
if !output.status.success() {
7064
return Err(Error::AgentExitStatus {

metrics-fetcher/src/payload.rs

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ impl Payload {
2121
fn content_type(&self) -> &'static str {
2222
match self {
2323
Self::KubeletStatsSummary(_) => "application/json",
24-
Self::CheckmkLinuxAgent { .. } => "text/plain; charset=utf-8",
24+
// No charset: plugins can make check_mk_agent output non-UTF-8,
25+
// so we don't assert one.
26+
Self::CheckmkLinuxAgent { .. } => "text/plain",
2527
}
2628
}
2729

@@ -73,32 +75,3 @@ impl Payload {
7375
Ok(response)
7476
}
7577
}
76-
77-
#[cfg(test)]
78-
mod tests {
79-
use super::*;
80-
81-
#[test]
82-
fn kubelet_stats_summary_endpoint_and_content_type() {
83-
let payload = Payload::KubeletStatsSummary(Bytes::from_static(b"{}"));
84-
assert_eq!(payload.metrics_cache_endpoint(), "/kubelet_stats_summary");
85-
assert_eq!(payload.content_type(), "application/json");
86-
assert_eq!(payload.extract(), Bytes::from_static(b"{}"));
87-
}
88-
89-
#[test]
90-
fn checkmk_linux_agent_endpoint_and_content_type() {
91-
for node_name in ["node-1", "node-with-dashes.example.com"] {
92-
let payload = Payload::CheckmkLinuxAgent {
93-
node_name: node_name.to_string(),
94-
body: Bytes::from_static(b"<<<check_mk>>>\n"),
95-
};
96-
assert_eq!(
97-
payload.metrics_cache_endpoint(),
98-
format!("/linux_agent/{node_name}")
99-
);
100-
assert_eq!(payload.content_type(), "text/plain; charset=utf-8");
101-
assert_eq!(payload.extract(), Bytes::from_static(b"<<<check_mk>>>\n"));
102-
}
103-
}
104-
}

0 commit comments

Comments
 (0)