|
| 1 | +//! Builds the exec-based startup and readiness probes that check NiFi 2.x's |
| 2 | +//! local, unauthenticated management-server endpoints (`/health` and |
| 3 | +//! `/health/cluster`). |
| 4 | +//! |
| 5 | +//! The management server binds `127.0.0.1` only, so these must be `exec` |
| 6 | +//! probes using `curl` from inside the container - a `httpGet` probe cannot |
| 7 | +//! reach a loopback-only address. |
| 8 | +
|
| 9 | +use stackable_operator::k8s_openapi::{ |
| 10 | + api::core::v1::{ExecAction, Probe, TCPSocketAction}, |
| 11 | + apimachinery::pkg::util::intstr::IntOrString, |
| 12 | +}; |
| 13 | + |
| 14 | +use crate::controller::build::{ |
| 15 | + HTTPS_PORT_NAME, MANAGEMENT_SERVER_ADDRESS, MANAGEMENT_SERVER_PORT, |
| 16 | +}; |
| 17 | + |
| 18 | +fn management_health_exec(path: &str) -> ExecAction { |
| 19 | + ExecAction { |
| 20 | + command: Some(vec![ |
| 21 | + "/bin/bash".to_string(), |
| 22 | + "-euo".to_string(), |
| 23 | + "pipefail".to_string(), |
| 24 | + "-c".to_string(), |
| 25 | + format!( |
| 26 | + "curl --fail --silent --show-error --output /dev/null http://{MANAGEMENT_SERVER_ADDRESS}:{MANAGEMENT_SERVER_PORT}{path}" |
| 27 | + ), |
| 28 | + ]), |
| 29 | + } |
| 30 | +} |
| 31 | +pub fn management_startup_probe() -> Probe { |
| 32 | + Probe { |
| 33 | + initial_delay_seconds: Some(10), |
| 34 | + period_seconds: Some(10), |
| 35 | + timeout_seconds: Some(5), |
| 36 | + failure_threshold: Some(20 * 6), |
| 37 | + exec: Some(management_health_exec("/health")), |
| 38 | + ..Probe::default() |
| 39 | + } |
| 40 | +} |
| 41 | +pub fn management_readiness_probe() -> Probe { |
| 42 | + Probe { |
| 43 | + period_seconds: Some(10), |
| 44 | + timeout_seconds: Some(5), |
| 45 | + failure_threshold: Some(3), |
| 46 | + exec: Some(management_health_exec("/health/cluster")), |
| 47 | + ..Probe::default() |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +pub fn tcp_liveliness_probe() -> Probe { |
| 52 | + Probe { |
| 53 | + initial_delay_seconds: Some(10), |
| 54 | + period_seconds: Some(10), |
| 55 | + tcp_socket: Some(TCPSocketAction { |
| 56 | + port: IntOrString::String(HTTPS_PORT_NAME.to_string()), |
| 57 | + ..TCPSocketAction::default() |
| 58 | + }), |
| 59 | + ..Probe::default() |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +#[cfg(test)] |
| 64 | +mod tests { |
| 65 | + use super::*; |
| 66 | + |
| 67 | + #[test] |
| 68 | + fn startup_probe_execs_curl_against_health_endpoint() { |
| 69 | + let probe = management_startup_probe(); |
| 70 | + |
| 71 | + let command = probe |
| 72 | + .exec |
| 73 | + .expect("startup probe must be an exec probe") |
| 74 | + .command |
| 75 | + .expect("exec action must have a command"); |
| 76 | + let script = command.last().expect("bash -c script argument"); |
| 77 | + |
| 78 | + assert!( |
| 79 | + script.contains("http://127.0.0.1:52020/health") && !script.contains("/health/cluster"), |
| 80 | + "expected curl against /health, got: {script}" |
| 81 | + ); |
| 82 | + assert_eq!(probe.failure_threshold, Some(120)); |
| 83 | + assert_eq!(probe.timeout_seconds, Some(5)); |
| 84 | + assert!( |
| 85 | + probe.tcp_socket.is_none(), |
| 86 | + "must not fall back to tcp_socket" |
| 87 | + ); |
| 88 | + } |
| 89 | + |
| 90 | + #[test] |
| 91 | + fn readiness_probe_execs_curl_against_cluster_health_endpoint() { |
| 92 | + let probe = management_readiness_probe(); |
| 93 | + |
| 94 | + let command = probe |
| 95 | + .exec |
| 96 | + .expect("readiness probe must be an exec probe") |
| 97 | + .command |
| 98 | + .expect("exec action must have a command"); |
| 99 | + let script = command.last().expect("bash -c script argument"); |
| 100 | + |
| 101 | + let cluster_health_url = |
| 102 | + format!("http://{MANAGEMENT_SERVER_ADDRESS}:{MANAGEMENT_SERVER_PORT}/health/cluster"); |
| 103 | + assert!( |
| 104 | + script.contains(&cluster_health_url), |
| 105 | + "expected curl against /health/cluster, got: {script}" |
| 106 | + ); |
| 107 | + assert_eq!(probe.failure_threshold, Some(3)); |
| 108 | + assert_eq!(probe.timeout_seconds, Some(5)); |
| 109 | + assert_eq!( |
| 110 | + probe.initial_delay_seconds, None, |
| 111 | + "readiness probe delay is redundant: k8s already suppresses readiness checks \ |
| 112 | + until the startup probe succeeds" |
| 113 | + ); |
| 114 | + } |
| 115 | + |
| 116 | + #[test] |
| 117 | + fn probes_use_bash_pipefail_wrapper_not_bare_curl_argv() { |
| 118 | + for probe in [management_startup_probe(), management_readiness_probe()] { |
| 119 | + let command = probe.exec.unwrap().command.unwrap(); |
| 120 | + assert_eq!( |
| 121 | + command[..4], |
| 122 | + [ |
| 123 | + "/bin/bash".to_string(), |
| 124 | + "-euo".to_string(), |
| 125 | + "pipefail".to_string(), |
| 126 | + "-c".to_string(), |
| 127 | + ], |
| 128 | + "exec command must follow the repo's bash -euo pipefail -c convention" |
| 129 | + ); |
| 130 | + } |
| 131 | + } |
| 132 | +} |
0 commit comments