diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index bd9b6115d..53bc0d2e1 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add `crd::containerdebug.rs` module with the `ContainerdebugConfig` CRD struct as well as helper functions on `ValidatedContainerdebugConfig` ([#1308]) + +[#1308]: https://github.com/stackabletech/operator-rs/pull/1308 + ### Changed - BREAKING: Bump the RustCrypto dependencies to their new generation, which affects the re-exported diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index 0f3a96838..555c1c2b7 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -435,6 +435,26 @@ spec: config: default: {} properties: + 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 logging: default: containers: {} @@ -1114,6 +1134,26 @@ spec: config: default: {} properties: + 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 logging: default: containers: {} diff --git a/crates/stackable-operator/src/crd/containerdebug/mod.rs b/crates/stackable-operator/src/crd/containerdebug/mod.rs new file mode 100644 index 000000000..6ed39c1f9 --- /dev/null +++ b/crates/stackable-operator/src/crd/containerdebug/mod.rs @@ -0,0 +1,168 @@ +//! Configuration of the `containerdebug` tool, which runs in the background of product containers +//! and periodically logs information about the container environment. +//! +//! Learn more in the +//! [container environment documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/observability/containerdebug). + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use snafu::{Snafu, ensure}; +use stackable_shared::time::Duration; + +use crate::{ + config::{fragment::Fragment, merge::Merge}, + versioned::versioned, +}; + +/// The interval used if the user does not configure one. +pub const DEFAULT_INTERVAL: Duration = Duration::from_minutes_unchecked(30); + +#[derive(Debug, Eq, PartialEq, Snafu)] +pub enum Error { + #[snafu(display("the containerdebug interval must not be zero"))] + IntervalZero, +} + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + /// 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. + #[derive(Clone, Debug, Eq, Fragment, JsonSchema, PartialEq)] + #[fragment(path_overrides(fragment = "crate::config::fragment"))] + #[fragment_attrs( + derive( + Clone, + Debug, + Default, + Deserialize, + Eq, + JsonSchema, + Merge, + PartialEq, + Serialize + ), + merge(path_overrides(merge = "crate::config::merge")), + serde(rename_all = "camelCase") + )] + pub struct ContainerdebugConfig { + /// Whether `containerdebug` runs in the background of the product container. Defaults to `true`. + pub enabled: bool, + + /// Time between two runs of `containerdebug`, e.g. `30m`, `1h` or `2d`. Must not be zero. + /// Defaults to `30m`. + pub interval: Duration, + } +} + +impl v1alpha1::ContainerdebugConfig { + /// Validates the merged configuration. + /// + /// The interval is only validated if `containerdebug` is enabled, so that a role group can + /// disable it without failing on an interval inherited from its role. + pub fn validate(&self) -> Result { + if !self.enabled { + return Ok(ValidatedContainerdebugConfig::Disabled); + } + + // `containerdebug` schedules the next run by adding the interval to the start of the + // previous one, so an interval of zero would collect (including DNS lookups) in a tight loop. + ensure!(!self.interval.is_zero(), IntervalZeroSnafu); + + Ok(ValidatedContainerdebugConfig::Enabled { + interval: self.interval, + }) + } +} + +/// A validated [`v1alpha1::ContainerdebugConfig`]. +/// +/// Unlike the user-facing configuration, this cannot carry an interval while `containerdebug` is +/// disabled, nor an interval of zero. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ValidatedContainerdebugConfig { + Disabled, + Enabled { interval: Duration }, +} + +impl ValidatedContainerdebugConfig { + /// Returns the shell command which starts `containerdebug` in the background, or [`None`] if + /// it is disabled. + /// + /// The collected state is written as JSON to `containerdebug-state.json` in `log_dir`. + pub fn background_command(&self, log_dir: &str) -> Option { + match self { + Self::Disabled => None, + // `containerdebug` requires the `=` between `--loop` and its value. + Self::Enabled { interval } => Some(format!( + "containerdebug --output={log_dir}/containerdebug-state.json --loop={interval} &" + )), + } + } +} + +/// Returns the default `containerdebug` configuration, which operators merge underneath the +/// role and role group configuration. +pub fn default_containerdebug_config() -> v1alpha1::ContainerdebugConfigFragment { + v1alpha1::ContainerdebugConfigFragment { + enabled: Some(true), + interval: Some(DEFAULT_INTERVAL), + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use rstest::rstest; + + use super::*; + use crate::config::fragment; + + const LOG_DIR: &str = "/stackable/log"; + + fn config(enabled: bool, interval: &str) -> v1alpha1::ContainerdebugConfig { + v1alpha1::ContainerdebugConfig { + enabled, + interval: Duration::from_str(interval).expect("valid duration"), + } + } + + #[test] + fn defaults_render_command() { + let config: v1alpha1::ContainerdebugConfig = + fragment::validate(default_containerdebug_config()).expect("defaults are complete"); + + assert_eq!( + config + .validate() + .expect("defaults are valid") + .background_command(LOG_DIR), + Some( + "containerdebug --output=/stackable/log/containerdebug-state.json --loop=30m &" + .to_owned() + ) + ); + } + + #[rstest] + #[case("0s", Err(Error::IntervalZero))] + #[case("1s", Ok(ValidatedContainerdebugConfig::Enabled { interval: Duration::from_secs(1) }))] + fn validate_rejects_zero_interval( + #[case] interval: &str, + #[case] expected: Result, + ) { + assert_eq!(config(true, interval).validate(), expected); + } + + /// A role group can disable `containerdebug` without failing on an interval inherited from + /// its role. + #[test] + fn disabled_ignores_interval_and_renders_nothing() { + let validated = config(false, "0s") + .validate() + .expect("the interval is not validated while disabled"); + + assert_eq!(validated.background_command(LOG_DIR), None); + } +} diff --git a/crates/stackable-operator/src/crd/mod.rs b/crates/stackable-operator/src/crd/mod.rs index 48681601c..35513616a 100644 --- a/crates/stackable-operator/src/crd/mod.rs +++ b/crates/stackable-operator/src/crd/mod.rs @@ -5,6 +5,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub mod authentication; +pub mod containerdebug; pub mod git_sync; pub mod listener; pub mod openlineage; diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index 67cbbfe11..6c4c9e1b6 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -117,7 +117,7 @@ pub mod versioned { >, } - #[derive(Debug, Default, PartialEq, Fragment, JsonSchema)] + #[derive(Debug, PartialEq, Fragment, JsonSchema)] #[fragment_attrs( derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema), schemars(crate = "stackable_operator::schemars"), @@ -130,6 +130,9 @@ pub mod versioned { #[fragment_attrs(serde(default))] pub logging: stackable_operator::product_logging::spec::Logging, + + #[fragment_attrs(serde(default))] + pub containerdebug: stackable_operator::crd::containerdebug::v1alpha1::ContainerdebugConfig, } #[derive(Debug, Default, PartialEq, Fragment, JsonSchema)]