diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e25997..214c78c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - The logs of the `containerdebug` process are now collected by the Vector agent and forwarded to the Vector aggregator ([#790]). - The metastore now has a default affinity to the OPA Pods when OPA authorization is configured ([#772]). +- Add `containerdebug` config CRD ([#794]). ### Removed @@ -86,6 +87,7 @@ All notable changes to this project will be documented in this file. [#785]: https://github.com/stackabletech/hive-operator/pull/785 [#787]: https://github.com/stackabletech/hive-operator/pull/787 [#790]: https://github.com/stackabletech/hive-operator/pull/790 +[#794]: https://github.com/stackabletech/hive-operator/pull/794 ## [26.7.0] - 2026-07-21 diff --git a/extra/crds.yaml b/extra/crds.yaml index 7783eae9..78faa5c1 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -541,6 +541,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 @@ -994,6 +1014,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.rs b/rust/operator-binary/src/controller.rs index 167cf6ea..a969ebaa 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -20,7 +20,7 @@ use stackable_operator::{ resources::{NoRuntimeLimits, Resources}, }, constant, - crd::{listener::v1alpha1::Listener, s3}, + crd::{containerdebug::ValidatedContainerdebugConfig, listener::v1alpha1::Listener, s3}, database_connections::drivers::jdbc::JdbcDatabaseConnectionDetails, k8s_openapi::api::{ apps::v1::StatefulSet, @@ -115,6 +115,7 @@ pub type HiveRoleGroupConfig = stackable_operator::v2::role_utils::RoleGroupConf #[derive(Clone, Debug, PartialEq)] pub struct ValidatedMetaStoreConfig { pub affinity: StackableAffinity, + pub containerdebug: ValidatedContainerdebugConfig, pub graceful_shutdown_timeout: Option, pub logging: validate::ValidatedLogging, pub resources: Resources, @@ -123,19 +124,25 @@ pub struct ValidatedMetaStoreConfig { impl ValidatedMetaStoreConfig { /// Builds the validated config from the merged [`MetaStoreConfig`], swapping in the - /// already-validated logging. - fn from_merged(merged: MetaStoreConfig, logging: validate::ValidatedLogging) -> Self { + /// already-validated `containerdebug` and logging configuration. + fn from_merged( + merged: MetaStoreConfig, + containerdebug: ValidatedContainerdebugConfig, + logging: validate::ValidatedLogging, + ) -> Self { Self { warehouse_dir: merged.warehouse_dir, resources: merged.resources, logging, affinity: merged.affinity, + containerdebug, graceful_shutdown_timeout: merged.graceful_shutdown_timeout, } } - /// Wraps a merged [`MetaStoreConfig`] with trivial automatic logging, for builder tests - /// that only exercise the non-logging parts of the config. + /// Wraps a merged [`MetaStoreConfig`] with trivial automatic logging and its own validated + /// `containerdebug` config, for builder tests that only exercise the non-logging parts of + /// the config. #[cfg(test)] pub(crate) fn from_merged_for_test(merged: MetaStoreConfig) -> Self { use stackable_operator::{ @@ -143,8 +150,14 @@ impl ValidatedMetaStoreConfig { v2::product_logging::framework::ValidatedContainerLogConfigChoice, }; + let containerdebug = merged + .containerdebug + .validate() + .expect("the containerdebug config is valid"); + Self::from_merged( merged, + containerdebug, validate::ValidatedLogging { hive_container: ValidatedContainerLogConfigChoice::Automatic( AutomaticContainerLogConfig::default(), diff --git a/rust/operator-binary/src/controller/build/properties/hive_site.rs b/rust/operator-binary/src/controller/build/properties/hive_site.rs index 448a7327..e2e1f1df 100644 --- a/rust/operator-binary/src/controller/build/properties/hive_site.rs +++ b/rust/operator-binary/src/controller/build/properties/hive_site.rs @@ -139,14 +139,16 @@ pub fn build( mod tests { use super::*; use crate::{ - controller::build::properties::test_support::{derby_cluster_config, s3_cluster_config}, + controller::build::properties::test_support::{ + default_metastore_config, derby_cluster_config, s3_cluster_config, + }, crd::MetaStoreConfig, }; #[test] fn defaults_present_for_minimal_derby_cluster() { let cluster_config = derby_cluster_config(); - let merged = ValidatedMetaStoreConfig::from_merged_for_test(MetaStoreConfig::default()); + let merged = ValidatedMetaStoreConfig::from_merged_for_test(default_metastore_config()); let data = build( &cluster_config, @@ -176,7 +178,7 @@ mod tests { let cluster_config = derby_cluster_config(); let merged = ValidatedMetaStoreConfig::from_merged_for_test(MetaStoreConfig { warehouse_dir: Some("/custom/warehouse".to_string()), - ..MetaStoreConfig::default() + ..default_metastore_config() }); let data = build( @@ -197,7 +199,7 @@ mod tests { #[test] fn s3_connection_emits_s3a_settings() { let cluster_config = s3_cluster_config(); - let merged = ValidatedMetaStoreConfig::from_merged_for_test(MetaStoreConfig::default()); + let merged = ValidatedMetaStoreConfig::from_merged_for_test(default_metastore_config()); let data = build( &cluster_config, @@ -225,7 +227,7 @@ mod tests { #[test] fn s3_connection_registers_the_s3_scheme() { let cluster_config = s3_cluster_config(); - let merged = ValidatedMetaStoreConfig::from_merged_for_test(MetaStoreConfig::default()); + let merged = ValidatedMetaStoreConfig::from_merged_for_test(default_metastore_config()); let data = build( &cluster_config, @@ -245,7 +247,7 @@ mod tests { #[test] fn no_s3_connection_leaves_the_s3_scheme_unregistered() { let cluster_config = derby_cluster_config(); - let merged = ValidatedMetaStoreConfig::from_merged_for_test(MetaStoreConfig::default()); + let merged = ValidatedMetaStoreConfig::from_merged_for_test(default_metastore_config()); let data = build( &cluster_config, @@ -262,7 +264,7 @@ mod tests { #[test] fn user_override_wins_over_everything() { let cluster_config = derby_cluster_config(); - let merged = ValidatedMetaStoreConfig::from_merged_for_test(MetaStoreConfig::default()); + let merged = ValidatedMetaStoreConfig::from_merged_for_test(default_metastore_config()); let overrides = [("hive.metastore.port".to_string(), "1234".to_string())] .into_iter() .collect(); diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index 93181588..6720e3ed 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -39,16 +39,27 @@ mod tests { #[cfg(test)] pub(crate) mod test_support { - use stackable_operator::crd::s3; + use stackable_operator::{config::fragment, crd::s3}; use crate::{ controller::{ValidatedClusterConfig, test_support::DERBY_YAML}, crd::{ + HiveRole, MetaStoreConfig, databases::{MetadataDatabaseConnection, derby_driver_class}, v1alpha1, }, }; + /// The merged [`MetaStoreConfig`] of a role group that configures nothing itself. + pub fn default_metastore_config() -> MetaStoreConfig { + fragment::validate(MetaStoreConfig::default_config( + "simple-hive", + &HiveRole::MetaStore, + None, + )) + .expect("the default config is complete") + } + /// Build a minimal Derby-backed [`ValidatedClusterConfig`] for builder tests. pub fn derby_cluster_config() -> ValidatedClusterConfig { let hive: v1alpha1::HiveCluster = diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 53d09f2f..92ca25b1 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -295,7 +295,7 @@ pub(crate) fn build_metastore_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} {start_command} wait_for_termination $! {create_vector_shutdown_file_command} @@ -304,6 +304,10 @@ pub(crate) fn build_metastore_rolegroup_statefulset( remove_vector_shutdown_file_command = remove_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(), create_vector_shutdown_file_command = create_vector_shutdown_file_command(STACKABLE_LOG_DIR), }, @@ -515,6 +519,11 @@ pub(crate) fn build_metastore_rolegroup_statefulset( mod tests { use std::str::FromStr; + use rstest::rstest; + use stackable_operator::{ + crd::containerdebug::ValidatedContainerdebugConfig, shared::time::Duration, + }; + use super::*; use crate::{ controller::test_support::{DERBY_YAML, minimal_hive, validated_cluster}, @@ -687,4 +696,51 @@ mod tests { [("KRB5_CONFIG".to_string(), "/custom/krb5.conf".to_string())] ); } + + /// The hive 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 hive_container_starts_containerdebug_from_config( + #[case] containerdebug: ValidatedContainerdebugConfig, + ) { + let expected = containerdebug.background_command(STACKABLE_LOG_DIR); + let hive = minimal_hive(DERBY_YAML); + let cluster = validated_cluster(&hive); + let role_group_name = RoleGroupName::from_str("default").expect("valid role group name"); + let mut rg = cluster.role_group_configs[&HiveRole::MetaStore][&role_group_name].clone(); + rg.config.containerdebug = containerdebug; + + let stateful_set = build_metastore_rolegroup_statefulset( + &HiveRole::MetaStore, + &cluster, + &role_group_name, + &rg, + ) + .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 == Container::Hive.name().to_string()) + .expect("the hive container exists") + .args + .expect("the hive 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/validate.rs b/rust/operator-binary/src/controller/validate.rs index 3ac69722..991a23ef 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -4,6 +4,7 @@ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::product_image_selection, config::fragment, + crd::containerdebug, product_logging::spec::Logging, role_utils::GenericRoleConfig, v2::{ @@ -65,6 +66,12 @@ pub enum Error { role_group: RoleGroupName, }, + #[snafu(display("invalid containerdebug configuration for role group {role_group}"))] + InvalidContainerdebugConfig { + source: containerdebug::Error, + role_group: RoleGroupName, + }, + #[snafu(display("invalid metadata database connection"))] InvalidMetadataDatabaseConnection { source: stackable_operator::database_connections::Error, @@ -251,6 +258,15 @@ fn validate_role_group_config( role_group: role_group_name.clone(), })?; + let containerdebug = merged + .config + .config + .containerdebug + .validate() + .with_context(|_| InvalidContainerdebugConfigSnafu { + role_group: role_group_name.clone(), + })?; + let logging = validate_logging( &merged.config.config.logging, vector_aggregator_config_map_name, @@ -258,7 +274,11 @@ fn validate_role_group_config( Ok(HiveRoleGroupConfig { replicas: merged.replicas, - config: ValidatedMetaStoreConfig::from_merged(merged.config.config, logging), + config: ValidatedMetaStoreConfig::from_merged( + merged.config.config, + containerdebug, + logging, + ), config_overrides: merged.config.config_overrides, env_overrides: merged.config.env_overrides.into(), // Hive does not use CLI overrides; the field is carried through the merge but unused. diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 04cf2411..0ef5b901 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -21,7 +21,10 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, constant, - crd::s3, + crd::{ + containerdebug::{default_containerdebug_config, v1alpha1::ContainerdebugConfig}, + s3, + }, deep_merger::ObjectOverrides, k8s_openapi::apimachinery::pkg::api::resource::Quantity, kube::CustomResource, @@ -329,7 +332,7 @@ pub struct MetastoreStorageConfig { pub data: PvcConfig, } -#[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( derive( Clone, @@ -357,6 +360,9 @@ pub struct MetaStoreConfig { #[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, @@ -389,6 +395,7 @@ impl MetaStoreConfig { }, logging: product_logging::spec::default_logging(), affinity: get_affinity(cluster_name, role, opa_config), + containerdebug: default_containerdebug_config(), graceful_shutdown_timeout: Some(DEFAULT_METASTORE_GRACEFUL_SHUTDOWN_TIMEOUT), } }