From 41a85285caf15c22e6670f9ff1e50831ba85ddbc Mon Sep 17 00:00:00 2001 From: Maxi Wittich Date: Wed, 11 Nov 2026 09:40:16 +0100 Subject: [PATCH 1/2] feat: Make containerdebug configurable Adds the containerdebug config (enabled, interval) from stackable-operator 0.122.0 to the NiFi node config and starts containerdebug from the validated, merged role group config. --- extra/crds.yaml | 40 +++++++++++++ .../build/properties/security_properties.rs | 16 +++-- .../controller/build/resource/statefulset.rs | 60 ++++++++++++++++++- rust/operator-binary/src/controller/mod.rs | 12 +++- .../src/controller/validate.rs | 20 ++++++- rust/operator-binary/src/crd/mod.rs | 12 +++- 6 files changed, 150 insertions(+), 10 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index 8d4b83a7..e4e872f2 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -564,6 +564,26 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true type: object + containerdebug: + default: + enabled: null + interval: null + description: |- + Configures the `containerdebug` tool, which periodically logs information about the container + environment (such as network interfaces, DNS lookups and disk usage) in the background of the + product container. + properties: + enabled: + description: Whether `containerdebug` runs in the background of the product container. Defaults to `true`. + nullable: true + type: boolean + interval: + description: |- + Time between two runs of `containerdebug`, e.g. `30m`, `1h` or `2d`. Must not be zero. + Defaults to `30m`. + nullable: true + type: string + type: object gracefulShutdownTimeout: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. nullable: true @@ -1427,6 +1447,26 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true type: object + containerdebug: + default: + enabled: null + interval: null + description: |- + Configures the `containerdebug` tool, which periodically logs information about the container + environment (such as network interfaces, DNS lookups and disk usage) in the background of the + product container. + properties: + enabled: + description: Whether `containerdebug` runs in the background of the product container. Defaults to `true`. + nullable: true + type: boolean + interval: + description: |- + Time between two runs of `containerdebug`, e.g. `30m`, `1h` or `2d`. Must not be zero. + Defaults to `30m`. + nullable: true + type: string + type: object gracefulShutdownTimeout: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. nullable: true diff --git a/rust/operator-binary/src/controller/build/properties/security_properties.rs b/rust/operator-binary/src/controller/build/properties/security_properties.rs index 67ac2fc7..73be8db8 100644 --- a/rust/operator-binary/src/controller/build/properties/security_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/security_properties.rs @@ -24,14 +24,15 @@ pub fn build(rg: &NifiRoleGroupConfig) -> Result mod tests { use std::collections::BTreeMap; - use stackable_operator::v2::{ - builder::pod::container::EnvVarSet, config_overrides::KeyValueConfigOverrides, + use stackable_operator::{ + config::fragment, + v2::{builder::pod::container::EnvVarSet, config_overrides::KeyValueConfigOverrides}, }; use super::*; use crate::{ controller::ValidatedNifiConfig, - crd::{NifiConfig, v1alpha1::NifiConfigOverrides}, + crd::{NifiConfig, NifiRole, v1alpha1::NifiConfigOverrides}, }; fn make_rg(overrides: Option>) -> NifiRoleGroupConfig { @@ -39,7 +40,14 @@ mod tests { NifiRoleGroupConfig { replicas: Some(1), - config: ValidatedNifiConfig::from_merged_for_test(NifiConfig::default()), + config: ValidatedNifiConfig::from_merged_for_test( + fragment::validate(NifiConfig::default_config( + "simple-nifi", + &NifiRole::Node, + None, + )) + .expect("the default config is complete"), + ), config_overrides: NifiConfigOverrides { security_properties: KeyValueConfigOverrides { overrides: overrides.unwrap_or_default(), diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 076ef169..06b35ed1 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -419,7 +419,7 @@ pub(crate) fn build_node_rolegroup_statefulset( {remove_vector_shutdown_file_command} prepare_signal_handlers mkdir --parents \"${file_log_directory}\" || true - containerdebug --output={STACKABLE_LOG_DIR}/containerdebug-state.json --loop & + {containerdebug_command} bin/nifi.sh run & wait_for_termination $! {create_vector_shutdown_file_command} @@ -427,6 +427,10 @@ pub(crate) fn build_node_rolegroup_statefulset( remove_vector_shutdown_file_command = remove_vector_shutdown_file_command(STACKABLE_LOG_DIR), file_log_directory = FILE_LOG_DIRECTORY_ENV.as_ref(), + containerdebug_command = merged_config + .containerdebug + .background_command(STACKABLE_LOG_DIR) + .unwrap_or_default(), create_vector_shutdown_file_command = create_vector_shutdown_file_command(STACKABLE_LOG_DIR), }]; @@ -783,9 +787,14 @@ fn get_volume_claim_templates( #[cfg(test)] mod tests { + use rstest::rstest; + use stackable_operator::{ + crd::containerdebug::ValidatedContainerdebugConfig, shared::time::Duration, + }; + use super::*; use crate::controller::build::properties::test_support::{ - default_rg, validated_cluster_from_yaml, + default_rg, minimal_validated_cluster, validated_cluster_from_yaml, }; #[test] @@ -921,4 +930,51 @@ mod tests { "unexpected error: {error:?}" ); } + + /// The nifi container starts `containerdebug` from the role group's validated config. The + /// command itself is owned (and tested) by operator-rs, so only the wiring is checked here. + #[rstest] + #[case::enabled(ValidatedContainerdebugConfig::Enabled { + interval: Duration::from_minutes_unchecked(10) + })] + #[case::disabled(ValidatedContainerdebugConfig::Disabled)] + fn nifi_container_starts_containerdebug_from_config( + #[case] containerdebug: ValidatedContainerdebugConfig, + ) { + let expected = containerdebug.background_command(STACKABLE_LOG_DIR); + let mut cluster = minimal_validated_cluster(); + let role_group_name = RoleGroupName::from_str("default").expect("valid role group name"); + cluster + .role_group_configs + .get_mut(&NifiRole::Node) + .and_then(|role_groups| role_groups.get_mut(&role_group_name)) + .expect("the default role group exists") + .config + .containerdebug = containerdebug; + let rg = default_rg(&cluster); + + let stateful_set = build_node_rolegroup_statefulset(&cluster, &role_group_name, rg, None) + .expect("the StatefulSet builds"); + + let args = stateful_set + .spec + .expect("the StatefulSet has a spec") + .template + .spec + .expect("the pod template has a spec") + .containers + .into_iter() + .find(|container| container.name == NIFI_CONTAINER_NAME.to_string()) + .expect("the nifi container exists") + .args + .expect("the nifi container has args") + .join("\n"); + assert_eq!( + args.lines() + .map(str::trim) + .find(|line| line.starts_with("containerdebug ")), + expected.as_deref(), + "args: {args}" + ); + } } diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs index aa1e5086..d56b96f0 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller/mod.rs @@ -12,7 +12,7 @@ use stackable_operator::{ resources::{NoRuntimeLimits, Resources}, }, constant, - crd::{git_sync, listener}, + crd::{containerdebug::ValidatedContainerdebugConfig, git_sync, listener}, k8s_openapi::{ api::{ apps::v1::StatefulSet, @@ -100,6 +100,8 @@ pub struct ValidatedNifiConfig { pub resources: Resources, /// Pod (anti-)affinity for the role group. pub affinity: StackableAffinity, + /// The validated `containerdebug` configuration. + pub containerdebug: ValidatedContainerdebugConfig, /// Time period Pods have to gracefully shut down. pub graceful_shutdown_timeout: Option, /// Requested lifetime of the auto-TLS secret. @@ -117,12 +119,14 @@ pub struct ValidatedNifiConfig { impl ValidatedNifiConfig { pub(crate) fn from_merged( merged: NifiConfig, + containerdebug: ValidatedContainerdebugConfig, logging: ValidatedLogging, git_sync_resources: git_sync::v1alpha2::GitSyncResources, ) -> Self { Self { resources: merged.resources, affinity: merged.affinity, + containerdebug, graceful_shutdown_timeout: merged.graceful_shutdown_timeout, requested_secret_lifetime: merged.requested_secret_lifetime, logging, @@ -134,8 +138,14 @@ impl ValidatedNifiConfig { pub(crate) fn from_merged_for_test(merged: NifiConfig) -> Self { use stackable_operator::product_logging::spec::AutomaticContainerLogConfig; + let containerdebug = merged + .containerdebug + .validate() + .expect("the containerdebug config is valid"); + Self::from_merged( merged, + containerdebug, ValidatedLogging { nifi_container: ValidatedContainerLogConfigChoice::Automatic( AutomaticContainerLogConfig::default(), diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index f6b8d340..6e7b8378 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -10,6 +10,7 @@ use stackable_operator::{ cli::OperatorEnvironmentOptions, commons::product_image_selection, config::fragment, + crd::containerdebug, k8s_openapi::api::core::v1::Secret, kube::{ResourceExt as _, runtime::reflector::ObjectRef}, product_logging::spec::Logging, @@ -70,6 +71,12 @@ pub enum Error { #[snafu(display("failed to validate the rolegroup config fragment"))] ValidateRoleGroupConfig { source: fragment::ValidationError }, + #[snafu(display("invalid containerdebug configuration for role group {role_group}"))] + InvalidContainerdebugConfig { + source: containerdebug::Error, + role_group: RoleGroupName, + }, + #[snafu(display("the role-group name {role_group:?} is invalid"))] ParseRoleGroupName { source: stackable_operator::v2::macros::attributed_string_type::Error, @@ -326,6 +333,12 @@ pub(crate) fn build_role_group_configs( // type; only the conversion into an `EnvVarSet` remains. let env_overrides_set: EnvVarSet = env_overrides.into(); + let containerdebug = config.containerdebug.validate().with_context(|_| { + InvalidContainerdebugConfigSnafu { + role_group: role_group_name.clone(), + } + })?; + // Validate the logging config (NiFi + optional Vector container) up-front so an invalid // custom log ConfigMap name, or a missing Vector aggregator discovery ConfigMap name, fails // during validation rather than at resource-build time. @@ -345,7 +358,12 @@ pub(crate) fn build_role_group_configs( role_group_name, NifiRoleGroupConfig { replicas: validated.replicas, - config: ValidatedNifiConfig::from_merged(config, logging, git_sync_resources), + config: ValidatedNifiConfig::from_merged( + config, + containerdebug, + logging, + git_sync_resources, + ), config_overrides, env_overrides: env_overrides_set, cli_overrides, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index d75eac9b..4dcb32c7 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -23,7 +23,11 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, constant, - crd::{authentication::core as auth_core, git_sync}, + crd::{ + authentication::core as auth_core, + containerdebug::{default_containerdebug_config, v1alpha1::ContainerdebugConfig}, + git_sync, + }, deep_merger::ObjectOverrides, k8s_openapi::{api::core::v1::Volume, apimachinery::pkg::api::resource::Quantity}, kube::CustomResource, @@ -273,7 +277,7 @@ pub enum Container { GitSync, } -#[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( derive( Clone, @@ -300,6 +304,9 @@ pub struct NifiConfig { #[fragment_attrs(serde(default))] pub affinity: StackableAffinity, + #[fragment_attrs(serde(default))] + pub containerdebug: ContainerdebugConfig, + /// Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. #[fragment_attrs(serde(default))] pub graceful_shutdown_timeout: Option, @@ -364,6 +371,7 @@ impl NifiConfig { }, }, affinity: get_affinity(cluster_name, role, opa_config), + containerdebug: default_containerdebug_config(), graceful_shutdown_timeout: Some(DEFAULT_NODE_GRACEFUL_SHUTDOWN_TIMEOUT), requested_secret_lifetime: Some(Self::DEFAULT_NODE_SECRET_LIFETIME), } From bc878da7889a37676022811cbcd2384346b6e77d Mon Sep 17 00:00:00 2001 From: Maxi Wittich Date: Wed, 11 Nov 2026 09:50:36 +0100 Subject: [PATCH 2/2] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a020747c..0dd2afc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. the Vector aggregator ([#1020]). - Add `/ready` endpoint to the operator Deployment, which reports the CRD installation status ([#999]). - Nodes now have a default affinity to the OPA Pods when OPA authorization is configured ([#1002]). +- Add `containerdebug` config CRD ([#1023]). ### Changed @@ -89,6 +90,7 @@ All notable changes to this project will be documented in this file. [#1015]: https://github.com/stackabletech/nifi-operator/pull/1015 [#1017]: https://github.com/stackabletech/nifi-operator/pull/1017 [#1020]: https://github.com/stackabletech/nifi-operator/pull/1020 +[#1023]: https://github.com/stackabletech/nifi-operator/pull/1023 ## [26.7.0] - 2026-07-21