From 2eefa4a24ed155f1b317f1d54e8676320fa0a8d0 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:30:15 +0200 Subject: [PATCH 01/13] feat: add exec-based NiFi 2.x management-server probe builders Co-Authored-By: Claude Sonnet 5 --- .../src/controller/build/resource/mod.rs | 1 + .../src/controller/build/resource/probes.rs | 119 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 rust/operator-binary/src/controller/build/resource/probes.rs diff --git a/rust/operator-binary/src/controller/build/resource/mod.rs b/rust/operator-binary/src/controller/build/resource/mod.rs index 9598a324..a475d2e9 100644 --- a/rust/operator-binary/src/controller/build/resource/mod.rs +++ b/rust/operator-binary/src/controller/build/resource/mod.rs @@ -5,6 +5,7 @@ pub mod config_map; pub mod listener; pub mod pdb; +pub mod probes; pub mod rbac; pub mod service; pub mod statefulset; diff --git a/rust/operator-binary/src/controller/build/resource/probes.rs b/rust/operator-binary/src/controller/build/resource/probes.rs new file mode 100644 index 00000000..abc55939 --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/probes.rs @@ -0,0 +1,119 @@ +//! Builds the exec-based startup and readiness probes that check NiFi 2.x's +//! local, unauthenticated management-server endpoints (`/health` and +//! `/health/cluster`), rather than only checking that the HTTPS port is open. +//! +//! The management server binds `127.0.0.1` only, so these must be `exec` +//! probes using `curl` from inside the container - a `httpGet` probe cannot +//! reach a loopback-only address. + +use stackable_operator::k8s_openapi::api::core::v1::{ExecAction, Probe}; + +/// Port NiFi's management server binds to by default +/// (`org.apache.nifi.management.server.address`, see +/// `ManagementServerProvider.MANAGEMENT_SERVER_DEFAULT_ADDRESS` upstream). +/// The operator pins the JVM system property to this same value (see +/// `build::jvm`), so this constant is the single source of truth for it. +pub const MANAGEMENT_SERVER_PORT: u16 = 52020; + +fn management_health_exec(path: &str) -> ExecAction { + ExecAction { + command: Some(vec![ + "/bin/bash".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + format!( + "curl --fail --silent --show-error --output /dev/null http://127.0.0.1:{MANAGEMENT_SERVER_PORT}{path}" + ), + ]), + } +} + +/// Gates startup on the management server reporting the app server has +/// booted. `/health` only starts responding once NiFi's own web server (and +/// flow load) has completed, so this replaces the old blunt TCP-socket check +/// on the HTTPS port. +pub fn management_startup_probe() -> Probe { + Probe { + initial_delay_seconds: Some(10), + period_seconds: Some(10), + failure_threshold: Some(20 * 6), + exec: Some(management_health_exec("/health")), + ..Probe::default() + } +} + +/// Gates readiness on cluster membership: `/health/cluster` returns 200 only +/// while the node's `ConnectionState` is `CONNECTED`/`CONNECTING`, and 503 +/// otherwise (e.g. during a rolling restart before the node has rejoined). +pub fn management_readiness_probe() -> Probe { + Probe { + initial_delay_seconds: Some(10), + period_seconds: Some(10), + failure_threshold: Some(3), + exec: Some(management_health_exec("/health/cluster")), + ..Probe::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn startup_probe_execs_curl_against_health_endpoint() { + let probe = management_startup_probe(); + + let command = probe + .exec + .expect("startup probe must be an exec probe") + .command + .expect("exec action must have a command"); + let script = command.last().expect("bash -c script argument"); + + assert!( + script.contains("http://127.0.0.1:52020/health") && !script.contains("/health/cluster"), + "expected curl against /health, got: {script}" + ); + assert_eq!(probe.failure_threshold, Some(120)); + assert!( + probe.tcp_socket.is_none(), + "must not fall back to tcp_socket" + ); + } + + #[test] + fn readiness_probe_execs_curl_against_cluster_health_endpoint() { + let probe = management_readiness_probe(); + + let command = probe + .exec + .expect("readiness probe must be an exec probe") + .command + .expect("exec action must have a command"); + let script = command.last().expect("bash -c script argument"); + + assert!( + script.contains("http://127.0.0.1:52020/health/cluster"), + "expected curl against /health/cluster, got: {script}" + ); + assert_eq!(probe.failure_threshold, Some(3)); + } + + #[test] + fn probes_use_bash_pipefail_wrapper_not_bare_curl_argv() { + for probe in [management_startup_probe(), management_readiness_probe()] { + let command = probe.exec.unwrap().command.unwrap(); + assert_eq!( + command[..4], + [ + "/bin/bash".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ], + "exec command must follow the repo's bash -euo pipefail -c convention" + ); + } + } +} From 51bdfa7912ec0a4b32cd8efabfd3afa3cdf5d292 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:34:13 +0200 Subject: [PATCH 02/13] feat: gate NiFi startup/readiness on management-server health endpoints --- .../controller/build/resource/statefulset.rs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 39346f9e..3278552f 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -54,9 +54,12 @@ use crate::{ graceful_shutdown::add_graceful_shutdown_config, object_meta, properties::ConfigFileName, - resource::listener::{ - LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build_group_listener_pvc, - group_listener_name, + resource::{ + listener::{ + LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build_group_listener_pvc, + group_listener_name, + }, + probes::{management_readiness_probe, management_startup_probe}, }, }, }, @@ -444,16 +447,14 @@ pub(crate) fn build_node_rolegroup_statefulset( }), ..Probe::default() }) - .startup_probe(Probe { - initial_delay_seconds: Some(10), - period_seconds: Some(10), - failure_threshold: Some(20 * 6), - tcp_socket: Some(TCPSocketAction { - port: IntOrString::String(HTTPS_PORT_NAME.to_string()), - ..TCPSocketAction::default() - }), - ..Probe::default() - }) + // Gates startup on the NiFi 2.x management server reporting the app + // server has booted (rather than just the HTTPS port accepting + // connections). + .startup_probe(management_startup_probe()) + // Gates readiness on the node actually being connected to the + // cluster, so a node that is up but not yet rejoined after a + // rolling restart is correctly reported as not Ready. + .readiness_probe(management_readiness_probe()) .resources(merged_config.resources.clone().into()); let mut pod_builder = PodBuilder::new(); From 236210b1d4bd74df5d3662011362de829c1475fe Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:38:05 +0200 Subject: [PATCH 03/13] feat: pin NiFi management-server address explicitly via JVM property Co-Authored-By: Claude Sonnet 5 --- .../src/controller/build/jvm.rs | 31 ++++++++++++++++++- .../build/properties/bootstrap_conf.rs | 10 +++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/rust/operator-binary/src/controller/build/jvm.rs b/rust/operator-binary/src/controller/build/jvm.rs index b6275a07..cbd21966 100644 --- a/rust/operator-binary/src/controller/build/jvm.rs +++ b/rust/operator-binary/src/controller/build/jvm.rs @@ -9,7 +9,10 @@ use stackable_operator::{ use crate::{ controller::{ ValidatedNifiConfig, - build::{NIFI_CONFIG_DIRECTORY, properties::ConfigFileName}, + build::{ + NIFI_CONFIG_DIRECTORY, properties::ConfigFileName, + resource::probes::MANAGEMENT_SERVER_PORT, + }, }, security::{ authentication::{STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD}, @@ -86,6 +89,10 @@ pub fn build_merged_jvm_config( "-Djava.security.properties={NIFI_CONFIG_DIRECTORY}/{}", ConfigFileName::SecurityProperties ), + // Pin the NiFi 2.x management server (used by the startup/readiness + // probes in resource::probes) to a known address instead of relying + // on its undocumented upstream default. + format!("-Dorg.apache.nifi.management.server.address=127.0.0.1:{MANAGEMENT_SERVER_PORT}"), ]; // Add JVM truststore properties when OPA TLS is enabled @@ -170,6 +177,28 @@ mod tests { ); } + /// The management-server bind address is pinned explicitly rather than + /// relying on NiFi's undocumented upstream default, so the probes in + /// `resource::probes` always target the right port even if that default + /// ever changes upstream. + #[test] + fn management_server_address_is_pinned_explicitly() { + let cluster = minimal_validated_cluster(); + let args = build_merged_jvm_config( + &default_rg(&cluster).config, + &JvmArgumentOverrides::default(), + None, + ) + .expect("jvm config should build"); + + assert!( + args.contains( + &"-Dorg.apache.nifi.management.server.address=127.0.0.1:52020".to_string() + ), + "expected an explicit management-server address JVM property, got: {args:?}" + ); + } + /// Without OPA TLS, no truststore properties are emitted. #[test] fn without_opa_tls_no_truststore_properties() { diff --git a/rust/operator-binary/src/controller/build/properties/bootstrap_conf.rs b/rust/operator-binary/src/controller/build/properties/bootstrap_conf.rs index 77602fdb..0898e5ea 100644 --- a/rust/operator-binary/src/controller/build/properties/bootstrap_conf.rs +++ b/rust/operator-binary/src/controller/build/properties/bootstrap_conf.rs @@ -122,6 +122,7 @@ mod tests { java.arg.10=-Djavax.security.auth.useSubjectCredsOnly=true java.arg.11=-Dzookeeper.admin.enableServer=false java.arg.12=-Djava.security.properties=/stackable/nifi/conf/security.properties + java.arg.13=-Dorg.apache.nifi.management.server.address=127.0.0.1:52020 java.arg.2=-Xms3276m java.arg.3=-XX:+UseG1GC java.arg.4=-Djava.awt.headless=true @@ -187,10 +188,11 @@ mod tests { java=java java.arg.1=-Xms34406m java.arg.10=-Djava.security.properties=/stackable/nifi/conf/security.properties - java.arg.11=-Dhttps.proxyHost=proxy.my.corp - java.arg.12=-Djava.net.preferIPv4Stack=true - java.arg.13=-Xmx40000m - java.arg.14=-Dhttps.proxyPort=1234 + java.arg.11=-Dorg.apache.nifi.management.server.address=127.0.0.1:52020 + java.arg.12=-Dhttps.proxyHost=proxy.my.corp + java.arg.13=-Djava.net.preferIPv4Stack=true + java.arg.14=-Xmx40000m + java.arg.15=-Dhttps.proxyPort=1234 java.arg.2=-Djava.awt.headless=true java.arg.3=-Dorg.apache.jasper.compiler.disablejsr199=true java.arg.4=-Djava.net.preferIPv4Stack=true From e67829a4ca1b8390a7151a8f81afbe8fc63020b6 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:41:39 +0200 Subject: [PATCH 04/13] test: assert NiFi 2.x management-server probes and readiness gating in cluster_operation kuttl test --- .../kuttl/cluster_operation/20-assert.yaml | 28 +++++++++++++++++++ .../kuttl/cluster_operation/50-assert.yaml | 17 +++++++++++ 2 files changed, 45 insertions(+) diff --git a/tests/templates/kuttl/cluster_operation/20-assert.yaml b/tests/templates/kuttl/cluster_operation/20-assert.yaml index 155b0d03..ed248438 100644 --- a/tests/templates/kuttl/cluster_operation/20-assert.yaml +++ b/tests/templates/kuttl/cluster_operation/20-assert.yaml @@ -12,3 +12,31 @@ metadata: status: readyReplicas: 2 replicas: 2 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-nifi-node-default +spec: + template: + spec: + containers: + - name: nifi + startupProbe: + failureThreshold: 120 + exec: + command: + - /bin/bash + - -euo + - pipefail + - -c + - curl --fail --silent --show-error --output /dev/null http://127.0.0.1:52020/health + readinessProbe: + failureThreshold: 3 + exec: + command: + - /bin/bash + - -euo + - pipefail + - -c + - curl --fail --silent --show-error --output /dev/null http://127.0.0.1:52020/health/cluster diff --git a/tests/templates/kuttl/cluster_operation/50-assert.yaml b/tests/templates/kuttl/cluster_operation/50-assert.yaml index c1304692..14a01a1a 100644 --- a/tests/templates/kuttl/cluster_operation/50-assert.yaml +++ b/tests/templates/kuttl/cluster_operation/50-assert.yaml @@ -3,6 +3,23 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 600 commands: + - script: | + # Confirm at least one pod is reported NotReady while the rollout is in + # progress - i.e. readiness is actually gating on cluster membership, + # not just passing immediately. Polls briefly right after the restart + # is triggered, before asserting the final converged state below. + for i in $(seq 1 30); do + NOT_READY=$(kubectl -n $NAMESPACE get pods -l app.kubernetes.io/name=nifi \ + -o jsonpath='{range .items[*]}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \ + | grep -c False || true) + if [ "$NOT_READY" -gt 0 ]; then + echo "observed $NOT_READY not-ready pod(s) mid-restart, as expected" + exit 0 + fi + sleep 2 + done + echo "never observed a not-ready pod during the restart window - readiness probe may not be gating correctly" + exit 1 - script: kubectl -n $NAMESPACE wait --for=condition=available nificlusters.nifi.stackable.tech/test-nifi --timeout 601s --- apiVersion: apps/v1 From c5ba5fcc543835e28b52e147714c0156a967aea8 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:14:40 +0200 Subject: [PATCH 05/13] fix: set explicit timeout_seconds on management-server exec probes Co-Authored-By: Claude Sonnet 5 --- rust/operator-binary/src/controller/build/resource/probes.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/operator-binary/src/controller/build/resource/probes.rs b/rust/operator-binary/src/controller/build/resource/probes.rs index abc55939..15493240 100644 --- a/rust/operator-binary/src/controller/build/resource/probes.rs +++ b/rust/operator-binary/src/controller/build/resource/probes.rs @@ -37,6 +37,7 @@ pub fn management_startup_probe() -> Probe { Probe { initial_delay_seconds: Some(10), period_seconds: Some(10), + timeout_seconds: Some(3), failure_threshold: Some(20 * 6), exec: Some(management_health_exec("/health")), ..Probe::default() @@ -50,6 +51,7 @@ pub fn management_readiness_probe() -> Probe { Probe { initial_delay_seconds: Some(10), period_seconds: Some(10), + timeout_seconds: Some(3), failure_threshold: Some(3), exec: Some(management_health_exec("/health/cluster")), ..Probe::default() @@ -76,6 +78,7 @@ mod tests { "expected curl against /health, got: {script}" ); assert_eq!(probe.failure_threshold, Some(120)); + assert_eq!(probe.timeout_seconds, Some(3)); assert!( probe.tcp_socket.is_none(), "must not fall back to tcp_socket" @@ -98,6 +101,7 @@ mod tests { "expected curl against /health/cluster, got: {script}" ); assert_eq!(probe.failure_threshold, Some(3)); + assert_eq!(probe.timeout_seconds, Some(3)); } #[test] From 2a8df57128c50e1c792d5e45bdd79d07496b06cf Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:14:43 +0200 Subject: [PATCH 06/13] fix: revert retry-unsafe NotReady polling assertion in cluster_operation kuttl test --- .../kuttl/cluster_operation/50-assert.yaml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/templates/kuttl/cluster_operation/50-assert.yaml b/tests/templates/kuttl/cluster_operation/50-assert.yaml index 14a01a1a..c1304692 100644 --- a/tests/templates/kuttl/cluster_operation/50-assert.yaml +++ b/tests/templates/kuttl/cluster_operation/50-assert.yaml @@ -3,23 +3,6 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 600 commands: - - script: | - # Confirm at least one pod is reported NotReady while the rollout is in - # progress - i.e. readiness is actually gating on cluster membership, - # not just passing immediately. Polls briefly right after the restart - # is triggered, before asserting the final converged state below. - for i in $(seq 1 30); do - NOT_READY=$(kubectl -n $NAMESPACE get pods -l app.kubernetes.io/name=nifi \ - -o jsonpath='{range .items[*]}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \ - | grep -c False || true) - if [ "$NOT_READY" -gt 0 ]; then - echo "observed $NOT_READY not-ready pod(s) mid-restart, as expected" - exit 0 - fi - sleep 2 - done - echo "never observed a not-ready pod during the restart window - readiness probe may not be gating correctly" - exit 1 - script: kubectl -n $NAMESPACE wait --for=condition=available nificlusters.nifi.stackable.tech/test-nifi --timeout 601s --- apiVersion: apps/v1 From cf041f0b3266c64ac34d0db70a0fc2c778976582 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:24:13 +0200 Subject: [PATCH 07/13] fix: increase probe timeout_seconds from 3 to 5 for real headroom Co-Authored-By: Claude Sonnet 5 --- .../src/controller/build/resource/probes.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/probes.rs b/rust/operator-binary/src/controller/build/resource/probes.rs index 15493240..75ab1fc3 100644 --- a/rust/operator-binary/src/controller/build/resource/probes.rs +++ b/rust/operator-binary/src/controller/build/resource/probes.rs @@ -37,7 +37,7 @@ pub fn management_startup_probe() -> Probe { Probe { initial_delay_seconds: Some(10), period_seconds: Some(10), - timeout_seconds: Some(3), + timeout_seconds: Some(5), failure_threshold: Some(20 * 6), exec: Some(management_health_exec("/health")), ..Probe::default() @@ -51,7 +51,7 @@ pub fn management_readiness_probe() -> Probe { Probe { initial_delay_seconds: Some(10), period_seconds: Some(10), - timeout_seconds: Some(3), + timeout_seconds: Some(5), failure_threshold: Some(3), exec: Some(management_health_exec("/health/cluster")), ..Probe::default() @@ -78,7 +78,7 @@ mod tests { "expected curl against /health, got: {script}" ); assert_eq!(probe.failure_threshold, Some(120)); - assert_eq!(probe.timeout_seconds, Some(3)); + assert_eq!(probe.timeout_seconds, Some(5)); assert!( probe.tcp_socket.is_none(), "must not fall back to tcp_socket" @@ -101,7 +101,7 @@ mod tests { "expected curl against /health/cluster, got: {script}" ); assert_eq!(probe.failure_threshold, Some(3)); - assert_eq!(probe.timeout_seconds, Some(3)); + assert_eq!(probe.timeout_seconds, Some(5)); } #[test] From 63267ee4ea171a24f325ac45759fc23410d21f29 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:32:16 +0200 Subject: [PATCH 08/13] docs: changelog entry for NiFi 2.x management-server health probes --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b88a5c..c279084c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ All notable changes to this project will be documented in this file. - BREAKING: The `nodes` role is now required by the CRD; a NifiCluster without it was previously accepted by the API server but failed reconciliation ([#966]). - All product containers now run with `securityContext.runAsNonRoot` set to `true` to improve security ([#975]). +- NiFi 2.x startup and readiness probes now use the local management server's `/health` and + `/health/cluster` endpoints instead of a bare TCP check, so a node is only reported Ready once + it has actually joined the cluster (e.g. during a rolling restart) ([#TBD]). ### Fixed @@ -25,6 +28,7 @@ All notable changes to this project will be documented in this file. [#966]: https://github.com/stackabletech/nifi-operator/pull/966 [#970]: https://github.com/stackabletech/nifi-operator/pull/970 [#975]: https://github.com/stackabletech/nifi-operator/pull/975 +[#TBD]: https://github.com/stackabletech/nifi-operator/pull/TBD ## [26.7.0] - 2026-07-21 From 88a8f27d36c273b02ca4196fd5eb9c42ce51a834 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:40:16 +0200 Subject: [PATCH 09/13] refactor: relocate MANAGEMENT_SERVER_PORT, document override caveat, drop dead readiness delay Co-Authored-By: Claude Sonnet 5 --- rust/operator-binary/src/controller/build.rs | 10 ++++++++++ rust/operator-binary/src/controller/build/jvm.rs | 5 +---- .../src/controller/build/resource/probes.rs | 13 ++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 6376f9df..5e3b7d73 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -46,6 +46,16 @@ pub const PROTOCOL_PORT: Port = Port(9088); pub const BALANCE_PORT_NAME: &str = "balance"; pub const BALANCE_PORT: Port = Port(6243); +/// Port NiFi's management server binds to by default +/// (`org.apache.nifi.management.server.address`, see +/// `ManagementServerProvider.MANAGEMENT_SERVER_DEFAULT_ADDRESS` upstream). +/// The operator pins the JVM system property to this same value (see +/// `build::jvm`), so this constant is the single source of truth for it. +/// Note: a user-supplied `jvmArgumentOverrides` that removes or repoints the +/// corresponding `-D` property would desync this from the actual +/// management-server address, silently breaking both probes. +pub const MANAGEMENT_SERVER_PORT: u16 = 52020; + // Filesystem paths shared by multiple builders. Single-consumer paths live in their builder. pub const NIFI_CONFIG_DIRECTORY: &str = "/stackable/nifi/conf"; pub const NIFI_PYTHON_WORKING_DIRECTORY: &str = "/nifi-python-working-directory"; diff --git a/rust/operator-binary/src/controller/build/jvm.rs b/rust/operator-binary/src/controller/build/jvm.rs index cbd21966..3ee66731 100644 --- a/rust/operator-binary/src/controller/build/jvm.rs +++ b/rust/operator-binary/src/controller/build/jvm.rs @@ -9,10 +9,7 @@ use stackable_operator::{ use crate::{ controller::{ ValidatedNifiConfig, - build::{ - NIFI_CONFIG_DIRECTORY, properties::ConfigFileName, - resource::probes::MANAGEMENT_SERVER_PORT, - }, + build::{MANAGEMENT_SERVER_PORT, NIFI_CONFIG_DIRECTORY, properties::ConfigFileName}, }, security::{ authentication::{STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD}, diff --git a/rust/operator-binary/src/controller/build/resource/probes.rs b/rust/operator-binary/src/controller/build/resource/probes.rs index 75ab1fc3..942903dd 100644 --- a/rust/operator-binary/src/controller/build/resource/probes.rs +++ b/rust/operator-binary/src/controller/build/resource/probes.rs @@ -8,12 +8,7 @@ use stackable_operator::k8s_openapi::api::core::v1::{ExecAction, Probe}; -/// Port NiFi's management server binds to by default -/// (`org.apache.nifi.management.server.address`, see -/// `ManagementServerProvider.MANAGEMENT_SERVER_DEFAULT_ADDRESS` upstream). -/// The operator pins the JVM system property to this same value (see -/// `build::jvm`), so this constant is the single source of truth for it. -pub const MANAGEMENT_SERVER_PORT: u16 = 52020; +use crate::controller::build::MANAGEMENT_SERVER_PORT; fn management_health_exec(path: &str) -> ExecAction { ExecAction { @@ -49,7 +44,6 @@ pub fn management_startup_probe() -> Probe { /// otherwise (e.g. during a rolling restart before the node has rejoined). pub fn management_readiness_probe() -> Probe { Probe { - initial_delay_seconds: Some(10), period_seconds: Some(10), timeout_seconds: Some(5), failure_threshold: Some(3), @@ -102,6 +96,11 @@ mod tests { ); assert_eq!(probe.failure_threshold, Some(3)); assert_eq!(probe.timeout_seconds, Some(5)); + assert_eq!( + probe.initial_delay_seconds, None, + "readiness probe delay is redundant: k8s already suppresses readiness checks \ + until the startup probe succeeds" + ); } #[test] From 6e048007ab1d502c42bd22b36a5ae1732577967d Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:27:49 +0200 Subject: [PATCH 10/13] Update the changelog and cleanups --- CHANGELOG.md | 5 ++--- .../src/controller/build/resource/probes.rs | 11 +---------- .../src/controller/build/resource/statefulset.rs | 6 ------ 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c279084c..5900c503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,7 @@ All notable changes to this project will be documented in this file. previously accepted by the API server but failed reconciliation ([#966]). - All product containers now run with `securityContext.runAsNonRoot` set to `true` to improve security ([#975]). - NiFi 2.x startup and readiness probes now use the local management server's `/health` and - `/health/cluster` endpoints instead of a bare TCP check, so a node is only reported Ready once - it has actually joined the cluster (e.g. during a rolling restart) ([#TBD]). + `/health/cluster` endpoints instead of a bare TCP check ([#976]). ### Fixed @@ -28,7 +27,7 @@ All notable changes to this project will be documented in this file. [#966]: https://github.com/stackabletech/nifi-operator/pull/966 [#970]: https://github.com/stackabletech/nifi-operator/pull/970 [#975]: https://github.com/stackabletech/nifi-operator/pull/975 -[#TBD]: https://github.com/stackabletech/nifi-operator/pull/TBD +[#976]: https://github.com/stackabletech/nifi-operator/pull/976 ## [26.7.0] - 2026-07-21 diff --git a/rust/operator-binary/src/controller/build/resource/probes.rs b/rust/operator-binary/src/controller/build/resource/probes.rs index 942903dd..03a45ab2 100644 --- a/rust/operator-binary/src/controller/build/resource/probes.rs +++ b/rust/operator-binary/src/controller/build/resource/probes.rs @@ -1,6 +1,6 @@ //! Builds the exec-based startup and readiness probes that check NiFi 2.x's //! local, unauthenticated management-server endpoints (`/health` and -//! `/health/cluster`), rather than only checking that the HTTPS port is open. +//! `/health/cluster`). //! //! The management server binds `127.0.0.1` only, so these must be `exec` //! probes using `curl` from inside the container - a `httpGet` probe cannot @@ -23,11 +23,6 @@ fn management_health_exec(path: &str) -> ExecAction { ]), } } - -/// Gates startup on the management server reporting the app server has -/// booted. `/health` only starts responding once NiFi's own web server (and -/// flow load) has completed, so this replaces the old blunt TCP-socket check -/// on the HTTPS port. pub fn management_startup_probe() -> Probe { Probe { initial_delay_seconds: Some(10), @@ -38,10 +33,6 @@ pub fn management_startup_probe() -> Probe { ..Probe::default() } } - -/// Gates readiness on cluster membership: `/health/cluster` returns 200 only -/// while the node's `ConnectionState` is `CONNECTED`/`CONNECTING`, and 503 -/// otherwise (e.g. during a rolling restart before the node has rejoined). pub fn management_readiness_probe() -> Probe { Probe { period_seconds: Some(10), diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 3278552f..54fa31dd 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -447,13 +447,7 @@ pub(crate) fn build_node_rolegroup_statefulset( }), ..Probe::default() }) - // Gates startup on the NiFi 2.x management server reporting the app - // server has booted (rather than just the HTTPS port accepting - // connections). .startup_probe(management_startup_probe()) - // Gates readiness on the node actually being connected to the - // cluster, so a node that is up but not yet rejoined after a - // rolling restart is correctly reported as not Ready. .readiness_probe(management_readiness_probe()) .resources(merged_config.resources.clone().into()); From 19c6f97da08eb93bcb8b04982aded17ca72823d7 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Tue, 11 Aug 2026 10:04:18 +0200 Subject: [PATCH 11/13] Move liveliness probe to probes.rs --- .../src/controller/build/resource/probes.rs | 19 ++++++++++++++++-- .../controller/build/resource/statefulset.rs | 20 +++++++------------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/probes.rs b/rust/operator-binary/src/controller/build/resource/probes.rs index 03a45ab2..1cac0cad 100644 --- a/rust/operator-binary/src/controller/build/resource/probes.rs +++ b/rust/operator-binary/src/controller/build/resource/probes.rs @@ -6,9 +6,12 @@ //! probes using `curl` from inside the container - a `httpGet` probe cannot //! reach a loopback-only address. -use stackable_operator::k8s_openapi::api::core::v1::{ExecAction, Probe}; +use stackable_operator::k8s_openapi::{ + api::core::v1::{ExecAction, Probe, TCPSocketAction}, + apimachinery::pkg::util::intstr::IntOrString, +}; -use crate::controller::build::MANAGEMENT_SERVER_PORT; +use crate::controller::build::{HTTPS_PORT_NAME, MANAGEMENT_SERVER_PORT}; fn management_health_exec(path: &str) -> ExecAction { ExecAction { @@ -43,6 +46,18 @@ pub fn management_readiness_probe() -> Probe { } } +pub fn tcp_liveliness_probe() -> Probe { + Probe { + initial_delay_seconds: Some(10), + period_seconds: Some(10), + tcp_socket: Some(TCPSocketAction { + port: IntOrString::String(HTTPS_PORT_NAME.to_string()), + ..TCPSocketAction::default() + }), + ..Probe::default() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 54fa31dd..af0c3cc4 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -21,11 +21,11 @@ use stackable_operator::{ apps::v1::{StatefulSet, StatefulSetSpec, StatefulSetUpdateStrategy}, core::v1::{ ConfigMapKeySelector, ConfigMapVolumeSource, EmptyDirVolumeSource, EnvVar, - EnvVarSource, ObjectFieldSelector, PersistentVolumeClaim, Probe, - SecretVolumeSource, TCPSocketAction, Volume, + EnvVarSource, ObjectFieldSelector, PersistentVolumeClaim, SecretVolumeSource, + Volume, }, }, - apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, + apimachinery::pkg::apis::meta::v1::LabelSelector, }, memory::{BinaryMultiple, MemoryQuantity}, product_logging::{ @@ -59,7 +59,9 @@ use crate::{ LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build_group_listener_pvc, group_listener_name, }, - probes::{management_readiness_probe, management_startup_probe}, + probes::{ + management_readiness_probe, management_startup_probe, tcp_liveliness_probe, + }, }, }, }, @@ -438,15 +440,7 @@ pub(crate) fn build_node_rolegroup_statefulset( .add_container_port(HTTPS_PORT_NAME, HTTPS_PORT.into()) .add_container_port(PROTOCOL_PORT_NAME, PROTOCOL_PORT.into()) .add_container_port(BALANCE_PORT_NAME, BALANCE_PORT.into()) - .liveness_probe(Probe { - initial_delay_seconds: Some(10), - period_seconds: Some(10), - tcp_socket: Some(TCPSocketAction { - port: IntOrString::String(HTTPS_PORT_NAME.to_string()), - ..TCPSocketAction::default() - }), - ..Probe::default() - }) + .liveness_probe(tcp_liveliness_probe()) .startup_probe(management_startup_probe()) .readiness_probe(management_readiness_probe()) .resources(merged_config.resources.clone().into()); From 2033b58ce1df70ed9454c521b06d41b73ae1313d Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:25:21 +0200 Subject: [PATCH 12/13] Fix the kuttl smoke test --- tests/templates/kuttl/smoke/35-assert.yaml.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/templates/kuttl/smoke/35-assert.yaml.j2 b/tests/templates/kuttl/smoke/35-assert.yaml.j2 index 57409959..3a8346d9 100644 --- a/tests/templates/kuttl/smoke/35-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/35-assert.yaml.j2 @@ -36,6 +36,7 @@ commands: java.arg.10=-Djavax.security.auth.useSubjectCredsOnly=true java.arg.11=-Dzookeeper.admin.enableServer=false java.arg.12=-Djava.security.properties=/stackable/nifi/conf/security.properties + java.arg.13=-Dorg.apache.nifi.management.server.address=127.0.0.1:52020 java.arg.2=-Xms3276m java.arg.3=-XX:+UseG1GC java.arg.4=-Djava.awt.headless=true From a5795a9185c949e5747be94dd7d107be1a089be4 Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:00:55 +0200 Subject: [PATCH 13/13] Extract the management IP into a constant --- rust/operator-binary/src/controller/build.rs | 5 ++++- rust/operator-binary/src/controller/build/jvm.rs | 9 +++++++-- .../src/controller/build/resource/probes.rs | 6 ++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 5e3b7d73..d0a2d7e6 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -46,7 +46,7 @@ pub const PROTOCOL_PORT: Port = Port(9088); pub const BALANCE_PORT_NAME: &str = "balance"; pub const BALANCE_PORT: Port = Port(6243); -/// Port NiFi's management server binds to by default +/// Loopback address NiFi's management server binds to by default /// (`org.apache.nifi.management.server.address`, see /// `ManagementServerProvider.MANAGEMENT_SERVER_DEFAULT_ADDRESS` upstream). /// The operator pins the JVM system property to this same value (see @@ -54,6 +54,9 @@ pub const BALANCE_PORT: Port = Port(6243); /// Note: a user-supplied `jvmArgumentOverrides` that removes or repoints the /// corresponding `-D` property would desync this from the actual /// management-server address, silently breaking both probes. +pub const MANAGEMENT_SERVER_ADDRESS: &str = "127.0.0.1"; +/// Port NiFi's management server binds to by default, see +/// [`MANAGEMENT_SERVER_ADDRESS`]. pub const MANAGEMENT_SERVER_PORT: u16 = 52020; // Filesystem paths shared by multiple builders. Single-consumer paths live in their builder. diff --git a/rust/operator-binary/src/controller/build/jvm.rs b/rust/operator-binary/src/controller/build/jvm.rs index 3ee66731..f28ec785 100644 --- a/rust/operator-binary/src/controller/build/jvm.rs +++ b/rust/operator-binary/src/controller/build/jvm.rs @@ -9,7 +9,10 @@ use stackable_operator::{ use crate::{ controller::{ ValidatedNifiConfig, - build::{MANAGEMENT_SERVER_PORT, NIFI_CONFIG_DIRECTORY, properties::ConfigFileName}, + build::{ + MANAGEMENT_SERVER_ADDRESS, MANAGEMENT_SERVER_PORT, NIFI_CONFIG_DIRECTORY, + properties::ConfigFileName, + }, }, security::{ authentication::{STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD}, @@ -89,7 +92,9 @@ pub fn build_merged_jvm_config( // Pin the NiFi 2.x management server (used by the startup/readiness // probes in resource::probes) to a known address instead of relying // on its undocumented upstream default. - format!("-Dorg.apache.nifi.management.server.address=127.0.0.1:{MANAGEMENT_SERVER_PORT}"), + format!( + "-Dorg.apache.nifi.management.server.address={MANAGEMENT_SERVER_ADDRESS}:{MANAGEMENT_SERVER_PORT}" + ), ]; // Add JVM truststore properties when OPA TLS is enabled diff --git a/rust/operator-binary/src/controller/build/resource/probes.rs b/rust/operator-binary/src/controller/build/resource/probes.rs index 1cac0cad..7045e6c3 100644 --- a/rust/operator-binary/src/controller/build/resource/probes.rs +++ b/rust/operator-binary/src/controller/build/resource/probes.rs @@ -11,7 +11,9 @@ use stackable_operator::k8s_openapi::{ apimachinery::pkg::util::intstr::IntOrString, }; -use crate::controller::build::{HTTPS_PORT_NAME, MANAGEMENT_SERVER_PORT}; +use crate::controller::build::{ + HTTPS_PORT_NAME, MANAGEMENT_SERVER_ADDRESS, MANAGEMENT_SERVER_PORT, +}; fn management_health_exec(path: &str) -> ExecAction { ExecAction { @@ -21,7 +23,7 @@ fn management_health_exec(path: &str) -> ExecAction { "pipefail".to_string(), "-c".to_string(), format!( - "curl --fail --silent --show-error --output /dev/null http://127.0.0.1:{MANAGEMENT_SERVER_PORT}{path}" + "curl --fail --silent --show-error --output /dev/null http://{MANAGEMENT_SERVER_ADDRESS}:{MANAGEMENT_SERVER_PORT}{path}" ), ]), }