diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f4b2685..e0992a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,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 ([#1090]). - The logs of the `containerdebug` process are now collected by the Vector agent and forwarded to the Vector aggregator ([#1106]). +- Add `containerdebug` config CRD ([#1108]). ### Changed @@ -75,6 +76,7 @@ All notable changes to this project will be documented in this file. [#1101]: https://github.com/stackabletech/zookeeper-operator/pull/1101 [#1102]: https://github.com/stackabletech/zookeeper-operator/pull/1102 [#1106]: https://github.com/stackabletech/zookeeper-operator/pull/1106 +[#1108]: https://github.com/stackabletech/zookeeper-operator/pull/1108 ## [26.7.0] - 2026-07-21 diff --git a/extra/crds.yaml b/extra/crds.yaml index 1c76e1b9..2445ea31 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -298,6 +298,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 @@ -849,6 +869,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/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 921d9f8d..6d1f2071 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -13,7 +13,10 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, constant, - crd::ClusterRef, + crd::{ + ClusterRef, + containerdebug::{default_containerdebug_config, v1alpha1::ContainerdebugConfig}, + }, deep_merger::ObjectOverrides, k8s_openapi::apimachinery::pkg::api::resource::Quantity, kube::CustomResource, @@ -203,7 +206,7 @@ pub mod versioned { pub tls: Option, } - #[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] + #[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( derive( Clone, @@ -232,6 +235,9 @@ pub mod versioned { #[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, @@ -433,6 +439,7 @@ impl v1alpha1::ZookeeperConfig { }, logging: product_logging::spec::default_logging(), affinity: get_affinity(cluster_name, role), + containerdebug: default_containerdebug_config(), graceful_shutdown_timeout: Some(DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT), requested_secret_lifetime: Some(Self::DEFAULT_SECRET_LIFETIME), } diff --git a/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs b/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs index 26c19b07..b825187e 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs @@ -292,13 +292,17 @@ pub fn build_server_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/zkServer.sh start-foreground {STACKABLE_RW_CONFIG_DIR}/{zoo_cfg} & wait_for_termination $! {create_vector_shutdown_file_command} ", zoo_cfg = ConfigFileName::ZooCfg, file_log_directory = FILE_LOG_DIRECTORY.as_ref(), + containerdebug_command = merged_config + .containerdebug + .background_command(STACKABLE_LOG_DIR) + .unwrap_or_default(), remove_vector_shutdown_file_command = remove_vector_shutdown_file_command(STACKABLE_LOG_DIR), create_vector_shutdown_file_command = @@ -480,7 +484,11 @@ fn common_env_vars(merged_config: &ValidatedZookeeperConfig) -> EnvVarSet { #[cfg(test)] mod tests { - use stackable_operator::k8s_openapi::api::core::v1::{Container, EnvVar}; + use stackable_operator::{ + crd::containerdebug::ValidatedContainerdebugConfig, + k8s_openapi::api::core::v1::{Container, EnvVar}, + shared::time::Duration, + }; use super::*; use crate::zk_controller::test_support::{minimal_zk, validated_cluster}; @@ -661,4 +669,64 @@ mod tests { ); assert_eq!(zoocfgdir[0].value.as_deref(), Some("/custom/conf/dir")); } + + /// Builds the `default` server StatefulSet with the given `containerdebug` config and returns + /// the `containerdebug` line of the zookeeper container's args, if any. + fn containerdebug_line(containerdebug: ValidatedContainerdebugConfig) -> Option { + let validated = validated_cluster(&minimal_zk( + r#" + apiVersion: zookeeper.stackable.tech/v1alpha1 + kind: ZookeeperCluster + metadata: + name: simple-zookeeper + spec: + image: + productVersion: "3.9.6" + servers: + roleGroups: + default: + replicas: 1 + "#, + )); + let rg_name = RoleGroupName::from_str("default").expect("valid role group name"); + let mut rg = validated.role_group_configs[&ZookeeperRole::Server][&rg_name].clone(); + rg.config.containerdebug = containerdebug; + + build_server_rolegroup_statefulset(&validated, &ZookeeperRole::Server, &rg_name, &rg) + .expect("statefulset builds") + .spec + .and_then(|spec| spec.template.spec) + .expect("pod spec") + .containers + .into_iter() + .find(|container| container.name == ZOOKEEPER_CONTAINER_NAME.to_string()) + .expect("the zookeeper container exists") + .args + .expect("the zookeeper container has args") + .join("\n") + .lines() + .map(str::trim) + .find(|line| line.starts_with("containerdebug ")) + .map(str::to_owned) + } + + /// The zookeeper 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 zookeeper_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 zookeeper_container_does_not_start_containerdebug_when_disabled() { + assert_eq!( + containerdebug_line(ValidatedContainerdebugConfig::Disabled), + None + ); + } } diff --git a/rust/operator-binary/src/zk_controller/validate.rs b/rust/operator-binary/src/zk_controller/validate.rs index 61ccc2d7..8ae0ad79 100644 --- a/rust/operator-binary/src/zk_controller/validate.rs +++ b/rust/operator-binary/src/zk_controller/validate.rs @@ -20,6 +20,7 @@ use stackable_operator::{ resources::{NoRuntimeLimits, Resources}, }, config::fragment, + crd::containerdebug::{self, ValidatedContainerdebugConfig}, deep_merger::ObjectOverrides, k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta, kube::Resource, @@ -80,6 +81,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("failed to get the cluster name"))] GetClusterName { source: stackable_operator::v2::controller_utils::Error, @@ -123,14 +130,19 @@ pub struct ValidatedZookeeperConfig { pub resources: Resources, pub logging: ValidatedLogging, pub affinity: StackableAffinity, + pub containerdebug: ValidatedContainerdebugConfig, pub graceful_shutdown_timeout: Option, pub requested_secret_lifetime: Option, } impl ValidatedZookeeperConfig { /// Builds the validated config from the merged [`ZookeeperConfig`], swapping in the - /// already-validated logging. - fn from_merged(merged: ZookeeperConfig, logging: ValidatedLogging) -> Self { + /// already-validated `containerdebug` and logging configuration. + fn from_merged( + merged: ZookeeperConfig, + containerdebug: ValidatedContainerdebugConfig, + logging: ValidatedLogging, + ) -> Self { Self { init_limit: merged.init_limit, sync_limit: merged.sync_limit, @@ -139,6 +151,7 @@ impl ValidatedZookeeperConfig { resources: merged.resources, logging, affinity: merged.affinity, + containerdebug, graceful_shutdown_timeout: merged.graceful_shutdown_timeout, requested_secret_lifetime: merged.requested_secret_lifetime, } @@ -493,11 +506,18 @@ fn validate_role_group_config( })?; let config = merged.config.config; + let containerdebug = + config + .containerdebug + .validate() + .with_context(|_| InvalidContainerdebugConfigSnafu { + role_group: role_group_name.to_owned(), + })?; let logging = validate_logging(&config.logging, vector_aggregator_config_map_name)?; Ok(ZookeeperRoleGroupConfig { replicas: merged.replicas, - config: ValidatedZookeeperConfig::from_merged(config, logging), + config: ValidatedZookeeperConfig::from_merged(config, containerdebug, logging), config_overrides: merged.config.config_overrides, // The env override names are validated on deserialization. env_overrides: merged.config.env_overrides.into(),