From 554cad222681574faa71a2a24d944b8517220939 Mon Sep 17 00:00:00 2001 From: Maxi Wittich Date: Wed, 11 Nov 2026 11:42:39 +0100 Subject: [PATCH 1/2] feat: Make containerdebug configurable Adds the containerdebug config (enabled, interval) from stackable-operator 0.122.0 to the OPA server config and starts containerdebug from the validated, merged role group config. --- extra/crds.yaml | 80 +++++++++++++++++++ .../controller/build/resource/workload/mod.rs | 80 ++++++++++++++++++- rust/operator-binary/src/controller/mod.rs | 9 ++- .../src/controller/validate.rs | 19 ++++- rust/operator-binary/src/crd/mod.rs | 7 +- 5 files changed, 191 insertions(+), 4 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index 927f1609..f6f7068a 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -875,6 +875,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 @@ -1622,6 +1642,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 @@ -3217,6 +3257,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 @@ -3964,6 +4024,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/resource/workload/mod.rs b/rust/operator-binary/src/controller/build/resource/workload/mod.rs index e3e7d87f..248b2bb3 100644 --- a/rust/operator-binary/src/controller/build/resource/workload/mod.rs +++ b/rust/operator-binary/src/controller/build/resource/workload/mod.rs @@ -674,7 +674,7 @@ fn build_opa_start_command( {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} opa run -s -a 0.0.0.0:{bind_port} -c {CONFIG_DIR}/{config_file} -l {opa_log_level} --shutdown-grace-period {shutdown_grace_period_s} --disable-telemetry {tls_flags} {extra_cli_args} {logging_redirects} & wait_for_termination $! {create_vector_shutdown_file_command} @@ -684,6 +684,10 @@ fn build_opa_start_command( create_vector_shutdown_file_command = create_vector_shutdown_file_command(STACKABLE_LOG_DIR), file_log_directory = FILE_LOG_DIRECTORY.as_ref(), + containerdebug_command = merged_config + .containerdebug + .background_command(STACKABLE_LOG_DIR) + .unwrap_or_default(), shutdown_grace_period_s = merged_config.graceful_shutdown_timeout.unwrap_or(DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT).as_secs(), opa_log_level = [console_log_level, file_log_level].iter().min().unwrap_or(&LogLevel::INFO).to_opa_literal(), extra_cli_args = extra_cli_args @@ -789,3 +793,77 @@ fn build_prepare_start_command( prepare_container_args } + +#[cfg(test)] +mod tests { + use serde_json::json; + use stackable_opa_operator::crd::OpaRole; + use stackable_operator::{ + commons::networking::DomainName, crd::containerdebug::ValidatedContainerdebugConfig, + shared::time::Duration, + }; + + use super::*; + use crate::controller::build::properties::test_support::validated_cluster_from_spec; + + /// Builds the `default` server Pod template with the given `containerdebug` config and returns + /// the `containerdebug` line of the opa container's args, if any. + fn containerdebug_line(containerdebug: ValidatedContainerdebugConfig) -> Option { + let cluster = validated_cluster_from_spec(json!({ + "image": { "productVersion": "1.2.3" }, + "servers": { "roleGroups": { "default": {} } }, + })); + let (role_group_name, role_group) = cluster.role_group_configs[&OpaRole::Server] + .iter() + .next() + .expect("the default role group should exist"); + let mut role_group = role_group.clone(); + role_group.config.containerdebug = containerdebug; + + build_server_rolegroup_pod_template( + &cluster, + role_group_name, + &role_group, + "bundle-builder-image", + "user-info-fetcher-image", + "resource-info-fetcher-image", + &KubernetesClusterInfo { + cluster_domain: DomainName::try_from("cluster.local").unwrap(), + }, + ) + .expect("the Pod template should build") + .spec + .expect("the Pod template has a spec") + .containers + .into_iter() + .find(|container| container.name == Container::Opa.name().to_string()) + .expect("the opa container exists") + .args + .expect("the opa container has args") + .join("\n") + .lines() + .map(str::trim) + .find(|line| line.starts_with("containerdebug ")) + .map(str::to_owned) + } + + /// The opa 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. + #[test] + fn opa_container_starts_containerdebug_when_enabled() { + let containerdebug = ValidatedContainerdebugConfig::Enabled { + interval: Duration::from_minutes_unchecked(10), + }; + let expected = containerdebug.background_command(STACKABLE_LOG_DIR); + + assert_eq!(containerdebug_line(containerdebug), expected); + } + + #[test] + fn opa_container_does_not_start_containerdebug_when_disabled() { + assert_eq!( + containerdebug_line(ValidatedContainerdebugConfig::Disabled), + None + ); + } +} diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs index 3f41e397..77b109fb 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller/mod.rs @@ -15,6 +15,7 @@ use stackable_operator::{ product_image_selection::ResolvedProductImage, resources::{NoRuntimeLimits, Resources}, }, + crd::containerdebug::ValidatedContainerdebugConfig, k8s_openapi::api::{ apps::v1::{DaemonSet, Deployment}, core::v1::{ConfigMap, Service, ServiceAccount}, @@ -241,15 +242,21 @@ pub struct ValidatedOpaConfig { pub resources: Resources, pub logging: validate::ValidatedLogging, pub affinity: StackableAffinity, + pub containerdebug: ValidatedContainerdebugConfig, pub graceful_shutdown_timeout: Option, } impl ValidatedOpaConfig { - pub(crate) fn from_merged(merged: OpaConfig, logging: validate::ValidatedLogging) -> Self { + pub(crate) fn from_merged( + merged: OpaConfig, + containerdebug: ValidatedContainerdebugConfig, + logging: validate::ValidatedLogging, + ) -> Self { Self { resources: merged.resources, logging, affinity: merged.affinity, + containerdebug, graceful_shutdown_timeout: merged.graceful_shutdown_timeout, } } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 4ceea8f5..6e972656 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -11,6 +11,7 @@ use stackable_opa_operator::crd::{Container, OpaConfig, OpaRole, OpaRoleType, v1 use stackable_operator::{ cli::OperatorEnvironmentOptions, commons::product_image_selection, + crd::containerdebug, product_logging::spec::Logging, v2::{ controller_utils::{get_cluster_name, get_namespace, get_uid}, @@ -54,6 +55,12 @@ pub enum Error { role_group: String, }, + #[snafu(display("invalid containerdebug config for role group {role_group:?}"))] + InvalidContainerdebugConfig { + source: containerdebug::Error, + role_group: String, + }, + #[snafu(display("the role group name {role_group:?} is invalid"))] ParseRoleGroupName { source: ::Err, @@ -234,6 +241,12 @@ pub fn validate( role_group: role_group_name.clone(), })?; + let containerdebug = merged.config.config.containerdebug.validate().context( + InvalidContainerdebugConfigSnafu { + role_group: role_group_name.clone(), + }, + )?; + // Validate the logging configuration up-front (borrows the merged config before it is // moved into the `OpaRoleGroupConfig` below). let logging = validate_logging( @@ -254,7 +267,11 @@ pub fn validate( // Only used in `Deployment` mode; a DaemonSet derives its Pod count from the // number of nodes. replicas: merged.replicas, - config: ValidatedOpaConfig::from_merged(merged.config.config, logging), + config: ValidatedOpaConfig::from_merged( + merged.config.config, + containerdebug, + logging, + ), config_overrides: merged.config.config_overrides, env_overrides: merged.config.env_overrides.into(), cli_overrides: merged.config.cli_overrides, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 3924b52a..15fa8e63 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -13,6 +13,7 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, constant, + crd::containerdebug::{default_containerdebug_config, v1alpha1::ContainerdebugConfig}, deep_merger::ObjectOverrides, k8s_openapi::apimachinery::pkg::api::resource::Quantity, kube::CustomResource, @@ -315,7 +316,7 @@ impl Container { // NOTE (@Techassi): This struct can currently NOT be versioned because it is used via Role which // makes it incredible hard to implement the From trait for conversions. -#[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( derive( Clone, @@ -339,6 +340,9 @@ pub struct OpaConfig { #[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, @@ -441,6 +445,7 @@ impl OpaConfig { // Spreads the role's Pods across nodes. v1alpha2::WorkloadKind::Deployment => affinity::get_affinity(cluster_name, role), }, + containerdebug: default_containerdebug_config(), graceful_shutdown_timeout: Some(DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT), } } From 5602db03f7dc0be7339ad040430b52564196bbea Mon Sep 17 00:00:00 2001 From: Maxi Wittich Date: Wed, 11 Nov 2026 12:14:53 +0100 Subject: [PATCH 2/2] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f85942d..17f807ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to this project will be documented in this file. - Add `/ready` endpoint to the operator Deployment, which reports the CRD installation status ([#894]). - The logs of the `containerdebug` process are now collected by the Vector agent and forwarded to the Vector aggregator ([#912]). +- Add `containerdebug` config CRD ([#914]). ### Changed @@ -133,6 +134,7 @@ All notable changes to this project will be documented in this file. [#908]: https://github.com/stackabletech/opa-operator/pull/908 [#909]: https://github.com/stackabletech/opa-operator/pull/909 [#912]: https://github.com/stackabletech/opa-operator/pull/912 +[#914]: https://github.com/stackabletech/opa-operator/pull/914 ## [26.7.0] - 2026-07-21