diff --git a/CHANGELOG.md b/CHANGELOG.md index c78656cc..2c110229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. using OAuth2 client credentials or a bearer token ([#950], [#972]). - The logs of the `containerdebug` process are now collected by the Vector agent and forwarded to the Vector aggregator ([#970]). +- Add `containerdebug` config CRD ([#967]). ### Changed @@ -105,6 +106,7 @@ All notable changes to this project will be documented in this file. [#961]: https://github.com/stackabletech/trino-operator/pull/961 [#963]: https://github.com/stackabletech/trino-operator/pull/963 [#966]: https://github.com/stackabletech/trino-operator/pull/966 +[#967]: https://github.com/stackabletech/trino-operator/pull/967 [#970]: https://github.com/stackabletech/trino-operator/pull/970 [#972]: https://github.com/stackabletech/trino-operator/pull/972 diff --git a/extra/crds.yaml b/extra/crds.yaml index 19ac8bd2..87225476 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -1135,6 +1135,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 @@ -1750,6 +1770,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 @@ -2464,6 +2504,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 @@ -3071,6 +3131,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/command.rs b/rust/operator-binary/src/controller/build/command.rs index 29899745..ff4a9b8e 100644 --- a/rust/operator-binary/src/controller/build/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -87,6 +87,7 @@ pub fn container_prepare_args( pub fn container_trino_args( authentication_config: &TrinoAuthenticationConfig, catalogs: &BTreeMap, + merged_config: &ValidatedTrinoConfig, ) -> Vec { let mut args = vec![ // copy config files to a writable empty folder @@ -152,7 +153,7 @@ pub fn container_trino_args( {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/launcher run --etc-dir={RW_CONFIG_DIR_NAME} & wait_for_termination $! {create_vector_shutdown_file_command} @@ -160,6 +161,10 @@ wait_for_termination $! 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), )); diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 8df9cfe8..61a7bdda 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -402,7 +402,8 @@ pub fn build_rolegroup_statefulset( "-c".to_string(), ]) .args(vec![ - command::container_trino_args(trino_authentication_config, catalogs).join("\n"), + command::container_trino_args(trino_authentication_config, catalogs, merged_config) + .join("\n"), ]) .add_env_vars(env.merge(env_overrides.clone())) .add_container_ports(container_ports(cluster)) @@ -835,8 +836,11 @@ fn add_derived_volumes_and_mounts( #[cfg(test)] mod tests { + use rstest::rstest; use stackable_operator::{ + crd::containerdebug::ValidatedContainerdebugConfig, k8s_openapi::api::core::v1::{Container as K8sContainer, PodSpec, VolumeMount}, + shared::time::Duration, v2::builder::pod::container::EnvVarName, }; @@ -933,6 +937,47 @@ mod tests { assert_eq!(containerdebug[0].value.as_deref(), Some("/custom/log/dir")); } + /// The trino 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 trino_container_starts_containerdebug_from_config( + #[case] containerdebug: ValidatedContainerdebugConfig, + ) { + let expected = containerdebug.background_command(STACKABLE_LOG_DIR); + let cluster = validated_cluster(); + let role_group_name = RoleGroupName::from_str("default").expect("valid role group name"); + let mut role_group_config = + cluster.coordinator_role_group_configs[&role_group_name].clone(); + role_group_config.config.containerdebug = containerdebug; + + let stateful_set = build_rolegroup_statefulset( + &cluster, + &TrinoRole::Coordinator, + &role_group_name, + &role_group_config, + ) + .expect("the StatefulSet builds"); + + let args = pod_spec(stateful_set) + .containers + .into_iter() + .find(|container| container.name == "trino") + .expect("the trino container exists") + .args + .expect("the trino container has args") + .join("\n"); + assert_eq!( + args.lines() + .find(|line| line.starts_with("containerdebug ")), + expected.as_deref(), + "args: {args}" + ); + } + /// The operator's own volume mounts are added with `expect`, which relies on every add whose /// name derives from user input coming afterwards. A catalog volume that reuses an operator /// volume name must therefore surface as an error, never as a panic. diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs index 681fc462..c8d0e7f7 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller/mod.rs @@ -6,7 +6,7 @@ use stackable_operator::{ product_image_selection::ResolvedProductImage, resources::{NoRuntimeLimits, Resources}, }, - crd::listener::v1alpha1::Listener, + crd::{containerdebug::ValidatedContainerdebugConfig, listener::v1alpha1::Listener}, k8s_openapi::api::{ apps::v1::StatefulSet, core::v1::{ConfigMap, Service, ServiceAccount}, @@ -136,6 +136,7 @@ impl ValidatedClusterConfig { #[derive(Clone, Debug)] pub struct ValidatedTrinoConfig { pub affinity: StackableAffinity, + pub containerdebug: ValidatedContainerdebugConfig, pub graceful_shutdown_timeout: Option, pub logging: validate::ValidatedLogging, pub query_max_memory: Option, @@ -146,10 +147,15 @@ pub struct ValidatedTrinoConfig { impl ValidatedTrinoConfig { /// Builds the validated config from the merged [`v1alpha1::TrinoConfig`], swapping in the - /// already-validated logging. - fn from_merged(merged: v1alpha1::TrinoConfig, logging: validate::ValidatedLogging) -> Self { + /// already-validated `containerdebug` and logging configuration. + fn from_merged( + merged: v1alpha1::TrinoConfig, + containerdebug: ValidatedContainerdebugConfig, + logging: validate::ValidatedLogging, + ) -> Self { Self { affinity: merged.affinity, + containerdebug, graceful_shutdown_timeout: merged.graceful_shutdown_timeout, logging, query_max_memory: merged.query_max_memory, diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index a642c6ed..478d3c7e 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, kube::ResourceExt as _, product_logging::spec::Logging, v2::{ @@ -87,6 +88,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("failed to validate logging configuration"))] ValidateLoggingConfig { source: stackable_operator::v2::product_logging::framework::Error, @@ -375,8 +382,8 @@ where role_group: role_group_name.clone(), })?; role_groups.insert( - role_group_name, - into_role_group_config(merged, vector_aggregator_config_map_name)?, + role_group_name.clone(), + into_role_group_config(merged, &role_group_name, vector_aggregator_config_map_name)?, ); } @@ -387,16 +394,22 @@ where /// [`TrinoRoleGroupConfig`] consumed by the build steps. fn into_role_group_config( merged: RoleGroup, + role_group_name: &RoleGroupName, vector_aggregator_config_map_name: &Option, ) -> Result { let replicas = merged.replicas; let common = merged.config; + let containerdebug = common.config.containerdebug.validate().with_context(|_| { + InvalidContainerdebugConfigSnafu { + role_group: role_group_name.clone(), + } + })?; let logging = validate_logging(&common.config.logging, vector_aggregator_config_map_name)?; Ok(RoleGroupConfig { replicas, - config: ValidatedTrinoConfig::from_merged(common.config, logging), + config: ValidatedTrinoConfig::from_merged(common.config, containerdebug, logging), config_overrides: common.config_overrides, env_overrides: common.env_overrides.into(), cli_overrides: common.cli_overrides, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index afc6dc4d..cae6c3ca 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -22,7 +22,11 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, constant, - crd::{authentication::core, openlineage}, + crd::{ + authentication::core, + containerdebug::{default_containerdebug_config, v1alpha1::ContainerdebugConfig}, + openlineage, + }, deep_merger::ObjectOverrides, k8s_openapi::apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::LabelSelector}, kube::{CustomResource, ResourceExt}, @@ -215,7 +219,7 @@ pub mod versioned { pub event_listener_properties: KeyValueConfigOverrides, } - #[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] + #[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( derive( Clone, @@ -233,6 +237,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, @@ -542,6 +549,7 @@ impl v1alpha1::TrinoConfig { v1alpha1::TrinoConfigFragment { logging: product_logging::spec::default_logging(), affinity: get_affinity(cluster_name, role, trino_catalogs, opa_config), + containerdebug: default_containerdebug_config(), resources: ResourcesFragment { cpu: CpuLimitsFragment { min: Some(Quantity(cpu_min.to_string())),