From a3d29c50b62e1ee93b7ad1e1270cf88318ff7020 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 5 Jun 2026 12:03:34 +0200 Subject: [PATCH 1/5] feat(v2): add shared config-file writers (java-properties/Hadoop-XML + Flask) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds v2::config_file_writer (to_java_properties_string + to_hadoop_xml, backed by the java-properties and xml crates) and v2::flask_config_writer (the Flask App Builder Python config writer), both originally from the product-config crate's writer modules and until now vendored separately into the operators (hdfs/hbase/hive byte-identical full copies; kafka/nifi/zookeeper java-only subsets; airflow/superset identical Flask copies). Unit tests moved along with the code; minor lint-driven cleanups only (use_self, format_push_string, identical match arms, no unwrap in Result-returning tests) — rendered output is unchanged and pinned by the tests. Operators will migrate to these in follow-up commits, deleting their vendored copies. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 6 +- Cargo.toml | 2 + crates/stackable-operator/Cargo.toml | 2 + .../src/v2/config_file_writer.rs | 148 ++++++++ .../src/v2/flask_config_writer.rs | 324 ++++++++++++++++++ crates/stackable-operator/src/v2/mod.rs | 2 + 6 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 crates/stackable-operator/src/v2/config_file_writer.rs create mode 100644 crates/stackable-operator/src/v2/flask_config_writer.rs diff --git a/Cargo.lock b/Cargo.lock index dc5b52aac..0eb9ee61d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3009,6 +3009,7 @@ dependencies = [ "http", "indexmap", "indoc", + "java-properties", "jiff", "json-patch", "k8s-openapi", @@ -3038,6 +3039,7 @@ dependencies = [ "url", "uuid", "winnow", + "xml", ] [[package]] @@ -4227,9 +4229,9 @@ dependencies = [ [[package]] name = "xml" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" +checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "xtask" diff --git a/Cargo.toml b/Cargo.toml index 00f8b8fd5..8d3409700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ insta = { version = "1.40", features = ["glob"] } hyper = { version = "1.4.1", features = ["full"] } hyper-util = "0.1.8" itertools = "0.14.0" +java-properties = "2.0" json-patch = "4.0.0" k8s-openapi = { version = "0.27.0", default-features = false, features = ["schemars", "v1_35"] } # We use rustls instead of openssl for easier portability, e.g. so that we can build stackablectl without the need to vendor (build from source) openssl @@ -89,6 +90,7 @@ url = { version = "2.5.2", features = ["serde"] } uuid = "1.23" winnow = "1.0.3" x509-cert = { version = "0.2.5", features = ["builder"] } +xml = "1.3" zeroize = "1.8.1" [workspace.lints.clippy] diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index beb9a8418..38663e762 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -36,6 +36,7 @@ educe.workspace = true futures.workspace = true http.workspace = true indexmap.workspace = true +java-properties.workspace = true jiff.workspace = true json-patch = { workspace = true, features = ["schemars"] } k8s-openapi.workspace = true @@ -57,6 +58,7 @@ tracing-subscriber.workspace = true url.workspace = true uuid.workspace = true winnow = { workspace = true, optional = true } +xml.workspace = true [dev-dependencies] indoc.workspace = true diff --git a/crates/stackable-operator/src/v2/config_file_writer.rs b/crates/stackable-operator/src/v2/config_file_writer.rs new file mode 100644 index 000000000..a8da60062 --- /dev/null +++ b/crates/stackable-operator/src/v2/config_file_writer.rs @@ -0,0 +1,148 @@ +//! Writers for Hadoop XML config files and Java `.properties` files. +//! +//! Originally part of the `product-config` crate's `writer` module; previously +//! vendored into the individual operators, now provided here as the shared home so +//! operators do not depend on `product-config` for rendering. + +use std::{fmt::Write as _, io::Write}; + +use java_properties::{PropertiesError, PropertiesWriter}; +use snafu::{ResultExt, Snafu}; +use xml::escape::escape_str_attribute; + +#[derive(Debug, Snafu)] +pub enum PropertiesWriterError { + #[snafu(display("failed to create properties file"))] + Properties { source: PropertiesError }, + + #[snafu(display("failed to convert properties file byte array to UTF-8"))] + FromUtf8 { source: std::string::FromUtf8Error }, +} + +/// Creates a common Java properties file string in the format: +/// `property_1=value_1\nproperty_2=value_2\n`. +pub fn to_java_properties_string<'a, T>(properties: T) -> Result +where + T: Iterator)>, +{ + let mut output = Vec::new(); + write_java_properties(&mut output, properties)?; + String::from_utf8(output).context(FromUtf8Snafu) +} + +/// Writes Java properties to the given writer. A `None` value is written as an +/// empty value (`key=`). +fn write_java_properties<'a, W, T>(writer: W, properties: T) -> Result<(), PropertiesWriterError> +where + W: Write, + T: Iterator)>, +{ + let mut writer = PropertiesWriter::new(writer); + for (k, v) in properties { + let property_value = v.as_deref().unwrap_or_default(); + writer.write(k, property_value).context(PropertiesSnafu)?; + } + writer.flush().context(PropertiesSnafu)?; + Ok(()) +} + +/// Converts properties into a Hadoop configuration XML, including the wrapping +/// `...` elements. Properties with a `None` value +/// are skipped. Keys and values are XML-escaped. +pub fn to_hadoop_xml<'a, T>(properties: T) -> String +where + T: Iterator)>, +{ + let mut snippet = String::new(); + for (k, v) in properties { + let escaped_value = match v { + Some(value) => escape_str_attribute(value), + None => continue, + }; + let escaped_key = escape_str_attribute(k); + write!( + snippet, + " \n {escaped_key}\n {escaped_value}\n \n" + ) + .expect("writing to a String is infallible"); + } + format!("\n\n{snippet}") +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + + fn xml(pairs: &[(&str, Option<&str>)]) -> String { + let map: BTreeMap> = pairs + .iter() + .map(|(k, v)| (k.to_string(), v.map(str::to_string))) + .collect(); + to_hadoop_xml(map.iter()) + } + + fn props(pairs: &[(&str, Option<&str>)]) -> String { + let map: BTreeMap> = pairs + .iter() + .map(|(k, v)| (k.to_string(), v.map(str::to_string))) + .collect(); + to_java_properties_string(map.iter()).unwrap() + } + + #[test] + fn hadoop_xml_wraps_empty_configuration() { + assert_eq!( + xml(&[]), + "\n\n" + ); + } + + #[test] + fn hadoop_xml_renders_single_property() { + assert_eq!( + xml(&[("fs.defaultFS", Some("hdfs://hdfs/"))]), + "\n\n \ + \n fs.defaultFS\n \ + hdfs://hdfs/\n \n" + ); + } + + #[test] + fn hadoop_xml_skips_none_values() { + assert_eq!( + xml(&[("kept", Some("1")), ("dropped", None)]), + "\n\n \ + \n kept\n \ + 1\n \n" + ); + } + + #[test] + fn hadoop_xml_escapes_special_characters() { + let rendered = xml(&[("k", Some("&b"))]); + assert!( + rendered.contains("<a>&b"), + "{rendered}" + ); + } + + #[test] + fn java_properties_renders_key_value() { + assert_eq!(props(&[("a", Some("1")), ("b", Some("2"))]), "a=1\nb=2\n"); + } + + #[test] + fn java_properties_renders_none_as_empty() { + assert_eq!(props(&[("none", None)]), "none=\n"); + } + + #[test] + fn java_properties_escapes_colon_in_value() { + assert_eq!( + props(&[("url", Some("file://this/location/file.abc"))]), + "url=file\\://this/location/file.abc\n" + ); + } +} diff --git a/crates/stackable-operator/src/v2/flask_config_writer.rs b/crates/stackable-operator/src/v2/flask_config_writer.rs new file mode 100644 index 000000000..211aa9198 --- /dev/null +++ b/crates/stackable-operator/src/v2/flask_config_writer.rs @@ -0,0 +1,324 @@ +//! Writer for Flask App configurations (Python config files). +//! +//! Originally `product_config::flask_app_config_writer`; previously vendored into +//! airflow- and superset-operator, now provided here as the shared home so operators +//! do not depend on the `product-config` crate. Applications based on the Flask App +//! Builder (e.g. Apache Airflow, Apache Superset) use configuration files written in +//! Python. This writer only covers top-level assignments of a few primitive types and +//! expressions — it is not a general Python code generator. +//! +//! Primitive types are escaped accordingly. Python expressions are written as-is; +//! invalid expressions produce invalid configuration files. Config overrides that do +//! not map to a known option are treated as plain expressions. + +use std::{ + io::{self, Write}, + num::ParseIntError, + str::{FromStr, ParseBoolError}, +}; + +use snafu::{ResultExt, Snafu}; + +/// Errors which can occur when using this module +// Variant names share the `Error` suffix; kept as-is from the vendored +// `product_config::flask_app_config_writer` source. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Snafu)] +pub enum FlaskAppConfigWriterError { + #[snafu(display("failed to convert '{value}' into a identifier"))] + ConvertIdentifierError { value: String }, + + #[snafu(display("failed to convert '{value}' into a boolean literal"))] + ConvertBoolLiteralError { + value: String, + source: ParseBoolError, + }, + + #[snafu(display("failed to convert '{value}' into an integer literal"))] + ConvertIntLiteralError { + value: String, + source: ParseIntError, + }, + + #[snafu(display("failed to convert '{value}' into an ASCII string literal"))] + ConvertStringLiteralError { value: String }, + + #[snafu(display("failed to convert '{value}' into a Python expression"))] + ConvertExpressionError { value: String }, + + #[snafu(display("Configuration cannot be written."))] + WriteConfigError { source: io::Error }, +} + +/// Mapping from configuration options to Python types. +pub trait FlaskAppConfigOptions { + fn python_type(&self) -> PythonType; +} + +/// All supported Python types +pub enum PythonType { + /// Python identifier + Identifier, + /// Boolean literal + BoolLiteral, + /// Integer literal + IntLiteral, + /// ASCII string literal + StringLiteral, + /// Python expression + Expression, +} + +impl PythonType { + /// Converts the given string to Python. + fn convert_to_python(&self, value: &str) -> Result { + let convert = match self { + Self::Identifier => Self::convert_to_python_identifier, + Self::BoolLiteral => Self::convert_to_python_bool_literal, + Self::IntLiteral => Self::convert_to_python_int_literal, + Self::StringLiteral => Self::convert_to_python_string_literal, + Self::Expression => Self::convert_to_python_expression, + }; + + convert(value) + } + + fn convert_to_python_identifier(value: &str) -> Result { + if value.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + && value + .chars() + .next() + .filter(|c| !c.is_ascii_digit()) + .is_some() + { + Ok(value.to_string()) + } else { + ConvertIdentifierSnafu { value }.fail() + } + } + + fn convert_to_python_bool_literal(value: &str) -> Result { + value + .parse::() + .map(|b| if b { "True".into() } else { "False".into() }) + .context(ConvertBoolLiteralSnafu { value }) + } + + fn convert_to_python_int_literal(value: &str) -> Result { + value + .parse::() + .map(|i| i.to_string()) + .context(ConvertIntLiteralSnafu { value }) + } + + fn convert_to_python_string_literal(value: &str) -> Result { + if value.is_ascii() { + Ok(format!("\"{}\"", value.escape_default())) + } else { + ConvertStringLiteralSnafu { value }.fail() + } + } + + fn convert_to_python_expression(value: &str) -> Result { + if value.trim().is_empty() { + ConvertExpressionSnafu { value }.fail() + } else { + Ok(value.to_string()) + } + } +} + +/// Writes a configuration file according to the given `FlaskAppConfigOptions` type. +pub fn write<'a, O, P, W>( + writer: &mut W, + properties: P, + imports: &[&str], +) -> Result<(), FlaskAppConfigWriterError> +where + O: FlaskAppConfigOptions + FromStr, + P: Iterator, + W: Write, +{ + for import in imports { + writeln!(writer, "{import}").context(WriteConfigSnafu)?; + } + + writeln!(writer).context(WriteConfigSnafu)?; + + for (name, value) in properties { + let variable = PythonType::Identifier.convert_to_python(name)?; + + // If an option cannot be mapped to a Python type then it is a config override and treated + // as Python expression. + let content = O::from_str(name) + .map(|option| option.python_type()) + .unwrap_or(PythonType::Expression) + .convert_to_python(value)?; + + writeln!(writer, "{variable} = {content}").context(WriteConfigSnafu)?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::BTreeMap, + str::{FromStr, from_utf8}, + }; + + use rstest::*; + + use super::{FlaskAppConfigOptions, FlaskAppConfigWriterError, PythonType, write}; + + #[rstest] + #[case::valid_identifiers_are_converted_to_python( + PythonType::Identifier, &[ + ("_", "_"), + ("a", "a"), + ("A", "A"), + ("__", "__"), + ("_a", "_a"), + ("_A", "_A"), + ("_0", "_0"), + ("SECRET_KEY", "SECRET_KEY"), + ] + )] + #[case::valid_booleans_are_converted_to_python( + PythonType::BoolLiteral, &[ + ("False", "false"), + ("True", "true"), + ] + )] + #[case::valid_integers_are_converted_to_python( + PythonType::IntLiteral, &[ + ("-9223372036854775808", "-9223372036854775808"), + ("0", "0"), + ("9223372036854775807", "9223372036854775807"), + ] + )] + #[case::valid_strings_are_converted_to_python( + PythonType::StringLiteral, &[ + (r#""""#, ""), + (r#"" ~""#, " ~"), + (r#""\t\r\n\'\"\\""#, "\t\r\n'\"\\"), + ] + )] + #[case::valid_expressions_are_converted_to_python( + PythonType::Expression, &[ + ("os.environ[\"HOME\"]", "os.environ[\"HOME\"]"), + ] + )] + fn valid_values_are_converted_to_python( + #[case] python_type: PythonType, + #[case] values: &[(&str, &str)], + ) -> Result<(), FlaskAppConfigWriterError> { + for (expected, input) in values { + assert_eq!(*expected, python_type.convert_to_python(input)?); + } + + Ok(()) + } + + #[rstest] + #[case::invalid_identifiers_are_not_converted_to_python( + PythonType::Identifier, &[ + "", "0", "-", "\n", "_-", "_\n", + ] + )] + #[case::invalid_booleans_are_not_converted_to_python( + PythonType::BoolLiteral, &[ + "", "False", "True", "0", "1", + ] + )] + #[case::invalid_integers_are_not_converted_to_python( + PythonType::IntLiteral, &[ + "", "a", "0x10", "inf", + ] + )] + #[case::invalid_strings_are_not_converted_to_python( + PythonType::StringLiteral, &[ + "ä", "❤" + ] + )] + #[case::invalid_expressions_are_not_converted_to_python( + PythonType::Expression, &[ + "" + ] + )] + fn invalid_values_are_converted_to_python( + #[case] python_type: PythonType, + #[case] values: &[&str], + ) { + for input in values { + assert!(python_type.convert_to_python(input).is_err()); + } + } + + #[test] + fn valid_options_are_written_into_a_configuration() { + #[allow(clippy::enum_variant_names)] + enum Options { + BoolOption, + IntOption, + StringOption, + ExpressionOption, + _UnusedOption, + } + + impl FromStr for Options { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "BOOL_OPTION" => Ok(Self::BoolOption), + "INT_OPTION" => Ok(Self::IntOption), + "STRING_OPTION" => Ok(Self::StringOption), + "EXPRESSION_OPTION" => Ok(Self::ExpressionOption), + _ => Err("unknown option"), + } + } + } + + impl FlaskAppConfigOptions for Options { + fn python_type(&self) -> PythonType { + match self { + Self::BoolOption => PythonType::BoolLiteral, + Self::IntOption => PythonType::IntLiteral, + Self::StringOption => PythonType::StringLiteral, + Self::ExpressionOption | Self::_UnusedOption => PythonType::Expression, + } + } + } + + let config: BTreeMap<_, _> = [ + ("BOOL_OPTION", "true"), + ("INT_OPTION", "0"), + ("STRING_OPTION", ""), + ("EXPRESSION_OPTION", "{ \"key\": \"value\" }"), + ("OVERRIDDEN_OPTION", "None"), + ] + .map(|(k, v)| (k.to_string(), v.to_string())) + .into(); + + let imports = ["import module", "from module import member"]; + + let mut config_file = Vec::new(); + write::(&mut config_file, config.iter(), &imports) + .expect("writing the test configuration should succeed"); + + assert_eq!( + r#"import module +from module import member + +BOOL_OPTION = True +EXPRESSION_OPTION = { "key": "value" } +INT_OPTION = 0 +OVERRIDDEN_OPTION = None +STRING_OPTION = "" +"#, + from_utf8(&config_file).expect("the Flask config writer only emits valid UTF-8") + ); + } +} diff --git a/crates/stackable-operator/src/v2/mod.rs b/crates/stackable-operator/src/v2/mod.rs index ca7b671fa..b7e0a0e74 100644 --- a/crates/stackable-operator/src/v2/mod.rs +++ b/crates/stackable-operator/src/v2/mod.rs @@ -1,7 +1,9 @@ use crate::v2::types::kubernetes::Uid; pub mod builder; +pub mod config_file_writer; pub mod config_overrides; +pub mod flask_config_writer; pub mod macros; pub mod role_group_utils; pub mod role_utils; From 60783b77c1b22418d34b57797213228bbeccade2 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 5 Jun 2026 12:18:45 +0200 Subject: [PATCH 2/5] docs: fix rustdoc warnings (broken intra-doc links, bare URLs) stackable-operator: intra-doc links within the crate must use `crate::` rather than the package name `stackable_operator::`, which is not in scope and produced `broken_intra_doc_links` warnings. Also wrap two bare URLs in the scaler docs in angle brackets so rustdoc renders them as hyperlinks. stackable-telemetry: `SettingsBuilder` is not imported in `tracing/mod.rs` (only `Settings` is), so the four doc links to it did not resolve. Point them at `settings::SettingsBuilder` via the child module, keeping the rendered link text unchanged. `cargo doc --document-private-items` is now warning-free across the workspace. --- crates/stackable-operator/src/crd/scaler/mod.rs | 4 ++-- crates/stackable-operator/src/v2/builder/meta.rs | 2 +- crates/stackable-operator/src/v2/builder/pdb.rs | 2 +- crates/stackable-operator/src/v2/builder/pod/container.rs | 2 +- crates/stackable-operator/src/v2/builder/pod/volume.rs | 4 ++-- crates/stackable-operator/src/v2/role_utils.rs | 4 ++-- crates/stackable-telemetry/src/tracing/mod.rs | 8 ++++---- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/stackable-operator/src/crd/scaler/mod.rs b/crates/stackable-operator/src/crd/scaler/mod.rs index 9583061d9..9fdd300b7 100644 --- a/crates/stackable-operator/src/crd/scaler/mod.rs +++ b/crates/stackable-operator/src/crd/scaler/mod.rs @@ -31,8 +31,8 @@ pub mod versioned { /// /// Upstream issues: /// - /// - https://github.com/kubernetes/kubernetes/issues/105533 - /// - https://github.com/Arnavion/k8s-openapi/issues/136 + /// - + /// - pub replicas: u16, } } diff --git a/crates/stackable-operator/src/v2/builder/meta.rs b/crates/stackable-operator/src/v2/builder/meta.rs index e034f5a12..c41ff6e89 100644 --- a/crates/stackable-operator/src/v2/builder/meta.rs +++ b/crates/stackable-operator/src/v2/builder/meta.rs @@ -6,7 +6,7 @@ use crate::{ }; /// Infallible variant of -/// [`stackable_operator::builder::meta::ObjectMetaBuilder::ownerreference_from_resource`] +/// [`crate::builder::meta::ObjectMetaBuilder::ownerreference_from_resource`] pub fn ownerreference_from_resource( resource: &(impl Resource + HasName + HasUid), block_owner_deletion: Option, diff --git a/crates/stackable-operator/src/v2/builder/pdb.rs b/crates/stackable-operator/src/v2/builder/pdb.rs index 81a5c3b39..783265d12 100644 --- a/crates/stackable-operator/src/v2/builder/pdb.rs +++ b/crates/stackable-operator/src/v2/builder/pdb.rs @@ -9,7 +9,7 @@ use crate::{ }; /// Infallible variant of -/// [`stackable_operator::builder::pdb::PodDisruptionBudgetBuilder::new_with_role`] +/// [`crate::builder::pdb::PodDisruptionBudgetBuilder::new_with_role`] pub fn pod_disruption_budget_builder_with_role( owner: &(impl Resource + HasName + NameIsValidLabelValue + HasUid), product_name: &ProductName, diff --git a/crates/stackable-operator/src/v2/builder/pod/container.rs b/crates/stackable-operator/src/v2/builder/pod/container.rs index 32f5477e6..29229b58d 100644 --- a/crates/stackable-operator/src/v2/builder/pod/container.rs +++ b/crates/stackable-operator/src/v2/builder/pod/container.rs @@ -23,7 +23,7 @@ pub enum Error { ParseEnvVarName { env_var_name: String }, } -/// Infallible variant of [`stackable_operator::builder::pod::container::ContainerBuilder::new`] +/// Infallible variant of [`crate::builder::pod::container::ContainerBuilder::new`] pub fn new_container_builder(container_name: &ContainerName) -> ContainerBuilder { ContainerBuilder::new(container_name.as_ref()).expect("should be a valid container name") } diff --git a/crates/stackable-operator/src/v2/builder/pod/volume.rs b/crates/stackable-operator/src/v2/builder/pod/volume.rs index a147cf9ba..5e9307e39 100644 --- a/crates/stackable-operator/src/v2/builder/pod/volume.rs +++ b/crates/stackable-operator/src/v2/builder/pod/volume.rs @@ -5,7 +5,7 @@ use crate::{ v2::types::kubernetes::{ListenerClassName, ListenerName, PersistentVolumeClaimName}, }; -/// Infallible variant of [`stackable_operator::builder::pod::volume::ListenerReference`] +/// Infallible variant of [`crate::builder::pod::volume::ListenerReference`] #[derive(Clone, Debug, Eq, PartialEq)] pub enum ListenerReference { ListenerClass(ListenerClassName), @@ -26,7 +26,7 @@ impl From<&ListenerReference> for crate::builder::pod::volume::ListenerReference } /// Infallible variant of -/// [`stackable_operator::builder::pod::volume::ListenerOperatorVolumeSourceBuilder::build_pvc`] +/// [`crate::builder::pod::volume::ListenerOperatorVolumeSourceBuilder::build_pvc`] pub fn listener_operator_volume_source_builder_build_pvc( listener_reference: &ListenerReference, labels: &Labels, diff --git a/crates/stackable-operator/src/v2/role_utils.rs b/crates/stackable-operator/src/v2/role_utils.rs index 8394bb77e..2c96f4dc0 100644 --- a/crates/stackable-operator/src/v2/role_utils.rs +++ b/crates/stackable-operator/src/v2/role_utils.rs @@ -22,7 +22,7 @@ use crate::{ schemars::{self, JsonSchema}, }; -// Variant of [`stackable_operator::role_utils::GenericCommonConfig`] that implements [`Merge`] +// Variant of [`crate::role_utils::GenericCommonConfig`] that implements [`Merge`] #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Eq, PartialEq, Serialize)] pub struct GenericCommonConfig {} @@ -30,7 +30,7 @@ impl Merge for GenericCommonConfig { fn merge(&mut self, _defaults: &Self) {} } -/// Variant of [`stackable_operator::role_utils::RoleGroup`] that is easier to work with +/// Variant of [`crate::role_utils::RoleGroup`] that is easier to work with /// /// Differences are: /// * `replicas` is non-optional. diff --git a/crates/stackable-telemetry/src/tracing/mod.rs b/crates/stackable-telemetry/src/tracing/mod.rs index 9ab820b9f..64b75e4be 100644 --- a/crates/stackable-telemetry/src/tracing/mod.rs +++ b/crates/stackable-telemetry/src/tracing/mod.rs @@ -133,7 +133,7 @@ pub enum Error { /// ## Builders /// /// When choosing the builder, there are two different styles to configure individual subscribers: -/// Using the sophisticated [`SettingsBuilder`] or the simplified tuple style for basic +/// Using the sophisticated [`SettingsBuilder`](settings::SettingsBuilder) or the simplified tuple style for basic /// configuration. Currently, three different subscribers are supported: console output, OTLP log /// export, and OTLP trace export. /// @@ -173,9 +173,9 @@ pub enum Error { /// subscriber provides specific settings based on a common set of options. These options can be /// customized via the following methods: /// -/// - [`SettingsBuilder::console_log_settings_builder`] -/// - [`SettingsBuilder::otlp_log_settings_builder`] -/// - [`SettingsBuilder::otlp_trace_settings_builder`] +/// - [`SettingsBuilder::console_log_settings_builder`](settings::SettingsBuilder::console_log_settings_builder) +/// - [`SettingsBuilder::otlp_log_settings_builder`](settings::SettingsBuilder::otlp_log_settings_builder) +/// - [`SettingsBuilder::otlp_trace_settings_builder`](settings::SettingsBuilder::otlp_trace_settings_builder) /// /// ``` /// # use stackable_telemetry::tracing::{Tracing, Error, settings::Settings}; From f648a2d92239fd0f596a3febe3f4753bda22ffdd Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:03:22 +0200 Subject: [PATCH 3/5] Update crates/stackable-operator/src/v2/config_file_writer.rs Co-authored-by: maltesander --- crates/stackable-operator/src/v2/config_file_writer.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/stackable-operator/src/v2/config_file_writer.rs b/crates/stackable-operator/src/v2/config_file_writer.rs index a8da60062..194ac1aaf 100644 --- a/crates/stackable-operator/src/v2/config_file_writer.rs +++ b/crates/stackable-operator/src/v2/config_file_writer.rs @@ -1,8 +1,4 @@ //! Writers for Hadoop XML config files and Java `.properties` files. -//! -//! Originally part of the `product-config` crate's `writer` module; previously -//! vendored into the individual operators, now provided here as the shared home so -//! operators do not depend on `product-config` for rendering. use std::{fmt::Write as _, io::Write}; From a4af6ea027df215bbcb8b3d3d9cf9a9ffc9727a1 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:04:10 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: maltesander --- crates/stackable-operator/src/v2/flask_config_writer.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/stackable-operator/src/v2/flask_config_writer.rs b/crates/stackable-operator/src/v2/flask_config_writer.rs index 211aa9198..aa06cc400 100644 --- a/crates/stackable-operator/src/v2/flask_config_writer.rs +++ b/crates/stackable-operator/src/v2/flask_config_writer.rs @@ -1,12 +1,5 @@ //! Writer for Flask App configurations (Python config files). //! -//! Originally `product_config::flask_app_config_writer`; previously vendored into -//! airflow- and superset-operator, now provided here as the shared home so operators -//! do not depend on the `product-config` crate. Applications based on the Flask App -//! Builder (e.g. Apache Airflow, Apache Superset) use configuration files written in -//! Python. This writer only covers top-level assignments of a few primitive types and -//! expressions — it is not a general Python code generator. -//! //! Primitive types are escaped accordingly. Python expressions are written as-is; //! invalid expressions produce invalid configuration files. Config overrides that do //! not map to a known option are treated as plain expressions. @@ -20,8 +13,6 @@ use std::{ use snafu::{ResultExt, Snafu}; /// Errors which can occur when using this module -// Variant names share the `Error` suffix; kept as-is from the vendored -// `product_config::flask_app_config_writer` source. #[allow(clippy::enum_variant_names)] #[derive(Debug, Snafu)] pub enum FlaskAppConfigWriterError { From 5b14c7cf9fd8d32ad34807cb3af28be2c0cf352b Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 5 Jun 2026 13:23:22 +0200 Subject: [PATCH 5/5] remove error suffix and clippy --- .../src/v2/flask_config_writer.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/stackable-operator/src/v2/flask_config_writer.rs b/crates/stackable-operator/src/v2/flask_config_writer.rs index aa06cc400..41f41e513 100644 --- a/crates/stackable-operator/src/v2/flask_config_writer.rs +++ b/crates/stackable-operator/src/v2/flask_config_writer.rs @@ -13,32 +13,31 @@ use std::{ use snafu::{ResultExt, Snafu}; /// Errors which can occur when using this module -#[allow(clippy::enum_variant_names)] #[derive(Debug, Snafu)] pub enum FlaskAppConfigWriterError { #[snafu(display("failed to convert '{value}' into a identifier"))] - ConvertIdentifierError { value: String }, + ConvertIdentifier { value: String }, #[snafu(display("failed to convert '{value}' into a boolean literal"))] - ConvertBoolLiteralError { + ConvertBoolLiteral { value: String, source: ParseBoolError, }, #[snafu(display("failed to convert '{value}' into an integer literal"))] - ConvertIntLiteralError { + ConvertIntLiteral { value: String, source: ParseIntError, }, #[snafu(display("failed to convert '{value}' into an ASCII string literal"))] - ConvertStringLiteralError { value: String }, + ConvertStringLiteral { value: String }, #[snafu(display("failed to convert '{value}' into a Python expression"))] - ConvertExpressionError { value: String }, + ConvertExpression { value: String }, #[snafu(display("Configuration cannot be written."))] - WriteConfigError { source: io::Error }, + WriteConfig { source: io::Error }, } /// Mapping from configuration options to Python types.