Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions crates/stackable-operator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/stackable-operator/src/crd/scaler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pub mod versioned {
///
/// Upstream issues:
///
/// - https://github.com/kubernetes/kubernetes/issues/105533
/// - https://github.com/Arnavion/k8s-openapi/issues/136
/// - <https://github.com/kubernetes/kubernetes/issues/105533>
/// - <https://github.com/Arnavion/k8s-openapi/issues/136>
pub replicas: u16,
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/v2/builder/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DynamicType = ()> + HasName + HasUid),
block_owner_deletion: Option<bool>,
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/v2/builder/pdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DynamicType = ()> + HasName + NameIsValidLabelValue + HasUid),
product_name: &ProductName,
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/v2/builder/pod/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
4 changes: 2 additions & 2 deletions crates/stackable-operator/src/v2/builder/pod/volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/v2/cluster_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
144 changes: 144 additions & 0 deletions crates/stackable-operator/src/v2/config_file_writer.rs
Original file line number Diff line number Diff line change
@@ -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<String, PropertiesWriterError>
where
T: Iterator<Item = (&'a String, &'a Option<String>)>,
{
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<Item = (&'a String, &'a Option<String>)>,
{
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
/// `<configuration>...</configuration>` 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<Item = (&'a String, &'a Option<String>)>,
{
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,
" <property>\n <name>{escaped_key}</name>\n <value>{escaped_value}</value>\n </property>\n"
)
.expect("writing to a String is infallible");
}
format!("<?xml version=\"1.0\"?>\n<configuration>\n{snippet}</configuration>")
}

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use super::*;

fn xml(pairs: &[(&str, Option<&str>)]) -> String {
let map: BTreeMap<String, Option<String>> = 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<String, Option<String>> = 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(&[]),
"<?xml version=\"1.0\"?>\n<configuration>\n</configuration>"
);
}

#[test]
fn hadoop_xml_renders_single_property() {
assert_eq!(
xml(&[("fs.defaultFS", Some("hdfs://hdfs/"))]),
"<?xml version=\"1.0\"?>\n<configuration>\n \
<property>\n <name>fs.defaultFS</name>\n \
<value>hdfs://hdfs/</value>\n </property>\n</configuration>"
);
}

#[test]
fn hadoop_xml_skips_none_values() {
assert_eq!(
xml(&[("kept", Some("1")), ("dropped", None)]),
"<?xml version=\"1.0\"?>\n<configuration>\n \
<property>\n <name>kept</name>\n \
<value>1</value>\n </property>\n</configuration>"
);
}

#[test]
fn hadoop_xml_escapes_special_characters() {
let rendered = xml(&[("k", Some("<a>&b"))]);
assert!(
rendered.contains("<value>&lt;a&gt;&amp;b</value>"),
"{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"
);
}
}
6 changes: 3 additions & 3 deletions crates/stackable-operator/src/v2/config_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -22,7 +22,7 @@ pub struct KeyValueConfigOverrides {
pub overrides: BTreeMap<String, Option<String>>,
}

// 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
Expand Down Expand Up @@ -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) => {
Expand Down
Loading
Loading