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/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/cluster_resources.rs b/crates/stackable-operator/src/v2/cluster_resources.rs index 1b61a69ab..8cea401c4 100644 --- a/crates/stackable-operator/src/v2/cluster_resources.rs +++ b/crates/stackable-operator/src/v2/cluster_resources.rs @@ -9,7 +9,7 @@ use crate::{ v2::{NameIsValidLabelValue, macros::attributed_string_type::MAX_LABEL_VALUE_LENGTH}, }; -/// Infallible variant of [`stackable_operator::cluster_resources::ClusterResources::new`] +/// Infallible variant of [`crate::cluster_resources::ClusterResources::new`] #[allow(clippy::too_many_arguments)] pub fn cluster_resources_new<'a>( product_name: &ProductName, 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..194ac1aaf --- /dev/null +++ b/crates/stackable-operator/src/v2/config_file_writer.rs @@ -0,0 +1,144 @@ +//! Writers for Hadoop XML config files and Java `.properties` files. + +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/config_overrides.rs b/crates/stackable-operator/src/v2/config_overrides.rs index befdeb1ba..baa95d7c2 100644 --- a/crates/stackable-operator/src/v2/config_overrides.rs +++ b/crates/stackable-operator/src/v2/config_overrides.rs @@ -9,7 +9,7 @@ use crate::{ config::merge::Merge, k8s_openapi::DeepMerge, schemars, utils::crds::raw_object_schema, }; -// Variant of [`stackable_operator::config_overrides::KeyValueConfigOverrides`] that implements +// Variant of [`crate::config_overrides::KeyValueConfigOverrides`] that implements // Merge /// Flat key-value overrides for `*.properties`, Hadoop XML, etc. /// @@ -22,7 +22,7 @@ pub struct KeyValueConfigOverrides { pub overrides: BTreeMap>, } -// Variant of [`stackable_operator::config_overrides::JsonConfigOverrides`] with the following +// Variant of [`crate::config_overrides::JsonConfigOverrides`] with the following // changes: // - Implements Default // - Implements Merge by using a Sequence variant which is not exposed in the CRD @@ -63,7 +63,7 @@ pub enum JsonConfigOverrides { } impl JsonConfigOverrides { - // Infallible variant of [`stackable_operator::config_overrides::JsonConfigOverrides::apply`] + // Infallible variant of [`crate::config_overrides::JsonConfigOverrides::apply`] pub fn apply(&self, base: &serde_json::Value) -> serde_json::Value { match self { Self::JsonMergePatch(patch) => { 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..41f41e513 --- /dev/null +++ b/crates/stackable-operator/src/v2/flask_config_writer.rs @@ -0,0 +1,314 @@ +//! Writer for Flask App configurations (Python config files). +//! +//! 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 +#[derive(Debug, Snafu)] +pub enum FlaskAppConfigWriterError { + #[snafu(display("failed to convert '{value}' into a identifier"))] + ConvertIdentifier { value: String }, + + #[snafu(display("failed to convert '{value}' into a boolean literal"))] + ConvertBoolLiteral { + value: String, + source: ParseBoolError, + }, + + #[snafu(display("failed to convert '{value}' into an integer literal"))] + ConvertIntLiteral { + value: String, + source: ParseIntError, + }, + + #[snafu(display("failed to convert '{value}' into an ASCII string literal"))] + ConvertStringLiteral { value: String }, + + #[snafu(display("failed to convert '{value}' into a Python expression"))] + ConvertExpression { value: String }, + + #[snafu(display("Configuration cannot be written."))] + WriteConfig { 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/kvp/label.rs b/crates/stackable-operator/src/v2/kvp/label.rs index 7fd395d5d..d2681413f 100644 --- a/crates/stackable-operator/src/v2/kvp/label.rs +++ b/crates/stackable-operator/src/v2/kvp/label.rs @@ -9,7 +9,7 @@ use crate::{ }, }; -/// Infallible variant of [`stackable_operator::kvp::Labels::recommended`] +/// Infallible variant of [`crate::kvp::Labels::recommended`] pub fn recommended_labels( owner: &(impl Resource + HasName + NameIsValidLabelValue), product_name: &ProductName, @@ -34,7 +34,7 @@ pub fn recommended_labels( ) } -/// Infallible variant of [`stackable_operator::kvp::Labels::role_selector`] +/// Infallible variant of [`crate::kvp::Labels::role_selector`] pub fn role_selector( owner: &(impl Resource + HasName + NameIsValidLabelValue), product_name: &ProductName, @@ -48,7 +48,7 @@ pub fn role_selector( .expect("Labels should be created because all given parameters produce valid label values") } -/// Infallible variant of [`stackable_operator::kvp::Labels::role_group_selector`] +/// Infallible variant of [`crate::kvp::Labels::role_group_selector`] pub fn role_group_selector( owner: &(impl Resource + HasName + NameIsValidLabelValue), product_name: &ProductName, diff --git a/crates/stackable-operator/src/v2/mod.rs b/crates/stackable-operator/src/v2/mod.rs index 609e1c1e5..6d3d9b881 100644 --- a/crates/stackable-operator/src/v2/mod.rs +++ b/crates/stackable-operator/src/v2/mod.rs @@ -2,8 +2,10 @@ use crate::v2::types::kubernetes::Uid; pub mod builder; pub mod cluster_resources; +pub mod config_file_writer; pub mod config_overrides; pub mod controller_utils; +pub mod flask_config_writer; pub mod kvp; pub mod macros; pub mod product_logging; 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};