From 9225150069f9b5e3db108e31f31700e268ee99f0 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 3 Jun 2026 17:41:06 +0200 Subject: [PATCH 01/47] build: switch to smooth-operator branch and vendor the java-properties writer Patches operator-rs to the smooth-operator branch (matching trino/hdfs) as the foundation for the v2 config_overrides adoption later in this series, and vendors the Java-properties writer into config/writer (backed by the java-properties crate, Apache-2.0) so ConfigMap rendering no longer goes through product_config::writer. Repointed resource/configmap.rs. No behaviour change (18 tests pass). Regenerated Cargo.nix/crate-hashes.json. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 31 +++-- Cargo.nix | 116 +++++++++++++++--- Cargo.toml | 2 + crate-hashes.json | 18 +-- rust/operator-binary/Cargo.toml | 1 + rust/operator-binary/src/config/mod.rs | 1 + rust/operator-binary/src/config/writer.rs | 78 ++++++++++++ .../operator-binary/src/resource/configmap.rs | 7 +- 8 files changed, 214 insertions(+), 40 deletions(-) create mode 100644 rust/operator-binary/src/config/writer.rs diff --git a/Cargo.lock b/Cargo.lock index 5357bf1f..2f759557 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1473,6 +1473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" dependencies = [ "jsonptr", + "schemars", "serde", "serde_json", "thiserror 1.0.69", @@ -1517,7 +1518,7 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "darling", "regex", @@ -2889,7 +2890,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackable-certs" version = "0.4.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "const-oid", "ecdsa", @@ -2920,6 +2921,7 @@ dependencies = [ "const_format", "futures", "indoc", + "java-properties", "product-config", "rstest", "serde", @@ -2935,7 +2937,7 @@ dependencies = [ [[package]] name = "stackable-operator" version = "0.111.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "base64", "clap", @@ -2971,12 +2973,13 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "url", + "uuid", ] [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "darling", "proc-macro2", @@ -2987,7 +2990,7 @@ dependencies = [ [[package]] name = "stackable-shared" version = "0.1.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "jiff", "k8s-openapi", @@ -3004,7 +3007,7 @@ dependencies = [ [[package]] name = "stackable-telemetry" version = "0.6.3" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "axum", "clap", @@ -3028,7 +3031,7 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "kube", "schemars", @@ -3042,7 +3045,7 @@ dependencies = [ [[package]] name = "stackable-versioned-macros" version = "0.10.0" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "convert_case", "convert_case_extras", @@ -3060,7 +3063,7 @@ dependencies = [ [[package]] name = "stackable-webhook" version = "0.9.1" -source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#7a5f0c3fbcd091340214a23f0607fcd4b4fcc152" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" dependencies = [ "arc-swap", "async-trait", @@ -3641,6 +3644,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.nix b/Cargo.nix index ff2e1a37..0cb45c8f 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4690,6 +4690,11 @@ rec { name = "jsonptr"; packageId = "jsonptr"; } + { + name = "schemars"; + packageId = "schemars"; + optional = true; + } { name = "serde"; packageId = "serde"; @@ -4705,6 +4710,10 @@ rec { } ]; devDependencies = [ + { + name = "schemars"; + packageId = "schemars"; + } { name = "serde_json"; packageId = "serde_json"; @@ -4716,7 +4725,7 @@ rec { "schemars" = [ "dep:schemars" ]; "utoipa" = [ "dep:utoipa" ]; }; - resolvedDefaultFeatures = [ "default" "diff" ]; + resolvedDefaultFeatures = [ "default" "diff" "schemars" ]; }; "jsonpath-rust" = rec { crateName = "jsonpath-rust"; @@ -4842,8 +4851,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "k8s_version"; @@ -9516,8 +9525,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_certs"; @@ -9649,6 +9658,10 @@ rec { name = "indoc"; packageId = "indoc"; } + { + name = "java-properties"; + packageId = "java-properties"; + } { name = "product-config"; packageId = "product-config"; @@ -9711,8 +9724,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_operator"; @@ -9770,6 +9783,7 @@ rec { { name = "json-patch"; packageId = "json-patch"; + features = [ "schemars" ]; } { name = "k8s-openapi"; @@ -9873,6 +9887,10 @@ rec { packageId = "url"; features = [ "serde" ]; } + { + name = "uuid"; + packageId = "uuid"; + } ]; features = { "certs" = [ "dep:stackable-certs" ]; @@ -9891,8 +9909,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; procMacro = true; @@ -9926,8 +9944,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_shared"; @@ -10007,8 +10025,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_telemetry"; @@ -10117,8 +10135,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_versioned"; @@ -10167,8 +10185,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; procMacro = true; @@ -10235,8 +10253,8 @@ rec { edition = "2024"; workspace_member = null; src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "7a5f0c3fbcd091340214a23f0607fcd4b4fcc152"; + url = "https://github.com/stackabletech//operator-rs.git"; + rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_webhook"; @@ -12260,6 +12278,66 @@ rec { }; resolvedDefaultFeatures = [ "default" ]; }; + "uuid" = rec { + crateName = "uuid"; + version = "1.23.2"; + edition = "2021"; + sha256 = "1xy942s4z0bi8p3441wvd4ry3hx6ry1c7s6fgrr38462xqybhn6j"; + authors = [ + "Ashley Mannix" + "Dylan DPC" + "Hunar Roop Kahlon" + ]; + dependencies = [ + { + name = "js-sys"; + packageId = "js-sys"; + optional = true; + usesDefaultFeatures = false; + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)) && (builtins.elem "atomics" targetFeatures)); + } + { + name = "wasm-bindgen"; + packageId = "wasm-bindgen"; + optional = true; + usesDefaultFeatures = false; + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); + } + ]; + devDependencies = [ + { + name = "wasm-bindgen"; + packageId = "wasm-bindgen"; + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); + } + ]; + features = { + "arbitrary" = [ "dep:arbitrary" ]; + "atomic" = [ "dep:atomic" ]; + "borsh" = [ "dep:borsh" "dep:borsh-derive" ]; + "bytemuck" = [ "dep:bytemuck" ]; + "default" = [ "std" ]; + "fast-rng" = [ "rng" "dep:rand" ]; + "js" = [ "dep:wasm-bindgen" "dep:js-sys" ]; + "md5" = [ "dep:md-5" ]; + "rng" = [ "dep:getrandom" ]; + "rng-getrandom" = [ "rng" "dep:getrandom" "uuid-rng-internal-lib" "uuid-rng-internal-lib/getrandom" ]; + "rng-rand" = [ "rng" "dep:rand" "uuid-rng-internal-lib" "uuid-rng-internal-lib/rand" ]; + "serde" = [ "dep:serde_core" ]; + "sha1" = [ "dep:sha1_smol" ]; + "slog" = [ "dep:slog" ]; + "std" = [ "wasm-bindgen?/std" "js-sys?/std" ]; + "uuid-rng-internal-lib" = [ "dep:uuid-rng-internal-lib" ]; + "v1" = [ "atomic" ]; + "v3" = [ "md5" ]; + "v4" = [ "rng" ]; + "v5" = [ "sha1" ]; + "v6" = [ "atomic" ]; + "v7" = [ "rng" ]; + "zerocopy" = [ "dep:zerocopy" ]; + }; + resolvedDefaultFeatures = [ "default" "std" ]; + }; "valuable" = rec { crateName = "valuable"; version = "0.1.1"; diff --git a/Cargo.toml b/Cargo.toml index 8620a9ef..2ce4ff81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ clap = "4.5" const_format = "0.2" futures = "0.3" indoc = "2.0" +java-properties = "2.0" rstest = "0.26" semver = "1.0" serde = { version = "1.0", features = ["derive"] } @@ -30,5 +31,6 @@ tokio = { version = "1.40", features = ["full"] } tracing = "0.1" [patch."https://github.com/stackabletech/operator-rs.git"] +stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "smooth-operator" } # stackable-operator = { path = "../operator-rs/crates/stackable-operator" } # stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" } diff --git a/crate-hashes.json b/crate-hashes.json index 86f2b840..5564a89e 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#k8s-version@0.1.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-certs@0.4.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator-derive@0.3.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-operator@0.111.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-shared@0.1.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-telemetry@0.6.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned-macros@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-versioned@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.111.1#stackable-webhook@0.9.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index f2903572..23a9234b 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -13,6 +13,7 @@ product-config.workspace = true stackable-operator.workspace = true indoc.workspace = true +java-properties.workspace = true anyhow.workspace = true clap.workspace = true const_format.workspace = true diff --git a/rust/operator-binary/src/config/mod.rs b/rust/operator-binary/src/config/mod.rs index ae92b3c2..162c9c09 100644 --- a/rust/operator-binary/src/config/mod.rs +++ b/rust/operator-binary/src/config/mod.rs @@ -1,3 +1,4 @@ pub mod command; pub mod jvm; pub mod node_id_hasher; +pub mod writer; diff --git a/rust/operator-binary/src/config/writer.rs b/rust/operator-binary/src/config/writer.rs new file mode 100644 index 00000000..a74babf0 --- /dev/null +++ b/rust/operator-binary/src/config/writer.rs @@ -0,0 +1,78 @@ +//! Writer for Java `.properties` files. +//! +//! Vendored from the `product-config` crate's `writer` module so the operator no +//! longer depends on `product-config` for rendering. + +use std::io::Write; + +use java_properties::{PropertiesError, PropertiesWriter}; +use snafu::{ResultExt, Snafu}; + +#[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(()) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + + 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 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/rust/operator-binary/src/resource/configmap.rs b/rust/operator-binary/src/resource/configmap.rs index 473120b3..7359a0d2 100644 --- a/rust/operator-binary/src/resource/configmap.rs +++ b/rust/operator-binary/src/resource/configmap.rs @@ -4,7 +4,7 @@ use std::{ }; use indoc::formatdoc; -use product_config::{types::PropertyNameKind, writer::to_java_properties_string}; +use product_config::types::PropertyNameKind; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, @@ -14,6 +14,7 @@ use stackable_operator::{ }; use crate::{ + config::writer::to_java_properties_string, controller::KAFKA_CONTROLLER_NAME, crd::{ JVM_SECURITY_PROPERTIES_FILE, KafkaPodDescriptor, MetadataManager, @@ -48,7 +49,7 @@ pub enum Error { rolegroup ))] JvmSecurityProperties { - source: product_config::writer::PropertiesWriterError, + source: crate::config::writer::PropertiesWriterError, rolegroup: String, }, @@ -64,7 +65,7 @@ pub enum Error { #[snafu(display("failed to serialize config for {rolegroup}"))] SerializeConfig { - source: product_config::writer::PropertiesWriterError, + source: crate::config::writer::PropertiesWriterError, rolegroup: RoleGroupRef, }, From 75a05194f194c8061cbf4ebfdb4a00416e60b99a Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 3 Jun 2026 18:04:31 +0200 Subject: [PATCH 02/47] refactor: extract per-file kafka .properties builders Splits server_properties_file into controller/build/properties/{broker,controller}_properties builders (base map + security settings + graceful-shutdown + user overrides), wired into resource/configmap.rs by role. The property assembly was moved verbatim, so the rendered broker.properties/controller.properties are unchanged (18 tests pass; byte parity to be confirmed via the kuttl ConfigMap snapshot). Override input stays BTreeMap; no product-config removed yet (later increment). Co-Authored-By: Claude Opus 4.8 --- rust/operator-binary/src/controller.rs | 1 + .../src/controller/build/mod.rs | 3 + .../build/properties/broker_properties.rs | 119 ++++++++ .../build/properties/controller_properties.rs | 76 ++++++ .../src/controller/build/properties/mod.rs | 35 +++ .../operator-binary/src/resource/configmap.rs | 255 +++--------------- 6 files changed, 273 insertions(+), 216 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/mod.rs create mode 100644 rust/operator-binary/src/controller/build/properties/broker_properties.rs create mode 100644 rust/operator-binary/src/controller/build/properties/controller_properties.rs create mode 100644 rust/operator-binary/src/controller/build/properties/mod.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index a064c1ac..bebda106 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -26,6 +26,7 @@ use stackable_operator::{ }; use strum::{EnumDiscriminants, IntoStaticStr}; +pub(crate) mod build; mod dereference; mod validate; diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs new file mode 100644 index 00000000..2f51c3f6 --- /dev/null +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -0,0 +1,3 @@ +//! Builders that assemble Kubernetes resources for kafka rolegroups. + +pub mod properties; diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs new file mode 100644 index 00000000..01bd0d1f --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -0,0 +1,119 @@ +use std::collections::BTreeMap; + +use snafu::OptionExt; + +use crate::{ + crd::{ + KafkaPodDescriptor, + listener::{KafkaListenerConfig, KafkaListenerName}, + role::{ + KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, + KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, + KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, + }, + security::KafkaTlsSecurity, + }, + operations::graceful_shutdown::graceful_shutdown_config_properties, +}; + +use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; + +pub fn build( + kafka_security: &KafkaTlsSecurity, + listener_config: &KafkaListenerConfig, + pod_descriptors: &[KafkaPodDescriptor], + opa_connect_string: Option<&str>, + kraft_mode: bool, + disable_broker_id_generation: bool, + overrides: BTreeMap, +) -> Result, Error> { + let kraft_controllers = kraft_controllers(pod_descriptors); + + let mut result = BTreeMap::from([ + ( + KAFKA_LOG_DIRS.to_string(), + "/stackable/data/topicdata".to_string(), + ), + (KAFKA_LISTENERS.to_string(), listener_config.listeners()), + ( + KAFKA_ADVERTISED_LISTENERS.to_string(), + listener_config.advertised_listeners(), + ), + ( + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), + listener_config.listener_security_protocol_map(), + ), + ( + "inter.broker.listener.name".to_string(), + KafkaListenerName::Internal.to_string(), + ), + ]); + + if kraft_mode { + let kraft_controllers = kraft_controllers.context(NoKraftControllersFoundSnafu)?; + + // Running in KRaft mode + result.extend([ + ( + "broker.id.generation.enable".to_string(), + "false".to_string(), + ), + (KAFKA_NODE_ID.to_string(), "${env:REPLICA_ID}".to_string()), + ( + KAFKA_PROCESS_ROLES.to_string(), + KafkaRole::Broker.to_string(), + ), + ( + "controller.listener.names".to_string(), + KafkaListenerName::Controller.to_string(), + ), + ( + KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), + kraft_controllers.clone(), + ), + ]); + } else { + // Running with ZooKeeper enabled + result.extend([( + "zookeeper.connect".to_string(), + "${env:ZOOKEEPER}".to_string(), + )]); + // We are in zookeeper mode and the user has defined a broker id mapping + // so we disable automatic id generation. + // This check ensures that existing clusters running in ZooKeeper mode do not + // suddenly break after the introduction of this change. + if disable_broker_id_generation { + result.extend([ + ( + "broker.id.generation.enable".to_string(), + "false".to_string(), + ), + (KAFKA_BROKER_ID.to_string(), "${env:REPLICA_ID}".to_string()), + ]); + } + } + + // Enable OPA authorization + if opa_connect_string.is_some() { + result.extend([ + ( + "authorizer.class.name".to_string(), + "org.openpolicyagent.kafka.OpaAuthorizer".to_string(), + ), + ( + "opa.authorizer.metrics.enabled".to_string(), + "true".to_string(), + ), + ( + "opa.authorizer.url".to_string(), + opa_connect_string.unwrap_or_default().to_string(), + ), + ]); + } + + result.extend(kafka_security.broker_config_settings()); + result.extend(graceful_shutdown_config_properties()); + result.extend(overrides); + + Ok(result) +} diff --git a/rust/operator-binary/src/controller/build/properties/controller_properties.rs b/rust/operator-binary/src/controller/build/properties/controller_properties.rs new file mode 100644 index 00000000..f9862dd3 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -0,0 +1,76 @@ +use std::collections::BTreeMap; + +use snafu::OptionExt; + +use crate::{ + crd::{ + KafkaPodDescriptor, + listener::{KafkaListenerConfig, KafkaListenerName}, + role::{ + KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, + KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, + }, + security::KafkaTlsSecurity, + }, + operations::graceful_shutdown::graceful_shutdown_config_properties, +}; + +use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; + +pub fn build( + kafka_security: &KafkaTlsSecurity, + listener_config: &KafkaListenerConfig, + pod_descriptors: &[KafkaPodDescriptor], + kraft_mode: bool, + overrides: BTreeMap, +) -> Result, Error> { + let kraft_controllers = kraft_controllers(pod_descriptors).context(NoKraftControllersFoundSnafu)?; + + let mut result = BTreeMap::from([ + ( + KAFKA_LOG_DIRS.to_string(), + "/stackable/data/kraft".to_string(), + ), + (KAFKA_PROCESS_ROLES.to_string(), KafkaRole::Controller.to_string()), + ( + "controller.listener.names".to_string(), + KafkaListenerName::Controller.to_string(), + ), + ( + KAFKA_NODE_ID.to_string(), + "${env:REPLICA_ID}".to_string(), + ), + ( + KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), + kraft_controllers.clone(), + ), + ( + KAFKA_LISTENERS.to_string(), + "CONTROLLER://${env:POD_NAME}.${env:ROLEGROUP_HEADLESS_SERVICE_NAME}.${env:NAMESPACE}.svc.${env:CLUSTER_DOMAIN}:${env:KAFKA_CLIENT_PORT}".to_string(), + ), + ( + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), + listener_config + .listener_security_protocol_map_for_controller()), + ]); + + result.insert( + "inter.broker.listener.name".to_string(), + KafkaListenerName::Internal.to_string(), + ); + + // The ZooKeeper connection is needed for migration from ZooKeeper to KRaft mode. + // It is not needed once the controller is fully running in KRaft mode. + if !kraft_mode { + result.insert( + "zookeeper.connect".to_string(), + "${env:ZOOKEEPER}".to_string(), + ); + } + + result.extend(kafka_security.controller_config_settings()); + result.extend(graceful_shutdown_config_properties()); + result.extend(overrides); + + Ok(result) +} diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs new file mode 100644 index 00000000..adcdcaae --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -0,0 +1,35 @@ +//! Property-file builders for Kafka rolegroup ConfigMaps. + +pub mod broker_properties; +pub mod controller_properties; + +use snafu::Snafu; + +use crate::crd::{KafkaPodDescriptor, role::KafkaRole}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("no Kraft controllers found to build"))] + NoKraftControllersFound, +} + +pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Option { + let result = pod_descriptors + .iter() + .filter(|pd| pd.role == KafkaRole::Controller.to_string()) + .map(|desc| { + format!( + "{fqdn}:{client_port}", + fqdn = desc.fqdn(), + client_port = desc.client_port + ) + }) + .collect::>() + .join(","); + + if result.is_empty() { + None + } else { + Some(result) + } +} diff --git a/rust/operator-binary/src/resource/configmap.rs b/rust/operator-binary/src/resource/configmap.rs index 7359a0d2..3f400e6c 100644 --- a/rust/operator-binary/src/resource/configmap.rs +++ b/rust/operator-binary/src/resource/configmap.rs @@ -1,11 +1,8 @@ -use std::{ - collections::{BTreeMap, HashMap}, - str::FromStr, -}; +use std::collections::{BTreeMap, HashMap}; use indoc::formatdoc; use product_config::types::PropertyNameKind; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, commons::product_image_selection::ResolvedProductImage, @@ -19,16 +16,11 @@ use crate::{ crd::{ JVM_SECURITY_PROPERTIES_FILE, KafkaPodDescriptor, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, - listener::{KafkaListenerConfig, KafkaListenerName, node_address_cmd}, - role::{ - AnyConfig, KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, - KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, - KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, - }, + listener::{KafkaListenerConfig, node_address_cmd}, + role::AnyConfig, security::KafkaTlsSecurity, v1alpha1, }, - operations::graceful_shutdown::graceful_shutdown_config_properties, product_logging::extend_role_group_config_map, utils::build_recommended_labels, }; @@ -69,13 +61,10 @@ pub enum Error { rolegroup: RoleGroupRef, }, - #[snafu(display("no Kraft controllers found to build"))] - NoKraftControllersFound, - - #[snafu(display("unknown Kafka role [{name}]"))] - UnknownKafkaRole { - source: strum::ParseError, - name: String, + #[snafu(display("failed to build properties for {rolegroup}"))] + BuildProperties { + source: crate::controller::build::properties::Error, + rolegroup: RoleGroupRef, }, #[snafu(display("failed to build jaas configuration file for {rolegroup}"))] @@ -101,35 +90,40 @@ pub fn build_rolegroup_config_map( .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; - let mut kafka_config = server_properties_file( - metadata_manager == MetadataManager::KRaft, - &rolegroup.role, - pod_descriptors, - listener_config, - opa_connect_string, - kafka - .spec - .cluster_config - .broker_id_pod_config_map_name - .is_some(), - )?; - - match merged_config { - AnyConfig::Broker(_) => kafka_config.extend(kafka_security.broker_config_settings()), + let overrides = rolegroup_config + .get(&PropertyNameKind::File(kafka_config_file_name.to_string())) + .cloned() + .unwrap_or_default(); + + let kafka_config = match merged_config { + AnyConfig::Broker(_) => { + crate::controller::build::properties::broker_properties::build( + kafka_security, + listener_config, + pod_descriptors, + opa_connect_string, + metadata_manager == MetadataManager::KRaft, + kafka + .spec + .cluster_config + .broker_id_pod_config_map_name + .is_some(), + overrides, + ) + } AnyConfig::Controller(_) => { - kafka_config.extend(kafka_security.controller_config_settings()) + crate::controller::build::properties::controller_properties::build( + kafka_security, + listener_config, + pod_descriptors, + metadata_manager == MetadataManager::KRaft, + overrides, + ) } } - - kafka_config.extend(graceful_shutdown_config_properties()); - - // Need to call this to get configOverrides :( - kafka_config.extend( - rolegroup_config - .get(&PropertyNameKind::File(kafka_config_file_name.to_string())) - .cloned() - .unwrap_or_default(), - ); + .with_context(|_| BuildPropertiesSnafu { + rolegroup: rolegroup.clone(), + })?; let kafka_config = kafka_config .into_iter() @@ -218,177 +212,6 @@ pub fn build_rolegroup_config_map( }) } -// Generate the content of both broker.properties and controller.properties files. -fn server_properties_file( - kraft_mode: bool, - role: &str, - pod_descriptors: &[KafkaPodDescriptor], - listener_config: &KafkaListenerConfig, - opa_connect_string: Option<&str>, - disable_broker_id_generation: bool, -) -> Result, Error> { - let kraft_controllers = kraft_controllers(pod_descriptors); - - let role = KafkaRole::from_str(role).context(UnknownKafkaRoleSnafu { - name: role.to_string(), - })?; - - match role { - KafkaRole::Controller => { - let kraft_controllers = kraft_controllers.context(NoKraftControllersFoundSnafu)?; - - let mut result = BTreeMap::from([ - ( - KAFKA_LOG_DIRS.to_string(), - "/stackable/data/kraft".to_string(), - ), - (KAFKA_PROCESS_ROLES.to_string(), role.to_string()), - ( - "controller.listener.names".to_string(), - KafkaListenerName::Controller.to_string(), - ), - ( - KAFKA_NODE_ID.to_string(), - "${env:REPLICA_ID}".to_string(), - ), - ( - KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), - kraft_controllers.clone(), - ), - ( - KAFKA_LISTENERS.to_string(), - "CONTROLLER://${env:POD_NAME}.${env:ROLEGROUP_HEADLESS_SERVICE_NAME}.${env:NAMESPACE}.svc.${env:CLUSTER_DOMAIN}:${env:KAFKA_CLIENT_PORT}".to_string(), - ), - ( - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), - listener_config - .listener_security_protocol_map_for_controller()), - ]); - - result.insert( - "inter.broker.listener.name".to_string(), - KafkaListenerName::Internal.to_string(), - ); - - // The ZooKeeper connection is needed for migration from ZooKeeper to KRaft mode. - // It is not needed once the controller is fully running in KRaft mode. - if !kraft_mode { - result.insert( - "zookeeper.connect".to_string(), - "${env:ZOOKEEPER}".to_string(), - ); - } - Ok(result) - } - KafkaRole::Broker => { - let mut result = BTreeMap::from([ - ( - KAFKA_LOG_DIRS.to_string(), - "/stackable/data/topicdata".to_string(), - ), - (KAFKA_LISTENERS.to_string(), listener_config.listeners()), - ( - KAFKA_ADVERTISED_LISTENERS.to_string(), - listener_config.advertised_listeners(), - ), - ( - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP.to_string(), - listener_config.listener_security_protocol_map(), - ), - ( - "inter.broker.listener.name".to_string(), - KafkaListenerName::Internal.to_string(), - ), - ]); - - if kraft_mode { - let kraft_controllers = kraft_controllers.context(NoKraftControllersFoundSnafu)?; - - // Running in KRaft mode - result.extend([ - ( - "broker.id.generation.enable".to_string(), - "false".to_string(), - ), - (KAFKA_NODE_ID.to_string(), "${env:REPLICA_ID}".to_string()), - ( - KAFKA_PROCESS_ROLES.to_string(), - KafkaRole::Broker.to_string(), - ), - ( - "controller.listener.names".to_string(), - KafkaListenerName::Controller.to_string(), - ), - ( - KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS.to_string(), - kraft_controllers.clone(), - ), - ]); - } else { - // Running with ZooKeeper enabled - result.extend([( - "zookeeper.connect".to_string(), - "${env:ZOOKEEPER}".to_string(), - )]); - // We are in zookeeper mode and the user has defined a broker id mapping - // so we disable automatic id generation. - // This check ensures that existing clusters running in ZooKeeper mode do not - // suddenly break after the introduction of this change. - if disable_broker_id_generation { - result.extend([ - ( - "broker.id.generation.enable".to_string(), - "false".to_string(), - ), - (KAFKA_BROKER_ID.to_string(), "${env:REPLICA_ID}".to_string()), - ]); - } - } - - // Enable OPA authorization - if opa_connect_string.is_some() { - result.extend([ - ( - "authorizer.class.name".to_string(), - "org.openpolicyagent.kafka.OpaAuthorizer".to_string(), - ), - ( - "opa.authorizer.metrics.enabled".to_string(), - "true".to_string(), - ), - ( - "opa.authorizer.url".to_string(), - opa_connect_string.unwrap_or_default().to_string(), - ), - ]); - } - - Ok(result) - } - } -} - -fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Option { - let result = pod_descriptors - .iter() - .filter(|pd| pd.role == KafkaRole::Controller.to_string()) - .map(|desc| { - format!( - "{fqdn}:{client_port}", - fqdn = desc.fqdn(), - client_port = desc.client_port - ) - }) - .collect::>() - .join(","); - - if result.is_empty() { - None - } else { - Some(result) - } -} - // Generate JAAS configuration file for Kerberos authentication // or an empty string if Kerberos is not enabled. // See https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html From c067058b801b082e29f414b0ad755418608b9c27 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 3 Jun 2026 18:05:03 +0200 Subject: [PATCH 03/47] formatting --- .../build/properties/broker_properties.rs | 9 +++--- .../build/properties/controller_properties.rs | 6 ++-- .../operator-binary/src/resource/configmap.rs | 28 +++++++++---------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs index 01bd0d1f..5d43ab79 100644 --- a/rust/operator-binary/src/controller/build/properties/broker_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -2,22 +2,21 @@ use std::collections::BTreeMap; use snafu::OptionExt; +use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; use crate::{ crd::{ KafkaPodDescriptor, listener::{KafkaListenerConfig, KafkaListenerName}, role::{ - KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, - KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, - KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, + KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, + KAFKA_PROCESS_ROLES, KafkaRole, }, security::KafkaTlsSecurity, }, operations::graceful_shutdown::graceful_shutdown_config_properties, }; -use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; - pub fn build( kafka_security: &KafkaTlsSecurity, listener_config: &KafkaListenerConfig, diff --git a/rust/operator-binary/src/controller/build/properties/controller_properties.rs b/rust/operator-binary/src/controller/build/properties/controller_properties.rs index f9862dd3..6a8172a8 100644 --- a/rust/operator-binary/src/controller/build/properties/controller_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use snafu::OptionExt; +use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; use crate::{ crd::{ KafkaPodDescriptor, @@ -15,8 +16,6 @@ use crate::{ operations::graceful_shutdown::graceful_shutdown_config_properties, }; -use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; - pub fn build( kafka_security: &KafkaTlsSecurity, listener_config: &KafkaListenerConfig, @@ -24,7 +23,8 @@ pub fn build( kraft_mode: bool, overrides: BTreeMap, ) -> Result, Error> { - let kraft_controllers = kraft_controllers(pod_descriptors).context(NoKraftControllersFoundSnafu)?; + let kraft_controllers = + kraft_controllers(pod_descriptors).context(NoKraftControllersFoundSnafu)?; let mut result = BTreeMap::from([ ( diff --git a/rust/operator-binary/src/resource/configmap.rs b/rust/operator-binary/src/resource/configmap.rs index 3f400e6c..bba2bac2 100644 --- a/rust/operator-binary/src/resource/configmap.rs +++ b/rust/operator-binary/src/resource/configmap.rs @@ -96,21 +96,19 @@ pub fn build_rolegroup_config_map( .unwrap_or_default(); let kafka_config = match merged_config { - AnyConfig::Broker(_) => { - crate::controller::build::properties::broker_properties::build( - kafka_security, - listener_config, - pod_descriptors, - opa_connect_string, - metadata_manager == MetadataManager::KRaft, - kafka - .spec - .cluster_config - .broker_id_pod_config_map_name - .is_some(), - overrides, - ) - } + AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( + kafka_security, + listener_config, + pod_descriptors, + opa_connect_string, + metadata_manager == MetadataManager::KRaft, + kafka + .spec + .cluster_config + .broker_id_pod_config_map_name + .is_some(), + overrides, + ), AnyConfig::Controller(_) => { crate::controller::build::properties::controller_properties::build( kafka_security, From cb850dcf63bffc7a5335de6c81b83ba236e7f3ec Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 3 Jun 2026 18:15:31 +0200 Subject: [PATCH 04/47] refactor: move rolegroup ConfigMap build into controller/build/config_map Relocates build_rolegroup_config_map (plus the jaas_config_file helper and its Error enum) from resource/configmap.rs to controller/build/config_map.rs, colocating the ConfigMap assembler with the per-file property builders and matching the controller/build/config_map.rs layout in hdfs/airflow. Pure move + repointed caller; no behaviour change (18 tests pass). Co-Authored-By: Claude Opus 4.8 --- rust/operator-binary/src/controller.rs | 5 ++--- .../configmap.rs => controller/build/config_map.rs} | 0 rust/operator-binary/src/controller/build/mod.rs | 1 + rust/operator-binary/src/resource/mod.rs | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) rename rust/operator-binary/src/{resource/configmap.rs => controller/build/config_map.rs} (100%) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index bebda106..45006a5e 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -40,7 +40,6 @@ use crate::{ discovery::{self, build_discovery_configmap}, operations::pdb::add_pdbs, resource::{ - configmap::build_rolegroup_config_map, listener::build_broker_rolegroup_bootstrap_listener, service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, statefulset::{build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset}, @@ -167,7 +166,7 @@ pub enum Error { #[snafu(display("failed to build configmap"))] BuildConfigMap { - source: crate::resource::configmap::Error, + source: crate::controller::build::config_map::Error, }, #[snafu(display("failed to build service"))] @@ -329,7 +328,7 @@ pub async fn reconcile_kafka( ) .context(BuildPodDescriptorsSnafu)?; - let rg_configmap = build_rolegroup_config_map( + let rg_configmap = build::config_map::build_rolegroup_config_map( kafka, &image, &kafka_security, diff --git a/rust/operator-binary/src/resource/configmap.rs b/rust/operator-binary/src/controller/build/config_map.rs similarity index 100% rename from rust/operator-binary/src/resource/configmap.rs rename to rust/operator-binary/src/controller/build/config_map.rs diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index 2f51c3f6..b8c4c422 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -1,3 +1,4 @@ //! Builders that assemble Kubernetes resources for kafka rolegroups. +pub mod config_map; pub mod properties; diff --git a/rust/operator-binary/src/resource/mod.rs b/rust/operator-binary/src/resource/mod.rs index a79483f8..514d0adb 100644 --- a/rust/operator-binary/src/resource/mod.rs +++ b/rust/operator-binary/src/resource/mod.rs @@ -1,4 +1,3 @@ -pub mod configmap; pub mod listener; pub mod service; pub mod statefulset; From 3215d08ebdd082b3dfb4a79f799787b575c37c70 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 3 Jun 2026 18:49:39 +0200 Subject: [PATCH 05/47] feat: remove product-config; merge config/env overrides directly in validate Replaces the product-config validation path with a ValidatedKafkaCluster that carries, per role group, the merged config plus the config-file, jvm-security, and env overrides resolved directly from the CRD (role <- role-group). Overrides now use stackable_operator::v2::config_overrides::KeyValueConfigOverrides (matching trino/hdfs); the v1 KeyValueOverridesProvider impls and the per-role Configuration impls are removed, and KAFKA_CLUSTER_ID injection moves into the override merge (collect_*_role_group_overrides). The dereferenced authorization config is folded into the validated cluster. Drops the product-config crate dependency (it remains transitive via stackable-operator). The CRD gains `nullable: true` on configOverrides values (v2 allows null to delete a key). Rendered .properties and env vars are unchanged (18 tests pass; byte parity to be confirmed via kuttl). Regenerated extra/crds.yaml and Cargo.nix. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 1 - Cargo.nix | 4 - Cargo.toml | 1 - extra/crds.yaml | 8 + rust/operator-binary/Cargo.toml | 1 - rust/operator-binary/src/controller.rs | 64 ++-- .../src/controller/build/config_map.rs | 22 +- .../src/controller/validate.rs | 353 +++++++++++++----- rust/operator-binary/src/crd/mod.rs | 30 +- rust/operator-binary/src/crd/role/broker.rs | 44 +-- .../src/crd/role/controller.rs | 44 +-- rust/operator-binary/src/crd/role/mod.rs | 2 + rust/operator-binary/src/main.rs | 8 +- .../src/resource/statefulset.rs | 22 +- 14 files changed, 314 insertions(+), 290 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f759557..87abc76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2922,7 +2922,6 @@ dependencies = [ "futures", "indoc", "java-properties", - "product-config", "rstest", "serde", "serde_json", diff --git a/Cargo.nix b/Cargo.nix index 0cb45c8f..debf5eef 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -9662,10 +9662,6 @@ rec { name = "java-properties"; packageId = "java-properties"; } - { - name = "product-config"; - packageId = "product-config"; - } { name = "serde"; packageId = "serde"; diff --git a/Cargo.toml b/Cargo.toml index 2ce4ff81..74c770d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ edition = "2021" repository = "https://github.com/stackabletech/kafka-operator" [workspace.dependencies] -product-config = { git = "https://github.com/stackabletech/product-config.git", tag = "0.8.0" } stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "stackable-operator-0.111.1", features = ["crds", "webhook"] } anyhow = "1.0" diff --git a/extra/crds.yaml b/extra/crds.yaml index a2c3b7d9..9a83de14 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -541,6 +541,7 @@ spec: properties: broker.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. @@ -551,6 +552,7 @@ spec: type: object security.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. @@ -1158,6 +1160,7 @@ spec: properties: broker.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. @@ -1168,6 +1171,7 @@ spec: type: object security.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. @@ -1786,6 +1790,7 @@ spec: properties: controller.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. @@ -1796,6 +1801,7 @@ spec: type: object security.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. @@ -2235,6 +2241,7 @@ spec: properties: controller.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. @@ -2245,6 +2252,7 @@ spec: type: object security.properties: additionalProperties: + nullable: true type: string description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 23a9234b..077ccccd 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -9,7 +9,6 @@ repository.workspace = true publish = false [dependencies] -product-config.workspace = true stackable-operator.workspace = true indoc.workspace = true diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 45006a5e..8f495905 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,9 +1,8 @@ //! Ensures that `Pod`s are configured and running for each [`v1alpha1::KafkaCluster`]. -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use const_format::concatcp; -use product_config::ProductConfigManager; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, @@ -51,7 +50,6 @@ pub const KAFKA_FULL_CONTROLLER_NAME: &str = concatcp!(KAFKA_CONTROLLER_NAME, '. pub struct Ctx { pub client: stackable_operator::client::Client, - pub product_config: ProductConfigManager, pub operator_environment: OperatorEnvironmentOptions, } @@ -114,9 +112,6 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to resolve and merge config for role and role group"))] - FailedToResolveConfig { source: crate::crd::role::Error }, - #[snafu(display("failed to patch service account"))] ApplyServiceAccount { source: stackable_operator::cluster_resources::Error, @@ -156,9 +151,6 @@ pub enum Error { #[snafu(display("KafkaCluster object is misconfigured"))] MisconfiguredKafkaCluster { source: crd::Error }, - #[snafu(display("failed to parse role: {source}"))] - ParseRole { source: strum::ParseError }, - #[snafu(display("failed to build statefulset"))] BuildStatefulset { source: crate::resource::statefulset::Error, @@ -198,7 +190,6 @@ impl ReconcilerError for Error { Error::ApplyDiscoveryConfig { .. } => None, Error::DeleteOrphans { .. } => None, Error::CreateClusterResources { .. } => None, - Error::FailedToResolveConfig { .. } => None, Error::ApplyServiceAccount { .. } => None, Error::ApplyRoleBinding { .. } => None, Error::ApplyStatus { .. } => None, @@ -207,7 +198,6 @@ impl ReconcilerError for Error { Error::GetRequiredLabels { .. } => None, Error::InvalidKafkaCluster { .. } => None, Error::MisconfiguredKafkaCluster { .. } => None, - Error::ParseRole { .. } => None, Error::BuildStatefulset { .. } => None, Error::BuildConfigMap { .. } => None, Error::BuildService { .. } => None, @@ -238,18 +228,13 @@ pub async fn reconcile_kafka( .context(DereferenceSnafu)?; // validate (no client required) - let validate::ValidatedInputs { + let validate::ValidatedKafkaCluster { authorization_config, image, kafka_security, - role_config: validated_config, - } = validate::validate( - kafka, - dereferenced_objects, - &ctx.operator_environment, - &ctx.product_config, - ) - .context(ValidateClusterSnafu)?; + role_groups, + } = validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) + .context(ValidateClusterSnafu)?; let opa_connect = authorization_config .as_ref() @@ -295,15 +280,9 @@ pub async fn reconcile_kafka( let mut bootstrap_listeners = Vec::::new(); - for (kafka_role_str, role_config) in &validated_config { - let kafka_role = KafkaRole::from_str(kafka_role_str).context(ParseRoleSnafu)?; - - for (rolegroup_name, rolegroup_config) in role_config.iter() { - let rolegroup_ref = kafka.rolegroup_ref(&kafka_role, rolegroup_name); - - let merged_config = kafka_role - .merged_config(kafka, &rolegroup_ref.role_group) - .context(FailedToResolveConfigSnafu)?; + for (kafka_role, rg_map) in &role_groups { + for (rolegroup_name, validated_rg) in rg_map { + let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name); let rg_headless_service = build_rolegroup_headless_service(kafka, &image, &rolegroup_ref, &kafka_security) @@ -333,8 +312,9 @@ pub async fn reconcile_kafka( &image, &kafka_security, &rolegroup_ref, - rolegroup_config, - &merged_config, + validated_rg.config_file_overrides.clone(), + validated_rg.jvm_security_overrides.clone(), + &validated_rg.merged_config, &kafka_listeners, &pod_descriptors, opa_connect.as_deref(), @@ -344,37 +324,37 @@ pub async fn reconcile_kafka( let rg_statefulset = match kafka_role { KafkaRole::Broker => build_broker_rolegroup_statefulset( kafka, - &kafka_role, + kafka_role, &image, &rolegroup_ref, - rolegroup_config, + &validated_rg.env_overrides, &kafka_security, - &merged_config, + &validated_rg.merged_config, &rbac_sa, &client.kubernetes_cluster_info, ) .context(BuildStatefulsetSnafu)?, KafkaRole::Controller => build_controller_rolegroup_statefulset( kafka, - &kafka_role, + kafka_role, &image, &rolegroup_ref, - rolegroup_config, + &validated_rg.env_overrides, &kafka_security, - &merged_config, + &validated_rg.merged_config, &rbac_sa, &client.kubernetes_cluster_info, ) .context(BuildStatefulsetSnafu)?, }; - if let AnyConfig::Broker(broker_config) = merged_config { + if let AnyConfig::Broker(broker_config) = &validated_rg.merged_config { let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( kafka, &image, &kafka_security, &rolegroup_ref, - &broker_config, + broker_config, ) .context(BuildListenerSnafu)?; bootstrap_listeners.push( @@ -417,12 +397,12 @@ pub async fn reconcile_kafka( ); } - let role_config = kafka.role_config(&kafka_role); + let role_cfg = kafka.role_config(kafka_role); if let Some(GenericRoleConfig { pod_disruption_budget: pdb, - }) = role_config + }) = role_cfg { - add_pdbs(pdb, kafka, &kafka_role, client, &mut cluster_resources) + add_pdbs(pdb, kafka, kafka_role, client, &mut cluster_resources) .await .context(FailedToCreatePdbSnafu)?; } diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index bba2bac2..b018f449 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -1,7 +1,6 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use indoc::formatdoc; -use product_config::types::PropertyNameKind; use snafu::{ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, @@ -78,7 +77,8 @@ pub fn build_rolegroup_config_map( resolved_product_image: &ResolvedProductImage, kafka_security: &KafkaTlsSecurity, rolegroup: &RoleGroupRef, - rolegroup_config: &HashMap>, + config_file_overrides: BTreeMap, + jvm_security_overrides: BTreeMap, merged_config: &AnyConfig, listener_config: &KafkaListenerConfig, pod_descriptors: &[KafkaPodDescriptor], @@ -90,11 +90,6 @@ pub fn build_rolegroup_config_map( .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; - let overrides = rolegroup_config - .get(&PropertyNameKind::File(kafka_config_file_name.to_string())) - .cloned() - .unwrap_or_default(); - let kafka_config = match merged_config { AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( kafka_security, @@ -107,7 +102,7 @@ pub fn build_rolegroup_config_map( .cluster_config .broker_id_pod_config_map_name .is_some(), - overrides, + config_file_overrides, ), AnyConfig::Controller(_) => { crate::controller::build::properties::controller_properties::build( @@ -115,7 +110,7 @@ pub fn build_rolegroup_config_map( listener_config, pod_descriptors, metadata_manager == MetadataManager::KRaft, - overrides, + config_file_overrides, ) } } @@ -128,12 +123,7 @@ pub fn build_rolegroup_config_map( .map(|(k, v)| (k, Some(v))) .collect::>(); - let jvm_sec_props: BTreeMap> = rolegroup_config - .get(&PropertyNameKind::File( - JVM_SECURITY_PROPERTIES_FILE.to_string(), - )) - .cloned() - .unwrap_or_default() + let jvm_sec_props: BTreeMap> = jvm_security_overrides .into_iter() .map(|(k, v)| (k, Some(v))) .collect(); diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index a3e441a0..72d6fa6a 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -1,28 +1,23 @@ //! The validate step in the KafkaCluster controller. //! //! Synchronously validates inputs that don't require a Kubernetes client. Produces -//! [`ValidatedInputs`], consumed by the rest of `reconcile_kafka`. +//! [`ValidatedKafkaCluster`], consumed by the rest of `reconcile_kafka`. -use std::collections::HashMap; +use std::collections::BTreeMap; -use product_config::{ProductConfigManager, types::PropertyNameKind}; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, commons::product_image_selection::{self, ResolvedProductImage}, - product_config_utils::{ - ValidatedRoleConfigByPropertyKind, transform_all_roles_to_config, - validate_all_roles_and_groups_config, - }, }; use crate::{ controller::dereference::DereferencedObjects, crd::{ - self, CONTAINER_IMAGE_BASE_NAME, JVM_SECURITY_PROPERTIES_FILE, + self, CONTAINER_IMAGE_BASE_NAME, authentication::{self}, authorization::KafkaAuthorizationConfig, - role::{KafkaRole, broker::BROKER_PROPERTIES_FILE, controller::CONTROLLER_PROPERTIES_FILE}, + role::{AnyConfig, KafkaRole}, security::{self, KafkaTlsSecurity}, v1alpha1, }, @@ -44,25 +39,39 @@ pub enum Error { #[snafu(display("cluster object defines no '{role}' role"))] MissingKafkaRole { source: crd::Error, role: KafkaRole }, - #[snafu(display("failed to generate product config"))] - GenerateProductConfig { - source: stackable_operator::product_config_utils::Error, - }, - - #[snafu(display("invalid product config"))] - InvalidProductConfig { - source: stackable_operator::product_config_utils::Error, - }, + #[snafu(display("failed to resolve merged config for rolegroup"))] + ResolveMergedConfig { source: crate::crd::role::Error }, } type Result = std::result::Result; -/// Synchronous inputs the rest of `reconcile_kafka` needs after dereferencing. -pub struct ValidatedInputs { - pub authorization_config: Option, +/// The validated cluster. Carries everything the build steps need, resolved once +/// here so downstream code never re-derives it or touches the raw spec. +pub struct ValidatedKafkaCluster { pub image: ResolvedProductImage, pub kafka_security: KafkaTlsSecurity, - pub role_config: ValidatedRoleConfigByPropertyKind, + // DESIGN DECISION: the dereferenced authorization config is folded into the + // validated cluster (read from here downstream). The other dereferenced input, + // the authentication classes, is intentionally NOT stored: it is fully consumed + // here to build `kafka_security`. Alternative: also store the resolved auth + // classes — rejected because nothing downstream needs them beyond kafka_security. + pub authorization_config: Option, + pub role_groups: BTreeMap>, +} + +pub struct ValidatedRoleGroupConfig { + pub merged_config: AnyConfig, + // DESIGN DECISION: overrides are resolved into flat maps HERE rather than stored + // as the typed KeyValueConfigOverrides and resolved in the per-file builders (the + // hdfs-operator pattern). Reason: broker and controller use different override + // struct types (KafkaBrokerConfigOverrides vs KafkaControllerConfigOverrides), so a + // single typed field would require an enum. Resolving here keeps the build/properties + // builders taking plain `BTreeMap`. Alternative: an enum over the two + // override types threaded to builders that call resolved_overrides() — more types for + // no behavioural gain. + pub config_file_overrides: BTreeMap, + pub jvm_security_overrides: BTreeMap, + pub env_overrides: BTreeMap, } /// Validates the cluster spec and the dereferenced inputs. @@ -70,8 +79,7 @@ pub fn validate( kafka: &v1alpha1::KafkaCluster, dereferenced_objects: DereferencedObjects, operator_environment: &OperatorEnvironmentOptions, - product_config: &ProductConfigManager, -) -> Result { +) -> Result { let image = kafka .spec .image @@ -99,81 +107,242 @@ pub fn validate( .validate_authentication_methods() .context(FailedToValidateAuthenticationMethodSnafu)?; - let role_config = validated_product_config(kafka, &image.product_version, product_config)?; + // DESIGN DECISION: build the per-rolegroup config (merged config + resolved overrides) + // here, so reconcile reads a fully-typed ValidatedKafkaCluster instead of re-deriving + // merged_config in the loop and threading a product-config HashMap. Alternative: keep + // deriving merged_config in the reconcile loop — rejected; validation is the right place + // to prove every rolegroup resolves before any resource is built. + let mut role_groups: BTreeMap> = + BTreeMap::new(); - Ok(ValidatedInputs { - authorization_config: dereferenced_objects.authorization_config, + // Brokers always exist. + let broker_role = kafka + .broker_role() + .cloned() + .context(MissingKafkaRoleSnafu { + role: KafkaRole::Broker, + })?; + + let mut broker_groups: BTreeMap = BTreeMap::new(); + for rolegroup_name in broker_role.role_groups.keys() { + let merged_config = KafkaRole::Broker + .merged_config(kafka, rolegroup_name) + .context(ResolveMergedConfigSnafu)?; + let (config_file_overrides, jvm_security_overrides, env_overrides) = + collect_broker_role_group_overrides(kafka, &broker_role, rolegroup_name); + broker_groups.insert( + rolegroup_name.clone(), + ValidatedRoleGroupConfig { + merged_config, + config_file_overrides, + jvm_security_overrides, + env_overrides, + }, + ); + } + role_groups.insert(KafkaRole::Broker, broker_groups); + + // We need this guard because controller_role() returns an error if controllers is None, + // which would stop reconciliation for ZooKeeper-mode clusters. + if kafka.spec.controllers.is_some() { + let controller_role = kafka + .controller_role() + .cloned() + .context(MissingKafkaRoleSnafu { + role: KafkaRole::Controller, + })?; + + let mut controller_groups: BTreeMap = BTreeMap::new(); + for rolegroup_name in controller_role.role_groups.keys() { + let merged_config = KafkaRole::Controller + .merged_config(kafka, rolegroup_name) + .context(ResolveMergedConfigSnafu)?; + let (config_file_overrides, jvm_security_overrides, env_overrides) = + collect_controller_role_group_overrides(kafka, &controller_role, rolegroup_name); + controller_groups.insert( + rolegroup_name.clone(), + ValidatedRoleGroupConfig { + merged_config, + config_file_overrides, + jvm_security_overrides, + env_overrides, + }, + ); + } + role_groups.insert(KafkaRole::Controller, controller_groups); + } + + Ok(ValidatedKafkaCluster { image, kafka_security, - role_config, + authorization_config: dereferenced_objects.authorization_config, + role_groups, }) } -fn validated_product_config( +// DESIGN DECISION: role-group overrides are merged role-level first, then role-group +// extended on top so role-group wins — identical to the precedent product-config used. +// We read the v2 KeyValueConfigOverrides `.overrides` map and BTreeMap::extend rather +// than using its `Merge` impl, because plain extend reproduces the old behaviour exactly +// (last-writer-wins per key) and avoids depending on Merge semantics. Alternative: +// KeyValueConfigOverrides::merge — equivalent here but an unnecessary semantic dependency. +fn collect_broker_role_group_overrides( kafka: &v1alpha1::KafkaCluster, - product_version: &str, - product_config: &ProductConfigManager, -) -> Result { - let mut role_config = HashMap::new(); - - let broker_role = [( - KafkaRole::Broker.to_string(), - ( - vec![ - PropertyNameKind::File(BROKER_PROPERTIES_FILE.to_string()), - PropertyNameKind::File(JVM_SECURITY_PROPERTIES_FILE.to_string()), - PropertyNameKind::Env, - ], - kafka - .broker_role() - .cloned() - .context(MissingKafkaRoleSnafu { - role: KafkaRole::Broker, - })? - .erase(), - ), - )] - .into(); - - let broker_role_config = - transform_all_roles_to_config(kafka, &broker_role).context(GenerateProductConfigSnafu)?; - - role_config.extend(broker_role_config); - - // We need this because controller_role() raises an error if non-existent, - // which would stop reconciliation. - if kafka.spec.controllers.is_some() { - let controller_role = [( - KafkaRole::Controller.to_string(), - ( - vec![ - PropertyNameKind::File(CONTROLLER_PROPERTIES_FILE.to_string()), - PropertyNameKind::File(JVM_SECURITY_PROPERTIES_FILE.to_string()), - PropertyNameKind::Env, - ], - kafka - .controller_role() - .cloned() - .context(MissingKafkaRoleSnafu { - role: KafkaRole::Controller, - })? - .erase(), - ), - )] - .into(); - - let controller_role_config = transform_all_roles_to_config(kafka, &controller_role) - .context(GenerateProductConfigSnafu)?; - - role_config.extend(controller_role_config); + broker_role: &crate::crd::BrokerRole, + rolegroup_name: &str, +) -> ( + BTreeMap, + BTreeMap, + BTreeMap, +) { + // --- broker.properties overrides --- + let role_broker_overrides: BTreeMap> = broker_role + .config + .config_overrides + .broker_properties + .as_ref() + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let rg_broker_overrides: BTreeMap> = broker_role + .role_groups + .get(rolegroup_name) + .and_then(|rg| rg.config.config_overrides.broker_properties.as_ref()) + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let mut merged_broker = role_broker_overrides; + merged_broker.extend(rg_broker_overrides); + let config_file_overrides: BTreeMap = merged_broker + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + .collect(); + + // --- security.properties overrides --- + let role_security_overrides: BTreeMap> = broker_role + .config + .config_overrides + .security_properties + .as_ref() + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let rg_security_overrides: BTreeMap> = broker_role + .role_groups + .get(rolegroup_name) + .and_then(|rg| rg.config.config_overrides.security_properties.as_ref()) + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let mut merged_security = role_security_overrides; + merged_security.extend(rg_security_overrides); + let jvm_security_overrides: BTreeMap = merged_security + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + .collect(); + + // --- env overrides --- + // DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides + // (role then role-group) are extended on top, so a user override of the same key wins. + // This mirrors product-config's old merge of compute_env() output with user envOverrides. + // Alternative: inject after user overrides (operator wins) — rejected to preserve the + // previous precedence. + // + // KAFKA_CLUSTER_ID injection moved here from crd/role/broker.rs::Configuration::compute_env. + let mut env_overrides: BTreeMap = BTreeMap::new(); + if let Some(cluster_id) = kafka.cluster_id() { + env_overrides.insert("KAFKA_CLUSTER_ID".to_string(), cluster_id.to_string()); + } + let role_env: &std::collections::HashMap = &broker_role.config.env_overrides; + env_overrides.extend(role_env.iter().map(|(k, v)| (k.clone(), v.clone()))); + if let Some(rg) = broker_role.role_groups.get(rolegroup_name) { + env_overrides.extend( + rg.config + .env_overrides + .iter() + .map(|(k, v)| (k.clone(), v.clone())), + ); + } + + (config_file_overrides, jvm_security_overrides, env_overrides) +} + +// DESIGN DECISION: role-group overrides are merged role-level first, then role-group +// extended on top so role-group wins — identical to the precedent product-config used. +// We read the v2 KeyValueConfigOverrides `.overrides` map and BTreeMap::extend rather +// than using its `Merge` impl, because plain extend reproduces the old behaviour exactly +// (last-writer-wins per key) and avoids depending on Merge semantics. Alternative: +// KeyValueConfigOverrides::merge — equivalent here but an unnecessary semantic dependency. +fn collect_controller_role_group_overrides( + kafka: &v1alpha1::KafkaCluster, + controller_role: &crate::crd::ControllerRole, + rolegroup_name: &str, +) -> ( + BTreeMap, + BTreeMap, + BTreeMap, +) { + // --- controller.properties overrides --- + let role_controller_overrides: BTreeMap> = controller_role + .config + .config_overrides + .controller_properties + .as_ref() + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let rg_controller_overrides: BTreeMap> = controller_role + .role_groups + .get(rolegroup_name) + .and_then(|rg| rg.config.config_overrides.controller_properties.as_ref()) + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let mut merged_controller = role_controller_overrides; + merged_controller.extend(rg_controller_overrides); + let config_file_overrides: BTreeMap = merged_controller + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + .collect(); + + // --- security.properties overrides --- + let role_security_overrides: BTreeMap> = controller_role + .config + .config_overrides + .security_properties + .as_ref() + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let rg_security_overrides: BTreeMap> = controller_role + .role_groups + .get(rolegroup_name) + .and_then(|rg| rg.config.config_overrides.security_properties.as_ref()) + .map(|o| o.overrides.clone()) + .unwrap_or_default(); + let mut merged_security = role_security_overrides; + merged_security.extend(rg_security_overrides); + let jvm_security_overrides: BTreeMap = merged_security + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + .collect(); + + // --- env overrides --- + // DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides + // (role then role-group) are extended on top, so a user override of the same key wins. + // This mirrors product-config's old merge of compute_env() output with user envOverrides. + // Alternative: inject after user overrides (operator wins) — rejected to preserve the + // previous precedence. + // + // KAFKA_CLUSTER_ID injection moved here from crd/role/controller.rs::Configuration::compute_env. + let mut env_overrides: BTreeMap = BTreeMap::new(); + if let Some(cluster_id) = kafka.cluster_id() { + env_overrides.insert("KAFKA_CLUSTER_ID".to_string(), cluster_id.to_string()); + } + let role_env: &std::collections::HashMap = + &controller_role.config.env_overrides; + env_overrides.extend(role_env.iter().map(|(k, v)| (k.clone(), v.clone()))); + if let Some(rg) = controller_role.role_groups.get(rolegroup_name) { + env_overrides.extend( + rg.config + .env_overrides + .iter() + .map(|(k, v)| (k.clone(), v.clone())), + ); } - validate_all_roles_and_groups_config( - product_version, - &role_config, - product_config, - false, - false, - ) - .context(InvalidProductConfigSnafu) + (config_file_overrides, jvm_security_overrides, env_overrides) } diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index d662de30..fa5b7498 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -16,13 +16,13 @@ use stackable_operator::{ cluster_operation::ClusterOperation, networking::DomainName, product_image_selection::ProductImage, }, - config_overrides::{KeyValueConfigOverrides, KeyValueOverridesProvider}, deep_merger::ObjectOverrides, kube::{CustomResource, runtime::reflector::ObjectRef}, role_utils::{GenericRoleConfig, JavaCommonConfig, Role, RoleGroupRef}, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, utils::cluster_info::KubernetesClusterInfo, + v2::config_overrides::KeyValueConfigOverrides, versioned::versioned, }; use strum::{Display, EnumIter, EnumString}; @@ -240,6 +240,8 @@ pub mod versioned { pub broker_id_pod_config_map_name: Option, } + // Uses the v2 KeyValueConfigOverrides (Merge-capable, `nullable` values) to match + // trino/hdfs. Resolution into flat maps happens in controller/validate.rs. #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaBrokerConfigOverrides { @@ -291,32 +293,6 @@ impl Default for v1alpha1::KafkaClusterConfig { } } -impl KeyValueOverridesProvider for v1alpha1::KafkaBrokerConfigOverrides { - fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { - let field = match file { - role::broker::BROKER_PROPERTIES_FILE => self.broker_properties.as_ref(), - JVM_SECURITY_PROPERTIES_FILE => self.security_properties.as_ref(), - _ => None, - }; - field - .map(KeyValueConfigOverrides::as_product_config_overrides) - .unwrap_or_default() - } -} - -impl KeyValueOverridesProvider for v1alpha1::KafkaControllerConfigOverrides { - fn get_key_value_overrides(&self, file: &str) -> BTreeMap> { - let field = match file { - role::controller::CONTROLLER_PROPERTIES_FILE => self.controller_properties.as_ref(), - JVM_SECURITY_PROPERTIES_FILE => self.security_properties.as_ref(), - _ => None, - }; - field - .map(KeyValueConfigOverrides::as_product_config_overrides) - .unwrap_or_default() - } -} - impl HasStatusCondition for v1alpha1::KafkaCluster { fn conditions(&self) -> Vec { match &self.status { diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 70ac85d0..674b9feb 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::resources::{ @@ -8,16 +6,12 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, k8s_openapi::apimachinery::pkg::api::resource::Quantity, - product_config_utils::Configuration, product_logging::{self, spec::Logging}, schemars::{self, JsonSchema}, }; use strum::{Display, EnumIter}; -use crate::crd::{ - role::commons::{CommonConfig, Storage, StorageFragment}, - v1alpha1, -}; +use crate::crd::role::commons::{CommonConfig, Storage, StorageFragment}; pub const BROKER_PROPERTIES_FILE: &str = "broker.properties"; @@ -102,38 +96,4 @@ impl BrokerConfig { } } -impl Configuration for BrokerConfigFragment { - type Configurable = v1alpha1::KafkaCluster; - - fn compute_env( - &self, - resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - let mut result = BTreeMap::new(); - if let Some(cluster_id) = resource.cluster_id() { - result.insert("KAFKA_CLUSTER_ID".to_string(), Some(cluster_id.to_string())); - } - Ok(result) - } - - fn compute_cli( - &self, - _resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } - - fn compute_files( - &self, - _resource: &Self::Configurable, - _role_name: &str, - _file: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } -} +// KAFKA_CLUSTER_ID injection moved to controller/validate.rs::collect_broker_role_group_overrides. diff --git a/rust/operator-binary/src/crd/role/controller.rs b/rust/operator-binary/src/crd/role/controller.rs index bf1468b6..27756ee1 100644 --- a/rust/operator-binary/src/crd/role/controller.rs +++ b/rust/operator-binary/src/crd/role/controller.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::resources::{ @@ -8,16 +6,12 @@ use stackable_operator::{ }, config::{fragment::Fragment, merge::Merge}, k8s_openapi::apimachinery::pkg::api::resource::Quantity, - product_config_utils::Configuration, product_logging::{self, spec::Logging}, schemars::{self, JsonSchema}, }; use strum::{Display, EnumIter}; -use crate::crd::{ - role::commons::{CommonConfig, Storage, StorageFragment}, - v1alpha1, -}; +use crate::crd::role::commons::{CommonConfig, Storage, StorageFragment}; pub const CONTROLLER_PROPERTIES_FILE: &str = "controller.properties"; @@ -92,38 +86,4 @@ impl ControllerConfig { } } -impl Configuration for ControllerConfigFragment { - type Configurable = v1alpha1::KafkaCluster; - - fn compute_env( - &self, - resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - let mut result = BTreeMap::new(); - if let Some(cluster_id) = resource.cluster_id() { - result.insert("KAFKA_CLUSTER_ID".to_string(), Some(cluster_id.to_string())); - } - Ok(result) - } - - fn compute_cli( - &self, - _resource: &Self::Configurable, - _role_name: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } - - fn compute_files( - &self, - _resource: &Self::Configurable, - _role_name: &str, - _file: &str, - ) -> Result>, stackable_operator::product_config_utils::Error> - { - Ok(BTreeMap::new()) - } -} +// KAFKA_CLUSTER_ID injection moved to controller/validate.rs::collect_controller_role_group_overrides. diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index e08474ed..6c7bac4f 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -98,7 +98,9 @@ pub enum Error { Eq, Hash, JsonSchema, + Ord, PartialEq, + PartialOrd, Serialize, EnumString, )] diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index c074f8af..25362c54 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -80,9 +80,9 @@ async fn main() -> anyhow::Result<()> { RunArguments { operator_environment, watch_namespace, - product_config, maintenance, common, + .. }, .. }) => { @@ -127,11 +127,6 @@ async fn main() -> anyhow::Result<()> { .run(sigterm_watcher.handle()) .map_err(|err| anyhow!(err).context("failed to run webhook server")); - let product_config = product_config.load(&[ - "deploy/config-spec/properties.yaml", - "/etc/stackable/kafka-operator/config-spec/properties.yaml", - ])?; - let event_recorder = Arc::new(Recorder::new( client.as_kube_client(), Reporter { @@ -188,7 +183,6 @@ async fn main() -> anyhow::Result<()> { Arc::new(controller::Ctx { client: client.clone(), operator_environment, - product_config, }), ) // We can let the reporting happen in the background diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 5cb262f0..7f232793 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -1,9 +1,5 @@ -use std::{ - collections::{BTreeMap, HashMap}, - ops::Deref, -}; +use std::{collections::BTreeMap, ops::Deref}; -use product_config::types::PropertyNameKind; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::{ @@ -171,7 +167,7 @@ pub fn build_broker_rolegroup_statefulset( kafka_role: &KafkaRole, resolved_product_image: &ResolvedProductImage, rolegroup_ref: &RoleGroupRef, - broker_config: &HashMap>, + env_overrides: &BTreeMap, kafka_security: &KafkaTlsSecurity, merged_config: &AnyConfig, service_account: &ServiceAccount, @@ -249,10 +245,8 @@ pub fn build_broker_rolegroup_statefulset( .context(AddKerberosConfigSnafu)?; } - let mut env = broker_config - .get(&PropertyNameKind::Env) - .into_iter() - .flatten() + let mut env = env_overrides + .iter() .map(|(k, v)| EnvVar { name: k.clone(), value: Some(v.clone()), @@ -581,7 +575,7 @@ pub fn build_controller_rolegroup_statefulset( kafka_role: &KafkaRole, resolved_product_image: &ResolvedProductImage, rolegroup_ref: &RoleGroupRef, - controller_config: &HashMap>, + env_overrides: &BTreeMap, kafka_security: &KafkaTlsSecurity, merged_config: &AnyConfig, service_account: &ServiceAccount, @@ -603,10 +597,8 @@ pub fn build_controller_rolegroup_statefulset( let mut pod_builder = PodBuilder::new(); - let mut env = controller_config - .get(&PropertyNameKind::Env) - .into_iter() - .flatten() + let mut env = env_overrides + .iter() .map(|(k, v)| EnvVar { name: k.clone(), value: Some(v.clone()), From b2183e3e97d329a246cd7d4a19bf9c4d895ccf75 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 3 Jun 2026 18:54:43 +0200 Subject: [PATCH 06/47] docs: remove product-config CLI param + env var; gut config-spec; changelog Removes the --product-config section from the commandline reference and the PRODUCT_CONFIG section from the environment-variables reference (the flag/var is now a no-op via the shared RunArguments), reduces the product-config properties.yaml files to an empty shell (retained pending a later Helm config refactor), and notes the product-config removal in the changelog. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 4 + deploy/config-spec/properties.yaml | 151 +----------------- .../kafka-operator/configs/properties.yaml | 151 +----------------- .../reference/commandline-parameters.adoc | 13 -- .../reference/environment-variables.adoc | 26 --- 5 files changed, 8 insertions(+), 337 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 323d05c6..c4e21fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ All notable changes to this project will be documented in this file. Previously, arbitrary file names were silently accepted and ignored ([#960]). - Bump `stackable-operator` to 0.111.1 and snafu to 0.9 ([#960], [#961]). - Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#968]). +- Removed the product-config based configuration validation. Config and environment overrides are + now merged directly from the CRD into the validated cluster, the Java-properties writer is + vendored locally, and the `product-config` crate dependency is dropped. The `--product-config` + CLI flag is now a no-op ([#XXX]). - test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#971]). [#953]: https://github.com/stackabletech/kafka-operator/pull/953 diff --git a/deploy/config-spec/properties.yaml b/deploy/config-spec/properties.yaml index b6f80cdd..9bd8c3b2 100644 --- a/deploy/config-spec/properties.yaml +++ b/deploy/config-spec/properties.yaml @@ -1,152 +1,5 @@ --- version: 0.1.0 spec: - units: - - unit: &unitPort - name: "port" - regex: "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$" - - - unit: &unitUrl - name: "url" - regex: "^((https?|ftp|file)://)?[-a-zA-Z0-9+&@#}/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]" - examples: - - "https://www.stackable.de/blog/" - - - unit: &unitCapacity - name: "capacity" - regex: "^[1-9]\\d*$" - - - unit: &unitMilliseconds - name: "milliseconds" - regex: "^[1-9]\\d*$" - -properties: - - property: - propertyNames: - - name: "networkaddress.cache.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "30" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for successfully resolved domain names." - description: "TTL for successfully resolved domain names." - - - property: - propertyNames: - - name: "networkaddress.cache.negative.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for domain names that cannot be resolved." - description: "TTL for domain names that cannot be resolved." - - - property: &opaAuthorizerClassName - propertyNames: - - name: "authorizer.class.name" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - defaultValues: - - fromVersion: "0.0.0" - value: "com.bisnode.kafka.authorization.OpaAuthorizer" - - fromVersion: "3.0.0" - value: "org.openpolicyagent.kafka.OpaAuthorizer" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer class name" - - - property: &opaAuthorizerUrl - propertyNames: - - name: "opa.authorizer.url" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - unit: *unitUrl - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer URL" - - - property: &opaAuthorizerInitialCacheCapacity - propertyNames: - - name: "opa.authorizer.cache.initial.capacity" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer initial cache capacity" - - - property: &opaAuthorizerMaxCacheSize - propertyNames: - - name: "opa.authorizer.cache.maximum.size" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA authorizer max cache size" - - - property: &opaAuthorizerCacheExpireAfterSeconds - propertyNames: - - name: "opa.authorizer.cache.expire.after.seconds" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "The number of seconds after which the OPA authorizer cache expires" + units: [] +properties: [] diff --git a/deploy/helm/kafka-operator/configs/properties.yaml b/deploy/helm/kafka-operator/configs/properties.yaml index b6f80cdd..9bd8c3b2 100644 --- a/deploy/helm/kafka-operator/configs/properties.yaml +++ b/deploy/helm/kafka-operator/configs/properties.yaml @@ -1,152 +1,5 @@ --- version: 0.1.0 spec: - units: - - unit: &unitPort - name: "port" - regex: "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$" - - - unit: &unitUrl - name: "url" - regex: "^((https?|ftp|file)://)?[-a-zA-Z0-9+&@#}/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]" - examples: - - "https://www.stackable.de/blog/" - - - unit: &unitCapacity - name: "capacity" - regex: "^[1-9]\\d*$" - - - unit: &unitMilliseconds - name: "milliseconds" - regex: "^[1-9]\\d*$" - -properties: - - property: - propertyNames: - - name: "networkaddress.cache.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "30" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for successfully resolved domain names." - description: "TTL for successfully resolved domain names." - - - property: - propertyNames: - - name: "networkaddress.cache.negative.ttl" - kind: - type: "file" - file: "security.properties" - datatype: - type: "integer" - min: "0" - recommendedValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: true - - name: "controller" - required: true - asOfVersion: "0.0.0" - comment: "TTL for domain names that cannot be resolved." - description: "TTL for domain names that cannot be resolved." - - - property: &opaAuthorizerClassName - propertyNames: - - name: "authorizer.class.name" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - defaultValues: - - fromVersion: "0.0.0" - value: "com.bisnode.kafka.authorization.OpaAuthorizer" - - fromVersion: "3.0.0" - value: "org.openpolicyagent.kafka.OpaAuthorizer" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer class name" - - - property: &opaAuthorizerUrl - propertyNames: - - name: "opa.authorizer.url" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "string" - unit: *unitUrl - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer URL" - - - property: &opaAuthorizerInitialCacheCapacity - propertyNames: - - name: "opa.authorizer.cache.initial.capacity" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA Authorizer initial cache capacity" - - - property: &opaAuthorizerMaxCacheSize - propertyNames: - - name: "opa.authorizer.cache.maximum.size" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "OPA authorizer max cache size" - - - property: &opaAuthorizerCacheExpireAfterSeconds - propertyNames: - - name: "opa.authorizer.cache.expire.after.seconds" - kind: - type: "file" - file: "broker.properties" - datatype: - type: "integer" - unit: *unitCapacity - defaultValues: - - fromVersion: "0.0.0" - value: "0" - roles: - - name: "broker" - required: false - asOfVersion: "0.0.0" - description: "The number of seconds after which the OPA authorizer cache expires" + units: [] +properties: [] diff --git a/docs/modules/kafka/pages/reference/commandline-parameters.adoc b/docs/modules/kafka/pages/reference/commandline-parameters.adoc index 9059a960..3c66dc41 100644 --- a/docs/modules/kafka/pages/reference/commandline-parameters.adoc +++ b/docs/modules/kafka/pages/reference/commandline-parameters.adoc @@ -2,19 +2,6 @@ This operator accepts the following command line parameters: -== product-config - -*Default value*: `/etc/stackable/kafka-operator/config-spec/properties.yaml` - -*Required*: false - -*Multiple values:* false - -[source] ----- -stackable-kafka-operator run --product-config /foo/bar/properties.yaml ----- - == watch-namespace *Default value*: All namespaces diff --git a/docs/modules/kafka/pages/reference/environment-variables.adoc b/docs/modules/kafka/pages/reference/environment-variables.adoc index cc7dd3a2..d2271300 100644 --- a/docs/modules/kafka/pages/reference/environment-variables.adoc +++ b/docs/modules/kafka/pages/reference/environment-variables.adoc @@ -33,32 +33,6 @@ docker run \ oci.stackable.tech/sdp/kafka-operator:0.0.0-dev ---- -== PRODUCT_CONFIG - -*Default value*: `/etc/stackable/kafka-operator/config-spec/properties.yaml` - -*Required*: false - -*Multiple values*: false - -[source] ----- -export PRODUCT_CONFIG=/foo/bar/properties.yaml -stackable-kafka-operator run ----- - -or via docker: - ----- -docker run \ - --name kafka-operator \ - --network host \ - --env KUBECONFIG=/home/stackable/.kube/config \ - --env PRODUCT_CONFIG=/my/product/config.yaml \ - --mount type=bind,source="$HOME/.kube/config",target="/home/stackable/.kube/config" \ - oci.stackable.tech/sdp/kafka-operator:0.0.0-dev ----- - == WATCH_NAMESPACE *Default value*: All namespaces From 5352a84a5110067959e04018fb8009206a27feae Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 5 Jun 2026 08:57:04 +0200 Subject: [PATCH 07/47] updated changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4e21fb8..7d34355e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,11 @@ All notable changes to this project will be documented in this file. Previously, arbitrary file names were silently accepted and ignored ([#960]). - Bump `stackable-operator` to 0.111.1 and snafu to 0.9 ([#960], [#961]). - Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#968]). +- test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#971]). - Removed the product-config based configuration validation. Config and environment overrides are now merged directly from the CRD into the validated cluster, the Java-properties writer is vendored locally, and the `product-config` crate dependency is dropped. The `--product-config` - CLI flag is now a no-op ([#XXX]). -- test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#971]). + CLI flag is now a no-op ([#976]). [#953]: https://github.com/stackabletech/kafka-operator/pull/953 [#960]: https://github.com/stackabletech/kafka-operator/pull/960 @@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file. [#968]: https://github.com/stackabletech/kafka-operator/pull/968 [#971]: https://github.com/stackabletech/kafka-operator/pull/971 [#973]: https://github.com/stackabletech/kafka-operator/pull/973 +[#976]: https://github.com/stackabletech/kafka-operator/pull/976 ## [26.3.0] - 2026-03-16 From cfd0efd1cd3855afe9f7939597af03efc3c8fb23 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 5 Jun 2026 17:13:02 +0200 Subject: [PATCH 08/47] refactor: consume the config-file writer from stackable-operator Replace the vendored Java-properties writer (rust/operator-binary/src/config/writer.rs) with stackable_operator::v2::config_file_writer (moved there via operator-rs #1217 on the smooth-operator branch). Kafka's copy was the java-only subset of the canonical hdfs writer; the upstream module's additional to_hadoop_xml simply goes unimported. Drop the now-unused java-properties dependency. No behaviour change; rendered .properties output is byte-identical by construction (same code, new home). Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 117 +++--- Cargo.nix | 341 ++++++++++-------- Cargo.toml | 1 - crate-hashes.json | 4 +- rust/operator-binary/Cargo.toml | 1 - rust/operator-binary/src/config/mod.rs | 1 - rust/operator-binary/src/config/writer.rs | 78 ---- .../src/controller/build/config_map.rs | 6 +- 8 files changed, 264 insertions(+), 285 deletions(-) delete mode 100644 rust/operator-binary/src/config/writer.rs diff --git a/Cargo.lock b/Cargo.lock index 87abc76f..1390fe52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1518,11 +1518,11 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "darling", "regex", - "snafu 0.9.0", + "snafu 0.9.1", ] [[package]] @@ -1843,9 +1843,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -1857,9 +1857,9 @@ dependencies = [ [[package]] name = "opentelemetry-appender-tracing" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" +checksum = "2c0080f0dc1d7c786f467cd85a4e395fcab11ee852004f39a29a18ab7c25d837" dependencies = [ "opentelemetry", "tracing", @@ -1869,9 +1869,9 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", @@ -1882,9 +1882,9 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", @@ -1896,14 +1896,14 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tonic", - "tracing", + "tonic-types", ] [[package]] name = "opentelemetry-proto" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -1914,21 +1914,22 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" +checksum = "6ca2f98a0437b427b4b08f19f1caa3c44db885a202bc12cfea13d6c702243d68" [[package]] name = "opentelemetry_sdk" -version = "0.31.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", + "portable-atomic", "rand 0.9.4", "thiserror 2.0.18", "tokio", @@ -2211,6 +2212,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.45" @@ -2350,9 +2360,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2368,9 +2378,6 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tower", @@ -2813,11 +2820,11 @@ dependencies = [ [[package]] name = "snafu" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1d4bced6a69f90b2056c03dcff2c4737f98d6fb9e0853493996e1d253ca29c6" +checksum = "d1a012328be2e3f5d5f6f3218147ca02588cea4cb865e876849ab6debcf36522" dependencies = [ - "snafu-derive 0.9.0", + "snafu-derive 0.9.1", ] [[package]] @@ -2845,9 +2852,9 @@ dependencies = [ [[package]] name = "snafu-derive" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54254b8531cafa275c5e096f62d48c81435d1015405a91198ddb11e967301d40" +checksum = "5f103c50866b8743da9429b8a581d81a27c2d3a9c4ac7df8f8571c1dd7896eda" dependencies = [ "heck", "proc-macro2", @@ -2890,7 +2897,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackable-certs" version = "0.4.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "const-oid", "ecdsa", @@ -2902,7 +2909,7 @@ dependencies = [ "rsa", "sha2", "signature", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-shared", "tokio", "tokio-rustls", @@ -2921,12 +2928,11 @@ dependencies = [ "const_format", "futures", "indoc", - "java-properties", "rstest", "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-operator", "strum", "tokio", @@ -2936,7 +2942,7 @@ dependencies = [ [[package]] name = "stackable-operator" version = "0.111.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "base64", "clap", @@ -2948,6 +2954,7 @@ dependencies = [ "futures", "http", "indexmap", + "java-properties", "jiff", "json-patch", "k8s-openapi", @@ -2960,7 +2967,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-operator-derive", "stackable-shared", "stackable-telemetry", @@ -2973,12 +2980,13 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "xml", ] [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "darling", "proc-macro2", @@ -2988,8 +2996,8 @@ dependencies = [ [[package]] name = "stackable-shared" -version = "0.1.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +version = "0.1.1" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "jiff", "k8s-openapi", @@ -2998,15 +3006,15 @@ dependencies = [ "semver", "serde", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "strum", "time", ] [[package]] name = "stackable-telemetry" -version = "0.6.3" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +version = "0.6.4" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "axum", "clap", @@ -3017,7 +3025,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry_sdk", "pin-project", - "snafu 0.9.0", + "snafu 0.9.1", "strum", "tokio", "tower", @@ -3030,21 +3038,21 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "kube", "schemars", "serde", "serde_json", "serde_yaml", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-versioned-macros", ] [[package]] name = "stackable-versioned-macros" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "convert_case", "convert_case_extras", @@ -3062,7 +3070,7 @@ dependencies = [ [[package]] name = "stackable-webhook" version = "0.9.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a31cd2514445b251038fc4ea7abc28c57b2a6ad9" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" dependencies = [ "arc-swap", "async-trait", @@ -3078,7 +3086,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "snafu 0.9.0", + "snafu 0.9.1", "stackable-certs", "stackable-shared", "stackable-telemetry", @@ -3414,6 +3422,17 @@ dependencies = [ "tonic", ] +[[package]] +name = "tonic-types" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a875a902255423d34c1f20838ab374126db8eb41625b7947a1d54113b0b7399" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -3525,9 +3544,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", @@ -3948,9 +3967,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 = "yoke" diff --git a/Cargo.nix b/Cargo.nix index debf5eef..321c6fe5 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4852,7 +4852,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "k8s_version"; @@ -4871,7 +4871,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } ]; features = { @@ -6052,9 +6052,9 @@ rec { }; "opentelemetry" = rec { crateName = "opentelemetry"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "18629xsj4rsyiby9aj511q6wcw6s9m09gx3ymw1yjcvix1mcsjxq"; + sha256 = "10ln14d1jgc8rvw97mblc9blzcgpg1bimim4d170b7ia4mijq55h"; dependencies = [ { name = "futures-core"; @@ -6091,24 +6091,24 @@ rec { ]; features = { "default" = [ "trace" "metrics" "logs" "internal-logs" "futures" ]; + "experimental_metrics_bound_instruments" = [ "metrics" ]; "futures" = [ "futures-core" "futures-sink" "pin-project-lite" ]; "futures-core" = [ "dep:futures-core" ]; "futures-sink" = [ "dep:futures-sink" ]; "internal-logs" = [ "tracing" ]; "pin-project-lite" = [ "dep:pin-project-lite" ]; - "spec_unstable_logs_enabled" = [ "logs" ]; "testing" = [ "trace" ]; "thiserror" = [ "dep:thiserror" ]; "trace" = [ "futures" "thiserror" ]; "tracing" = [ "dep:tracing" ]; }; - resolvedDefaultFeatures = [ "default" "futures" "futures-core" "futures-sink" "internal-logs" "logs" "metrics" "pin-project-lite" "spec_unstable_logs_enabled" "thiserror" "trace" "tracing" ]; + resolvedDefaultFeatures = [ "default" "futures" "futures-core" "futures-sink" "internal-logs" "logs" "metrics" "pin-project-lite" "thiserror" "trace" "tracing" ]; }; "opentelemetry-appender-tracing" = rec { crateName = "opentelemetry-appender-tracing"; - version = "0.31.1"; + version = "0.32.0"; edition = "2021"; - sha256 = "1hnwizzgfhpjfnvml638yy846py8hf2gl1n3p1igbk1srb2ilspg"; + sha256 = "0dyq4myan64sl8wly02jx0gb3jjz7575mn3w8rpphz0xvkq8001c"; libName = "opentelemetry_appender_tracing"; dependencies = [ { @@ -6151,18 +6151,15 @@ rec { ]; features = { "experimental_metadata_attributes" = [ "dep:tracing-log" ]; - "experimental_use_tracing_span_context" = [ "tracing-opentelemetry" ]; "log" = [ "dep:log" ]; - "spec_unstable_logs_enabled" = [ "opentelemetry/spec_unstable_logs_enabled" ]; - "tracing-opentelemetry" = [ "dep:tracing-opentelemetry" ]; }; resolvedDefaultFeatures = [ "default" ]; }; "opentelemetry-http" = rec { crateName = "opentelemetry-http"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "0pc5nw1ds8v8w0nvyall39m92v8m1xl1p3vwvxk6nkhrffdd19np"; + sha256 = "0ca3drvm4fx5nskl7yn42dimy3bg35ppzc85y1p27pz215fh30sn"; libName = "opentelemetry_http"; dependencies = [ { @@ -6198,16 +6195,16 @@ rec { "internal-logs" = [ "opentelemetry/internal-logs" ]; "reqwest" = [ "dep:reqwest" ]; "reqwest-blocking" = [ "dep:reqwest" "reqwest/blocking" ]; - "reqwest-rustls" = [ "dep:reqwest" "reqwest/rustls-tls-native-roots" ]; - "reqwest-rustls-webpki-roots" = [ "dep:reqwest" "reqwest/rustls-tls-webpki-roots" ]; + "reqwest-rustls" = [ "dep:reqwest" "reqwest/default-tls" ]; + "reqwest-rustls-webpki-roots" = [ "dep:reqwest" "reqwest/default-tls" "reqwest/webpki-roots" ]; }; - resolvedDefaultFeatures = [ "internal-logs" "reqwest" "reqwest-blocking" ]; + resolvedDefaultFeatures = [ "reqwest" "reqwest-blocking" ]; }; "opentelemetry-otlp" = rec { crateName = "opentelemetry-otlp"; - version = "0.31.1"; + version = "0.32.0"; edition = "2021"; - sha256 = "07zp0b62b9dajnvvcd6j2ppw5zg7wp4ixka9z6fr3bxrrdmcss8z"; + sha256 = "0d9cys2flpidfxbr6h1103hjc633cax47ihnqgbj0xnicscr4rlr"; libName = "opentelemetry_otlp"; dependencies = [ { @@ -6268,10 +6265,9 @@ rec { usesDefaultFeatures = false; } { - name = "tracing"; - packageId = "tracing"; + name = "tonic-types"; + packageId = "tonic-types"; optional = true; - usesDefaultFeatures = false; } ]; devDependencies = [ @@ -6296,16 +6292,19 @@ rec { ]; features = { "default" = [ "http-proto" "reqwest-blocking-client" "trace" "metrics" "logs" "internal-logs" ]; + "experimental-grpc-retry" = [ "grpc-tonic" "opentelemetry_sdk/experimental_async_runtime" "opentelemetry_sdk/rt-tokio" ]; + "experimental-http-retry" = [ "opentelemetry_sdk/experimental_async_runtime" "opentelemetry_sdk/rt-tokio" "tokio" "httpdate" ]; "flate2" = [ "dep:flate2" ]; - "grpc-tonic" = [ "tonic" "prost" "http" "tokio" "opentelemetry-proto/gen-tonic" ]; + "grpc-tonic" = [ "tonic" "tonic-types" "prost" "http" "tokio" "opentelemetry-proto/gen-tonic" ]; "gzip-http" = [ "flate2" ]; "gzip-tonic" = [ "tonic/gzip" ]; "http" = [ "dep:http" ]; "http-json" = [ "serde_json" "prost" "opentelemetry-http" "opentelemetry-proto/gen-tonic-messages" "opentelemetry-proto/with-serde" "http" "trace" "metrics" ]; "http-proto" = [ "prost" "opentelemetry-http" "opentelemetry-proto/gen-tonic-messages" "http" "trace" "metrics" ]; + "httpdate" = [ "dep:httpdate" ]; "hyper-client" = [ "opentelemetry-http/hyper" ]; "integration-testing" = [ "tonic" "prost" "tokio/full" "trace" "logs" ]; - "internal-logs" = [ "tracing" "opentelemetry_sdk/internal-logs" "opentelemetry-http/internal-logs" ]; + "internal-logs" = [ "opentelemetry_sdk/internal-logs" "opentelemetry/internal-logs" ]; "logs" = [ "opentelemetry/logs" "opentelemetry_sdk/logs" "opentelemetry-proto/logs" ]; "metrics" = [ "opentelemetry/metrics" "opentelemetry_sdk/metrics" "opentelemetry-proto/metrics" ]; "opentelemetry-http" = [ "dep:opentelemetry-http" ]; @@ -6318,27 +6317,27 @@ rec { "serde" = [ "dep:serde" ]; "serde_json" = [ "dep:serde_json" ]; "serialize" = [ "serde" "serde_json" ]; - "tls" = [ "tonic/tls-ring" ]; + "tls" = [ "tls-ring" ]; "tls-aws-lc" = [ "tonic/tls-aws-lc" ]; "tls-provider-agnostic" = [ "tonic/_tls-any" ]; "tls-ring" = [ "tonic/tls-ring" ]; - "tls-roots" = [ "tls" "tonic/tls-native-roots" ]; - "tls-webpki-roots" = [ "tls" "tonic/tls-webpki-roots" ]; + "tls-roots" = [ "tonic/tls-native-roots" ]; + "tls-webpki-roots" = [ "tonic/tls-webpki-roots" ]; "tokio" = [ "dep:tokio" ]; "tonic" = [ "dep:tonic" ]; + "tonic-types" = [ "dep:tonic-types" ]; "trace" = [ "opentelemetry/trace" "opentelemetry_sdk/trace" "opentelemetry-proto/trace" ]; - "tracing" = [ "dep:tracing" ]; "zstd" = [ "dep:zstd" ]; "zstd-http" = [ "zstd" ]; "zstd-tonic" = [ "tonic/zstd" ]; }; - resolvedDefaultFeatures = [ "default" "grpc-tonic" "gzip-tonic" "http" "http-proto" "internal-logs" "logs" "metrics" "opentelemetry-http" "prost" "reqwest" "reqwest-blocking-client" "tokio" "tonic" "trace" "tracing" ]; + resolvedDefaultFeatures = [ "default" "grpc-tonic" "gzip-tonic" "http" "http-proto" "internal-logs" "logs" "metrics" "opentelemetry-http" "prost" "reqwest" "reqwest-blocking-client" "tokio" "tonic" "tonic-types" "trace" ]; }; "opentelemetry-proto" = rec { crateName = "opentelemetry-proto"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "03xkjsjrsm7zkkx5gascqd9bg2z20wymm06l16cyxsp5dpq5s5x7"; + sha256 = "0f5ny4rpnpq6q5q34b8k2q548rf31rpbxkwjqjwzfqxg3yx5imjn"; libName = "opentelemetry_proto"; dependencies = [ { @@ -6382,30 +6381,29 @@ rec { "const-hex" = [ "dep:const-hex" ]; "default" = [ "full" ]; "full" = [ "gen-tonic" "trace" "logs" "metrics" "zpages" "with-serde" "internal-logs" ]; - "gen-tonic" = [ "gen-tonic-messages" "tonic/channel" ]; - "gen-tonic-messages" = [ "tonic" "tonic-prost" "prost" ]; + "gen-tonic" = [ "gen-tonic-messages" "tonic" "tonic-prost" "tonic/channel" ]; + "gen-tonic-messages" = [ "prost" ]; "internal-logs" = [ "opentelemetry/internal-logs" ]; "logs" = [ "opentelemetry/logs" "opentelemetry_sdk/logs" ]; "metrics" = [ "opentelemetry/metrics" "opentelemetry_sdk/metrics" ]; "prost" = [ "dep:prost" ]; "schemars" = [ "dep:schemars" ]; "serde" = [ "dep:serde" ]; - "serde_json" = [ "dep:serde_json" ]; "testing" = [ "opentelemetry/testing" ]; "tonic" = [ "dep:tonic" ]; "tonic-prost" = [ "dep:tonic-prost" ]; "trace" = [ "opentelemetry/trace" "opentelemetry_sdk/trace" ]; "with-schemars" = [ "schemars" ]; - "with-serde" = [ "serde" "const-hex" "base64" "serde_json" ]; + "with-serde" = [ "serde" "const-hex" "base64" ]; "zpages" = [ "trace" ]; }; resolvedDefaultFeatures = [ "gen-tonic" "gen-tonic-messages" "logs" "metrics" "prost" "tonic" "tonic-prost" "trace" ]; }; "opentelemetry-semantic-conventions" = rec { crateName = "opentelemetry-semantic-conventions"; - version = "0.31.0"; + version = "0.32.0"; edition = "2021"; - sha256 = "0in8plv2l2ar7anzi7lrbll0fjfvaymkg5vc5bnvibs1w3gjjbp6"; + sha256 = "0s1x4h1cgmhkxb7i5g02la2vhkf4lg5g26cgn2s2gd1p0j5gk8kc"; libName = "opentelemetry_semantic_conventions"; features = { }; @@ -6413,9 +6411,9 @@ rec { }; "opentelemetry_sdk" = rec { crateName = "opentelemetry_sdk"; - version = "0.31.0"; + version = "0.32.1"; edition = "2021"; - sha256 = "1gbjsggdxfpjbanjvaxa3nq32vfa37i3v13dvx4gsxhrk7sy8jp1"; + sha256 = "1ycl11syranrinhgn4c2hlzhyzyvpa06ryxq5mxgzmf4387ghncv"; dependencies = [ { name = "futures-channel"; @@ -6441,6 +6439,13 @@ rec { packageId = "percent-encoding"; optional = true; } + { + name = "portable-atomic"; + packageId = "portable-atomic"; + usesDefaultFeatures = false; + target = { target, features }: (!("64" == target."has_atomic" or null)); + features = [ "fallback" ]; + } { name = "rand"; packageId = "rand 0.9.4"; @@ -6465,10 +6470,18 @@ rec { optional = true; } ]; + devDependencies = [ + { + name = "tokio"; + packageId = "tokio"; + usesDefaultFeatures = false; + features = [ "macros" "rt-multi-thread" ]; + } + ]; features = { "default" = [ "trace" "metrics" "logs" "internal-logs" ]; "experimental_logs_batch_log_processor_with_async_runtime" = [ "logs" "experimental_async_runtime" ]; - "experimental_logs_concurrent_log_processor" = [ "logs" ]; + "experimental_metrics_bound_instruments" = [ "metrics" "opentelemetry/experimental_metrics_bound_instruments" ]; "experimental_metrics_custom_reader" = [ "metrics" ]; "experimental_metrics_disable_name_validation" = [ "metrics" ]; "experimental_metrics_periodicreader_with_async_runtime" = [ "metrics" "experimental_async_runtime" ]; @@ -6485,15 +6498,14 @@ rec { "rt-tokio-current-thread" = [ "tokio/rt" "tokio/time" "tokio-stream" "experimental_async_runtime" ]; "serde" = [ "dep:serde" ]; "serde_json" = [ "dep:serde_json" ]; - "spec_unstable_logs_enabled" = [ "logs" "opentelemetry/spec_unstable_logs_enabled" ]; "spec_unstable_metrics_views" = [ "metrics" ]; - "testing" = [ "opentelemetry/testing" "trace" "metrics" "logs" "rt-tokio" "rt-tokio-current-thread" "tokio/macros" "tokio/rt-multi-thread" ]; + "testing" = [ "opentelemetry/testing" "trace" "metrics" "logs" "tokio/sync" ]; "tokio" = [ "dep:tokio" ]; "tokio-stream" = [ "dep:tokio-stream" ]; "trace" = [ "opentelemetry/trace" "rand" "percent-encoding" ]; "url" = [ "dep:url" ]; }; - resolvedDefaultFeatures = [ "default" "experimental_async_runtime" "internal-logs" "logs" "metrics" "percent-encoding" "rand" "rt-tokio" "spec_unstable_logs_enabled" "tokio" "tokio-stream" "trace" ]; + resolvedDefaultFeatures = [ "default" "experimental_async_runtime" "internal-logs" "logs" "metrics" "percent-encoding" "rand" "rt-tokio" "tokio" "tokio-stream" "trace" ]; }; "ordered-float" = rec { crateName = "ordered-float"; @@ -7002,7 +7014,7 @@ rec { "default" = [ "fallback" ]; "serde" = [ "dep:serde" ]; }; - resolvedDefaultFeatures = [ "require-cas" ]; + resolvedDefaultFeatures = [ "fallback" "require-cas" ]; }; "portable-atomic-util" = rec { crateName = "portable-atomic-util"; @@ -7270,6 +7282,34 @@ rec { ]; }; + "prost-types" = rec { + crateName = "prost-types"; + version = "0.14.3"; + edition = "2021"; + sha256 = "1mrxrciryfgi6a0vmrgyj3g27r9hdhlgwkq71cgv3icbvg5w94c9"; + libName = "prost_types"; + authors = [ + "Dan Burkert " + "Lucio Franco " + "Casper Meijn " + "Tokio Contributors " + ]; + dependencies = [ + { + name = "prost"; + packageId = "prost"; + usesDefaultFeatures = false; + features = [ "derive" ]; + } + ]; + features = { + "arbitrary" = [ "dep:arbitrary" ]; + "chrono" = [ "dep:chrono" ]; + "default" = [ "std" ]; + "std" = [ "prost/std" ]; + }; + resolvedDefaultFeatures = [ "default" "std" ]; + }; "quote" = rec { crateName = "quote"; version = "1.0.45"; @@ -7699,9 +7739,9 @@ rec { }; "reqwest" = rec { crateName = "reqwest"; - version = "0.12.28"; + version = "0.13.4"; edition = "2021"; - sha256 = "0iqidijghgqbzl3bjg5hb4zmigwa4r612bgi0yiq0c90b6jkrpgd"; + sha256 = "1hy1plns9krbh3h1dy2sdjygsfkdcnxm6pbxdi0ya9b5vq8mi711"; authors = [ "Sean McArthur " ]; @@ -7718,7 +7758,7 @@ rec { name = "futures-channel"; packageId = "futures-channel"; optional = true; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "futures-core"; @@ -7738,62 +7778,44 @@ rec { { name = "http-body"; packageId = "http-body"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "http-body-util"; packageId = "http-body-util"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "hyper"; packageId = "hyper"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "client" ]; } { name = "hyper-util"; packageId = "hyper-util"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "client" "client-legacy" "client-proxy" "tokio" ]; } { name = "js-sys"; packageId = "js-sys"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); } { name = "log"; packageId = "log"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "percent-encoding"; packageId = "percent-encoding"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "pin-project-lite"; packageId = "pin-project-lite"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } - { - name = "serde"; - packageId = "serde"; - } - { - name = "serde_json"; - packageId = "serde_json"; - optional = true; - } - { - name = "serde_json"; - packageId = "serde_json"; - target = { target, features }: ("wasm32" == target."arch" or null); - } - { - name = "serde_urlencoded"; - packageId = "serde_urlencoded"; + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "sync_wrapper"; @@ -7804,27 +7826,27 @@ rec { name = "tokio"; packageId = "tokio"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "net" "time" ]; } { name = "tower"; packageId = "tower"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "retry" "timeout" "util" ]; } { name = "tower-http"; packageId = "tower-http"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "follow-redirect" ]; } { name = "tower-service"; packageId = "tower-service"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); } { name = "url"; @@ -7833,17 +7855,17 @@ rec { { name = "wasm-bindgen"; packageId = "wasm-bindgen"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); } { name = "wasm-bindgen-futures"; packageId = "wasm-bindgen-futures"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); } { name = "web-sys"; packageId = "web-sys"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); features = [ "AbortController" "AbortSignal" "Headers" "Request" "RequestInit" "RequestMode" "Response" "Window" "FormData" "Blob" "BlobPropertyBag" "ServiceWorkerGlobalScope" "RequestCredentials" "File" "ReadableStream" "RequestCache" ]; } ]; @@ -7852,33 +7874,27 @@ rec { name = "futures-util"; packageId = "futures-util"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "std" "alloc" ]; } { name = "hyper"; packageId = "hyper"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "http2" "client" "server" ]; } { name = "hyper-util"; packageId = "hyper-util"; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "http1" "http2" "client" "client-legacy" "server-auto" "server-graceful" "tokio" ]; } - { - name = "serde"; - packageId = "serde"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - features = [ "derive" ]; - } { name = "tokio"; packageId = "tokio"; usesDefaultFeatures = false; - target = { target, features }: (!("wasm32" == target."arch" or null)); + target = { target, features }: (!(("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null)))); features = [ "macros" "rt-multi-thread" ]; } { @@ -7890,40 +7906,37 @@ rec { { name = "wasm-bindgen"; packageId = "wasm-bindgen"; - target = { target, features }: ("wasm32" == target."arch" or null); + target = { target, features }: (("wasm32" == target."arch" or null) && (("unknown" == target."os" or null) || ("none" == target."os" or null))); features = [ "serde-serialize" ]; } ]; features = { + "__native-tls" = [ "dep:hyper-tls" "dep:native-tls-crate" "__tls" "dep:tokio-native-tls" ]; + "__native-tls-alpn" = [ "native-tls-crate?/alpn" "hyper-tls?/alpn" ]; "__rustls" = [ "dep:hyper-rustls" "dep:tokio-rustls" "dep:rustls" "__tls" ]; - "__rustls-ring" = [ "hyper-rustls?/ring" "tokio-rustls?/ring" "rustls?/ring" "quinn?/ring" ]; + "__rustls-aws-lc-rs" = [ "hyper-rustls?/aws-lc-rs" "tokio-rustls?/aws-lc-rs" "rustls?/aws-lc-rs" "quinn?/rustls-aws-lc-rs" ]; "__tls" = [ "dep:rustls-pki-types" "tokio/io-util" ]; "blocking" = [ "dep:futures-channel" "futures-channel?/sink" "dep:futures-util" "futures-util?/io" "futures-util?/sink" "tokio/sync" ]; "brotli" = [ "tower-http/decompression-br" ]; "charset" = [ "dep:encoding_rs" "dep:mime" ]; "cookies" = [ "dep:cookie_crate" "dep:cookie_store" ]; "default" = [ "default-tls" "charset" "http2" "system-proxy" ]; - "default-tls" = [ "dep:hyper-tls" "dep:native-tls-crate" "__tls" "dep:tokio-native-tls" ]; + "default-tls" = [ "rustls" ]; "deflate" = [ "tower-http/decompression-deflate" ]; + "form" = [ "dep:serde" "dep:serde_urlencoded" ]; "gzip" = [ "tower-http/decompression-gzip" ]; - "h2" = [ "dep:h2" ]; "hickory-dns" = [ "dep:hickory-resolver" "dep:once_cell" ]; - "http2" = [ "h2" "hyper/http2" "hyper-util/http2" "hyper-rustls?/http2" ]; - "http3" = [ "rustls-tls-manual-roots" "dep:h3" "dep:h3-quinn" "dep:quinn" "tokio/macros" ]; - "json" = [ "dep:serde_json" ]; - "macos-system-configuration" = [ "system-proxy" ]; + "http2" = [ "dep:h2" "hyper/http2" "hyper-util/http2" "hyper-rustls?/http2" ]; + "http3" = [ "rustls" "dep:h3" "dep:h3-quinn" "dep:quinn" "tokio/macros" ]; + "json" = [ "dep:serde" "dep:serde_json" ]; "multipart" = [ "dep:mime_guess" "dep:futures-util" ]; - "native-tls" = [ "default-tls" ]; - "native-tls-alpn" = [ "native-tls" "native-tls-crate?/alpn" "hyper-tls?/alpn" ]; - "native-tls-vendored" = [ "native-tls" "native-tls-crate?/vendored" ]; - "rustls-tls" = [ "rustls-tls-webpki-roots" ]; - "rustls-tls-manual-roots" = [ "rustls-tls-manual-roots-no-provider" "__rustls-ring" ]; - "rustls-tls-manual-roots-no-provider" = [ "__rustls" ]; - "rustls-tls-native-roots" = [ "rustls-tls-native-roots-no-provider" "__rustls-ring" ]; - "rustls-tls-native-roots-no-provider" = [ "dep:rustls-native-certs" "hyper-rustls?/native-tokio" "__rustls" ]; - "rustls-tls-no-provider" = [ "rustls-tls-manual-roots-no-provider" ]; - "rustls-tls-webpki-roots" = [ "rustls-tls-webpki-roots-no-provider" "__rustls-ring" ]; - "rustls-tls-webpki-roots-no-provider" = [ "dep:webpki-roots" "hyper-rustls?/webpki-tokio" "__rustls" ]; + "native-tls" = [ "__native-tls" "__native-tls-alpn" ]; + "native-tls-no-alpn" = [ "__native-tls" ]; + "native-tls-vendored" = [ "__native-tls" "native-tls-crate?/vendored" "__native-tls-alpn" ]; + "native-tls-vendored-no-alpn" = [ "__native-tls" "native-tls-crate?/vendored" ]; + "query" = [ "dep:serde" "dep:serde_urlencoded" ]; + "rustls" = [ "__rustls-aws-lc-rs" "dep:rustls-platform-verifier" "__rustls" ]; + "rustls-no-provider" = [ "dep:rustls-platform-verifier" "__rustls" ]; "stream" = [ "tokio/fs" "dep:futures-util" "dep:tokio-util" "dep:wasm-streams" ]; "system-proxy" = [ "hyper-util/client-proxy-system" ]; "zstd" = [ "tower-http/decompression-zstd" ]; @@ -9297,29 +9310,25 @@ rec { }; resolvedDefaultFeatures = [ "alloc" "default" "rust_1_61" "rust_1_65" "std" ]; }; - "snafu 0.9.0" = rec { + "snafu 0.9.1" = rec { crateName = "snafu"; - version = "0.9.0"; + version = "0.9.1"; edition = "2018"; - sha256 = "1ii9r99x5qcn754m624yzgb9hzvkqkrcygf0aqh0pyb9dbnvrm6i"; + sha256 = "08k5yfydxdlshivfhrdq9km8qn02r93q28gkyvazbqz2icr1586i"; authors = [ "Jake Goulding " ]; dependencies = [ { name = "snafu-derive"; - packageId = "snafu-derive 0.9.0"; + packageId = "snafu-derive 0.9.1"; } ]; features = { - "backtrace" = [ "dep:backtrace" ]; - "backtraces-impl-backtrace-crate" = [ "backtrace" ]; + "backtraces-impl-backtrace-crate" = [ "dep:backtrace" ]; "default" = [ "std" "rust_1_81" ]; - "futures" = [ "futures-core-crate" "pin-project" ]; - "futures-core-crate" = [ "dep:futures-core-crate" ]; - "futures-crate" = [ "dep:futures-crate" ]; - "internal-dev-dependencies" = [ "futures-crate" ]; - "pin-project" = [ "dep:pin-project" ]; + "futures" = [ "dep:futures-core" "dep:pin-project" ]; + "internal-dev-dependencies" = [ "dep:futures" ]; "std" = [ "alloc" ]; "unstable-provider-api" = [ "snafu-derive/unstable-provider-api" ]; }; @@ -9387,11 +9396,11 @@ rec { }; resolvedDefaultFeatures = [ "rust_1_61" ]; }; - "snafu-derive 0.9.0" = rec { + "snafu-derive 0.9.1" = rec { crateName = "snafu-derive"; - version = "0.9.0"; + version = "0.9.1"; edition = "2018"; - sha256 = "0h0x61kyj4fvilcr2nj02l85shw1ika64vq9brf2gyna662ln9al"; + sha256 = "1nkfi7bis72pz3w7vb64m79w49qsv20sbf19jkd471vbhr83q42z"; procMacro = true; libName = "snafu_derive"; authors = [ @@ -9417,7 +9426,7 @@ rec { name = "syn"; packageId = "syn 2.0.117"; usesDefaultFeatures = false; - features = [ "clone-impls" "derive" "full" "parsing" "printing" "proc-macro" ]; + features = [ "clone-impls" "derive" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } ]; features = { @@ -9526,7 +9535,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_certs"; @@ -9585,7 +9594,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-shared"; @@ -9658,10 +9667,6 @@ rec { name = "indoc"; packageId = "indoc"; } - { - name = "java-properties"; - packageId = "java-properties"; - } { name = "serde"; packageId = "serde"; @@ -9673,7 +9678,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-operator"; @@ -9721,7 +9726,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_operator"; @@ -9772,6 +9777,10 @@ rec { name = "indexmap"; packageId = "indexmap"; } + { + name = "java-properties"; + packageId = "java-properties"; + } { name = "jiff"; packageId = "jiff"; @@ -9829,7 +9838,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-operator-derive"; @@ -9887,12 +9896,17 @@ rec { name = "uuid"; packageId = "uuid"; } + { + name = "xml"; + packageId = "xml"; + } ]; features = { "certs" = [ "dep:stackable-certs" ]; + "client-feature-gates" = [ "dep:winnow" ]; "crds" = [ "dep:stackable-versioned" ]; "default" = [ "crds" ]; - "full" = [ "crds" "certs" "time" "webhook" "kube-ws" ]; + "full" = [ "client-feature-gates" "crds" "certs" "time" "webhook" "kube-ws" ]; "kube-ws" = [ "kube/ws" ]; "time" = [ "stackable-shared/time" ]; "webhook" = [ "dep:stackable-webhook" ]; @@ -9906,7 +9920,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; procMacro = true; @@ -9936,12 +9950,12 @@ rec { }; "stackable-shared" = rec { crateName = "stackable-shared"; - version = "0.1.0"; + version = "0.1.1"; edition = "2024"; workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_shared"; @@ -9986,7 +10000,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "strum"; @@ -10017,12 +10031,12 @@ rec { }; "stackable-telemetry" = rec { crateName = "stackable-telemetry"; - version = "0.6.3"; + version = "0.6.4"; edition = "2024"; workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_telemetry"; @@ -10066,7 +10080,7 @@ rec { { name = "opentelemetry_sdk"; packageId = "opentelemetry_sdk"; - features = [ "rt-tokio" "logs" "rt-tokio" "spec_unstable_logs_enabled" ]; + features = [ "rt-tokio" "logs" "rt-tokio" ]; } { name = "pin-project"; @@ -10074,7 +10088,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "strum"; @@ -10132,7 +10146,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_versioned"; @@ -10166,7 +10180,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-versioned-macros"; @@ -10182,7 +10196,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; procMacro = true; @@ -10250,7 +10264,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a31cd2514445b251038fc4ea7abc28c57b2a6ad9"; + rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; }; libName = "stackable_webhook"; @@ -10323,7 +10337,7 @@ rec { } { name = "snafu"; - packageId = "snafu 0.9.0"; + packageId = "snafu 0.9.1"; } { name = "stackable-certs"; @@ -11414,6 +11428,33 @@ rec { } ]; + }; + "tonic-types" = rec { + crateName = "tonic-types"; + version = "0.14.5"; + edition = "2021"; + sha256 = "16bk1cxi2m0xgaabf98nnj7dn9j16ymkh27jq4s3shjm4a85m1ra"; + libName = "tonic_types"; + authors = [ + "Lucio Franco " + "Rafael Lemos " + ]; + dependencies = [ + { + name = "prost"; + packageId = "prost"; + } + { + name = "prost-types"; + packageId = "prost-types"; + } + { + name = "tonic"; + packageId = "tonic"; + usesDefaultFeatures = false; + } + ]; + }; "tower" = rec { crateName = "tower"; @@ -11870,9 +11911,9 @@ rec { }; "tracing-opentelemetry" = rec { crateName = "tracing-opentelemetry"; - version = "0.32.1"; + version = "0.33.0"; edition = "2021"; - sha256 = "1z2jjmxbkm1qawlb3bm99x8xwf4g8wjkbcknm9z4fv1w14nqzhhs"; + sha256 = "09nvxy5m7nxmifz4b6szdcyczapp2jcgxcac0jw4ax8klz5n9g5d"; libName = "tracing_opentelemetry"; dependencies = [ { @@ -13992,9 +14033,9 @@ rec { }; "xml" = rec { crateName = "xml"; - version = "1.2.1"; + version = "1.3.0"; edition = "2021"; - sha256 = "0ak4k990faralbli5a0rb8kvwihccb2rp0r94d4azfy94a6lkamq"; + sha256 = "128s58qhq8whrx90zbw8r5algr7lakgbf7mn05jfk234rbjqavv3"; authors = [ "Vladimir Matveev " "Kornel (https://github.com/kornelski)" diff --git a/Cargo.toml b/Cargo.toml index 74c770d8..02c687e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ clap = "4.5" const_format = "0.2" futures = "0.3" indoc = "2.0" -java-properties = "2.0" rstest = "0.26" semver = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/crate-hashes.json b/crate-hashes.json index 5564a89e..5b0037c5 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -3,8 +3,8 @@ "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 077ccccd..b9d9d40f 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -12,7 +12,6 @@ publish = false stackable-operator.workspace = true indoc.workspace = true -java-properties.workspace = true anyhow.workspace = true clap.workspace = true const_format.workspace = true diff --git a/rust/operator-binary/src/config/mod.rs b/rust/operator-binary/src/config/mod.rs index 162c9c09..ae92b3c2 100644 --- a/rust/operator-binary/src/config/mod.rs +++ b/rust/operator-binary/src/config/mod.rs @@ -1,4 +1,3 @@ pub mod command; pub mod jvm; pub mod node_id_hasher; -pub mod writer; diff --git a/rust/operator-binary/src/config/writer.rs b/rust/operator-binary/src/config/writer.rs deleted file mode 100644 index a74babf0..00000000 --- a/rust/operator-binary/src/config/writer.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Writer for Java `.properties` files. -//! -//! Vendored from the `product-config` crate's `writer` module so the operator no -//! longer depends on `product-config` for rendering. - -use std::io::Write; - -use java_properties::{PropertiesError, PropertiesWriter}; -use snafu::{ResultExt, Snafu}; - -#[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(()) -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use super::*; - - 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 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/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index b018f449..2c1d1767 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -7,10 +7,10 @@ use stackable_operator::{ commons::product_image_selection::ResolvedProductImage, k8s_openapi::api::core::v1::ConfigMap, role_utils::RoleGroupRef, + v2::config_file_writer::{PropertiesWriterError, to_java_properties_string}, }; use crate::{ - config::writer::to_java_properties_string, controller::KAFKA_CONTROLLER_NAME, crd::{ JVM_SECURITY_PROPERTIES_FILE, KafkaPodDescriptor, MetadataManager, @@ -40,7 +40,7 @@ pub enum Error { rolegroup ))] JvmSecurityProperties { - source: crate::config::writer::PropertiesWriterError, + source: PropertiesWriterError, rolegroup: String, }, @@ -56,7 +56,7 @@ pub enum Error { #[snafu(display("failed to serialize config for {rolegroup}"))] SerializeConfig { - source: crate::config::writer::PropertiesWriterError, + source: PropertiesWriterError, rolegroup: RoleGroupRef, }, From fc277503a2863b0ed78d780cc2489336ebebfb91 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 8 Jun 2026 18:01:58 +0200 Subject: [PATCH 09/47] pass validate cluster and rg rather than multiple parameters --- rust/operator-binary/src/controller.rs | 103 +++++++++++------- .../src/controller/build/config_map.rs | 28 ++--- .../src/controller/validate.rs | 41 +------ rust/operator-binary/src/discovery.rs | 11 +- rust/operator-binary/src/resource/listener.rs | 11 +- .../src/resource/statefulset.rs | 31 +++--- 6 files changed, 112 insertions(+), 113 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 8f495905..1193bb77 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,13 +1,13 @@ //! Ensures that `Pod`s are configured and running for each [`v1alpha1::KafkaCluster`]. -use std::sync::Arc; +use std::{collections::BTreeMap, sync::Arc}; use const_format::concatcp; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::rbac::build_rbac_resources, + commons::{product_image_selection::ResolvedProductImage, rbac::build_rbac_resources}, crd::listener, kube::{ Resource, @@ -32,8 +32,10 @@ mod validate; use crate::{ crd::{ self, APP_NAME, KafkaClusterStatus, OPERATOR_NAME, + authorization::KafkaAuthorizationConfig, listener::get_kafka_listener_config, role::{AnyConfig, KafkaRole}, + security::KafkaTlsSecurity, v1alpha1, }, discovery::{self, build_discovery_configmap}, @@ -208,6 +210,35 @@ impl ReconcilerError for Error { } } +/// The validated cluster. Carries everything the build steps need, resolved once +/// here so downstream code never re-derives it or touches the raw spec. +pub struct ValidatedKafkaCluster { + pub image: ResolvedProductImage, + pub kafka_security: KafkaTlsSecurity, + // DESIGN DECISION: the dereferenced authorization config is folded into the + // validated cluster (read from here downstream). The other dereferenced input, + // the authentication classes, is intentionally NOT stored: it is fully consumed + // here to build `kafka_security`. Alternative: also store the resolved auth + // classes — rejected because nothing downstream needs them beyond kafka_security. + pub authorization_config: Option, + pub role_groups: BTreeMap>, +} + +pub struct ValidatedRoleGroupConfig { + pub merged_config: AnyConfig, + // DESIGN DECISION: overrides are resolved into flat maps HERE rather than stored + // as the typed KeyValueConfigOverrides and resolved in the per-file builders (the + // hdfs-operator pattern). Reason: broker and controller use different override + // struct types (KafkaBrokerConfigOverrides vs KafkaControllerConfigOverrides), so a + // single typed field would require an enum. Resolving here keeps the build/properties + // builders taking plain `BTreeMap`. Alternative: an enum over the two + // override types threaded to builders that call resolved_overrides() — more types for + // no behavioural gain. + pub config_file_overrides: BTreeMap, + pub jvm_security_overrides: BTreeMap, + pub env_overrides: BTreeMap, +} + pub async fn reconcile_kafka( kafka: Arc>, ctx: Arc, @@ -228,15 +259,12 @@ pub async fn reconcile_kafka( .context(DereferenceSnafu)?; // validate (no client required) - let validate::ValidatedKafkaCluster { - authorization_config, - image, - kafka_security, - role_groups, - } = validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) - .context(ValidateClusterSnafu)?; - - let opa_connect = authorization_config + let validated_cluster = + validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) + .context(ValidateClusterSnafu)?; + + let opa_connect = validated_cluster + .authorization_config .as_ref() .map(|auth_config| auth_config.opa_connect.clone()); @@ -251,10 +279,10 @@ pub async fn reconcile_kafka( .context(CreateClusterResourcesSnafu)?; tracing::debug!( - kerberos_enabled = kafka_security.has_kerberos_enabled(), - kerberos_secret_class = ?kafka_security.kerberos_secret_class(), - tls_enabled = kafka_security.tls_enabled(), - tls_client_authentication_class = ?kafka_security.tls_client_authentication_class(), + kerberos_enabled = validated_cluster.kafka_security.has_kerberos_enabled(), + kerberos_secret_class = ?validated_cluster.kafka_security.kerberos_secret_class(), + tls_enabled = validated_cluster.kafka_security.tls_enabled(), + tls_client_authentication_class = ?validated_cluster.kafka_security.tls_client_authentication_class(), "The following security settings are used" ); @@ -280,20 +308,25 @@ pub async fn reconcile_kafka( let mut bootstrap_listeners = Vec::::new(); - for (kafka_role, rg_map) in &role_groups { + for (kafka_role, rg_map) in &validated_cluster.role_groups { for (rolegroup_name, validated_rg) in rg_map { let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name); - let rg_headless_service = - build_rolegroup_headless_service(kafka, &image, &rolegroup_ref, &kafka_security) - .context(BuildServiceSnafu)?; + let rg_headless_service = build_rolegroup_headless_service( + kafka, + &validated_cluster.image, + &rolegroup_ref, + &validated_cluster.kafka_security, + ) + .context(BuildServiceSnafu)?; - let rg_metrics_service = build_rolegroup_metrics_service(kafka, &image, &rolegroup_ref) - .context(BuildServiceSnafu)?; + let rg_metrics_service = + build_rolegroup_metrics_service(kafka, &validated_cluster.image, &rolegroup_ref) + .context(BuildServiceSnafu)?; let kafka_listeners = get_kafka_listener_config( kafka, - &kafka_security, + &validated_cluster.kafka_security, &rolegroup_ref, &client.kubernetes_cluster_info, ) @@ -303,18 +336,15 @@ pub async fn reconcile_kafka( .pod_descriptors( None, &client.kubernetes_cluster_info, - kafka_security.client_port(), + validated_cluster.kafka_security.client_port(), ) .context(BuildPodDescriptorsSnafu)?; let rg_configmap = build::config_map::build_rolegroup_config_map( kafka, - &image, - &kafka_security, + &validated_cluster, &rolegroup_ref, - validated_rg.config_file_overrides.clone(), - validated_rg.jvm_security_overrides.clone(), - &validated_rg.merged_config, + validated_rg, &kafka_listeners, &pod_descriptors, opa_connect.as_deref(), @@ -325,11 +355,9 @@ pub async fn reconcile_kafka( KafkaRole::Broker => build_broker_rolegroup_statefulset( kafka, kafka_role, - &image, + &validated_cluster, &rolegroup_ref, - &validated_rg.env_overrides, - &kafka_security, - &validated_rg.merged_config, + validated_rg, &rbac_sa, &client.kubernetes_cluster_info, ) @@ -337,11 +365,9 @@ pub async fn reconcile_kafka( KafkaRole::Controller => build_controller_rolegroup_statefulset( kafka, kafka_role, - &image, + &validated_cluster, &rolegroup_ref, - &validated_rg.env_overrides, - &kafka_security, - &validated_rg.merged_config, + validated_rg, &rbac_sa, &client.kubernetes_cluster_info, ) @@ -351,8 +377,7 @@ pub async fn reconcile_kafka( if let AnyConfig::Broker(broker_config) = &validated_rg.merged_config { let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( kafka, - &image, - &kafka_security, + &validated_cluster, &rolegroup_ref, broker_config, ) @@ -409,7 +434,7 @@ pub async fn reconcile_kafka( } let discovery_cm = - build_discovery_configmap(kafka, kafka, &image, &kafka_security, &bootstrap_listeners) + build_discovery_configmap(kafka, kafka, validated_cluster, &bootstrap_listeners) .context(BuildDiscoveryConfigSnafu)?; cluster_resources diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 2c1d1767..6d99412c 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -4,20 +4,18 @@ use indoc::formatdoc; use snafu::{ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, - commons::product_image_selection::ResolvedProductImage, k8s_openapi::api::core::v1::ConfigMap, role_utils::RoleGroupRef, v2::config_file_writer::{PropertiesWriterError, to_java_properties_string}, }; use crate::{ - controller::KAFKA_CONTROLLER_NAME, + controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster, ValidatedRoleGroupConfig}, crd::{ JVM_SECURITY_PROPERTIES_FILE, KafkaPodDescriptor, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, listener::{KafkaListenerConfig, node_address_cmd}, role::AnyConfig, - security::KafkaTlsSecurity, v1alpha1, }, product_logging::extend_role_group_config_map, @@ -74,23 +72,23 @@ pub enum Error { #[allow(clippy::too_many_arguments)] pub fn build_rolegroup_config_map( kafka: &v1alpha1::KafkaCluster, - resolved_product_image: &ResolvedProductImage, - kafka_security: &KafkaTlsSecurity, + validated_cluster: &ValidatedKafkaCluster, rolegroup: &RoleGroupRef, - config_file_overrides: BTreeMap, - jvm_security_overrides: BTreeMap, - merged_config: &AnyConfig, + validated_rg: &ValidatedRoleGroupConfig, listener_config: &KafkaListenerConfig, pod_descriptors: &[KafkaPodDescriptor], opa_connect_string: Option<&str>, ) -> Result { - let kafka_config_file_name = merged_config.config_file_name(); + let kafka_security = &validated_cluster.kafka_security; + let resolved_product_image = &validated_cluster.image; + let kafka_config_file_name = validated_rg.merged_config.config_file_name(); + let config_overrides = validated_rg.config_file_overrides.clone(); let metadata_manager = kafka .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; - let kafka_config = match merged_config { + let kafka_config = match &validated_rg.merged_config { AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( kafka_security, listener_config, @@ -102,7 +100,7 @@ pub fn build_rolegroup_config_map( .cluster_config .broker_id_pod_config_map_name .is_some(), - config_file_overrides, + config_overrides, ), AnyConfig::Controller(_) => { crate::controller::build::properties::controller_properties::build( @@ -110,7 +108,7 @@ pub fn build_rolegroup_config_map( listener_config, pod_descriptors, metadata_manager == MetadataManager::KRaft, - config_file_overrides, + config_overrides, ) } } @@ -123,7 +121,9 @@ pub fn build_rolegroup_config_map( .map(|(k, v)| (k, Some(v))) .collect::>(); - let jvm_sec_props: BTreeMap> = jvm_security_overrides + let jvm_sec_props: BTreeMap> = validated_rg + .jvm_security_overrides + .clone() .into_iter() .map(|(k, v)| (k, Some(v))) .collect(); @@ -189,7 +189,7 @@ pub fn build_rolegroup_config_map( extend_role_group_config_map( &resolved_product_image.product_version, rolegroup, - merged_config, + &validated_rg.merged_config, &mut cm_builder, ); diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 72d6fa6a..6c541117 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -6,18 +6,16 @@ use std::collections::BTreeMap; use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - cli::OperatorEnvironmentOptions, - commons::product_image_selection::{self, ResolvedProductImage}, -}; +use stackable_operator::{cli::OperatorEnvironmentOptions, commons::product_image_selection}; use crate::{ - controller::dereference::DereferencedObjects, + controller::{ + ValidatedKafkaCluster, ValidatedRoleGroupConfig, dereference::DereferencedObjects, + }, crd::{ self, CONTAINER_IMAGE_BASE_NAME, authentication::{self}, - authorization::KafkaAuthorizationConfig, - role::{AnyConfig, KafkaRole}, + role::KafkaRole, security::{self, KafkaTlsSecurity}, v1alpha1, }, @@ -45,35 +43,6 @@ pub enum Error { type Result = std::result::Result; -/// The validated cluster. Carries everything the build steps need, resolved once -/// here so downstream code never re-derives it or touches the raw spec. -pub struct ValidatedKafkaCluster { - pub image: ResolvedProductImage, - pub kafka_security: KafkaTlsSecurity, - // DESIGN DECISION: the dereferenced authorization config is folded into the - // validated cluster (read from here downstream). The other dereferenced input, - // the authentication classes, is intentionally NOT stored: it is fully consumed - // here to build `kafka_security`. Alternative: also store the resolved auth - // classes — rejected because nothing downstream needs them beyond kafka_security. - pub authorization_config: Option, - pub role_groups: BTreeMap>, -} - -pub struct ValidatedRoleGroupConfig { - pub merged_config: AnyConfig, - // DESIGN DECISION: overrides are resolved into flat maps HERE rather than stored - // as the typed KeyValueConfigOverrides and resolved in the per-file builders (the - // hdfs-operator pattern). Reason: broker and controller use different override - // struct types (KafkaBrokerConfigOverrides vs KafkaControllerConfigOverrides), so a - // single typed field would require an enum. Resolving here keeps the build/properties - // builders taking plain `BTreeMap`. Alternative: an enum over the two - // override types threaded to builders that call resolved_overrides() — more types for - // no behavioural gain. - pub config_file_overrides: BTreeMap, - pub jvm_security_overrides: BTreeMap, - pub env_overrides: BTreeMap, -} - /// Validates the cluster spec and the dereferenced inputs. pub fn validate( kafka: &v1alpha1::KafkaCluster, diff --git a/rust/operator-binary/src/discovery.rs b/rust/operator-binary/src/discovery.rs index e0cc6b36..a978e8dd 100644 --- a/rust/operator-binary/src/discovery.rs +++ b/rust/operator-binary/src/discovery.rs @@ -3,15 +3,14 @@ use std::num::TryFromIntError; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, - commons::product_image_selection::ResolvedProductImage, crd::listener, k8s_openapi::api::core::v1::ConfigMap, kube::{Resource, ResourceExt, runtime::reflector::ObjectRef}, }; use crate::{ - controller::KAFKA_CONTROLLER_NAME, - crd::{role::KafkaRole, security::KafkaTlsSecurity, v1alpha1}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster}, + crd::{role::KafkaRole, v1alpha1}, utils::build_recommended_labels, }; @@ -48,10 +47,12 @@ pub enum Error { pub fn build_discovery_configmap( kafka: &v1alpha1::KafkaCluster, owner: &impl Resource, - resolved_product_image: &ResolvedProductImage, - kafka_security: &KafkaTlsSecurity, + validated_cluster: ValidatedKafkaCluster, listeners: &[listener::v1alpha1::Listener], ) -> Result { + let kafka_security = &validated_cluster.kafka_security; + let resolved_product_image = &validated_cluster.image; + let port_name = if kafka_security.has_kerberos_enabled() { kafka_security.bootstrap_port_name() } else { diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/resource/listener.rs index 4afde134..35f360f7 100644 --- a/rust/operator-binary/src/resource/listener.rs +++ b/rust/operator-binary/src/resource/listener.rs @@ -1,11 +1,10 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::meta::ObjectMetaBuilder, commons::product_image_selection::ResolvedProductImage, - crd::listener, role_utils::RoleGroupRef, + builder::meta::ObjectMetaBuilder, crd::listener, role_utils::RoleGroupRef, }; use crate::{ - controller::KAFKA_CONTROLLER_NAME, + controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster}, crd::{role::broker::BrokerConfig, security::KafkaTlsSecurity, v1alpha1}, utils::build_recommended_labels, }; @@ -28,11 +27,13 @@ pub enum Error { // TODO (@NickLarsenNZ): Move shared functionality to stackable-operator pub fn build_broker_rolegroup_bootstrap_listener( kafka: &v1alpha1::KafkaCluster, - resolved_product_image: &ResolvedProductImage, - kafka_security: &KafkaTlsSecurity, + validated_cluster: &ValidatedKafkaCluster, rolegroup: &RoleGroupRef, merged_config: &BrokerConfig, ) -> Result { + let kafka_security = &validated_cluster.kafka_security; + let resolved_product_image = &validated_cluster.image; + Ok(listener::v1alpha1::Listener { metadata: ObjectMetaBuilder::new() .name_and_namespace(kafka) diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 7f232793..5a433d50 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, ops::Deref}; +use std::ops::Deref; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ @@ -12,7 +12,6 @@ use stackable_operator::{ volume::{ListenerOperatorVolumeSourceBuilder, ListenerReference, VolumeBuilder}, }, }, - commons::product_image_selection::ResolvedProductImage, constants::RESTART_CONTROLLER_ENABLED_LABEL, k8s_openapi::{ DeepMerge, @@ -44,14 +43,14 @@ use crate::{ command::{broker_kafka_container_commands, controller_kafka_container_command}, node_id_hasher::node_id_hash32_offset, }, - controller::KAFKA_CONTROLLER_NAME, + controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster, ValidatedRoleGroupConfig}, crd::{ self, APP_NAME, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, MetadataManager, STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, role::{ - AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, + KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, controller::ControllerContainer, }, security::KafkaTlsSecurity, @@ -165,14 +164,15 @@ pub enum Error { pub fn build_broker_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, - resolved_product_image: &ResolvedProductImage, + validated_cluster: &ValidatedKafkaCluster, rolegroup_ref: &RoleGroupRef, - env_overrides: &BTreeMap, - kafka_security: &KafkaTlsSecurity, - merged_config: &AnyConfig, + validated_rg: &ValidatedRoleGroupConfig, service_account: &ServiceAccount, cluster_info: &KubernetesClusterInfo, ) -> Result { + let kafka_security = &validated_cluster.kafka_security; + let resolved_product_image = &validated_cluster.image; + let merged_config = &validated_rg.merged_config; let recommended_object_labels = build_recommended_labels( kafka, KAFKA_CONTROLLER_NAME, @@ -245,7 +245,8 @@ pub fn build_broker_rolegroup_statefulset( .context(AddKerberosConfigSnafu)?; } - let mut env = env_overrides + let mut env = validated_rg + .env_overrides .iter() .map(|(k, v)| EnvVar { name: k.clone(), @@ -573,14 +574,15 @@ pub fn build_broker_rolegroup_statefulset( pub fn build_controller_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, - resolved_product_image: &ResolvedProductImage, + validated_cluster: &ValidatedKafkaCluster, rolegroup_ref: &RoleGroupRef, - env_overrides: &BTreeMap, - kafka_security: &KafkaTlsSecurity, - merged_config: &AnyConfig, + validated_rg: &ValidatedRoleGroupConfig, service_account: &ServiceAccount, cluster_info: &KubernetesClusterInfo, ) -> Result { + let kafka_security = &validated_cluster.kafka_security; + let resolved_product_image = &validated_cluster.image; + let merged_config = &validated_rg.merged_config; let recommended_object_labels = build_recommended_labels( kafka, KAFKA_CONTROLLER_NAME, @@ -597,7 +599,8 @@ pub fn build_controller_rolegroup_statefulset( let mut pod_builder = PodBuilder::new(); - let mut env = env_overrides + let mut env = validated_rg + .env_overrides .iter() .map(|(k, v)| EnvVar { name: k.clone(), From ae6a58b315509fbd5c3d486c0fe567558759dbef Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 11:11:56 +0200 Subject: [PATCH 10/47] removed redundant parameter --- rust/operator-binary/src/controller.rs | 5 ++--- .../src/controller/build/config_map.rs | 1 - rust/operator-binary/src/discovery.rs | 11 +++++------ rust/operator-binary/src/resource/statefulset.rs | 2 -- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 1193bb77..3c5ecfee 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -433,9 +433,8 @@ pub async fn reconcile_kafka( } } - let discovery_cm = - build_discovery_configmap(kafka, kafka, validated_cluster, &bootstrap_listeners) - .context(BuildDiscoveryConfigSnafu)?; + let discovery_cm = build_discovery_configmap(kafka, validated_cluster, &bootstrap_listeners) + .context(BuildDiscoveryConfigSnafu)?; cluster_resources .add(client, discovery_cm) diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 6d99412c..9228675d 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -69,7 +69,6 @@ pub enum Error { } /// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator -#[allow(clippy::too_many_arguments)] pub fn build_rolegroup_config_map( kafka: &v1alpha1::KafkaCluster, validated_cluster: &ValidatedKafkaCluster, diff --git a/rust/operator-binary/src/discovery.rs b/rust/operator-binary/src/discovery.rs index a978e8dd..98c4516a 100644 --- a/rust/operator-binary/src/discovery.rs +++ b/rust/operator-binary/src/discovery.rs @@ -5,7 +5,7 @@ use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, crd::listener, k8s_openapi::api::core::v1::ConfigMap, - kube::{Resource, ResourceExt, runtime::reflector::ObjectRef}, + kube::{ResourceExt, runtime::reflector::ObjectRef}, }; use crate::{ @@ -45,8 +45,7 @@ pub enum Error { /// Build a discovery [`ConfigMap`] containing information about how to connect to a certain /// [`v1alpha1::KafkaCluster`]. pub fn build_discovery_configmap( - kafka: &v1alpha1::KafkaCluster, - owner: &impl Resource, + owner: &v1alpha1::KafkaCluster, validated_cluster: ValidatedKafkaCluster, listeners: &[listener::v1alpha1::Listener], ) -> Result { @@ -69,14 +68,14 @@ pub fn build_discovery_configmap( ConfigMapBuilder::new() .metadata( ObjectMetaBuilder::new() - .name_and_namespace(kafka) + .name_and_namespace(owner) .name(owner.name_unchecked()) .ownerreference_from_resource(owner, None, Some(true)) .with_context(|_| ObjectMissingMetadataForOwnerRefSnafu { - kafka: ObjectRef::from_obj(kafka), + kafka: ObjectRef::from_obj(owner), })? .with_recommended_labels(&build_recommended_labels( - kafka, + owner, KAFKA_CONTROLLER_NAME, &resolved_product_image.product_version, &KafkaRole::Broker.to_string(), diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 5a433d50..840382e9 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -160,7 +160,6 @@ pub enum Error { /// /// The [`Pod`](`stackable_operator::k8s_openapi::api::core::v1::Pod`)s are accessible through the corresponding /// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_service`](`crate::resource::service::build_rolegroup_headless_service`). -#[allow(clippy::too_many_arguments)] pub fn build_broker_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, @@ -570,7 +569,6 @@ pub fn build_broker_rolegroup_statefulset( } /// The controller rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. -#[allow(clippy::too_many_arguments)] pub fn build_controller_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, From 7706e88d66add0e56ffae69335eaa75a70e3c541 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 11:19:21 +0200 Subject: [PATCH 11/47] push opa connect string calc down to function --- rust/operator-binary/src/controller.rs | 6 ------ rust/operator-binary/src/controller/build/config_map.rs | 8 ++++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 3c5ecfee..1cbf1a71 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -263,11 +263,6 @@ pub async fn reconcile_kafka( validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) .context(ValidateClusterSnafu)?; - let opa_connect = validated_cluster - .authorization_config - .as_ref() - .map(|auth_config| auth_config.opa_connect.clone()); - let mut cluster_resources = ClusterResources::new( APP_NAME, OPERATOR_NAME, @@ -347,7 +342,6 @@ pub async fn reconcile_kafka( validated_rg, &kafka_listeners, &pod_descriptors, - opa_connect.as_deref(), ) .context(BuildConfigMapSnafu)?; diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 9228675d..7f715ad5 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -76,13 +76,17 @@ pub fn build_rolegroup_config_map( validated_rg: &ValidatedRoleGroupConfig, listener_config: &KafkaListenerConfig, pod_descriptors: &[KafkaPodDescriptor], - opa_connect_string: Option<&str>, ) -> Result { let kafka_security = &validated_cluster.kafka_security; let resolved_product_image = &validated_cluster.image; let kafka_config_file_name = validated_rg.merged_config.config_file_name(); let config_overrides = validated_rg.config_file_overrides.clone(); + let opa_connect = validated_cluster + .authorization_config + .as_ref() + .map(|auth_config| auth_config.opa_connect.clone()); + let metadata_manager = kafka .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; @@ -92,7 +96,7 @@ pub fn build_rolegroup_config_map( kafka_security, listener_config, pod_descriptors, - opa_connect_string, + opa_connect.as_deref(), metadata_manager == MetadataManager::KRaft, kafka .spec From 4eb786b984bf3794d23b8b7524516ade660752a8 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 11:46:31 +0200 Subject: [PATCH 12/47] push pod_descriptors calc down to build_rolegroup_config_map --- rust/operator-binary/src/controller.rs | 15 ++------------- .../src/controller/build/config_map.rs | 16 +++++++++++++--- .../src/controller/dereference.rs | 4 +++- rust/operator-binary/src/controller/validate.rs | 1 + 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 1cbf1a71..896e12e4 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -22,6 +22,7 @@ use stackable_operator::{ compute_conditions, operations::ClusterOperationsConditionBuilder, statefulset::StatefulSetConditionBuilder, }, + utils::cluster_info::KubernetesClusterInfo, }; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -65,9 +66,6 @@ pub enum Error { #[snafu(display("failed to validate cluster"))] ValidateCluster { source: validate::Error }, - #[snafu(display("failed to build pod descriptors"))] - BuildPodDescriptors { source: crate::crd::Error }, - #[snafu(display("invalid kafka listeners"))] InvalidKafkaListeners { source: crate::crd::listener::KafkaListenerError, @@ -205,7 +203,6 @@ impl ReconcilerError for Error { Error::BuildService { .. } => None, Error::BuildListener { .. } => None, Error::InvalidKafkaListeners { .. } => None, - Error::BuildPodDescriptors { .. } => None, } } } @@ -222,6 +219,7 @@ pub struct ValidatedKafkaCluster { // classes — rejected because nothing downstream needs them beyond kafka_security. pub authorization_config: Option, pub role_groups: BTreeMap>, + pub kubernetes_cluster_info: KubernetesClusterInfo, } pub struct ValidatedRoleGroupConfig { @@ -327,21 +325,12 @@ pub async fn reconcile_kafka( ) .context(InvalidKafkaListenersSnafu)?; - let pod_descriptors = kafka - .pod_descriptors( - None, - &client.kubernetes_cluster_info, - validated_cluster.kafka_security.client_port(), - ) - .context(BuildPodDescriptorsSnafu)?; - let rg_configmap = build::config_map::build_rolegroup_config_map( kafka, &validated_cluster, &rolegroup_ref, validated_rg, &kafka_listeners, - &pod_descriptors, ) .context(BuildConfigMapSnafu)?; diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 7f715ad5..d248b5ba 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -12,8 +12,8 @@ use stackable_operator::{ use crate::{ controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster, ValidatedRoleGroupConfig}, crd::{ - JVM_SECURITY_PROPERTIES_FILE, KafkaPodDescriptor, MetadataManager, - STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, + JVM_SECURITY_PROPERTIES_FILE, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, + STACKABLE_LISTENER_BROKER_DIR, listener::{KafkaListenerConfig, node_address_cmd}, role::AnyConfig, v1alpha1, @@ -66,6 +66,9 @@ pub enum Error { #[snafu(display("failed to build jaas configuration file for {rolegroup}"))] BuildJaasConfig { rolegroup: String }, + + #[snafu(display("failed to build pod descriptors"))] + BuildPodDescriptors { source: crate::crd::Error }, } /// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator @@ -75,7 +78,6 @@ pub fn build_rolegroup_config_map( rolegroup: &RoleGroupRef, validated_rg: &ValidatedRoleGroupConfig, listener_config: &KafkaListenerConfig, - pod_descriptors: &[KafkaPodDescriptor], ) -> Result { let kafka_security = &validated_cluster.kafka_security; let resolved_product_image = &validated_cluster.image; @@ -87,6 +89,14 @@ pub fn build_rolegroup_config_map( .as_ref() .map(|auth_config| auth_config.opa_connect.clone()); + let pod_descriptors = &kafka + .pod_descriptors( + None, + &validated_cluster.kubernetes_cluster_info, + validated_cluster.kafka_security.client_port(), + ) + .context(BuildPodDescriptorsSnafu)?; + let metadata_manager = kafka .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; diff --git a/rust/operator-binary/src/controller/dereference.rs b/rust/operator-binary/src/controller/dereference.rs index b7a22107..c90d1258 100644 --- a/rust/operator-binary/src/controller/dereference.rs +++ b/rust/operator-binary/src/controller/dereference.rs @@ -9,7 +9,7 @@ //! and stays here as-is. use snafu::{ResultExt, Snafu}; -use stackable_operator::client::Client; +use stackable_operator::{client::Client, utils::cluster_info::KubernetesClusterInfo}; use crate::crd::{ authentication::{self, ResolvedAuthenticationClasses}, @@ -33,6 +33,7 @@ type Result = std::result::Result; pub struct DereferencedObjects { pub authentication_classes: ResolvedAuthenticationClasses, pub authorization_config: Option, + pub kubernetes_cluster_info: KubernetesClusterInfo, } /// Fetches all Kubernetes objects referenced from the [`v1alpha1::KafkaCluster`] spec. @@ -59,5 +60,6 @@ pub async fn dereference( Ok(DereferencedObjects { authentication_classes, authorization_config, + kubernetes_cluster_info: client.kubernetes_cluster_info.clone(), }) } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 6c541117..c03f381c 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -146,6 +146,7 @@ pub fn validate( kafka_security, authorization_config: dereferenced_objects.authorization_config, role_groups, + kubernetes_cluster_info: dereferenced_objects.kubernetes_cluster_info, }) } From af0ceba907103852fc0f2fdbc293f3e8170aac12 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 12:48:44 +0200 Subject: [PATCH 13/47] move resolution of metadata_manager and pod descriptors to validate stage --- rust/operator-binary/src/controller.rs | 6 ++-- .../src/controller/build/config_map.rs | 36 +++++++------------ .../build/properties/broker_properties.rs | 10 +++--- .../build/properties/controller_properties.rs | 11 +++--- .../src/controller/build/properties/mod.rs | 19 ++-------- .../src/controller/validate.rs | 21 ++++++++++- 6 files changed, 45 insertions(+), 58 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 896e12e4..64e9a8b9 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -22,7 +22,6 @@ use stackable_operator::{ compute_conditions, operations::ClusterOperationsConditionBuilder, statefulset::StatefulSetConditionBuilder, }, - utils::cluster_info::KubernetesClusterInfo, }; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -32,7 +31,7 @@ mod validate; use crate::{ crd::{ - self, APP_NAME, KafkaClusterStatus, OPERATOR_NAME, + self, APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, authorization::KafkaAuthorizationConfig, listener::get_kafka_listener_config, role::{AnyConfig, KafkaRole}, @@ -219,7 +218,8 @@ pub struct ValidatedKafkaCluster { // classes — rejected because nothing downstream needs them beyond kafka_security. pub authorization_config: Option, pub role_groups: BTreeMap>, - pub kubernetes_cluster_info: KubernetesClusterInfo, + pub pod_descriptors: Vec, + pub metadata_manager: MetadataManager, } pub struct ValidatedRoleGroupConfig { diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index d248b5ba..c9dcd501 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -58,17 +58,14 @@ pub enum Error { rolegroup: RoleGroupRef, }, - #[snafu(display("failed to build properties for {rolegroup}"))] - BuildProperties { - source: crate::controller::build::properties::Error, - rolegroup: RoleGroupRef, - }, - #[snafu(display("failed to build jaas configuration file for {rolegroup}"))] BuildJaasConfig { rolegroup: String }, #[snafu(display("failed to build pod descriptors"))] BuildPodDescriptors { source: crate::crd::Error }, + + #[snafu(display("no Kraft controllers found to build"))] + NoKraftControllersFound, } /// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator @@ -89,25 +86,19 @@ pub fn build_rolegroup_config_map( .as_ref() .map(|auth_config| auth_config.opa_connect.clone()); - let pod_descriptors = &kafka - .pod_descriptors( - None, - &validated_cluster.kubernetes_cluster_info, - validated_cluster.kafka_security.client_port(), - ) - .context(BuildPodDescriptorsSnafu)?; + let kraft_mode = validated_cluster.metadata_manager == MetadataManager::KRaft; - let metadata_manager = kafka - .effective_metadata_manager() - .context(InvalidMetadataManagerSnafu)?; + if kraft_mode && validated_cluster.pod_descriptors.is_empty() { + return NoKraftControllersFoundSnafu.fail(); + } let kafka_config = match &validated_rg.merged_config { AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( kafka_security, listener_config, - pod_descriptors, + &validated_cluster.pod_descriptors, opa_connect.as_deref(), - metadata_manager == MetadataManager::KRaft, + kraft_mode, kafka .spec .cluster_config @@ -119,15 +110,12 @@ pub fn build_rolegroup_config_map( crate::controller::build::properties::controller_properties::build( kafka_security, listener_config, - pod_descriptors, - metadata_manager == MetadataManager::KRaft, + &validated_cluster.pod_descriptors, + kraft_mode, config_overrides, ) } - } - .with_context(|_| BuildPropertiesSnafu { - rolegroup: rolegroup.clone(), - })?; + }; let kafka_config = kafka_config .into_iter() diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs index 5d43ab79..44840363 100644 --- a/rust/operator-binary/src/controller/build/properties/broker_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -1,8 +1,6 @@ use std::collections::BTreeMap; -use snafu::OptionExt; - -use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; +use super::kraft_controllers; use crate::{ crd::{ KafkaPodDescriptor, @@ -25,7 +23,7 @@ pub fn build( kraft_mode: bool, disable_broker_id_generation: bool, overrides: BTreeMap, -) -> Result, Error> { +) -> BTreeMap { let kraft_controllers = kraft_controllers(pod_descriptors); let mut result = BTreeMap::from([ @@ -49,7 +47,7 @@ pub fn build( ]); if kraft_mode { - let kraft_controllers = kraft_controllers.context(NoKraftControllersFoundSnafu)?; + let kraft_controllers = kraft_controllers.join(","); // Running in KRaft mode result.extend([ @@ -114,5 +112,5 @@ pub fn build( result.extend(graceful_shutdown_config_properties()); result.extend(overrides); - Ok(result) + result } diff --git a/rust/operator-binary/src/controller/build/properties/controller_properties.rs b/rust/operator-binary/src/controller/build/properties/controller_properties.rs index 6a8172a8..ba825aec 100644 --- a/rust/operator-binary/src/controller/build/properties/controller_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -1,8 +1,6 @@ use std::collections::BTreeMap; -use snafu::OptionExt; - -use super::{Error, NoKraftControllersFoundSnafu, kraft_controllers}; +use super::kraft_controllers; use crate::{ crd::{ KafkaPodDescriptor, @@ -22,9 +20,8 @@ pub fn build( pod_descriptors: &[KafkaPodDescriptor], kraft_mode: bool, overrides: BTreeMap, -) -> Result, Error> { - let kraft_controllers = - kraft_controllers(pod_descriptors).context(NoKraftControllersFoundSnafu)?; +) -> BTreeMap { + let kraft_controllers = kraft_controllers(pod_descriptors).join(","); let mut result = BTreeMap::from([ ( @@ -72,5 +69,5 @@ pub fn build( result.extend(graceful_shutdown_config_properties()); result.extend(overrides); - Ok(result) + result } diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index adcdcaae..116f5c2f 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -3,18 +3,10 @@ pub mod broker_properties; pub mod controller_properties; -use snafu::Snafu; - use crate::crd::{KafkaPodDescriptor, role::KafkaRole}; -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("no Kraft controllers found to build"))] - NoKraftControllersFound, -} - -pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Option { - let result = pod_descriptors +pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Vec { + pod_descriptors .iter() .filter(|pd| pd.role == KafkaRole::Controller.to_string()) .map(|desc| { @@ -25,11 +17,4 @@ pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Optio ) }) .collect::>() - .join(","); - - if result.is_empty() { - None - } else { - Some(result) - } } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index c03f381c..669febd9 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -39,6 +39,12 @@ pub enum Error { #[snafu(display("failed to resolve merged config for rolegroup"))] ResolveMergedConfig { source: crate::crd::role::Error }, + + #[snafu(display("failed to build pod descriptors"))] + BuildPodDescriptors { source: crate::crd::Error }, + + #[snafu(display("invalid metadata manager"))] + InvalidMetadataManager { source: crate::crd::Error }, } type Result = std::result::Result; @@ -141,12 +147,25 @@ pub fn validate( role_groups.insert(KafkaRole::Controller, controller_groups); } + let pod_descriptors = kafka + .pod_descriptors( + None, + &dereferenced_objects.kubernetes_cluster_info, + kafka_security.client_port(), + ) + .context(BuildPodDescriptorsSnafu)?; + + let metadata_manager = kafka + .effective_metadata_manager() + .context(InvalidMetadataManagerSnafu)?; + Ok(ValidatedKafkaCluster { image, kafka_security, authorization_config: dereferenced_objects.authorization_config, role_groups, - kubernetes_cluster_info: dereferenced_objects.kubernetes_cluster_info, + pod_descriptors, + metadata_manager, }) } From b6a01a0d5198a410ebbfc0f56cd1591ee37eafb3 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 15:08:17 +0200 Subject: [PATCH 14/47] add config map data explicitly --- .../src/controller/build/config_map.rs | 10 +- rust/operator-binary/src/product_logging.rs | 132 +++++++++--------- 2 files changed, 76 insertions(+), 66 deletions(-) diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index c9dcd501..ffd7ef03 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -18,7 +18,7 @@ use crate::{ role::AnyConfig, v1alpha1, }, - product_logging::extend_role_group_config_map, + product_logging::role_group_config_map_data, utils::build_recommended_labels, }; @@ -187,12 +187,16 @@ pub fn build_rolegroup_config_map( tracing::debug!(?kafka_config, "Applied kafka config"); tracing::debug!(?jvm_sec_props, "Applied JVM config"); - extend_role_group_config_map( + let config_data = role_group_config_map_data( &resolved_product_image.product_version, rolegroup, &validated_rg.merged_config, - &mut cm_builder, ); + for (file_name, data) in config_data { + if let Some(data) = data { + cm_builder.add_data(file_name, data); + } + } cm_builder .build() diff --git a/rust/operator-binary/src/product_logging.rs b/rust/operator-binary/src/product_logging.rs index 8336f5f7..40780435 100644 --- a/rust/operator-binary/src/product_logging.rs +++ b/rust/operator-binary/src/product_logging.rs @@ -1,7 +1,6 @@ -use std::{borrow::Cow, fmt::Display}; +use std::{borrow::Cow, collections::BTreeMap, fmt::Display}; use stackable_operator::{ - builder::configmap::ConfigMapBuilder, memory::{BinaryMultiple, MemoryQuantity}, product_logging::{ self, @@ -45,36 +44,43 @@ pub fn kafka_log_opts_env_var() -> String { "KAFKA_LOG4J_OPTS".to_string() } -/// Extend the role group ConfigMap with logging and Vector configurations -pub fn extend_role_group_config_map( +/// Get the role group ConfigMap data with logging and Vector configurations +pub fn role_group_config_map_data( product_version: &str, rolegroup: &RoleGroupRef, merged_config: &AnyConfig, - cm_builder: &mut ConfigMapBuilder, -) { +) -> BTreeMap> { let container_name = match merged_config { AnyConfig::Broker(_) => BrokerContainer::Kafka.to_string(), AnyConfig::Controller(_) => ControllerContainer::Kafka.to_string(), }; + let mut configs: BTreeMap> = BTreeMap::new(); + // Starting with Kafka 4.0, log4j2 is used instead of log4j. match product_version.starts_with("3.") { - true => add_log4j_config_if_automatic( - cm_builder, - Some(merged_config.kafka_logging()), - LOG4J_CONFIG_FILE, - container_name, - KAFKA_LOG4J_FILE, - MAX_KAFKA_LOG_FILES_SIZE, - ), - false => add_log4j2_config_if_automatic( - cm_builder, - Some(merged_config.kafka_logging()), - LOG4J2_CONFIG_FILE, - container_name, - KAFKA_LOG4J2_FILE, - MAX_KAFKA_LOG_FILES_SIZE, - ), + true => { + configs.insert( + LOG4J_CONFIG_FILE.to_string(), + log4j_config_if_automatic( + Some(merged_config.kafka_logging()), + container_name, + KAFKA_LOG4J_FILE, + MAX_KAFKA_LOG_FILES_SIZE, + ), + ); + } + false => { + configs.insert( + LOG4J2_CONFIG_FILE.to_string(), + log4j2_config_if_automatic( + Some(merged_config.kafka_logging()), + container_name, + KAFKA_LOG4J2_FILE, + MAX_KAFKA_LOG_FILES_SIZE, + ), + ); + } } let vector_log_config = merged_config.vector_logging(); @@ -88,65 +94,65 @@ pub fn extend_role_group_config_map( }; if merged_config.vector_logging_enabled() { - cm_builder.add_data( - product_logging::framework::VECTOR_CONFIG_FILE, - product_logging::framework::create_vector_config(rolegroup, vector_log_config), + configs.insert( + product_logging::framework::VECTOR_CONFIG_FILE.to_string(), + Some(product_logging::framework::create_vector_config( + rolegroup, + vector_log_config, + )), ); } + configs } -fn add_log4j_config_if_automatic( - cm_builder: &mut ConfigMapBuilder, +fn log4j_config_if_automatic( log_config: Option>, - log_config_file: &str, container_name: impl Display, log_file: &str, max_log_file_size: MemoryQuantity, -) { - if let Some(ContainerLogConfig { +) -> Option { + let config = if let Some(ContainerLogConfig { choice: Some(ContainerLogConfigChoice::Automatic(log_config)), }) = log_config.as_deref() { - cm_builder.add_data( - log_config_file, - product_logging::framework::create_log4j_config( - &format!("{STACKABLE_LOG_DIR}/{container_name}"), - log_file, - max_log_file_size - .scale_to(BinaryMultiple::Mebi) - .floor() - .value as u32, - CONSOLE_CONVERSION_PATTERN_LOG4J, - log_config, - ), - ); - } + Some(product_logging::framework::create_log4j_config( + &format!("{STACKABLE_LOG_DIR}/{container_name}"), + log_file, + max_log_file_size + .scale_to(BinaryMultiple::Mebi) + .floor() + .value as u32, + CONSOLE_CONVERSION_PATTERN_LOG4J, + log_config, + )) + } else { + None + }; + config } -fn add_log4j2_config_if_automatic( - cm_builder: &mut ConfigMapBuilder, +fn log4j2_config_if_automatic( log_config: Option>, - log_config_file: &str, container_name: impl Display, log_file: &str, max_log_file_size: MemoryQuantity, -) { - if let Some(ContainerLogConfig { +) -> Option { + let config = if let Some(ContainerLogConfig { choice: Some(ContainerLogConfigChoice::Automatic(log_config)), }) = log_config.as_deref() { - cm_builder.add_data( - log_config_file, - product_logging::framework::create_log4j2_config( - &format!("{STACKABLE_LOG_DIR}/{container_name}",), - log_file, - max_log_file_size - .scale_to(BinaryMultiple::Mebi) - .floor() - .value as u32, - CONSOLE_CONVERSION_PATTERN_LOG4J2, - log_config, - ), - ); - } + Some(product_logging::framework::create_log4j2_config( + &format!("{STACKABLE_LOG_DIR}/{container_name}",), + log_file, + max_log_file_size + .scale_to(BinaryMultiple::Mebi) + .floor() + .value as u32, + CONSOLE_CONVERSION_PATTERN_LOG4J2, + log_config, + )) + } else { + None + }; + config } From 8e48f00393ad86e7e2c212312323f9d5b22e4f7f Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 15:24:23 +0200 Subject: [PATCH 15/47] use merge instead of extend with KafkaBrokerConfigOverrides --- extra/crds.yaml | 16 +- .../src/controller/validate.rs | 217 ++++++++++-------- rust/operator-binary/src/crd/mod.rs | 42 ++-- 3 files changed, 144 insertions(+), 131 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index 9a83de14..471aadbf 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -543,23 +543,23 @@ spec: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object security.properties: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object type: object envOverrides: @@ -1162,23 +1162,23 @@ spec: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object security.properties: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object type: object envOverrides: @@ -1792,23 +1792,23 @@ spec: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object security.properties: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object type: object envOverrides: @@ -2243,23 +2243,23 @@ spec: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object security.properties: additionalProperties: nullable: true type: string + default: {} description: |- Flat key-value overrides for `*.properties`, Hadoop XML, etc. This is backwards-compatible with the existing flat key-value YAML format used by `HashMap`. - nullable: true type: object type: object envOverrides: diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 669febd9..ceaaad1a 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -6,7 +6,12 @@ use std::collections::BTreeMap; use snafu::{ResultExt, Snafu}; -use stackable_operator::{cli::OperatorEnvironmentOptions, commons::product_image_selection}; +use stackable_operator::{ + cli::OperatorEnvironmentOptions, + commons::product_image_selection, + config::merge::{Merge, merge}, + v2::config_overrides::KeyValueConfigOverrides, +}; use crate::{ controller::{ @@ -169,12 +174,30 @@ pub fn validate( }) } -// DESIGN DECISION: role-group overrides are merged role-level first, then role-group -// extended on top so role-group wins — identical to the precedent product-config used. -// We read the v2 KeyValueConfigOverrides `.overrides` map and BTreeMap::extend rather -// than using its `Merge` impl, because plain extend reproduces the old behaviour exactly -// (last-writer-wins per key) and avoids depending on Merge semantics. Alternative: -// KeyValueConfigOverrides::merge — equivalent here but an unnecessary semantic dependency. +/// Merge role-group overrides over the role-level overrides (role-group wins per key) via the +/// `Merge` impl derived on the override structs. +/// +/// NOTE on semantics: `Merge` treats a role-group `null` value as "inherit the role-level value", +/// *not* "unset it". This differs from `main`'s product-config layering, which `.extend()`ed the +/// maps so a role-group `null` *removed* a role-level key. The `tests` module has a worked +/// example of the difference. +fn merge_role_group_overrides(role: &O, role_group: Option<&O>) -> O { + match role_group { + Some(role_group) => merge(role_group.clone(), role), + None => role.clone(), + } +} + +/// Flatten resolved key/value overrides into a plain map, dropping entries whose value is +/// unset (`null`). +fn flatten_overrides(overrides: KeyValueConfigOverrides) -> BTreeMap { + overrides + .overrides + .into_iter() + .filter_map(|(key, value)| value.map(|value| (key, value))) + .collect() +} + fn collect_broker_role_group_overrides( kafka: &v1alpha1::KafkaCluster, broker_role: &crate::crd::BrokerRole, @@ -184,47 +207,15 @@ fn collect_broker_role_group_overrides( BTreeMap, BTreeMap, ) { - // --- broker.properties overrides --- - let role_broker_overrides: BTreeMap> = broker_role - .config - .config_overrides - .broker_properties - .as_ref() - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let rg_broker_overrides: BTreeMap> = broker_role - .role_groups - .get(rolegroup_name) - .and_then(|rg| rg.config.config_overrides.broker_properties.as_ref()) - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let mut merged_broker = role_broker_overrides; - merged_broker.extend(rg_broker_overrides); - let config_file_overrides: BTreeMap = merged_broker - .into_iter() - .filter_map(|(k, v)| v.map(|v| (k, v))) - .collect(); - - // --- security.properties overrides --- - let role_security_overrides: BTreeMap> = broker_role - .config - .config_overrides - .security_properties - .as_ref() - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let rg_security_overrides: BTreeMap> = broker_role - .role_groups - .get(rolegroup_name) - .and_then(|rg| rg.config.config_overrides.security_properties.as_ref()) - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let mut merged_security = role_security_overrides; - merged_security.extend(rg_security_overrides); - let jvm_security_overrides: BTreeMap = merged_security - .into_iter() - .filter_map(|(k, v)| v.map(|v| (k, v))) - .collect(); + let merged_overrides = merge_role_group_overrides( + &broker_role.config.config_overrides, + broker_role + .role_groups + .get(rolegroup_name) + .map(|rg| &rg.config.config_overrides), + ); + let config_file_overrides = flatten_overrides(merged_overrides.broker_properties); + let jvm_security_overrides = flatten_overrides(merged_overrides.security_properties); // --- env overrides --- // DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides @@ -252,12 +243,6 @@ fn collect_broker_role_group_overrides( (config_file_overrides, jvm_security_overrides, env_overrides) } -// DESIGN DECISION: role-group overrides are merged role-level first, then role-group -// extended on top so role-group wins — identical to the precedent product-config used. -// We read the v2 KeyValueConfigOverrides `.overrides` map and BTreeMap::extend rather -// than using its `Merge` impl, because plain extend reproduces the old behaviour exactly -// (last-writer-wins per key) and avoids depending on Merge semantics. Alternative: -// KeyValueConfigOverrides::merge — equivalent here but an unnecessary semantic dependency. fn collect_controller_role_group_overrides( kafka: &v1alpha1::KafkaCluster, controller_role: &crate::crd::ControllerRole, @@ -267,47 +252,15 @@ fn collect_controller_role_group_overrides( BTreeMap, BTreeMap, ) { - // --- controller.properties overrides --- - let role_controller_overrides: BTreeMap> = controller_role - .config - .config_overrides - .controller_properties - .as_ref() - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let rg_controller_overrides: BTreeMap> = controller_role - .role_groups - .get(rolegroup_name) - .and_then(|rg| rg.config.config_overrides.controller_properties.as_ref()) - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let mut merged_controller = role_controller_overrides; - merged_controller.extend(rg_controller_overrides); - let config_file_overrides: BTreeMap = merged_controller - .into_iter() - .filter_map(|(k, v)| v.map(|v| (k, v))) - .collect(); - - // --- security.properties overrides --- - let role_security_overrides: BTreeMap> = controller_role - .config - .config_overrides - .security_properties - .as_ref() - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let rg_security_overrides: BTreeMap> = controller_role - .role_groups - .get(rolegroup_name) - .and_then(|rg| rg.config.config_overrides.security_properties.as_ref()) - .map(|o| o.overrides.clone()) - .unwrap_or_default(); - let mut merged_security = role_security_overrides; - merged_security.extend(rg_security_overrides); - let jvm_security_overrides: BTreeMap = merged_security - .into_iter() - .filter_map(|(k, v)| v.map(|v| (k, v))) - .collect(); + let merged_overrides = merge_role_group_overrides( + &controller_role.config.config_overrides, + controller_role + .role_groups + .get(rolegroup_name) + .map(|rg| &rg.config.config_overrides), + ); + let config_file_overrides = flatten_overrides(merged_overrides.controller_properties); + let jvm_security_overrides = flatten_overrides(merged_overrides.security_properties); // --- env overrides --- // DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides @@ -335,3 +288,77 @@ fn collect_controller_role_group_overrides( (config_file_overrides, jvm_security_overrides, env_overrides) } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use stackable_operator::v2::config_overrides::KeyValueConfigOverrides; + + use super::{flatten_overrides, merge_role_group_overrides}; + + /// Build a `KeyValueConfigOverrides` from `(key, value)` pairs, where a `None` value + /// represents an explicit `null` (unset) in the CRD. + fn overrides(pairs: &[(&str, Option<&str>)]) -> KeyValueConfigOverrides { + KeyValueConfigOverrides { + overrides: pairs + .iter() + .map(|(key, value)| (key.to_string(), value.map(str::to_string))) + .collect(), + } + } + + /// Run the full role/role-group resolution (merge then flatten) for a single config file. + fn resolve( + role: KeyValueConfigOverrides, + role_group: Option, + ) -> BTreeMap { + flatten_overrides(merge_role_group_overrides(&role, role_group.as_ref())) + } + + #[test] + fn role_group_value_wins_over_role() { + let role = overrides(&[("a", Some("role")), ("b", Some("role-only"))]); + let role_group = overrides(&[("a", Some("rg"))]); + + let merged = resolve(role, Some(role_group)); + + assert_eq!( + merged, + BTreeMap::from([ + ("a".to_string(), "rg".to_string()), // role-group wins for shared keys + ("b".to_string(), "role-only".to_string()), // role-only keys are kept + ]) + ); + } + + /// Illustrates the key consequence of using `Merge` (rather than `.extend()`, as `main`'s + /// product-config did): a role-group `null` is treated as "inherit", so the role-level value + /// is *kept* — it does NOT unset the key. Under the old `.extend()` behaviour this same input + /// would have removed `a` entirely. + #[test] + fn role_group_null_inherits_role_value_rather_than_unsetting_it() { + let role = overrides(&[("a", Some("role"))]); + let role_group = overrides(&[("a", None)]); // explicit `null` at the more specific level + + let merged = resolve(role, Some(role_group)); + + assert_eq!( + merged, + BTreeMap::from([("a".to_string(), "role".to_string())]), + "a role-group `null` should inherit the role-level value under Merge semantics" + ); + } + + #[test] + fn without_a_role_group_role_values_are_kept_and_nulls_dropped() { + let role = overrides(&[("a", Some("role")), ("b", None)]); + + let merged = resolve(role, None); + + assert_eq!( + merged, + BTreeMap::from([("a".to_string(), "role".to_string())]) + ); + } +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index fa5b7498..132c5f8b 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -16,6 +16,7 @@ use stackable_operator::{ cluster_operation::ClusterOperation, networking::DomainName, product_image_selection::ProductImage, }, + config::merge::Merge, deep_merger::ObjectOverrides, kube::{CustomResource, runtime::reflector::ObjectRef}, role_utils::{GenericRoleConfig, JavaCommonConfig, Role, RoleGroupRef}, @@ -240,42 +241,27 @@ pub mod versioned { pub broker_id_pod_config_map_name: Option, } - // Uses the v2 KeyValueConfigOverrides (Merge-capable, `nullable` values) to match - // trino/hdfs. Resolution into flat maps happens in controller/validate.rs. - #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] + // Uses the v2 KeyValueConfigOverrides (`nullable` values) to match trino/hdfs. + // Derives `Merge` so role/role-group overrides combine via the shared merge logic; + // resolution into flat maps happens in controller/validate.rs. + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Merge, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaBrokerConfigOverrides { - #[serde( - default, - rename = "broker.properties", - skip_serializing_if = "Option::is_none" - )] - pub broker_properties: Option, + #[serde(default, rename = "broker.properties")] + pub broker_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "security.properties", - skip_serializing_if = "Option::is_none" - )] - pub security_properties: Option, + #[serde(default, rename = "security.properties")] + pub security_properties: KeyValueConfigOverrides, } - #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Merge, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaControllerConfigOverrides { - #[serde( - default, - rename = "controller.properties", - skip_serializing_if = "Option::is_none" - )] - pub controller_properties: Option, + #[serde(default, rename = "controller.properties")] + pub controller_properties: KeyValueConfigOverrides, - #[serde( - default, - rename = "security.properties", - skip_serializing_if = "Option::is_none" - )] - pub security_properties: Option, + #[serde(default, rename = "security.properties")] + pub security_properties: KeyValueConfigOverrides, } } From d792d747de5740ddb22f34bcb291f52c9bd65484 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 15:26:42 +0200 Subject: [PATCH 16/47] updated changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d34355e..75204042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,10 @@ All notable changes to this project will be documented in this file. - Bump `stackable-operator` to 0.111.1 and snafu to 0.9 ([#960], [#961]). - Internal operator refactoring: introduce dereference() and validate() steps in the reconciler ([#968]). - test: Bump vector-aggregator to 0.55.0, replace /graphql call with gRPC call ([#971]). -- Removed the product-config based configuration validation. Config and environment overrides are - now merged directly from the CRD into the validated cluster, the Java-properties writer is - vendored locally, and the `product-config` crate dependency is dropped. The `--product-config` - CLI flag is now a no-op ([#976]). +- BREAKING: Removed product-config machinery which is a breaking change in terms of configuration. + Users relying on the product-config `properties.yaml` file have to set these properties via the CRD. + Config and environment overrides are now merged directly from the CRD into the validated cluster. + The `--product-config` CLI flag is now a no-op ([#976]). [#953]: https://github.com/stackabletech/kafka-operator/pull/953 [#960]: https://github.com/stackabletech/kafka-operator/pull/960 From 94102ab34e568a9abf9020d775e67a96ef0c965a Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 15:47:43 +0200 Subject: [PATCH 17/47] extend test comparison --- .../src/controller/validate.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index ceaaad1a..7afe7056 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -341,12 +341,28 @@ mod tests { let role = overrides(&[("a", Some("role"))]); let role_group = overrides(&[("a", None)]); // explicit `null` at the more specific level - let merged = resolve(role, Some(role_group)); + // For contrast: `main`'s product-config used a blind `.extend()`, so the role-group + // `null` overwrote the role value and the key was then dropped — i.e. unset entirely. + let old_extend_behaviour: BTreeMap = { + let mut combined = role.overrides.clone(); + combined.extend(role_group.overrides.clone()); + combined + .into_iter() + .filter_map(|(key, value)| value.map(|value| (key, value))) + .collect() + }; + assert!( + old_extend_behaviour.is_empty(), + "under the old `.extend()` behaviour the role-group `null` unsets `a`" + ); + // What we do now (Merge): the role-group `null` means "inherit", so the role-level + // value is kept rather than unset. + let merged = resolve(role, Some(role_group)); assert_eq!( merged, BTreeMap::from([("a".to_string(), "role".to_string())]), - "a role-group `null` should inherit the role-level value under Merge semantics" + "under Merge semantics the role-group `null` inherits the role-level value" ); } From e33b155d9154be629c862698194837917d139e68 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 16:54:59 +0200 Subject: [PATCH 18/47] unnecessary let binding --- rust/operator-binary/src/product_logging.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/product_logging.rs b/rust/operator-binary/src/product_logging.rs index 40780435..eb0dc55a 100644 --- a/rust/operator-binary/src/product_logging.rs +++ b/rust/operator-binary/src/product_logging.rs @@ -137,7 +137,7 @@ fn log4j2_config_if_automatic( log_file: &str, max_log_file_size: MemoryQuantity, ) -> Option { - let config = if let Some(ContainerLogConfig { + if let Some(ContainerLogConfig { choice: Some(ContainerLogConfigChoice::Automatic(log_config)), }) = log_config.as_deref() { @@ -153,6 +153,5 @@ fn log4j2_config_if_automatic( )) } else { None - }; - config + } } From 3f09a7b9b1a521dc2154aab998cdfb26bd4d0519 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Tue, 9 Jun 2026 17:38:05 +0200 Subject: [PATCH 19/47] regenerate nix --- Cargo.nix | 18 +++++++++--------- crate-hashes.json | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index e70182ee..0de574b7 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4853,7 +4853,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; libName = "k8s_version"; authors = [ @@ -9536,7 +9536,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; libName = "stackable_certs"; authors = [ @@ -9727,7 +9727,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; libName = "stackable_operator"; authors = [ @@ -9921,7 +9921,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -9956,7 +9956,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; libName = "stackable_shared"; authors = [ @@ -10037,7 +10037,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; libName = "stackable_telemetry"; authors = [ @@ -10147,7 +10147,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; libName = "stackable_versioned"; authors = [ @@ -10197,7 +10197,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10265,7 +10265,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk"; + sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; }; libName = "stackable_webhook"; authors = [ diff --git a/crate-hashes.json b/crate-hashes.json index 5b0037c5..deac3bf4 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "0lj969rjbxairjglrnaq0xhabvdrq5nd6wl1i0y9pr50nhh7zvgk", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file From 3b9390e0eb1f24c6b8be7d6aed77e3fe23dffad2 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 10 Jun 2026 16:31:02 +0200 Subject: [PATCH 20/47] bump op-rs branch and fix clippy warning --- Cargo.lock | 18 +++--- Cargo.nix | 36 +++++------ crate-hashes.json | 18 +++--- .../src/controller/build/config_map.rs | 24 ++----- .../src/controller/validate.rs | 64 +++---------------- rust/operator-binary/src/crd/mod.rs | 2 +- rust/operator-binary/src/product_logging.rs | 5 +- 7 files changed, 55 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1390fe52..92a70017 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1518,7 +1518,7 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "darling", "regex", @@ -2897,7 +2897,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackable-certs" version = "0.4.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "const-oid", "ecdsa", @@ -2942,7 +2942,7 @@ dependencies = [ [[package]] name = "stackable-operator" version = "0.111.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "base64", "clap", @@ -2986,7 +2986,7 @@ dependencies = [ [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "darling", "proc-macro2", @@ -2997,7 +2997,7 @@ dependencies = [ [[package]] name = "stackable-shared" version = "0.1.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "jiff", "k8s-openapi", @@ -3014,7 +3014,7 @@ dependencies = [ [[package]] name = "stackable-telemetry" version = "0.6.4" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "axum", "clap", @@ -3038,7 +3038,7 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "kube", "schemars", @@ -3052,7 +3052,7 @@ dependencies = [ [[package]] name = "stackable-versioned-macros" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "convert_case", "convert_case_extras", @@ -3070,7 +3070,7 @@ dependencies = [ [[package]] name = "stackable-webhook" version = "0.9.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#46cd3f93a788d44d177a8794fde91fbefa3156d7" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" dependencies = [ "arc-swap", "async-trait", diff --git a/Cargo.nix b/Cargo.nix index 0de574b7..43e426e5 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4852,8 +4852,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "k8s_version"; authors = [ @@ -9535,8 +9535,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_certs"; authors = [ @@ -9726,8 +9726,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_operator"; authors = [ @@ -9920,8 +9920,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -9955,8 +9955,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_shared"; authors = [ @@ -10036,8 +10036,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_telemetry"; authors = [ @@ -10146,8 +10146,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_versioned"; authors = [ @@ -10196,8 +10196,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10264,8 +10264,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "46cd3f93a788d44d177a8794fde91fbefa3156d7"; - sha256 = "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47"; + rev = "451088f77acee6c3d296754698260256c250ecb2"; + sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_webhook"; authors = [ diff --git a/crate-hashes.json b/crate-hashes.json index deac3bf4..c9a6e6a9 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "05zvdnnmjvpma9yds4kp8rwkna2d3kkws3yry2080jdy753npz47", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index ffd7ef03..5941ac62 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use indoc::formatdoc; use snafu::{ResultExt, Snafu}; use stackable_operator::{ @@ -117,17 +115,7 @@ pub fn build_rolegroup_config_map( } }; - let kafka_config = kafka_config - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect::>(); - - let jvm_sec_props: BTreeMap> = validated_rg - .jvm_security_overrides - .clone() - .into_iter() - .map(|(k, v)| (k, Some(v))) - .collect(); + let jvm_sec_props = &validated_rg.jvm_security_overrides; let mut cm_builder = ConfigMapBuilder::new(); cm_builder @@ -149,11 +137,11 @@ pub fn build_rolegroup_config_map( ) .add_data( kafka_config_file_name, - to_java_properties_string(kafka_config.iter().map(|(k, v)| (k, v))).with_context( - |_| SerializeConfigSnafu { + to_java_properties_string(kafka_config.iter()).with_context(|_| { + SerializeConfigSnafu { rolegroup: rolegroup.clone(), - }, - )?, + } + })?, ) .add_data( JVM_SECURITY_PROPERTIES_FILE, @@ -169,7 +157,7 @@ pub fn build_rolegroup_config_map( kafka_security .client_properties() .iter() - .map(|(k, v)| (k, v)), + .filter_map(|(k, v)| v.as_ref().map(|v| (k, v))), ) .with_context(|_| JvmSecurityPropertiesSnafu { rolegroup: rolegroup.role_group.clone(), diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 7afe7056..a6e8cee3 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -176,11 +176,6 @@ pub fn validate( /// Merge role-group overrides over the role-level overrides (role-group wins per key) via the /// `Merge` impl derived on the override structs. -/// -/// NOTE on semantics: `Merge` treats a role-group `null` value as "inherit the role-level value", -/// *not* "unset it". This differs from `main`'s product-config layering, which `.extend()`ed the -/// maps so a role-group `null` *removed* a role-level key. The `tests` module has a worked -/// example of the difference. fn merge_role_group_overrides(role: &O, role_group: Option<&O>) -> O { match role_group { Some(role_group) => merge(role_group.clone(), role), @@ -188,14 +183,10 @@ fn merge_role_group_overrides(role: &O, role_group: Option<&O> } } -/// Flatten resolved key/value overrides into a plain map, dropping entries whose value is -/// unset (`null`). +/// Flatten resolved key/value overrides into a plain map. operator-rs #1219 made the override +/// values plain `String`, so there is no longer any `null`/unset entry to drop. fn flatten_overrides(overrides: KeyValueConfigOverrides) -> BTreeMap { - overrides - .overrides - .into_iter() - .filter_map(|(key, value)| value.map(|value| (key, value))) - .collect() + overrides.overrides } fn collect_broker_role_group_overrides( @@ -297,13 +288,12 @@ mod tests { use super::{flatten_overrides, merge_role_group_overrides}; - /// Build a `KeyValueConfigOverrides` from `(key, value)` pairs, where a `None` value - /// represents an explicit `null` (unset) in the CRD. - fn overrides(pairs: &[(&str, Option<&str>)]) -> KeyValueConfigOverrides { + /// Build a `KeyValueConfigOverrides` from `(key, value)` pairs. + fn overrides(pairs: &[(&str, &str)]) -> KeyValueConfigOverrides { KeyValueConfigOverrides { overrides: pairs .iter() - .map(|(key, value)| (key.to_string(), value.map(str::to_string))) + .map(|(key, value)| (key.to_string(), value.to_string())) .collect(), } } @@ -318,8 +308,8 @@ mod tests { #[test] fn role_group_value_wins_over_role() { - let role = overrides(&[("a", Some("role")), ("b", Some("role-only"))]); - let role_group = overrides(&[("a", Some("rg"))]); + let role = overrides(&[("a", "role"), ("b", "role-only")]); + let role_group = overrides(&[("a", "rg")]); let merged = resolve(role, Some(role_group)); @@ -332,43 +322,9 @@ mod tests { ); } - /// Illustrates the key consequence of using `Merge` (rather than `.extend()`, as `main`'s - /// product-config did): a role-group `null` is treated as "inherit", so the role-level value - /// is *kept* — it does NOT unset the key. Under the old `.extend()` behaviour this same input - /// would have removed `a` entirely. #[test] - fn role_group_null_inherits_role_value_rather_than_unsetting_it() { - let role = overrides(&[("a", Some("role"))]); - let role_group = overrides(&[("a", None)]); // explicit `null` at the more specific level - - // For contrast: `main`'s product-config used a blind `.extend()`, so the role-group - // `null` overwrote the role value and the key was then dropped — i.e. unset entirely. - let old_extend_behaviour: BTreeMap = { - let mut combined = role.overrides.clone(); - combined.extend(role_group.overrides.clone()); - combined - .into_iter() - .filter_map(|(key, value)| value.map(|value| (key, value))) - .collect() - }; - assert!( - old_extend_behaviour.is_empty(), - "under the old `.extend()` behaviour the role-group `null` unsets `a`" - ); - - // What we do now (Merge): the role-group `null` means "inherit", so the role-level - // value is kept rather than unset. - let merged = resolve(role, Some(role_group)); - assert_eq!( - merged, - BTreeMap::from([("a".to_string(), "role".to_string())]), - "under Merge semantics the role-group `null` inherits the role-level value" - ); - } - - #[test] - fn without_a_role_group_role_values_are_kept_and_nulls_dropped() { - let role = overrides(&[("a", Some("role")), ("b", None)]); + fn without_a_role_group_role_values_are_kept() { + let role = overrides(&[("a", "role")]); let merged = resolve(role, None); diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 132c5f8b..e69c7a4c 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -241,7 +241,7 @@ pub mod versioned { pub broker_id_pod_config_map_name: Option, } - // Uses the v2 KeyValueConfigOverrides (`nullable` values) to match trino/hdfs. + // Uses the v2 KeyValueConfigOverrides (plain string values) to match trino/hdfs. // Derives `Merge` so role/role-group overrides combine via the shared merge logic; // resolution into flat maps happens in controller/validate.rs. #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Merge, PartialEq, Serialize)] diff --git a/rust/operator-binary/src/product_logging.rs b/rust/operator-binary/src/product_logging.rs index eb0dc55a..a16d0148 100644 --- a/rust/operator-binary/src/product_logging.rs +++ b/rust/operator-binary/src/product_logging.rs @@ -111,7 +111,7 @@ fn log4j_config_if_automatic( log_file: &str, max_log_file_size: MemoryQuantity, ) -> Option { - let config = if let Some(ContainerLogConfig { + if let Some(ContainerLogConfig { choice: Some(ContainerLogConfigChoice::Automatic(log_config)), }) = log_config.as_deref() { @@ -127,8 +127,7 @@ fn log4j_config_if_automatic( )) } else { None - }; - config + } } fn log4j2_config_if_automatic( From 38f278ca1e3b787b8f603fffbe254ba8537c2f42 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 10 Jun 2026 16:38:31 +0200 Subject: [PATCH 21/47] regenerate charts --- extra/crds.yaml | 56 +++++++------------------------------------------ 1 file changed, 8 insertions(+), 48 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index 471aadbf..ea71c1c4 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -541,25 +541,15 @@ spec: properties: broker.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: @@ -1160,25 +1150,15 @@ spec: properties: broker.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: @@ -1790,25 +1770,15 @@ spec: properties: controller.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: @@ -2241,25 +2211,15 @@ spec: properties: controller.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object security.properties: additionalProperties: - nullable: true type: string default: {} - description: |- - Flat key-value overrides for `*.properties`, Hadoop XML, etc. - - This is backwards-compatible with the existing flat key-value YAML format - used by `HashMap`. + description: Flat key-value overrides for `*.properties`, Hadoop XML, etc. type: object type: object envOverrides: From 662e9a14e0d1ba993bb6624cf151fbdc9ce73770 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 10 Jun 2026 17:23:05 +0200 Subject: [PATCH 22/47] thread through name, namespace, uid via ObjectMeta --- rust/operator-binary/src/controller.rs | 102 ++++++++++++++++-- .../src/controller/build/config_map.rs | 6 +- .../src/{ => controller/build}/discovery.rs | 24 +++-- .../src/controller/build/mod.rs | 1 + .../src/controller/validate.rs | 48 +++++++-- rust/operator-binary/src/main.rs | 1 - rust/operator-binary/src/resource/listener.rs | 6 +- rust/operator-binary/src/resource/service.rs | 22 ++-- .../src/resource/statefulset.rs | 18 ++-- rust/operator-binary/src/utils.rs | 13 ++- 10 files changed, 181 insertions(+), 60 deletions(-) rename rust/operator-binary/src/{ => controller/build}/discovery.rs (83%) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 64e9a8b9..ec00b969 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,6 +1,6 @@ //! Ensures that `Pod`s are configured and running for each [`v1alpha1::KafkaCluster`]. -use std::{collections::BTreeMap, sync::Arc}; +use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; use const_format::concatcp; use snafu::{ResultExt, Snafu}; @@ -11,7 +11,7 @@ use stackable_operator::{ crd::listener, kube::{ Resource, - api::DynamicObject, + api::{DynamicObject, ObjectMeta}, core::{DeserializeGuard, error_boundary}, runtime::{controller::Action, reflector::ObjectRef}, }, @@ -22,6 +22,10 @@ use stackable_operator::{ compute_conditions, operations::ClusterOperationsConditionBuilder, statefulset::StatefulSetConditionBuilder, }, + v2::types::{ + kubernetes::{NamespaceName, Uid}, + operator::ClusterName, + }, }; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -38,7 +42,6 @@ use crate::{ security::KafkaTlsSecurity, v1alpha1, }, - discovery::{self, build_discovery_configmap}, operations::pdb::add_pdbs, resource::{ listener::build_broker_rolegroup_bootstrap_listener, @@ -94,7 +97,7 @@ pub enum Error { }, #[snafu(display("failed to build discovery ConfigMap"))] - BuildDiscoveryConfig { source: discovery::Error }, + BuildDiscoveryConfig { source: build::discovery::Error }, #[snafu(display("failed to apply discovery ConfigMap"))] ApplyDiscoveryConfig { @@ -208,7 +211,17 @@ impl ReconcilerError for Error { /// The validated cluster. Carries everything the build steps need, resolved once /// here so downstream code never re-derives it or touches the raw spec. +/// +/// The cluster identity (`name`, `namespace`, `uid`) is captured here so that owner +/// references for child objects can be built straight from this struct (via its +/// [`Resource`] impl) without threading the raw [`v1alpha1::KafkaCluster`] around. +/// This mirrors the hive-/opensearch-operator's `ValidatedCluster`. pub struct ValidatedKafkaCluster { + /// `ObjectMeta` carrying `name`, `namespace` and `uid`, so this struct can act as the + /// owner [`Resource`] for child objects. + metadata: ObjectMeta, + pub name: ClusterName, + pub namespace: NamespaceName, pub image: ResolvedProductImage, pub kafka_security: KafkaTlsSecurity, // DESIGN DECISION: the dereferenced authorization config is folded into the @@ -222,6 +235,69 @@ pub struct ValidatedKafkaCluster { pub metadata_manager: MetadataManager, } +impl ValidatedKafkaCluster { + #[allow(clippy::too_many_arguments)] + pub fn new( + name: ClusterName, + namespace: NamespaceName, + uid: Uid, + image: ResolvedProductImage, + kafka_security: KafkaTlsSecurity, + authorization_config: Option, + role_groups: BTreeMap>, + pod_descriptors: Vec, + metadata_manager: MetadataManager, + ) -> Self { + Self { + metadata: ObjectMeta { + name: Some(name.to_string()), + namespace: Some(namespace.to_string()), + uid: Some(uid.to_string()), + ..ObjectMeta::default() + }, + name, + namespace, + image, + kafka_security, + authorization_config, + role_groups, + pod_descriptors, + metadata_manager, + } + } +} + +/// Lets [`ValidatedKafkaCluster`] act as the owner [`Resource`] for child objects, so owner +/// references are built from it (via the captured `metadata`) rather than the raw CR. +impl Resource for ValidatedKafkaCluster { + type DynamicType = ::DynamicType; + type Scope = ::Scope; + + fn kind(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::kind(dt) + } + + fn group(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::group(dt) + } + + fn version(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::version(dt) + } + + fn plural(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::plural(dt) + } + + fn meta(&self) -> &ObjectMeta { + &self.metadata + } + + fn meta_mut(&mut self) -> &mut ObjectMeta { + &mut self.metadata + } +} + pub struct ValidatedRoleGroupConfig { pub merged_config: AnyConfig, // DESIGN DECISION: overrides are resolved into flat maps HERE rather than stored @@ -265,7 +341,7 @@ pub async fn reconcile_kafka( APP_NAME, OPERATOR_NAME, KAFKA_CONTROLLER_NAME, - &kafka.object_ref(&()), + &validated_cluster.object_ref(&()), ClusterResourceApplyStrategy::from(&kafka.spec.cluster_operation), &kafka.spec.object_overrides, ) @@ -306,16 +382,19 @@ pub async fn reconcile_kafka( let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name); let rg_headless_service = build_rolegroup_headless_service( - kafka, + &validated_cluster, &validated_cluster.image, &rolegroup_ref, &validated_cluster.kafka_security, ) .context(BuildServiceSnafu)?; - let rg_metrics_service = - build_rolegroup_metrics_service(kafka, &validated_cluster.image, &rolegroup_ref) - .context(BuildServiceSnafu)?; + let rg_metrics_service = build_rolegroup_metrics_service( + &validated_cluster, + &validated_cluster.image, + &rolegroup_ref, + ) + .context(BuildServiceSnafu)?; let kafka_listeners = get_kafka_listener_config( kafka, @@ -416,8 +495,9 @@ pub async fn reconcile_kafka( } } - let discovery_cm = build_discovery_configmap(kafka, validated_cluster, &bootstrap_listeners) - .context(BuildDiscoveryConfigSnafu)?; + let discovery_cm = + build::discovery::build_discovery_configmap(&validated_cluster, &bootstrap_listeners) + .context(BuildDiscoveryConfigSnafu)?; cluster_resources .add(client, discovery_cm) diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 5941ac62..0af0fcf8 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -121,12 +121,12 @@ pub fn build_rolegroup_config_map( cm_builder .metadata( ObjectMetaBuilder::new() - .name_and_namespace(kafka) + .name_and_namespace(validated_cluster) .name(rolegroup.object_name()) - .ownerreference_from_resource(kafka, None, Some(true)) + .ownerreference_from_resource(validated_cluster, None, Some(true)) .context(ObjectMissingMetadataForOwnerRefSnafu)? .with_recommended_labels(&build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup.role, diff --git a/rust/operator-binary/src/discovery.rs b/rust/operator-binary/src/controller/build/discovery.rs similarity index 83% rename from rust/operator-binary/src/discovery.rs rename to rust/operator-binary/src/controller/build/discovery.rs index cf19f32e..60e30940 100644 --- a/rust/operator-binary/src/discovery.rs +++ b/rust/operator-binary/src/controller/build/discovery.rs @@ -5,7 +5,7 @@ use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, crd::listener, k8s_openapi::api::core::v1::ConfigMap, - kube::{ResourceExt, runtime::reflector::ObjectRef}, + kube::runtime::reflector::ObjectRef, }; use crate::{ @@ -22,9 +22,6 @@ pub enum Error { kafka: ObjectRef, }, - #[snafu(display("object has no name associated"))] - NoName, - #[snafu(display("could not find service port with name {}", port_name))] NoServicePort { port_name: String }, @@ -45,8 +42,7 @@ pub enum Error { /// Build a discovery [`ConfigMap`] containing information about how to connect to a certain /// [`v1alpha1::KafkaCluster`]. pub fn build_discovery_configmap( - owner: &v1alpha1::KafkaCluster, - validated_cluster: ValidatedKafkaCluster, + validated_cluster: &ValidatedKafkaCluster, listeners: &[listener::v1alpha1::Listener], ) -> Result { let kafka_security = &validated_cluster.kafka_security; @@ -68,14 +64,14 @@ pub fn build_discovery_configmap( ConfigMapBuilder::new() .metadata( ObjectMetaBuilder::new() - .name_and_namespace(owner) - .name(owner.name_unchecked()) - .ownerreference_from_resource(owner, None, Some(true)) + .name_and_namespace(validated_cluster) + .name(validated_cluster.name.to_string()) + .ownerreference_from_resource(validated_cluster, None, Some(true)) .with_context(|_| ObjectMissingMetadataForOwnerRefSnafu { - kafka: ObjectRef::from_obj(owner), + kafka: cluster_object_ref(validated_cluster), })? .with_recommended_labels(&build_recommended_labels( - owner, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.product_version, &KafkaRole::Broker.to_string(), @@ -89,6 +85,12 @@ pub fn build_discovery_configmap( .context(BuildConfigMapSnafu) } +/// An [`ObjectRef`] to the owning cluster, built from the validated identity — used only for +/// error context. +fn cluster_object_ref(cluster: &ValidatedKafkaCluster) -> ObjectRef { + ObjectRef::new(cluster.name.as_ref()).within(cluster.namespace.as_ref()) +} + fn listener_hosts( listeners: &[listener::v1alpha1::Listener], port_name: &str, diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index b8c4c422..0cbab809 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -1,4 +1,5 @@ //! Builders that assemble Kubernetes resources for kafka rolegroups. pub mod config_map; +pub mod discovery; pub mod properties; diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index a6e8cee3..85c2e844 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -3,14 +3,21 @@ //! Synchronously validates inputs that don't require a Kubernetes client. Produces //! [`ValidatedKafkaCluster`], consumed by the rest of `reconcile_kafka`. -use std::collections::BTreeMap; +use std::{collections::BTreeMap, str::FromStr}; -use snafu::{ResultExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, commons::product_image_selection, config::merge::{Merge, merge}, - v2::config_overrides::KeyValueConfigOverrides, + kube::ResourceExt, + v2::{ + config_overrides::KeyValueConfigOverrides, + types::{ + kubernetes::{NamespaceName, Uid}, + operator::ClusterName, + }, + }, }; use crate::{ @@ -50,6 +57,27 @@ pub enum Error { #[snafu(display("invalid metadata manager"))] InvalidMetadataManager { source: crate::crd::Error }, + + #[snafu(display("invalid cluster name"))] + InvalidClusterName { + source: stackable_operator::v2::macros::attributed_string_type::Error, + }, + + #[snafu(display("object defines no namespace"))] + ObjectHasNoNamespace, + + #[snafu(display("invalid cluster namespace"))] + InvalidNamespace { + source: stackable_operator::v2::macros::attributed_string_type::Error, + }, + + #[snafu(display("object has no uid"))] + ObjectHasNoUid, + + #[snafu(display("invalid cluster uid"))] + InvalidUid { + source: stackable_operator::v2::macros::attributed_string_type::Error, + }, } type Result = std::result::Result; @@ -164,14 +192,22 @@ pub fn validate( .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; - Ok(ValidatedKafkaCluster { + let name = ClusterName::from_str(&kafka.name_any()).context(InvalidClusterNameSnafu)?; + let namespace = NamespaceName::from_str(&kafka.namespace().context(ObjectHasNoNamespaceSnafu)?) + .context(InvalidNamespaceSnafu)?; + let uid = Uid::from_str(&kafka.uid().context(ObjectHasNoUidSnafu)?).context(InvalidUidSnafu)?; + + Ok(ValidatedKafkaCluster::new( + name, + namespace, + uid, image, kafka_security, - authorization_config: dereferenced_objects.authorization_config, + dereferenced_objects.authorization_config, role_groups, pod_descriptors, metadata_manager, - }) + )) } /// Merge role-group overrides over the role-level overrides (role-group wins per key) via the diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 25362c54..a967294a 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -43,7 +43,6 @@ use crate::{ mod config; mod controller; mod crd; -mod discovery; mod kerberos; mod operations; mod product_logging; diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/resource/listener.rs index 35f360f7..3161ed0e 100644 --- a/rust/operator-binary/src/resource/listener.rs +++ b/rust/operator-binary/src/resource/listener.rs @@ -36,12 +36,12 @@ pub fn build_broker_rolegroup_bootstrap_listener( Ok(listener::v1alpha1::Listener { metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) + .name_and_namespace(validated_cluster) .name(kafka.bootstrap_service_name(rolegroup)) - .ownerreference_from_resource(kafka, None, Some(true)) + .ownerreference_from_resource(validated_cluster, None, Some(true)) .context(ObjectMissingMetadataForOwnerRefSnafu)? .with_recommended_labels(&build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup.role, diff --git a/rust/operator-binary/src/resource/service.rs b/rust/operator-binary/src/resource/service.rs index f430fc8e..bc2fdbfe 100644 --- a/rust/operator-binary/src/resource/service.rs +++ b/rust/operator-binary/src/resource/service.rs @@ -8,7 +8,7 @@ use stackable_operator::{ }; use crate::{ - controller::KAFKA_CONTROLLER_NAME, + controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster}, crd::{APP_NAME, METRICS_PORT, METRICS_PORT_NAME, security::KafkaTlsSecurity, v1alpha1}, utils::build_recommended_labels, }; @@ -35,19 +35,19 @@ pub enum Error { /// /// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing. pub fn build_rolegroup_headless_service( - kafka: &v1alpha1::KafkaCluster, + validated_cluster: &ValidatedKafkaCluster, resolved_product_image: &ResolvedProductImage, rolegroup: &RoleGroupRef, kafka_security: &KafkaTlsSecurity, ) -> Result { Ok(Service { metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) + .name_and_namespace(validated_cluster) .name(rolegroup.rolegroup_headless_service_name()) - .ownerreference_from_resource(kafka, None, Some(true)) + .ownerreference_from_resource(validated_cluster, None, Some(true)) .context(ObjectMissingMetadataForOwnerRefSnafu)? .with_recommended_labels(&build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup.role, @@ -60,7 +60,7 @@ pub fn build_rolegroup_headless_service( ports: Some(headless_ports(kafka_security)), selector: Some( Labels::role_group_selector( - kafka, + validated_cluster, APP_NAME, &rolegroup.role, &rolegroup.role_group, @@ -77,18 +77,18 @@ pub fn build_rolegroup_headless_service( /// The rolegroup metrics [`Service`] is a service that exposes metrics and a prometheus scraping label pub fn build_rolegroup_metrics_service( - kafka: &v1alpha1::KafkaCluster, + validated_cluster: &ValidatedKafkaCluster, resolved_product_image: &ResolvedProductImage, rolegroup: &RoleGroupRef, ) -> Result { let metrics_service = Service { metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) + .name_and_namespace(validated_cluster) .name(rolegroup.rolegroup_metrics_service_name()) - .ownerreference_from_resource(kafka, None, Some(true)) + .ownerreference_from_resource(validated_cluster, None, Some(true)) .context(ObjectMissingMetadataForOwnerRefSnafu)? .with_recommended_labels(&build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup.role, @@ -105,7 +105,7 @@ pub fn build_rolegroup_metrics_service( ports: Some(metrics_ports()), selector: Some( Labels::role_group_selector( - kafka, + validated_cluster, APP_NAME, &rolegroup.role, &rolegroup.role_group, diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 840382e9..3212c650 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -173,7 +173,7 @@ pub fn build_broker_rolegroup_statefulset( let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.merged_config; let recommended_object_labels = build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup_ref.role, @@ -183,7 +183,7 @@ pub fn build_broker_rolegroup_statefulset( Labels::recommended(&recommended_object_labels).context(LabelBuildSnafu)?; // Used for PVC templates that cannot be modified once they are deployed let unversioned_recommended_labels = Labels::recommended(&build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, // A version value is required, and we do want to use the "recommended" format for the other desired labels "none", @@ -526,12 +526,12 @@ pub fn build_broker_rolegroup_statefulset( Ok(StatefulSet { metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) + .name_and_namespace(validated_cluster) .name(rolegroup_ref.object_name()) - .ownerreference_from_resource(kafka, None, Some(true)) + .ownerreference_from_resource(validated_cluster, None, Some(true)) .context(ObjectMissingMetadataForOwnerRefSnafu)? .with_recommended_labels(&build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup_ref.role, @@ -582,7 +582,7 @@ pub fn build_controller_rolegroup_statefulset( let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.merged_config; let recommended_object_labels = build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup_ref.role, @@ -852,12 +852,12 @@ pub fn build_controller_rolegroup_statefulset( Ok(StatefulSet { metadata: ObjectMetaBuilder::new() - .name_and_namespace(kafka) + .name_and_namespace(validated_cluster) .name(rolegroup_ref.object_name()) - .ownerreference_from_resource(kafka, None, Some(true)) + .ownerreference_from_resource(validated_cluster, None, Some(true)) .context(ObjectMissingMetadataForOwnerRefSnafu)? .with_recommended_labels(&build_recommended_labels( - kafka, + validated_cluster, KAFKA_CONTROLLER_NAME, &resolved_product_image.app_version_label_value, &rolegroup_ref.role, diff --git a/rust/operator-binary/src/utils.rs b/rust/operator-binary/src/utils.rs index 7abbafff..1db2241a 100644 --- a/rust/operator-binary/src/utils.rs +++ b/rust/operator-binary/src/utils.rs @@ -1,15 +1,18 @@ use stackable_operator::kvp::ObjectLabels; -use crate::crd::{APP_NAME, OPERATOR_NAME, v1alpha1}; +use crate::crd::{APP_NAME, OPERATOR_NAME}; -/// Build recommended values for labels -pub fn build_recommended_labels<'a>( - owner: &'a v1alpha1::KafkaCluster, +/// Build recommended values for labels. +/// +/// Generic over the owner `T` so the owner can be either the raw `KafkaCluster` or the +/// `ValidatedKafkaCluster` (which also implements `Resource`). +pub fn build_recommended_labels<'a, T>( + owner: &'a T, controller_name: &'a str, app_version: &'a str, role: &'a str, role_group: &'a str, -) -> ObjectLabels<'a, v1alpha1::KafkaCluster> { +) -> ObjectLabels<'a, T> { ObjectLabels { owner, app_name: APP_NAME, From 6c2336112540a19c83a0a2dbfec456f48c64fde8 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 10 Jun 2026 19:02:41 +0200 Subject: [PATCH 23/47] refactor: introduce ValidatedClusterConfig --- rust/operator-binary/src/controller.rs | 62 +++++++++---------- .../src/controller/build/config_map.rs | 15 ++--- .../src/controller/build/discovery.rs | 8 +-- .../src/controller/validate.rs | 41 ++++++------ rust/operator-binary/src/resource/listener.rs | 6 +- rust/operator-binary/src/resource/service.rs | 6 +- .../src/resource/statefulset.rs | 10 +-- rust/operator-binary/src/utils.rs | 2 +- 8 files changed, 75 insertions(+), 75 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index ec00b969..142c77e1 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -209,6 +209,8 @@ impl ReconcilerError for Error { } } +pub type RoleGroupName = String; + /// The validated cluster. Carries everything the build steps need, resolved once /// here so downstream code never re-derives it or touches the raw spec. /// @@ -216,37 +218,25 @@ impl ReconcilerError for Error { /// references for child objects can be built straight from this struct (via its /// [`Resource`] impl) without threading the raw [`v1alpha1::KafkaCluster`] around. /// This mirrors the hive-/opensearch-operator's `ValidatedCluster`. -pub struct ValidatedKafkaCluster { +pub struct ValidatedCluster { /// `ObjectMeta` carrying `name`, `namespace` and `uid`, so this struct can act as the /// owner [`Resource`] for child objects. metadata: ObjectMeta, pub name: ClusterName, pub namespace: NamespaceName, pub image: ResolvedProductImage, - pub kafka_security: KafkaTlsSecurity, - // DESIGN DECISION: the dereferenced authorization config is folded into the - // validated cluster (read from here downstream). The other dereferenced input, - // the authentication classes, is intentionally NOT stored: it is fully consumed - // here to build `kafka_security`. Alternative: also store the resolved auth - // classes — rejected because nothing downstream needs them beyond kafka_security. - pub authorization_config: Option, - pub role_groups: BTreeMap>, - pub pod_descriptors: Vec, - pub metadata_manager: MetadataManager, + pub cluster_config: ValidatedClusterConfig, + pub role_group_configs: BTreeMap>, } -impl ValidatedKafkaCluster { - #[allow(clippy::too_many_arguments)] +impl ValidatedCluster { pub fn new( name: ClusterName, namespace: NamespaceName, uid: Uid, image: ResolvedProductImage, - kafka_security: KafkaTlsSecurity, - authorization_config: Option, - role_groups: BTreeMap>, - pod_descriptors: Vec, - metadata_manager: MetadataManager, + cluster_config: ValidatedClusterConfig, + role_group_configs: BTreeMap>, ) -> Self { Self { metadata: ObjectMeta { @@ -258,18 +248,26 @@ impl ValidatedKafkaCluster { name, namespace, image, - kafka_security, - authorization_config, - role_groups, - pod_descriptors, - metadata_manager, + cluster_config, + role_group_configs, } } } -/// Lets [`ValidatedKafkaCluster`] act as the owner [`Resource`] for child objects, so owner +/// Cluster-wide settings resolved during validation and dereferencing. +/// +/// Everything the build steps need is resolved here so they never have to read the +/// raw [`v1alpha1::KafkaCluster`] spec. +pub struct ValidatedClusterConfig { + pub kafka_security: KafkaTlsSecurity, + pub authorization_config: Option, + pub pod_descriptors: Vec, + pub metadata_manager: MetadataManager, +} + +/// Lets [`ValidatedCluster`] act as the owner [`Resource`] for child objects, so owner /// references are built from it (via the captured `metadata`) rather than the raw CR. -impl Resource for ValidatedKafkaCluster { +impl Resource for ValidatedCluster { type DynamicType = ::DynamicType; type Scope = ::Scope; @@ -348,10 +346,10 @@ pub async fn reconcile_kafka( .context(CreateClusterResourcesSnafu)?; tracing::debug!( - kerberos_enabled = validated_cluster.kafka_security.has_kerberos_enabled(), - kerberos_secret_class = ?validated_cluster.kafka_security.kerberos_secret_class(), - tls_enabled = validated_cluster.kafka_security.tls_enabled(), - tls_client_authentication_class = ?validated_cluster.kafka_security.tls_client_authentication_class(), + kerberos_enabled = validated_cluster.cluster_config.kafka_security.has_kerberos_enabled(), + kerberos_secret_class = ?validated_cluster.cluster_config.kafka_security.kerberos_secret_class(), + tls_enabled = validated_cluster.cluster_config.kafka_security.tls_enabled(), + tls_client_authentication_class = ?validated_cluster.cluster_config.kafka_security.tls_client_authentication_class(), "The following security settings are used" ); @@ -377,7 +375,7 @@ pub async fn reconcile_kafka( let mut bootstrap_listeners = Vec::::new(); - for (kafka_role, rg_map) in &validated_cluster.role_groups { + for (kafka_role, rg_map) in &validated_cluster.role_group_configs { for (rolegroup_name, validated_rg) in rg_map { let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name); @@ -385,7 +383,7 @@ pub async fn reconcile_kafka( &validated_cluster, &validated_cluster.image, &rolegroup_ref, - &validated_cluster.kafka_security, + &validated_cluster.cluster_config.kafka_security, ) .context(BuildServiceSnafu)?; @@ -398,7 +396,7 @@ pub async fn reconcile_kafka( let kafka_listeners = get_kafka_listener_config( kafka, - &validated_cluster.kafka_security, + &validated_cluster.cluster_config.kafka_security, &rolegroup_ref, &client.kubernetes_cluster_info, ) diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 0af0fcf8..207caa3a 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -8,7 +8,7 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster, ValidatedRoleGroupConfig}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, ValidatedRoleGroupConfig}, crd::{ JVM_SECURITY_PROPERTIES_FILE, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, @@ -69,24 +69,25 @@ pub enum Error { /// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator pub fn build_rolegroup_config_map( kafka: &v1alpha1::KafkaCluster, - validated_cluster: &ValidatedKafkaCluster, + validated_cluster: &ValidatedCluster, rolegroup: &RoleGroupRef, validated_rg: &ValidatedRoleGroupConfig, listener_config: &KafkaListenerConfig, ) -> Result { - let kafka_security = &validated_cluster.kafka_security; + let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let kafka_config_file_name = validated_rg.merged_config.config_file_name(); let config_overrides = validated_rg.config_file_overrides.clone(); let opa_connect = validated_cluster + .cluster_config .authorization_config .as_ref() .map(|auth_config| auth_config.opa_connect.clone()); - let kraft_mode = validated_cluster.metadata_manager == MetadataManager::KRaft; + let kraft_mode = validated_cluster.cluster_config.metadata_manager == MetadataManager::KRaft; - if kraft_mode && validated_cluster.pod_descriptors.is_empty() { + if kraft_mode && validated_cluster.cluster_config.pod_descriptors.is_empty() { return NoKraftControllersFoundSnafu.fail(); } @@ -94,7 +95,7 @@ pub fn build_rolegroup_config_map( AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( kafka_security, listener_config, - &validated_cluster.pod_descriptors, + &validated_cluster.cluster_config.pod_descriptors, opa_connect.as_deref(), kraft_mode, kafka @@ -108,7 +109,7 @@ pub fn build_rolegroup_config_map( crate::controller::build::properties::controller_properties::build( kafka_security, listener_config, - &validated_cluster.pod_descriptors, + &validated_cluster.cluster_config.pod_descriptors, kraft_mode, config_overrides, ) diff --git a/rust/operator-binary/src/controller/build/discovery.rs b/rust/operator-binary/src/controller/build/discovery.rs index 60e30940..10ac621c 100644 --- a/rust/operator-binary/src/controller/build/discovery.rs +++ b/rust/operator-binary/src/controller/build/discovery.rs @@ -9,7 +9,7 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster}, crd::{role::KafkaRole, v1alpha1}, utils::build_recommended_labels, }; @@ -42,10 +42,10 @@ pub enum Error { /// Build a discovery [`ConfigMap`] containing information about how to connect to a certain /// [`v1alpha1::KafkaCluster`]. pub fn build_discovery_configmap( - validated_cluster: &ValidatedKafkaCluster, + validated_cluster: &ValidatedCluster, listeners: &[listener::v1alpha1::Listener], ) -> Result { - let kafka_security = &validated_cluster.kafka_security; + let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let port_name = if kafka_security.has_kerberos_enabled() { @@ -87,7 +87,7 @@ pub fn build_discovery_configmap( /// An [`ObjectRef`] to the owning cluster, built from the validated identity — used only for /// error context. -fn cluster_object_ref(cluster: &ValidatedKafkaCluster) -> ObjectRef { +fn cluster_object_ref(cluster: &ValidatedCluster) -> ObjectRef { ObjectRef::new(cluster.name.as_ref()).within(cluster.namespace.as_ref()) } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 85c2e844..228156b1 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -1,7 +1,7 @@ //! The validate step in the KafkaCluster controller. //! //! Synchronously validates inputs that don't require a Kubernetes client. Produces -//! [`ValidatedKafkaCluster`], consumed by the rest of `reconcile_kafka`. +//! [`ValidatedCluster`], consumed by the rest of `reconcile_kafka`. use std::{collections::BTreeMap, str::FromStr}; @@ -22,7 +22,8 @@ use stackable_operator::{ use crate::{ controller::{ - ValidatedKafkaCluster, ValidatedRoleGroupConfig, dereference::DereferencedObjects, + RoleGroupName, ValidatedCluster, ValidatedClusterConfig, ValidatedRoleGroupConfig, + dereference::DereferencedObjects, }, crd::{ self, CONTAINER_IMAGE_BASE_NAME, @@ -87,7 +88,7 @@ pub fn validate( kafka: &v1alpha1::KafkaCluster, dereferenced_objects: DereferencedObjects, operator_environment: &OperatorEnvironmentOptions, -) -> Result { +) -> Result { let image = kafka .spec .image @@ -115,13 +116,10 @@ pub fn validate( .validate_authentication_methods() .context(FailedToValidateAuthenticationMethodSnafu)?; - // DESIGN DECISION: build the per-rolegroup config (merged config + resolved overrides) - // here, so reconcile reads a fully-typed ValidatedKafkaCluster instead of re-deriving - // merged_config in the loop and threading a product-config HashMap. Alternative: keep - // deriving merged_config in the reconcile loop — rejected; validation is the right place - // to prove every rolegroup resolves before any resource is built. - let mut role_groups: BTreeMap> = - BTreeMap::new(); + let mut role_group_configs: BTreeMap< + KafkaRole, + BTreeMap, + > = BTreeMap::new(); // Brokers always exist. let broker_role = kafka @@ -131,7 +129,7 @@ pub fn validate( role: KafkaRole::Broker, })?; - let mut broker_groups: BTreeMap = BTreeMap::new(); + let mut broker_groups: BTreeMap = BTreeMap::new(); for rolegroup_name in broker_role.role_groups.keys() { let merged_config = KafkaRole::Broker .merged_config(kafka, rolegroup_name) @@ -148,7 +146,7 @@ pub fn validate( }, ); } - role_groups.insert(KafkaRole::Broker, broker_groups); + role_group_configs.insert(KafkaRole::Broker, broker_groups); // We need this guard because controller_role() returns an error if controllers is None, // which would stop reconciliation for ZooKeeper-mode clusters. @@ -160,7 +158,8 @@ pub fn validate( role: KafkaRole::Controller, })?; - let mut controller_groups: BTreeMap = BTreeMap::new(); + let mut controller_groups: BTreeMap = + BTreeMap::new(); for rolegroup_name in controller_role.role_groups.keys() { let merged_config = KafkaRole::Controller .merged_config(kafka, rolegroup_name) @@ -177,7 +176,7 @@ pub fn validate( }, ); } - role_groups.insert(KafkaRole::Controller, controller_groups); + role_group_configs.insert(KafkaRole::Controller, controller_groups); } let pod_descriptors = kafka @@ -197,16 +196,18 @@ pub fn validate( .context(InvalidNamespaceSnafu)?; let uid = Uid::from_str(&kafka.uid().context(ObjectHasNoUidSnafu)?).context(InvalidUidSnafu)?; - Ok(ValidatedKafkaCluster::new( + Ok(ValidatedCluster::new( name, namespace, uid, image, - kafka_security, - dereferenced_objects.authorization_config, - role_groups, - pod_descriptors, - metadata_manager, + ValidatedClusterConfig { + kafka_security, + authorization_config: dereferenced_objects.authorization_config, + pod_descriptors, + metadata_manager, + }, + role_group_configs, )) } diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/resource/listener.rs index 3161ed0e..bd62f668 100644 --- a/rust/operator-binary/src/resource/listener.rs +++ b/rust/operator-binary/src/resource/listener.rs @@ -4,7 +4,7 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster}, crd::{role::broker::BrokerConfig, security::KafkaTlsSecurity, v1alpha1}, utils::build_recommended_labels, }; @@ -27,11 +27,11 @@ pub enum Error { // TODO (@NickLarsenNZ): Move shared functionality to stackable-operator pub fn build_broker_rolegroup_bootstrap_listener( kafka: &v1alpha1::KafkaCluster, - validated_cluster: &ValidatedKafkaCluster, + validated_cluster: &ValidatedCluster, rolegroup: &RoleGroupRef, merged_config: &BrokerConfig, ) -> Result { - let kafka_security = &validated_cluster.kafka_security; + let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; Ok(listener::v1alpha1::Listener { diff --git a/rust/operator-binary/src/resource/service.rs b/rust/operator-binary/src/resource/service.rs index bc2fdbfe..631be187 100644 --- a/rust/operator-binary/src/resource/service.rs +++ b/rust/operator-binary/src/resource/service.rs @@ -8,7 +8,7 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster}, crd::{APP_NAME, METRICS_PORT, METRICS_PORT_NAME, security::KafkaTlsSecurity, v1alpha1}, utils::build_recommended_labels, }; @@ -35,7 +35,7 @@ pub enum Error { /// /// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing. pub fn build_rolegroup_headless_service( - validated_cluster: &ValidatedKafkaCluster, + validated_cluster: &ValidatedCluster, resolved_product_image: &ResolvedProductImage, rolegroup: &RoleGroupRef, kafka_security: &KafkaTlsSecurity, @@ -77,7 +77,7 @@ pub fn build_rolegroup_headless_service( /// The rolegroup metrics [`Service`] is a service that exposes metrics and a prometheus scraping label pub fn build_rolegroup_metrics_service( - validated_cluster: &ValidatedKafkaCluster, + validated_cluster: &ValidatedCluster, resolved_product_image: &ResolvedProductImage, rolegroup: &RoleGroupRef, ) -> Result { diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 3212c650..d3a225de 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -43,7 +43,7 @@ use crate::{ command::{broker_kafka_container_commands, controller_kafka_container_command}, node_id_hasher::node_id_hash32_offset, }, - controller::{KAFKA_CONTROLLER_NAME, ValidatedKafkaCluster, ValidatedRoleGroupConfig}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, ValidatedRoleGroupConfig}, crd::{ self, APP_NAME, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, @@ -163,13 +163,13 @@ pub enum Error { pub fn build_broker_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, - validated_cluster: &ValidatedKafkaCluster, + validated_cluster: &ValidatedCluster, rolegroup_ref: &RoleGroupRef, validated_rg: &ValidatedRoleGroupConfig, service_account: &ServiceAccount, cluster_info: &KubernetesClusterInfo, ) -> Result { - let kafka_security = &validated_cluster.kafka_security; + let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.merged_config; let recommended_object_labels = build_recommended_labels( @@ -572,13 +572,13 @@ pub fn build_broker_rolegroup_statefulset( pub fn build_controller_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, - validated_cluster: &ValidatedKafkaCluster, + validated_cluster: &ValidatedCluster, rolegroup_ref: &RoleGroupRef, validated_rg: &ValidatedRoleGroupConfig, service_account: &ServiceAccount, cluster_info: &KubernetesClusterInfo, ) -> Result { - let kafka_security = &validated_cluster.kafka_security; + let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.merged_config; let recommended_object_labels = build_recommended_labels( diff --git a/rust/operator-binary/src/utils.rs b/rust/operator-binary/src/utils.rs index 1db2241a..a6edb3c2 100644 --- a/rust/operator-binary/src/utils.rs +++ b/rust/operator-binary/src/utils.rs @@ -5,7 +5,7 @@ use crate::crd::{APP_NAME, OPERATOR_NAME}; /// Build recommended values for labels. /// /// Generic over the owner `T` so the owner can be either the raw `KafkaCluster` or the -/// `ValidatedKafkaCluster` (which also implements `Resource`). +/// `ValidatedCluster` (which also implements `Resource`). pub fn build_recommended_labels<'a, T>( owner: &'a T, controller_name: &'a str, From bc3e8e232652033f7eebe5715b7f2f9e07b2b442 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 10 Jun 2026 19:29:07 +0200 Subject: [PATCH 24/47] refactor: add framework module and merging --- rust/operator-binary/src/config/jvm.rs | 23 +- rust/operator-binary/src/controller.rs | 30 +- .../src/controller/build/config_map.rs | 17 +- .../src/controller/validate.rs | 324 +++++++----------- rust/operator-binary/src/crd/affinity.rs | 16 +- rust/operator-binary/src/crd/role/mod.rs | 121 ++----- rust/operator-binary/src/framework.rs | 11 + .../src/framework/role_utils.rs | 152 ++++++++ rust/operator-binary/src/main.rs | 1 + .../src/resource/statefulset.rs | 24 +- 10 files changed, 383 insertions(+), 336 deletions(-) create mode 100644 rust/operator-binary/src/framework.rs create mode 100644 rust/operator-binary/src/framework/role_utils.rs diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/config/jvm.rs index 79023618..b8e0acd6 100644 --- a/rust/operator-binary/src/config/jvm.rs +++ b/rust/operator-binary/src/config/jvm.rs @@ -110,8 +110,17 @@ fn is_heap_jvm_argument(jvm_argument: &str) -> bool { #[cfg(test)] mod tests { + use stackable_operator::kube::ResourceExt; + use super::*; - use crate::crd::{BrokerRole, role::KafkaRole, v1alpha1}; + use crate::{ + crd::{ + BrokerRole, + role::{KafkaRole, broker::BrokerConfig}, + v1alpha1, + }, + framework::role_utils::with_validated_config, + }; #[test] fn test_construct_jvm_arguments_defaults() { @@ -197,12 +206,12 @@ mod tests { let kafka: v1alpha1::KafkaCluster = serde_yaml::from_str(kafka_cluster).expect("illegal test input"); - let kafka_role = KafkaRole::Broker; - let rolegroup_ref = kafka.rolegroup_ref(&kafka_role, "default"); - let merged_config = kafka_role - .merged_config(&kafka, &rolegroup_ref.role_group) - .unwrap(); - let role = kafka.spec.brokers.unwrap(); + let role = kafka.spec.brokers.clone().unwrap(); + let role_group = role.role_groups.get("default").unwrap(); + let default_config = + BrokerConfig::default_config(&kafka.name_any(), &KafkaRole::Broker.to_string()); + let validated = with_validated_config(role_group, &role, &default_config).unwrap(); + let merged_config = AnyConfig::Broker(validated.config); (merged_config, role, "default".to_owned()) } diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 142c77e1..7eca5ff7 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -38,7 +38,7 @@ use crate::{ self, APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, authorization::KafkaAuthorizationConfig, listener::get_kafka_listener_config, - role::{AnyConfig, KafkaRole}, + role::{AnyConfig, AnyConfigOverrides, KafkaRole}, security::KafkaTlsSecurity, v1alpha1, }, @@ -296,20 +296,18 @@ impl Resource for ValidatedCluster { } } -pub struct ValidatedRoleGroupConfig { - pub merged_config: AnyConfig, - // DESIGN DECISION: overrides are resolved into flat maps HERE rather than stored - // as the typed KeyValueConfigOverrides and resolved in the per-file builders (the - // hdfs-operator pattern). Reason: broker and controller use different override - // struct types (KafkaBrokerConfigOverrides vs KafkaControllerConfigOverrides), so a - // single typed field would require an enum. Resolving here keeps the build/properties - // builders taking plain `BTreeMap`. Alternative: an enum over the two - // override types threaded to builders that call resolved_overrides() — more types for - // no behavioural gain. - pub config_file_overrides: BTreeMap, - pub jvm_security_overrides: BTreeMap, - pub env_overrides: BTreeMap, -} +/// A validated, merged Kafka role-group config. +/// +/// The merged config fragment is wrapped in [`AnyConfig`] and the merged +/// `configOverrides` in [`AnyConfigOverrides`], so a single role-agnostic type +/// carries both broker and controller role groups (their concrete config and +/// override types differ). Produced via the local-`framework` +/// [`with_validated_config`](crate::framework::role_utils::with_validated_config). +pub type ValidatedRoleGroupConfig = crate::framework::role_utils::RoleGroupConfig< + AnyConfig, + stackable_operator::role_utils::JavaCommonConfig, + AnyConfigOverrides, +>; pub async fn reconcile_kafka( kafka: Arc>, @@ -434,7 +432,7 @@ pub async fn reconcile_kafka( .context(BuildStatefulsetSnafu)?, }; - if let AnyConfig::Broker(broker_config) = &validated_rg.merged_config { + if let AnyConfig::Broker(broker_config) = &validated_rg.config { let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( kafka, &validated_cluster, diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 207caa3a..e966d30a 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -76,8 +76,12 @@ pub fn build_rolegroup_config_map( ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; - let kafka_config_file_name = validated_rg.merged_config.config_file_name(); - let config_overrides = validated_rg.config_file_overrides.clone(); + let kafka_config_file_name = validated_rg.config.config_file_name(); + let config_overrides = validated_rg + .config_overrides + .config_file_overrides() + .overrides + .clone(); let opa_connect = validated_cluster .cluster_config @@ -91,7 +95,7 @@ pub fn build_rolegroup_config_map( return NoKraftControllersFoundSnafu.fail(); } - let kafka_config = match &validated_rg.merged_config { + let kafka_config = match &validated_rg.config { AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( kafka_security, listener_config, @@ -116,7 +120,10 @@ pub fn build_rolegroup_config_map( } }; - let jvm_sec_props = &validated_rg.jvm_security_overrides; + let jvm_sec_props = &validated_rg + .config_overrides + .security_properties() + .overrides; let mut cm_builder = ConfigMapBuilder::new(); cm_builder @@ -179,7 +186,7 @@ pub fn build_rolegroup_config_map( let config_data = role_group_config_map_data( &resolved_product_image.product_version, rolegroup, - &validated_rg.merged_config, + &validated_rg.config, ); for (file_name, data) in config_data { if let Some(data) = data { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 228156b1..1f60f095 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -5,14 +5,17 @@ use std::{collections::BTreeMap, str::FromStr}; +use serde::Serialize; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, commons::product_image_selection, - config::merge::{Merge, merge}, + config::{fragment::FromFragment, merge::Merge}, kube::ResourceExt, + role_utils::{GenericRoleConfig, JavaCommonConfig, Role}, + schemars::JsonSchema, v2::{ - config_overrides::KeyValueConfigOverrides, + builder::pod::container::{self, EnvVarName, EnvVarSet}, types::{ kubernetes::{NamespaceName, Uid}, operator::ClusterName, @@ -28,12 +31,19 @@ use crate::{ crd::{ self, CONTAINER_IMAGE_BASE_NAME, authentication::{self}, - role::KafkaRole, + role::{ + AnyConfig, AnyConfigOverrides, KafkaRole, broker::BrokerConfig, + controller::ControllerConfig, + }, security::{self, KafkaTlsSecurity}, v1alpha1, }, + framework::role_utils::with_validated_config, }; +/// The operator-managed env var carrying the Kafka cluster id. +const KAFKA_CLUSTER_ID_ENV: &str = "KAFKA_CLUSTER_ID"; + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("failed to resolve product image"))] @@ -50,8 +60,13 @@ pub enum Error { #[snafu(display("cluster object defines no '{role}' role"))] MissingKafkaRole { source: crd::Error, role: KafkaRole }, - #[snafu(display("failed to resolve merged config for rolegroup"))] - ResolveMergedConfig { source: crate::crd::role::Error }, + #[snafu(display("failed to merge and validate the role group config"))] + ValidateRoleGroupConfig { + source: crate::framework::role_utils::Error, + }, + + #[snafu(display("invalid environment variable name"))] + InvalidEnvVarName { source: container::Error }, #[snafu(display("failed to build pod descriptors"))] BuildPodDescriptors { source: crate::crd::Error }, @@ -116,66 +131,36 @@ pub fn validate( .validate_authentication_methods() .context(FailedToValidateAuthenticationMethodSnafu)?; + let cluster_id = kafka.cluster_id(); + let mut role_group_configs: BTreeMap< KafkaRole, BTreeMap, > = BTreeMap::new(); // Brokers always exist. - let broker_role = kafka - .broker_role() - .cloned() - .context(MissingKafkaRoleSnafu { - role: KafkaRole::Broker, - })?; - - let mut broker_groups: BTreeMap = BTreeMap::new(); - for rolegroup_name in broker_role.role_groups.keys() { - let merged_config = KafkaRole::Broker - .merged_config(kafka, rolegroup_name) - .context(ResolveMergedConfigSnafu)?; - let (config_file_overrides, jvm_security_overrides, env_overrides) = - collect_broker_role_group_overrides(kafka, &broker_role, rolegroup_name); - broker_groups.insert( - rolegroup_name.clone(), - ValidatedRoleGroupConfig { - merged_config, - config_file_overrides, - jvm_security_overrides, - env_overrides, - }, - ); - } + let broker_role = kafka.broker_role().context(MissingKafkaRoleSnafu { + role: KafkaRole::Broker, + })?; + let broker_groups = validate_role_group_configs( + broker_role, + BrokerConfig::default_config(&kafka.name_any(), &KafkaRole::Broker.to_string()), + cluster_id, + AnyConfig::Broker, + AnyConfigOverrides::Broker, + )?; role_group_configs.insert(KafkaRole::Broker, broker_groups); - // We need this guard because controller_role() returns an error if controllers is None, - // which would stop reconciliation for ZooKeeper-mode clusters. - if kafka.spec.controllers.is_some() { - let controller_role = kafka - .controller_role() - .cloned() - .context(MissingKafkaRoleSnafu { - role: KafkaRole::Controller, - })?; - - let mut controller_groups: BTreeMap = - BTreeMap::new(); - for rolegroup_name in controller_role.role_groups.keys() { - let merged_config = KafkaRole::Controller - .merged_config(kafka, rolegroup_name) - .context(ResolveMergedConfigSnafu)?; - let (config_file_overrides, jvm_security_overrides, env_overrides) = - collect_controller_role_group_overrides(kafka, &controller_role, rolegroup_name); - controller_groups.insert( - rolegroup_name.clone(), - ValidatedRoleGroupConfig { - merged_config, - config_file_overrides, - jvm_security_overrides, - env_overrides, - }, - ); - } + // Controllers are optional: ZooKeeper-mode clusters have none, and `controller_role()` + // errors when `controllers` is unset, which would stop their reconciliation. + if let Some(controller_role) = kafka.spec.controllers.as_ref() { + let controller_groups = validate_role_group_configs( + controller_role, + ControllerConfig::default_config(&kafka.name_any(), &KafkaRole::Controller.to_string()), + cluster_id, + AnyConfig::Controller, + AnyConfigOverrides::Controller, + )?; role_group_configs.insert(KafkaRole::Controller, controller_groups); } @@ -211,163 +196,108 @@ pub fn validate( )) } -/// Merge role-group overrides over the role-level overrides (role-group wins per key) via the -/// `Merge` impl derived on the override structs. -fn merge_role_group_overrides(role: &O, role_group: Option<&O>) -> O { - match role_group { - Some(role_group) => merge(role_group.clone(), role), - None => role.clone(), - } -} - -/// Flatten resolved key/value overrides into a plain map. operator-rs #1219 made the override -/// values plain `String`, so there is no longer any `null`/unset entry to drop. -fn flatten_overrides(overrides: KeyValueConfigOverrides) -> BTreeMap { - overrides.overrides +/// Validates every role group of a role into a map keyed by role group name. +/// +/// Each role group is merged and validated via the local-`framework` +/// [`with_validated_config`], which folds the config fragment (default <- role <- +/// role group) plus the `configOverrides`, `envOverrides`, `cliOverrides` and +/// `podOverrides` (role group wins) into a single +/// [`RoleGroupConfig`](crate::framework::role_utils::RoleGroupConfig). The concrete +/// per-role validated config and overrides are wrapped into the role-agnostic +/// [`AnyConfig`]/[`AnyConfigOverrides`] via `wrap_config`/`wrap_overrides`, and the +/// operator-managed `KAFKA_CLUSTER_ID` is injected into the env overrides. +fn validate_role_group_configs( + role: &Role, + default_config: Config, + cluster_id: Option<&str>, + wrap_config: fn(ValidatedConfig) -> AnyConfig, + wrap_overrides: fn(ConfigOverrides) -> AnyConfigOverrides, +) -> Result> +where + Config: Clone + Merge, + ValidatedConfig: FromFragment, + ConfigOverrides: Clone + Default + JsonSchema + Merge + Serialize, +{ + role.role_groups + .iter() + .map(|(role_group_name, role_group)| { + let validated = with_validated_config::< + ValidatedConfig, + JavaCommonConfig, + Config, + GenericRoleConfig, + ConfigOverrides, + >(role_group, role, &default_config) + .context(ValidateRoleGroupConfigSnafu)?; + + // Re-wrap the per-role validated config and overrides into the role-agnostic + // enums; the merged env/cli/pod overrides carry over unchanged, except that + // `KAFKA_CLUSTER_ID` is injected into the env overrides. + let validated = ValidatedRoleGroupConfig { + replicas: validated.replicas, + config: wrap_config(validated.config), + config_overrides: wrap_overrides(validated.config_overrides), + env_overrides: inject_cluster_id(validated.env_overrides, cluster_id)?, + cli_overrides: validated.cli_overrides, + pod_overrides: validated.pod_overrides, + product_specific_common_config: validated.product_specific_common_config, + }; + Ok((role_group_name.clone(), validated)) + }) + .collect() } -fn collect_broker_role_group_overrides( - kafka: &v1alpha1::KafkaCluster, - broker_role: &crate::crd::BrokerRole, - rolegroup_name: &str, -) -> ( - BTreeMap, - BTreeMap, - BTreeMap, -) { - let merged_overrides = merge_role_group_overrides( - &broker_role.config.config_overrides, - broker_role - .role_groups - .get(rolegroup_name) - .map(|rg| &rg.config.config_overrides), - ); - let config_file_overrides = flatten_overrides(merged_overrides.broker_properties); - let jvm_security_overrides = flatten_overrides(merged_overrides.security_properties); - - // --- env overrides --- - // DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides - // (role then role-group) are extended on top, so a user override of the same key wins. - // This mirrors product-config's old merge of compute_env() output with user envOverrides. - // Alternative: inject after user overrides (operator wins) — rejected to preserve the - // previous precedence. - // - // KAFKA_CLUSTER_ID injection moved here from crd/role/broker.rs::Configuration::compute_env. - let mut env_overrides: BTreeMap = BTreeMap::new(); - if let Some(cluster_id) = kafka.cluster_id() { - env_overrides.insert("KAFKA_CLUSTER_ID".to_string(), cluster_id.to_string()); - } - let role_env: &std::collections::HashMap = &broker_role.config.env_overrides; - env_overrides.extend(role_env.iter().map(|(k, v)| (k.clone(), v.clone()))); - if let Some(rg) = broker_role.role_groups.get(rolegroup_name) { - env_overrides.extend( - rg.config - .env_overrides - .iter() - .map(|(k, v)| (k.clone(), v.clone())), - ); +/// Injects the operator-managed `KAFKA_CLUSTER_ID` into the merged env overrides, +/// but only when the user has not already set it via `envOverrides` (user value +/// wins, preserving product-config's old precedence). +/// +/// `KAFKA_CLUSTER_ID` injection moved here from the now-removed +/// `crd::role::*::Configuration::compute_env`. +fn inject_cluster_id(env_overrides: EnvVarSet, cluster_id: Option<&str>) -> Result { + let Some(cluster_id) = cluster_id else { + return Ok(env_overrides); + }; + let name = EnvVarName::from_str(KAFKA_CLUSTER_ID_ENV).context(InvalidEnvVarNameSnafu)?; + if env_overrides.get(&name).is_some() { + // The user set `KAFKA_CLUSTER_ID` via envOverrides; their value wins. + Ok(env_overrides) + } else { + Ok(env_overrides.with_value(&name, cluster_id)) } - - (config_file_overrides, jvm_security_overrides, env_overrides) -} - -fn collect_controller_role_group_overrides( - kafka: &v1alpha1::KafkaCluster, - controller_role: &crate::crd::ControllerRole, - rolegroup_name: &str, -) -> ( - BTreeMap, - BTreeMap, - BTreeMap, -) { - let merged_overrides = merge_role_group_overrides( - &controller_role.config.config_overrides, - controller_role - .role_groups - .get(rolegroup_name) - .map(|rg| &rg.config.config_overrides), - ); - let config_file_overrides = flatten_overrides(merged_overrides.controller_properties); - let jvm_security_overrides = flatten_overrides(merged_overrides.security_properties); - - // --- env overrides --- - // DESIGN DECISION: KAFKA_CLUSTER_ID is injected first, then the user env overrides - // (role then role-group) are extended on top, so a user override of the same key wins. - // This mirrors product-config's old merge of compute_env() output with user envOverrides. - // Alternative: inject after user overrides (operator wins) — rejected to preserve the - // previous precedence. - // - // KAFKA_CLUSTER_ID injection moved here from crd/role/controller.rs::Configuration::compute_env. - let mut env_overrides: BTreeMap = BTreeMap::new(); - if let Some(cluster_id) = kafka.cluster_id() { - env_overrides.insert("KAFKA_CLUSTER_ID".to_string(), cluster_id.to_string()); - } - let role_env: &std::collections::HashMap = - &controller_role.config.env_overrides; - env_overrides.extend(role_env.iter().map(|(k, v)| (k.clone(), v.clone()))); - if let Some(rg) = controller_role.role_groups.get(rolegroup_name) { - env_overrides.extend( - rg.config - .env_overrides - .iter() - .map(|(k, v)| (k.clone(), v.clone())), - ); - } - - (config_file_overrides, jvm_security_overrides, env_overrides) } #[cfg(test)] mod tests { - use std::collections::BTreeMap; + use std::str::FromStr; - use stackable_operator::v2::config_overrides::KeyValueConfigOverrides; + use stackable_operator::v2::builder::pod::container::{EnvVarName, EnvVarSet}; - use super::{flatten_overrides, merge_role_group_overrides}; + use super::{KAFKA_CLUSTER_ID_ENV, inject_cluster_id}; - /// Build a `KeyValueConfigOverrides` from `(key, value)` pairs. - fn overrides(pairs: &[(&str, &str)]) -> KeyValueConfigOverrides { - KeyValueConfigOverrides { - overrides: pairs - .iter() - .map(|(key, value)| (key.to_string(), value.to_string())) - .collect(), - } - } - - /// Run the full role/role-group resolution (merge then flatten) for a single config file. - fn resolve( - role: KeyValueConfigOverrides, - role_group: Option, - ) -> BTreeMap { - flatten_overrides(merge_role_group_overrides(&role, role_group.as_ref())) + fn cluster_id_value(env: &EnvVarSet) -> Option { + let name = EnvVarName::from_str(KAFKA_CLUSTER_ID_ENV).unwrap(); + env.get(&name).and_then(|var| var.value.clone()) } #[test] - fn role_group_value_wins_over_role() { - let role = overrides(&[("a", "role"), ("b", "role-only")]); - let role_group = overrides(&[("a", "rg")]); - - let merged = resolve(role, Some(role_group)); - - assert_eq!( - merged, - BTreeMap::from([ - ("a".to_string(), "rg".to_string()), // role-group wins for shared keys - ("b".to_string(), "role-only".to_string()), // role-only keys are kept - ]) - ); + fn injects_cluster_id_when_absent() { + let env = inject_cluster_id(EnvVarSet::new(), Some("my-id")).unwrap(); + assert_eq!(cluster_id_value(&env), Some("my-id".to_string())); } #[test] - fn without_a_role_group_role_values_are_kept() { - let role = overrides(&[("a", "role")]); + fn user_cluster_id_override_wins() { + let name = EnvVarName::from_str(KAFKA_CLUSTER_ID_ENV).unwrap(); + let env = EnvVarSet::new().with_value(&name, "user-value"); - let merged = resolve(role, None); + let env = inject_cluster_id(env, Some("operator-value")).unwrap(); - assert_eq!( - merged, - BTreeMap::from([("a".to_string(), "role".to_string())]) - ); + assert_eq!(cluster_id_value(&env), Some("user-value".to_string())); + } + + #[test] + fn without_cluster_id_nothing_is_injected() { + let env = inject_cluster_id(EnvVarSet::new(), None).unwrap(); + assert_eq!(cluster_id_value(&env), None); } } diff --git a/rust/operator-binary/src/crd/affinity.rs b/rust/operator-binary/src/crd/affinity.rs index da01acca..51e0ae6b 100644 --- a/rust/operator-binary/src/crd/affinity.rs +++ b/rust/operator-binary/src/crd/affinity.rs @@ -30,9 +30,17 @@ mod tests { api::core::v1::{PodAffinityTerm, PodAntiAffinity, WeightedPodAffinityTerm}, apimachinery::pkg::apis::meta::v1::LabelSelector, }, + kube::ResourceExt, }; - use crate::crd::{KafkaRole, v1alpha1}; + use crate::{ + crd::{ + KafkaRole, + role::{AnyConfig, broker::BrokerConfig}, + v1alpha1, + }, + framework::role_utils::with_validated_config, + }; #[rstest] #[case(KafkaRole::Broker)] @@ -55,7 +63,11 @@ mod tests { let kafka: v1alpha1::KafkaCluster = serde_yaml::from_str(input).expect("illegal test input"); - let merged_config = role.merged_config(&kafka, "default").unwrap(); + let broker_role = kafka.spec.brokers.clone().unwrap(); + let role_group = broker_role.role_groups.get("default").unwrap(); + let default_config = BrokerConfig::default_config(&kafka.name_any(), &role.to_string()); + let validated = with_validated_config(role_group, &broker_role, &default_config).unwrap(); + let merged_config = AnyConfig::Broker(validated.config); assert_eq!( merged_config.affinity, diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index 6c7bac4f..a7e05881 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -8,15 +8,12 @@ use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::resources::{NoRuntimeLimits, Resources}, - config::{ - fragment::{self, ValidationError}, - merge::Merge, - }, k8s_openapi::api::core::v1::PodTemplateSpec, - kube::{ResourceExt, runtime::reflector::ObjectRef}, + kube::runtime::reflector::ObjectRef, product_logging::spec::ContainerLogConfig, role_utils::RoleGroupRef, schemars::{self, JsonSchema}, + v2::config_overrides::KeyValueConfigOverrides, }; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; @@ -73,9 +70,6 @@ pub const KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS: &str = "controller.quorum.b #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("fragment validation failure"))] - FragmentValidationFailure { source: ValidationError }, - #[snafu(display("the Kafka role [{role}] is missing from spec"))] MissingRole { source: crate::crd::Error, @@ -140,87 +134,6 @@ impl KafkaRole { "kafka" } - /// Merge the [Broker|Controller]ConfigFragment defaults, role and role group settings. - /// The priority is: default < role config < role_group config - pub fn merged_config( - &self, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - match self { - Self::Broker => { - // Initialize the result with all default values as baseline - let default_config = - BrokerConfig::default_config(&kafka.name_any(), &self.to_string()); - - // Retrieve role resource config - let role = kafka.broker_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?; - - let mut role_config = role.config.config.clone(); - // Retrieve rolegroup specific resource config - let mut role_group_config = role - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .config - .clone(); - - // Merge more specific configs into default config - // Hierarchy is: - // 1. RoleGroup - // 2. Role - // 3. Default - role_config.merge(&default_config); - role_group_config.merge(&role_config); - Ok(AnyConfig::Broker( - fragment::validate::(role_group_config) - .context(FragmentValidationFailureSnafu)?, - )) - } - Self::Controller => { - // Initialize the result with all default values as baseline - let default_config = - ControllerConfig::default_config(&kafka.name_any(), &self.to_string()); - - // Retrieve role resource config - let role = kafka.controller_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?; - - let mut role_config = role.config.config.clone(); - // Retrieve rolegroup specific resource config - let mut role_group_config = role - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .config - .clone(); - - // Merge more specific configs into default config - // Hierarchy is: - // 1. RoleGroup - // 2. Role - // 3. Default - role_config.merge(&default_config); - role_group_config.merge(&role_config); - Ok(AnyConfig::Controller( - fragment::validate::(role_group_config) - .context(FragmentValidationFailureSnafu)?, - )) - } - } - } - pub fn construct_non_heap_jvm_args( &self, merged_config: &AnyConfig, @@ -449,3 +362,33 @@ impl AnyConfig { } } } + +/// Merged role/role-group `configOverrides` for a role group of an unknown type. +/// +/// Mirrors [`AnyConfig`] for the override side: broker and controller use distinct +/// override structs, so this enum lets the build layer carry the typed, merged +/// overrides through a single role-agnostic `RoleGroupConfig`. +#[derive(Clone, Debug, PartialEq)] +pub enum AnyConfigOverrides { + Broker(v1alpha1::KafkaBrokerConfigOverrides), + Controller(v1alpha1::KafkaControllerConfigOverrides), +} + +impl AnyConfigOverrides { + /// The merged product config-file overrides (`broker.properties` for brokers, + /// `controller.properties` for controllers). + pub fn config_file_overrides(&self) -> &KeyValueConfigOverrides { + match self { + AnyConfigOverrides::Broker(o) => &o.broker_properties, + AnyConfigOverrides::Controller(o) => &o.controller_properties, + } + } + + /// The merged `security.properties` overrides (shared by both roles). + pub fn security_properties(&self) -> &KeyValueConfigOverrides { + match self { + AnyConfigOverrides::Broker(o) => &o.security_properties, + AnyConfigOverrides::Controller(o) => &o.security_properties, + } + } +} diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs new file mode 100644 index 00000000..6e28e88a --- /dev/null +++ b/rust/operator-binary/src/framework.rs @@ -0,0 +1,11 @@ +//! Local framework helpers that mirror the work-in-progress upstream +//! `stackable_operator::v2::*` modules. +//! +//! We vendor `role_utils` because the upstream `v2::role_utils` requires +//! `CommonConfig: Merge`. Kafka (like hdfs and trino) uses `JavaCommonConfig`, +//! whose JVM-argument merge is fallible and so does not implement `Merge`. +//! +//! Follow-up: replace with `stackable_operator::v2::role_utils::*` once upstream +//! relaxes the `Merge` bound. + +pub mod role_utils; diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs new file mode 100644 index 00000000..b4bc9a8b --- /dev/null +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -0,0 +1,152 @@ +//! Vendored variant of `stackable_operator::v2::role_utils` from the +//! `smooth-operator` branch, with simplifications appropriate for kafka-operator. +//! +//! Differences from upstream: +//! - No `cli_overrides_to_vec` helper, `ResourceNames`, or service-account helpers. +//! - The `CommonConfig` (a.k.a. `product_specific_common_config`) does NOT need to +//! implement `Merge`. Kafka uses `JavaCommonConfig`, which intentionally does not +//! implement `Merge` because its inner `JvmArgumentOverrides::try_merge` is +//! fallible (regex validation). The `RoleGroupConfig::product_specific_common_config` +//! field here simply carries the role-group level value through. +//! +//! Replace with `stackable_operator::v2::role_utils::*` once upstream relaxes the +//! `Merge` bound. + +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; + +use serde::Serialize; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + config::{ + fragment::{self, FromFragment}, + merge::{Merge, merge}, + }, + k8s_openapi::{DeepMerge, api::core::v1::PodTemplateSpec}, + role_utils::{Role, RoleGroup}, + schemars::JsonSchema, + v2::builder::pod::container::{self, EnvVarName, EnvVarSet}, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to validate the role group config"))] + ValidateConfig { source: fragment::ValidationError }, + + #[snafu(display("invalid environment variable override name"))] + ParseEnvVarName { source: container::Error }, +} + +/// Kafka-friendly view of a validated, merged `RoleGroup`. +#[derive(Clone, Debug, PartialEq)] +pub struct RoleGroupConfig { + pub replicas: u16, + pub config: Config, + pub config_overrides: ConfigOverrides, + pub env_overrides: EnvVarSet, + pub cli_overrides: BTreeMap, + pub pod_overrides: PodTemplateSpec, + pub product_specific_common_config: CommonConfig, +} + +/// Merges and validates the `RoleGroup` with the given `role` and `default_config`. +pub fn with_validated_config( + role_group: &RoleGroup, + role: &Role, + default_config: &Config, +) -> Result, Error> +where + ValidatedConfig: FromFragment, + CommonConfig: Clone + Default + JsonSchema + Serialize, + Config: Clone + Merge, + RoleConfig: Default + JsonSchema + Serialize, + ConfigOverrides: Clone + Default + JsonSchema + Merge + Serialize, +{ + let validated_config = + validate_config(role_group, role, default_config).context(ValidateConfigSnafu)?; + Ok(RoleGroupConfig { + replicas: role_group.replicas.unwrap_or(1), + config: validated_config, + config_overrides: merged_config_overrides( + &role.config.config_overrides, + role_group.config.config_overrides.clone(), + ), + env_overrides: merged_env_overrides( + &role.config.env_overrides, + &role_group.config.env_overrides, + )?, + cli_overrides: merged_cli_overrides( + role.config.cli_overrides.clone(), + role_group.config.cli_overrides.clone(), + ), + pod_overrides: merged_pod_overrides( + role.config.pod_overrides.clone(), + role_group.config.pod_overrides.clone(), + ), + product_specific_common_config: role_group.config.product_specific_common_config.clone(), + }) +} + +fn validate_config( + role_group: &RoleGroup, + role: &Role, + default_config: &Config, +) -> Result +where + ValidatedConfig: FromFragment, + CommonConfig: Default + JsonSchema + Serialize, + Config: Clone + Merge, + RoleConfig: Default + JsonSchema + Serialize, + ConfigOverrides: Default + JsonSchema + Serialize, +{ + role_group.validate_config(role, default_config) +} + +fn merged_config_overrides( + role_config_overrides: &ConfigOverrides, + role_group_config_overrides: ConfigOverrides, +) -> ConfigOverrides +where + ConfigOverrides: Merge, +{ + merge(role_group_config_overrides, role_config_overrides) +} + +fn merged_env_overrides( + role_env_overrides: &HashMap, + role_group_env_overrides: &HashMap, +) -> Result { + // Process the role first, then the role group, so that role-group overrides win on key + // collisions (`EnvVarSet::with_value` overrides earlier entries with the same name). + let mut env_overrides = EnvVarSet::new(); + for (name, value) in role_env_overrides + .iter() + .chain(role_group_env_overrides.iter()) + { + env_overrides = env_overrides.with_value( + &EnvVarName::from_str(name).context(ParseEnvVarNameSnafu)?, + value.clone(), + ); + } + Ok(env_overrides) +} + +fn merged_cli_overrides( + role_cli_overrides: BTreeMap, + role_group_cli_overrides: BTreeMap, +) -> BTreeMap { + let mut merged = role_cli_overrides; + merged.extend(role_group_cli_overrides); + merged +} + +fn merged_pod_overrides( + role_pod_overrides: PodTemplateSpec, + role_group_pod_overrides: PodTemplateSpec, +) -> PodTemplateSpec { + let mut merged = role_pod_overrides; + merged.merge_from(role_group_pod_overrides); + merged +} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index a967294a..cd1c9434 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -43,6 +43,7 @@ use crate::{ mod config; mod controller; mod crd; +mod framework; mod kerberos; mod operations; mod product_logging; diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index d3a225de..cea62ae0 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -171,7 +171,7 @@ pub fn build_broker_rolegroup_statefulset( ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; - let merged_config = &validated_rg.merged_config; + let merged_config = &validated_rg.config; let recommended_object_labels = build_recommended_labels( validated_cluster, KAFKA_CONTROLLER_NAME, @@ -244,15 +244,7 @@ pub fn build_broker_rolegroup_statefulset( .context(AddKerberosConfigSnafu)?; } - let mut env = validated_rg - .env_overrides - .iter() - .map(|(k, v)| EnvVar { - name: k.clone(), - value: Some(v.clone()), - ..EnvVar::default() - }) - .collect::>(); + let mut env = Vec::::from(validated_rg.env_overrides.clone()); if let Some(zookeeper_config_map_name) = &kafka.spec.cluster_config.zookeeper_config_map_name { env.push(EnvVar { @@ -580,7 +572,7 @@ pub fn build_controller_rolegroup_statefulset( ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; - let merged_config = &validated_rg.merged_config; + let merged_config = &validated_rg.config; let recommended_object_labels = build_recommended_labels( validated_cluster, KAFKA_CONTROLLER_NAME, @@ -597,15 +589,7 @@ pub fn build_controller_rolegroup_statefulset( let mut pod_builder = PodBuilder::new(); - let mut env = validated_rg - .env_overrides - .iter() - .map(|(k, v)| EnvVar { - name: k.clone(), - value: Some(v.clone()), - ..EnvVar::default() - }) - .collect::>(); + let mut env = Vec::::from(validated_rg.env_overrides.clone()); env.push(EnvVar { name: "NAMESPACE".to_string(), From 241284eb77637dafb0f87457a7bf8485697b724a Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 10 Jun 2026 19:34:39 +0200 Subject: [PATCH 25/47] chore: regenerate charts --- extra/crds.yaml | 160 ------------------------------------------------ 1 file changed, 160 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index ea71c1c4..6177a6e5 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -98,86 +98,6 @@ spec: containers: description: Log configuration per container. properties: - get-service: - anyOf: - - required: - - custom - - {} - - {} - description: Log configuration of the container - properties: - console: - description: Configuration for the console appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - custom: - description: Log configuration provided in a ConfigMap - properties: - configMap: - description: ConfigMap containing the log configuration files - nullable: true - type: string - type: object - file: - description: Configuration for the file appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - loggers: - additionalProperties: - description: Configuration of a logger - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - default: {} - description: Configuration per logger - type: object - type: object kafka: anyOf: - required: @@ -707,86 +627,6 @@ spec: containers: description: Log configuration per container. properties: - get-service: - anyOf: - - required: - - custom - - {} - - {} - description: Log configuration of the container - properties: - console: - description: Configuration for the console appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - custom: - description: Log configuration provided in a ConfigMap - properties: - configMap: - description: ConfigMap containing the log configuration files - nullable: true - type: string - type: object - file: - description: Configuration for the file appender - nullable: true - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - loggers: - additionalProperties: - description: Configuration of a logger - properties: - level: - description: |- - The log level threshold. - Log events with a lower log level are discarded. - enum: - - TRACE - - DEBUG - - INFO - - WARN - - ERROR - - FATAL - - NONE - - null - nullable: true - type: string - type: object - default: {} - description: Configuration per logger - type: object - type: object kafka: anyOf: - required: From b4fd4dcf7f8d0e88f0a46b68cf092fe9a0266f2c Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 10 Jun 2026 19:35:21 +0200 Subject: [PATCH 26/47] fix: remove obsolete errors, dead enum variant, tighten visibility of constants --- rust/operator-binary/src/controller.rs | 14 +++++--------- .../src/controller/build/config_map.rs | 9 --------- rust/operator-binary/src/crd/role/broker.rs | 1 - rust/operator-binary/src/product_logging.rs | 8 ++++---- rust/operator-binary/src/resource/statefulset.rs | 12 +----------- 5 files changed, 10 insertions(+), 34 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 7eca5ff7..5b14b9e7 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -35,7 +35,7 @@ mod validate; use crate::{ crd::{ - self, APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, + APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, authorization::KafkaAuthorizationConfig, listener::get_kafka_listener_config, role::{AnyConfig, AnyConfigOverrides, KafkaRole}, @@ -73,8 +73,8 @@ pub enum Error { source: crate::crd::listener::KafkaListenerError, }, - #[snafu(display("failed to apply role Service"))] - ApplyRoleService { + #[snafu(display("failed to apply bootstrap Listener"))] + ApplyBootstrapListener { source: stackable_operator::cluster_resources::Error, }, @@ -150,9 +150,6 @@ pub enum Error { source: error_boundary::InvalidObject, }, - #[snafu(display("KafkaCluster object is misconfigured"))] - MisconfiguredKafkaCluster { source: crd::Error }, - #[snafu(display("failed to build statefulset"))] BuildStatefulset { source: crate::resource::statefulset::Error, @@ -184,7 +181,7 @@ impl ReconcilerError for Error { match self { Error::Dereference { .. } => None, Error::ValidateCluster { .. } => None, - Error::ApplyRoleService { .. } => None, + Error::ApplyBootstrapListener { .. } => None, Error::ApplyRoleGroupService { .. } => None, Error::ApplyRoleGroupConfig { .. } => None, Error::ApplyRoleGroupStatefulSet { .. } => None, @@ -199,7 +196,6 @@ impl ReconcilerError for Error { Error::FailedToCreatePdb { .. } => None, Error::GetRequiredLabels { .. } => None, Error::InvalidKafkaCluster { .. } => None, - Error::MisconfiguredKafkaCluster { .. } => None, Error::BuildStatefulset { .. } => None, Error::BuildConfigMap { .. } => None, Error::BuildService { .. } => None, @@ -444,7 +440,7 @@ pub async fn reconcile_kafka( cluster_resources .add(client, rg_bootstrap_listener) .await - .context(ApplyRoleServiceSnafu)?, + .context(ApplyBootstrapListenerSnafu)?, ); } diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index e966d30a..5445e6fd 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -22,9 +22,6 @@ use crate::{ #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("invalid metadata manager"))] - InvalidMetadataManager { source: crate::crd::Error }, - #[snafu(display("failed to build ConfigMap for {}", rolegroup))] BuildRoleGroupConfig { source: stackable_operator::builder::configmap::Error, @@ -56,12 +53,6 @@ pub enum Error { rolegroup: RoleGroupRef, }, - #[snafu(display("failed to build jaas configuration file for {rolegroup}"))] - BuildJaasConfig { rolegroup: String }, - - #[snafu(display("failed to build pod descriptors"))] - BuildPodDescriptors { source: crate::crd::Error }, - #[snafu(display("no Kraft controllers found to build"))] NoKraftControllersFound, } diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 674b9feb..64ebf848 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -33,7 +33,6 @@ pub const BROKER_PROPERTIES_FILE: &str = "broker.properties"; pub enum BrokerContainer { Vector, KcatProber, - GetService, Kafka, } diff --git a/rust/operator-binary/src/product_logging.rs b/rust/operator-binary/src/product_logging.rs index a16d0148..74afff30 100644 --- a/rust/operator-binary/src/product_logging.rs +++ b/rust/operator-binary/src/product_logging.rs @@ -18,11 +18,11 @@ pub const BROKER_ID_POD_MAP_DIR: &str = "/stackable/broker-id-pod-map"; pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; pub const STACKABLE_LOG_DIR: &str = "/stackable/log"; // log4j -pub const LOG4J_CONFIG_FILE: &str = "log4j.properties"; -pub const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; +const LOG4J_CONFIG_FILE: &str = "log4j.properties"; +const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; // log4j2 -pub const LOG4J2_CONFIG_FILE: &str = "log4j2.properties"; -pub const KAFKA_LOG4J2_FILE: &str = "kafka.log4j2.xml"; +const LOG4J2_CONFIG_FILE: &str = "log4j2.properties"; +const KAFKA_LOG4J2_FILE: &str = "kafka.log4j2.xml"; // max size pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { value: 10.0, diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index cea62ae0..1d274bd6 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -118,11 +118,6 @@ pub enum Error { source: stackable_operator::builder::pod::container::Error, }, - #[snafu(display("invalid kafka listeners"))] - InvalidKafkaListeners { - source: crate::crd::listener::KafkaListenerError, - }, - #[snafu(display("failed to build Labels"))] LabelBuild { source: stackable_operator::kvp::LabelError, @@ -147,11 +142,6 @@ pub enum Error { #[snafu(display("failed to retrieve rolegroup replicas"))] RoleGroupReplicas { source: crd::role::Error }, - #[snafu(display( - "cluster does not define 'metadata.name' which is required for the Kafka cluster id" - ))] - ClusterIdMissing, - #[snafu(display("vector agent is enabled but vector aggregator ConfigMap is missing"))] VectorAggregatorConfigMapMissing, } @@ -159,7 +149,7 @@ pub enum Error { /// The broker rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. /// /// The [`Pod`](`stackable_operator::k8s_openapi::api::core::v1::Pod`)s are accessible through the corresponding -/// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_service`](`crate::resource::service::build_rolegroup_headless_service`). +/// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_headless_service`](`crate::resource::service::build_rolegroup_headless_service`). pub fn build_broker_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, From 63d820c1399a7d75745ea9f87a8caec2e2a9a941 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 10 Jun 2026 19:56:05 +0200 Subject: [PATCH 27/47] refactor: move logging mod to controller, remove util mod --- rust/operator-binary/src/config/command.rs | 28 ++++++++++---- rust/operator-binary/src/controller.rs | 30 +++++++++++++++ .../src/controller/build/config_map.rs | 7 ++-- .../src/controller/build/discovery.rs | 3 +- .../build/properties/logging.rs} | 37 +++++-------------- .../src/controller/build/properties/mod.rs | 1 + rust/operator-binary/src/crd/mod.rs | 6 +++ rust/operator-binary/src/main.rs | 2 - rust/operator-binary/src/resource/listener.rs | 3 +- rust/operator-binary/src/resource/service.rs | 3 +- .../src/resource/statefulset.rs | 20 +++++----- rust/operator-binary/src/utils.rs | 25 ------------- 12 files changed, 86 insertions(+), 79 deletions(-) rename rust/operator-binary/src/{product_logging.rs => controller/build/properties/logging.rs} (80%) delete mode 100644 rust/operator-binary/src/utils.rs diff --git a/rust/operator-binary/src/config/command.rs b/rust/operator-binary/src/config/command.rs index a4540001..233ef4b1 100644 --- a/rust/operator-binary/src/config/command.rs +++ b/rust/operator-binary/src/config/command.rs @@ -6,15 +6,29 @@ use stackable_operator::{ utils::COMMON_BASH_TRAP_FUNCTIONS, }; -use crate::{ - crd::{ - KafkaPodDescriptor, STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, - role::{broker::BROKER_PROPERTIES_FILE, controller::CONTROLLER_PROPERTIES_FILE}, - security::KafkaTlsSecurity, - }, - product_logging::{BROKER_ID_POD_MAP_DIR, STACKABLE_LOG_DIR}, +use crate::crd::{ + BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, LOG4J_CONFIG_FILE, LOG4J2_CONFIG_FILE, + STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LOG_CONFIG_DIR, + STACKABLE_LOG_DIR, + role::{broker::BROKER_PROPERTIES_FILE, controller::CONTROLLER_PROPERTIES_FILE}, + security::KafkaTlsSecurity, }; +/// The JVM options selecting the Kafka log4j/log4j2 config file. Kafka 3.x uses log4j, +/// Kafka 4.0 and higher use log4j2. +pub fn kafka_log_opts(product_version: &str) -> String { + if product_version.starts_with("3.") { + format!("-Dlog4j.configuration=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J_CONFIG_FILE}") + } else { + format!("-Dlog4j2.configurationFile=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J2_CONFIG_FILE}") + } +} + +/// The env var carrying the Kafka log4j options (see [`kafka_log_opts`]). +pub fn kafka_log_opts_env_var() -> String { + "KAFKA_LOG4J_OPTS".to_string() +} + /// Returns the commands to start the main Kafka container pub fn broker_kafka_container_commands( kraft_mode: bool, diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 5b14b9e7..09d16d4f 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -15,7 +15,9 @@ use stackable_operator::{ core::{DeserializeGuard, error_boundary}, runtime::{controller::Action, reflector::ObjectRef}, }, + kvp::ObjectLabels, logging::controller::ReconcilerError, + memory::{BinaryMultiple, MemoryQuantity}, role_utils::{GenericRoleConfig, RoleGroupRef}, shared::time::Duration, status::condition::{ @@ -53,6 +55,34 @@ use crate::{ pub const KAFKA_CONTROLLER_NAME: &str = "kafkacluster"; pub const KAFKA_FULL_CONTROLLER_NAME: &str = concatcp!(KAFKA_CONTROLLER_NAME, '.', OPERATOR_NAME); +/// The maximum size of a single Kafka log file before it is rotated. +pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { + value: 10.0, + unit: BinaryMultiple::Mebi, +}; + +/// Build recommended values for labels. +/// +/// Generic over the owner `T` so the owner can be either the raw `KafkaCluster` or the +/// [`ValidatedCluster`] (which also implements `Resource`). +pub fn build_recommended_labels<'a, T>( + owner: &'a T, + controller_name: &'a str, + app_version: &'a str, + role: &'a str, + role_group: &'a str, +) -> ObjectLabels<'a, T> { + ObjectLabels { + owner, + app_name: APP_NAME, + app_version, + operator_name: OPERATOR_NAME, + controller_name, + role, + role_group, + } +} + pub struct Ctx { pub client: stackable_operator::client::Client, pub operator_environment: OperatorEnvironmentOptions, diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 5445e6fd..9f87b8be 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -8,7 +8,10 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, ValidatedRoleGroupConfig}, + controller::{ + KAFKA_CONTROLLER_NAME, ValidatedCluster, ValidatedRoleGroupConfig, + build::properties::logging::role_group_config_map_data, build_recommended_labels, + }, crd::{ JVM_SECURITY_PROPERTIES_FILE, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, @@ -16,8 +19,6 @@ use crate::{ role::AnyConfig, v1alpha1, }, - product_logging::role_group_config_map_data, - utils::build_recommended_labels, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/controller/build/discovery.rs b/rust/operator-binary/src/controller/build/discovery.rs index 10ac621c..58a90499 100644 --- a/rust/operator-binary/src/controller/build/discovery.rs +++ b/rust/operator-binary/src/controller/build/discovery.rs @@ -9,9 +9,8 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, build_recommended_labels}, crd::{role::KafkaRole, v1alpha1}, - utils::build_recommended_labels, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/product_logging.rs b/rust/operator-binary/src/controller/build/properties/logging.rs similarity index 80% rename from rust/operator-binary/src/product_logging.rs rename to rust/operator-binary/src/controller/build/properties/logging.rs index 74afff30..76570a91 100644 --- a/rust/operator-binary/src/product_logging.rs +++ b/rust/operator-binary/src/controller/build/properties/logging.rs @@ -1,3 +1,6 @@ +//! Renders the logging config files (`log4j.properties` / `log4j2.properties` and the +//! Vector agent config) assembled into the rolegroup `ConfigMap`. + use std::{borrow::Cow, collections::BTreeMap, fmt::Display}; use stackable_operator::{ @@ -9,41 +12,21 @@ use stackable_operator::{ role_utils::RoleGroupRef, }; -use crate::crd::{ - role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, - v1alpha1, +use crate::{ + controller::MAX_KAFKA_LOG_FILES_SIZE, + crd::{ + LOG4J_CONFIG_FILE, LOG4J2_CONFIG_FILE, STACKABLE_LOG_DIR, + role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, + v1alpha1, + }, }; -pub const BROKER_ID_POD_MAP_DIR: &str = "/stackable/broker-id-pod-map"; -pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; -pub const STACKABLE_LOG_DIR: &str = "/stackable/log"; -// log4j -const LOG4J_CONFIG_FILE: &str = "log4j.properties"; const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; -// log4j2 -const LOG4J2_CONFIG_FILE: &str = "log4j2.properties"; const KAFKA_LOG4J2_FILE: &str = "kafka.log4j2.xml"; -// max size -pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { - value: 10.0, - unit: BinaryMultiple::Mebi, -}; const CONSOLE_CONVERSION_PATTERN_LOG4J: &str = "[%d] %p %m (%c)%n"; const CONSOLE_CONVERSION_PATTERN_LOG4J2: &str = "%d{ISO8601} %p [%t] %c - %m%n"; -pub fn kafka_log_opts(product_version: &str) -> String { - if product_version.starts_with("3.") { - format!("-Dlog4j.configuration=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J_CONFIG_FILE}") - } else { - format!("-Dlog4j2.configurationFile=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J2_CONFIG_FILE}") - } -} - -pub fn kafka_log_opts_env_var() -> String { - "KAFKA_LOG4J_OPTS".to_string() -} - /// Get the role group ConfigMap data with logging and Vector configurations pub fn role_group_config_map_data( product_version: &str, diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index 116f5c2f..4f83b22c 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -2,6 +2,7 @@ pub mod broker_properties; pub mod controller_properties; +pub mod logging; use crate::crd::{KafkaPodDescriptor, role::KafkaRole}; diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index e69c7a4c..740bc9ad 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -60,6 +60,12 @@ pub const STACKABLE_CONFIG_DIR: &str = "/stackable/config"; // kerberos pub const STACKABLE_KERBEROS_DIR: &str = "/stackable/kerberos"; pub const STACKABLE_KERBEROS_KRB5_PATH: &str = "/stackable/kerberos/krb5.conf"; +// logging +pub const STACKABLE_LOG_DIR: &str = "/stackable/log"; +pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; +pub const BROKER_ID_POD_MAP_DIR: &str = "/stackable/broker-id-pod-map"; +pub const LOG4J_CONFIG_FILE: &str = "log4j.properties"; +pub const LOG4J2_CONFIG_FILE: &str = "log4j2.properties"; #[derive(Snafu, Debug)] pub enum Error { diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index cd1c9434..21b2f466 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -46,9 +46,7 @@ mod crd; mod framework; mod kerberos; mod operations; -mod product_logging; mod resource; -mod utils; mod webhooks; mod built_info { diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/resource/listener.rs index bd62f668..f85245ec 100644 --- a/rust/operator-binary/src/resource/listener.rs +++ b/rust/operator-binary/src/resource/listener.rs @@ -4,9 +4,8 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, build_recommended_labels}, crd::{role::broker::BrokerConfig, security::KafkaTlsSecurity, v1alpha1}, - utils::build_recommended_labels, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/resource/service.rs b/rust/operator-binary/src/resource/service.rs index 631be187..5b3802d3 100644 --- a/rust/operator-binary/src/resource/service.rs +++ b/rust/operator-binary/src/resource/service.rs @@ -8,9 +8,8 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster}, + controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, build_recommended_labels}, crd::{APP_NAME, METRICS_PORT, METRICS_PORT_NAME, security::KafkaTlsSecurity, v1alpha1}, - utils::build_recommended_labels, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 1d274bd6..330331af 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -40,15 +40,22 @@ use stackable_operator::{ use crate::{ config::{ - command::{broker_kafka_container_commands, controller_kafka_container_command}, + command::{ + broker_kafka_container_commands, controller_kafka_container_command, kafka_log_opts, + kafka_log_opts_env_var, + }, node_id_hasher::node_id_hash32_offset, }, - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, ValidatedRoleGroupConfig}, + controller::{ + KAFKA_CONTROLLER_NAME, MAX_KAFKA_LOG_FILES_SIZE, ValidatedCluster, + ValidatedRoleGroupConfig, build_recommended_labels, + }, crd::{ - self, APP_NAME, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, + self, APP_NAME, BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, MetadataManager, STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, - STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, + STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, + STACKABLE_LOG_DIR, role::{ KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, controller::ControllerContainer, @@ -58,11 +65,6 @@ use crate::{ }, kerberos::add_kerberos_pod_config, operations::graceful_shutdown::add_graceful_shutdown_config, - product_logging::{ - BROKER_ID_POD_MAP_DIR, MAX_KAFKA_LOG_FILES_SIZE, STACKABLE_LOG_CONFIG_DIR, - STACKABLE_LOG_DIR, kafka_log_opts, kafka_log_opts_env_var, - }, - utils::build_recommended_labels, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/utils.rs b/rust/operator-binary/src/utils.rs deleted file mode 100644 index a6edb3c2..00000000 --- a/rust/operator-binary/src/utils.rs +++ /dev/null @@ -1,25 +0,0 @@ -use stackable_operator::kvp::ObjectLabels; - -use crate::crd::{APP_NAME, OPERATOR_NAME}; - -/// Build recommended values for labels. -/// -/// Generic over the owner `T` so the owner can be either the raw `KafkaCluster` or the -/// `ValidatedCluster` (which also implements `Resource`). -pub fn build_recommended_labels<'a, T>( - owner: &'a T, - controller_name: &'a str, - app_version: &'a str, - role: &'a str, - role_group: &'a str, -) -> ObjectLabels<'a, T> { - ObjectLabels { - owner, - app_name: APP_NAME, - app_version, - operator_name: OPERATOR_NAME, - controller_name, - role, - role_group, - } -} From ef12f34839d683fe7703cf367f8bc064988f0db4 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 10 Jun 2026 20:21:27 +0200 Subject: [PATCH 28/47] refactor: move ValidatedCluster to controller/mod.rs; add ConfigFileName enum; remove raw KafkaCluster from build_configmap --- rust/operator-binary/src/config/command.rs | 29 ++-- rust/operator-binary/src/config/jvm.rs | 9 +- .../src/controller/build/config_map.rs | 27 ++-- .../src/controller/build/discovery.rs | 3 +- .../controller/build/properties/logging.rs | 8 +- rust/operator-binary/src/controller/mod.rs | 137 ++++++++++++++++++ .../src/controller/validate.rs | 5 + rust/operator-binary/src/crd/config_file.rs | 51 +++++++ rust/operator-binary/src/crd/mod.rs | 6 +- rust/operator-binary/src/crd/role/broker.rs | 2 - .../src/crd/role/controller.rs | 2 - rust/operator-binary/src/crd/role/mod.rs | 17 ++- .../{controller.rs => kafka_controller.rs} | 122 +--------------- rust/operator-binary/src/main.rs | 9 +- rust/operator-binary/src/operations/pdb.rs | 2 +- rust/operator-binary/src/resource/listener.rs | 3 +- rust/operator-binary/src/resource/service.rs | 3 +- .../src/resource/statefulset.rs | 6 +- 18 files changed, 262 insertions(+), 179 deletions(-) create mode 100644 rust/operator-binary/src/controller/mod.rs create mode 100644 rust/operator-binary/src/crd/config_file.rs rename rust/operator-binary/src/{controller.rs => kafka_controller.rs} (78%) diff --git a/rust/operator-binary/src/config/command.rs b/rust/operator-binary/src/config/command.rs index 233ef4b1..7cdc5738 100644 --- a/rust/operator-binary/src/config/command.rs +++ b/rust/operator-binary/src/config/command.rs @@ -7,10 +7,8 @@ use stackable_operator::{ }; use crate::crd::{ - BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, LOG4J_CONFIG_FILE, LOG4J2_CONFIG_FILE, - STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LOG_CONFIG_DIR, - STACKABLE_LOG_DIR, - role::{broker::BROKER_PROPERTIES_FILE, controller::CONTROLLER_PROPERTIES_FILE}, + BROKER_ID_POD_MAP_DIR, ConfigFileName, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, + STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, security::KafkaTlsSecurity, }; @@ -18,9 +16,15 @@ use crate::crd::{ /// Kafka 4.0 and higher use log4j2. pub fn kafka_log_opts(product_version: &str) -> String { if product_version.starts_with("3.") { - format!("-Dlog4j.configuration=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J_CONFIG_FILE}") + format!( + "-Dlog4j.configuration=file:{STACKABLE_LOG_CONFIG_DIR}/{log4j}", + log4j = ConfigFileName::Log4j + ) } else { - format!("-Dlog4j2.configurationFile=file:{STACKABLE_LOG_CONFIG_DIR}/{LOG4J2_CONFIG_FILE}") + format!( + "-Dlog4j2.configurationFile=file:{STACKABLE_LOG_CONFIG_DIR}/{log4j2}", + log4j2 = ConfigFileName::Log4j2 + ) } } @@ -77,12 +81,13 @@ fn broker_start_command( cp {config_dir}/{properties_file} /tmp/{properties_file} config-utils template /tmp/{properties_file} - cp {config_dir}/jaas.properties /tmp/jaas.properties - config-utils template /tmp/jaas.properties + cp {config_dir}/{jaas_file} /tmp/{jaas_file} + config-utils template /tmp/{jaas_file} ", broker_id_pod_map_dir = BROKER_ID_POD_MAP_DIR, config_dir = STACKABLE_CONFIG_DIR, - properties_file = BROKER_PROPERTIES_FILE, + properties_file = ConfigFileName::BrokerProperties, + jaas_file = ConfigFileName::Jaas, }; if kraft_mode { @@ -92,7 +97,7 @@ fn broker_start_command( bin/kafka-storage.sh format --cluster-id \"$KAFKA_CLUSTER_ID\" --config /tmp/{properties_file} --ignore-formatted {initial_controller_command} bin/kafka-server-start.sh /tmp/{properties_file} & ", - properties_file = BROKER_PROPERTIES_FILE, + properties_file = ConfigFileName::BrokerProperties, initial_controller_command = initial_controllers_command(&controller_descriptors, product_version), } } else { @@ -100,7 +105,7 @@ fn broker_start_command( {common_command} bin/kafka-server-start.sh /tmp/{properties_file} &", - properties_file = BROKER_PROPERTIES_FILE, + properties_file = ConfigFileName::BrokerProperties, } } } @@ -172,7 +177,7 @@ pub fn controller_kafka_container_command( ", remove_vector_shutdown_file_command = remove_vector_shutdown_file_command(STACKABLE_LOG_DIR), config_dir = STACKABLE_CONFIG_DIR, - properties_file = CONTROLLER_PROPERTIES_FILE, + properties_file = ConfigFileName::ControllerProperties, initial_controller_command = initial_controllers_command(&controller_descriptors, product_version), create_vector_shutdown_file_command = create_vector_shutdown_file_command(STACKABLE_LOG_DIR) } diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/config/jvm.rs index b8e0acd6..f0233b66 100644 --- a/rust/operator-binary/src/config/jvm.rs +++ b/rust/operator-binary/src/config/jvm.rs @@ -6,9 +6,7 @@ use stackable_operator::{ schemars::JsonSchema, }; -use crate::crd::{ - JVM_SECURITY_PROPERTIES_FILE, METRICS_PORT, STACKABLE_CONFIG_DIR, role::AnyConfig, -}; +use crate::crd::{ConfigFileName, METRICS_PORT, STACKABLE_CONFIG_DIR, role::AnyConfig}; const JAVA_HEAP_FACTOR: f32 = 0.8; @@ -54,7 +52,10 @@ where // Heap settings format!("-Xmx{java_heap}"), format!("-Xms{java_heap}"), - format!("-Djava.security.properties={STACKABLE_CONFIG_DIR}/{JVM_SECURITY_PROPERTIES_FILE}"), + format!( + "-Djava.security.properties={STACKABLE_CONFIG_DIR}/{security}", + security = ConfigFileName::Security + ), format!( "-javaagent:/stackable/jmx/jmx_prometheus_javaagent.jar={METRICS_PORT}:/stackable/jmx/server.yaml" ), diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 9f87b8be..919f3a66 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -9,16 +9,17 @@ use stackable_operator::{ use crate::{ controller::{ - KAFKA_CONTROLLER_NAME, ValidatedCluster, ValidatedRoleGroupConfig, - build::properties::logging::role_group_config_map_data, build_recommended_labels, + ValidatedCluster, ValidatedRoleGroupConfig, + build::properties::logging::role_group_config_map_data, }, crd::{ - JVM_SECURITY_PROPERTIES_FILE, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, + ConfigFileName, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, listener::{KafkaListenerConfig, node_address_cmd}, role::AnyConfig, v1alpha1, }, + kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, }; #[derive(Snafu, Debug)] @@ -29,10 +30,7 @@ pub enum Error { rolegroup: RoleGroupRef, }, - #[snafu(display( - "failed to serialize [{JVM_SECURITY_PROPERTIES_FILE}] for {}", - rolegroup - ))] + #[snafu(display("failed to serialize [{}] for {rolegroup}", ConfigFileName::Security))] JvmSecurityProperties { source: PropertiesWriterError, rolegroup: String, @@ -60,7 +58,6 @@ pub enum Error { /// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator pub fn build_rolegroup_config_map( - kafka: &v1alpha1::KafkaCluster, validated_cluster: &ValidatedCluster, rolegroup: &RoleGroupRef, validated_rg: &ValidatedRoleGroupConfig, @@ -68,7 +65,7 @@ pub fn build_rolegroup_config_map( ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; - let kafka_config_file_name = validated_rg.config.config_file_name(); + let kafka_config_file_name = validated_rg.config.config_file_name().to_string(); let config_overrides = validated_rg .config_overrides .config_file_overrides() @@ -94,11 +91,9 @@ pub fn build_rolegroup_config_map( &validated_cluster.cluster_config.pod_descriptors, opa_connect.as_deref(), kraft_mode, - kafka - .spec + validated_cluster .cluster_config - .broker_id_pod_config_map_name - .is_some(), + .disable_broker_id_generation, config_overrides, ), AnyConfig::Controller(_) => { @@ -144,7 +139,7 @@ pub fn build_rolegroup_config_map( })?, ) .add_data( - JVM_SECURITY_PROPERTIES_FILE, + ConfigFileName::Security.to_string(), to_java_properties_string(jvm_sec_props.iter()).with_context(|_| { JvmSecurityPropertiesSnafu { rolegroup: rolegroup.role_group.clone(), @@ -152,7 +147,7 @@ pub fn build_rolegroup_config_map( })?, ) .add_data( - "client.properties", + ConfigFileName::Client.to_string(), to_java_properties_string( kafka_security .client_properties() @@ -168,7 +163,7 @@ pub fn build_rolegroup_config_map( // It is processed by `config-utils` to substitute "env:" and "file:" variables // and this tool currently doesn't support the JAAS login configuration format. .add_data( - "jaas.properties", + ConfigFileName::Jaas.to_string(), jaas_config_file(kafka_security.has_kerberos_enabled()), ); diff --git a/rust/operator-binary/src/controller/build/discovery.rs b/rust/operator-binary/src/controller/build/discovery.rs index 58a90499..598cb20e 100644 --- a/rust/operator-binary/src/controller/build/discovery.rs +++ b/rust/operator-binary/src/controller/build/discovery.rs @@ -9,8 +9,9 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, build_recommended_labels}, + controller::ValidatedCluster, crd::{role::KafkaRole, v1alpha1}, + kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/controller/build/properties/logging.rs b/rust/operator-binary/src/controller/build/properties/logging.rs index 76570a91..da198541 100644 --- a/rust/operator-binary/src/controller/build/properties/logging.rs +++ b/rust/operator-binary/src/controller/build/properties/logging.rs @@ -13,12 +13,12 @@ use stackable_operator::{ }; use crate::{ - controller::MAX_KAFKA_LOG_FILES_SIZE, crd::{ - LOG4J_CONFIG_FILE, LOG4J2_CONFIG_FILE, STACKABLE_LOG_DIR, + ConfigFileName, STACKABLE_LOG_DIR, role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, v1alpha1, }, + kafka_controller::MAX_KAFKA_LOG_FILES_SIZE, }; const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; @@ -44,7 +44,7 @@ pub fn role_group_config_map_data( match product_version.starts_with("3.") { true => { configs.insert( - LOG4J_CONFIG_FILE.to_string(), + ConfigFileName::Log4j.to_string(), log4j_config_if_automatic( Some(merged_config.kafka_logging()), container_name, @@ -55,7 +55,7 @@ pub fn role_group_config_map_data( } false => { configs.insert( - LOG4J2_CONFIG_FILE.to_string(), + ConfigFileName::Log4j2.to_string(), log4j2_config_if_automatic( Some(merged_config.kafka_logging()), container_name, diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs new file mode 100644 index 00000000..98ffb4f8 --- /dev/null +++ b/rust/operator-binary/src/controller/mod.rs @@ -0,0 +1,137 @@ +//! The validated cluster model and the steps that produce it. +//! +//! [`ValidatedCluster`] carries everything the build steps need, resolved once during +//! [`validate`] (after [`dereference`]) so downstream code never re-derives it or +//! touches the raw [`v1alpha1::KafkaCluster`] spec. The reconcile loop that consumes +//! it lives in [`crate::kafka_controller`]. + +use std::{borrow::Cow, collections::BTreeMap}; + +use stackable_operator::{ + commons::product_image_selection::ResolvedProductImage, + kube::{Resource, api::ObjectMeta}, + v2::types::{ + kubernetes::{NamespaceName, Uid}, + operator::ClusterName, + }, +}; + +pub(crate) mod build; +pub(crate) mod dereference; +pub(crate) mod validate; + +use crate::{ + crd::{ + KafkaPodDescriptor, MetadataManager, + authorization::KafkaAuthorizationConfig, + role::{AnyConfig, AnyConfigOverrides, KafkaRole}, + security::KafkaTlsSecurity, + v1alpha1, + }, + framework::role_utils::RoleGroupConfig, +}; + +pub type RoleGroupName = String; + +/// The validated cluster. Carries everything the build steps need, resolved once +/// here so downstream code never re-derives it or touches the raw spec. +/// +/// The cluster identity (`name`, `namespace`, `uid`) is captured here so that owner +/// references for child objects can be built straight from this struct (via its +/// [`Resource`] impl) without threading the raw [`v1alpha1::KafkaCluster`] around. +/// This mirrors the hive-/opensearch-operator's `ValidatedCluster`. +pub struct ValidatedCluster { + /// `ObjectMeta` carrying `name`, `namespace` and `uid`, so this struct can act as the + /// owner [`Resource`] for child objects. + metadata: ObjectMeta, + pub name: ClusterName, + pub namespace: NamespaceName, + pub image: ResolvedProductImage, + pub cluster_config: ValidatedClusterConfig, + pub role_group_configs: BTreeMap>, +} + +impl ValidatedCluster { + pub fn new( + name: ClusterName, + namespace: NamespaceName, + uid: Uid, + image: ResolvedProductImage, + cluster_config: ValidatedClusterConfig, + role_group_configs: BTreeMap>, + ) -> Self { + Self { + metadata: ObjectMeta { + name: Some(name.to_string()), + namespace: Some(namespace.to_string()), + uid: Some(uid.to_string()), + ..ObjectMeta::default() + }, + name, + namespace, + image, + cluster_config, + role_group_configs, + } + } +} + +/// Cluster-wide settings resolved during validation and dereferencing. +/// +/// Everything the build steps need is resolved here so they never have to read the +/// raw [`v1alpha1::KafkaCluster`] spec. +pub struct ValidatedClusterConfig { + pub kafka_security: KafkaTlsSecurity, + pub authorization_config: Option, + pub pod_descriptors: Vec, + pub metadata_manager: MetadataManager, + + /// Whether the operator must not generate broker ids itself, because the user + /// supplied a `broker_id_pod_config_map_name`. Resolved from the raw spec during + /// validation so the config-map builder never has to read it. + pub disable_broker_id_generation: bool, +} + +/// Lets [`ValidatedCluster`] act as the owner [`Resource`] for child objects, so owner +/// references are built from it (via the captured `metadata`) rather than the raw CR. +impl Resource for ValidatedCluster { + type DynamicType = ::DynamicType; + type Scope = ::Scope; + + fn kind(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::kind(dt) + } + + fn group(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::group(dt) + } + + fn version(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::version(dt) + } + + fn plural(dt: &Self::DynamicType) -> Cow<'_, str> { + v1alpha1::KafkaCluster::plural(dt) + } + + fn meta(&self) -> &ObjectMeta { + &self.metadata + } + + fn meta_mut(&mut self) -> &mut ObjectMeta { + &mut self.metadata + } +} + +/// A validated, merged Kafka role-group config. +/// +/// The merged config fragment is wrapped in [`AnyConfig`] and the merged +/// `configOverrides` in [`AnyConfigOverrides`], so a single role-agnostic type +/// carries both broker and controller role groups (their concrete config and +/// override types differ). Produced via the local-`framework` +/// [`with_validated_config`](crate::framework::role_utils::with_validated_config). +pub type ValidatedRoleGroupConfig = RoleGroupConfig< + AnyConfig, + stackable_operator::role_utils::JavaCommonConfig, + AnyConfigOverrides, +>; diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 1f60f095..5c5edc06 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -191,6 +191,11 @@ pub fn validate( authorization_config: dereferenced_objects.authorization_config, pod_descriptors, metadata_manager, + disable_broker_id_generation: kafka + .spec + .cluster_config + .broker_id_pod_config_map_name + .is_some(), }, role_group_configs, )) diff --git a/rust/operator-binary/src/crd/config_file.rs b/rust/operator-binary/src/crd/config_file.rs new file mode 100644 index 00000000..1801be9d --- /dev/null +++ b/rust/operator-binary/src/crd/config_file.rs @@ -0,0 +1,51 @@ +//! The names of the config files assembled into the rolegroup `ConfigMap`. +//! +//! A single source of truth for the on-disk file names, used by the config-map +//! builder, the per-file property builders, the JVM/command builders and +//! [`AnyConfig::config_file_name`](crate::crd::role::AnyConfig::config_file_name). +//! Mirrors the hive-operator's `ConfigFileName`. + +/// The names of the Kafka config files assembled into the rolegroup `ConfigMap`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] +pub enum ConfigFileName { + #[strum(serialize = "broker.properties")] + BrokerProperties, + #[strum(serialize = "controller.properties")] + ControllerProperties, + #[strum(serialize = "security.properties")] + Security, + #[strum(serialize = "client.properties")] + Client, + /// JAAS configuration for Kerberos authentication. It has the `.properties` + /// extension but is not a Java properties file. + #[strum(serialize = "jaas.properties")] + Jaas, + /// Used by Kafka 3.x. + #[strum(serialize = "log4j.properties")] + Log4j, + /// Used by Kafka 4.0 and later. + #[strum(serialize = "log4j2.properties")] + Log4j2, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_names_match_the_kafka_on_disk_names() { + assert_eq!( + ConfigFileName::BrokerProperties.to_string(), + "broker.properties" + ); + assert_eq!( + ConfigFileName::ControllerProperties.to_string(), + "controller.properties" + ); + assert_eq!(ConfigFileName::Security.to_string(), "security.properties"); + assert_eq!(ConfigFileName::Client.to_string(), "client.properties"); + assert_eq!(ConfigFileName::Jaas.to_string(), "jaas.properties"); + assert_eq!(ConfigFileName::Log4j.to_string(), "log4j.properties"); + assert_eq!(ConfigFileName::Log4j2.to_string(), "log4j2.properties"); + } +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 740bc9ad..01a38ffd 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,6 +1,7 @@ pub mod affinity; pub mod authentication; pub mod authorization; +pub mod config_file; pub mod listener; pub mod role; pub mod security; @@ -9,6 +10,7 @@ pub mod tls; use std::collections::{BTreeMap, HashMap}; use authentication::KafkaAuthentication; +pub use config_file::ConfigFileName; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, Snafu}; use stackable_operator::{ @@ -44,8 +46,6 @@ pub const FIELD_MANAGER: &str = "kafka-operator"; // metrics pub const METRICS_PORT_NAME: &str = "metrics"; pub const METRICS_PORT: u16 = 9606; -// config files -pub const JVM_SECURITY_PROPERTIES_FILE: &str = "security.properties"; // env vars pub const KAFKA_HEAP_OPTS: &str = "KAFKA_HEAP_OPTS"; // server_properties @@ -64,8 +64,6 @@ pub const STACKABLE_KERBEROS_KRB5_PATH: &str = "/stackable/kerberos/krb5.conf"; pub const STACKABLE_LOG_DIR: &str = "/stackable/log"; pub const STACKABLE_LOG_CONFIG_DIR: &str = "/stackable/log_config"; pub const BROKER_ID_POD_MAP_DIR: &str = "/stackable/broker-id-pod-map"; -pub const LOG4J_CONFIG_FILE: &str = "log4j.properties"; -pub const LOG4J2_CONFIG_FILE: &str = "log4j2.properties"; #[derive(Snafu, Debug)] pub enum Error { diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 64ebf848..67396876 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -13,8 +13,6 @@ use strum::{Display, EnumIter}; use crate::crd::role::commons::{CommonConfig, Storage, StorageFragment}; -pub const BROKER_PROPERTIES_FILE: &str = "broker.properties"; - #[derive( Clone, Debug, diff --git a/rust/operator-binary/src/crd/role/controller.rs b/rust/operator-binary/src/crd/role/controller.rs index 27756ee1..75f93cdb 100644 --- a/rust/operator-binary/src/crd/role/controller.rs +++ b/rust/operator-binary/src/crd/role/controller.rs @@ -13,8 +13,6 @@ use strum::{Display, EnumIter}; use crate::crd::role::commons::{CommonConfig, Storage, StorageFragment}; -pub const CONTROLLER_PROPERTIES_FILE: &str = "controller.properties"; - #[derive( Clone, Debug, diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index a7e05881..41cdcf1f 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -19,10 +19,13 @@ use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; use crate::{ config::jvm::{construct_heap_jvm_args, construct_non_heap_jvm_args}, - crd::role::{ - broker::{BROKER_PROPERTIES_FILE, BrokerConfig}, - commons::{CommonConfig, Storage}, - controller::{CONTROLLER_PROPERTIES_FILE, ControllerConfig}, + crd::{ + ConfigFileName, + role::{ + broker::BrokerConfig, + commons::{CommonConfig, Storage}, + controller::ControllerConfig, + }, }, v1alpha1, }; @@ -355,10 +358,10 @@ impl AnyConfig { } } - pub fn config_file_name(&self) -> &str { + pub fn config_file_name(&self) -> ConfigFileName { match self { - AnyConfig::Broker(_) => BROKER_PROPERTIES_FILE, - AnyConfig::Controller(_) => CONTROLLER_PROPERTIES_FILE, + AnyConfig::Broker(_) => ConfigFileName::BrokerProperties, + AnyConfig::Controller(_) => ConfigFileName::ControllerProperties, } } } diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/kafka_controller.rs similarity index 78% rename from rust/operator-binary/src/controller.rs rename to rust/operator-binary/src/kafka_controller.rs index 09d16d4f..a1c0bf54 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/kafka_controller.rs @@ -1,17 +1,17 @@ //! Ensures that `Pod`s are configured and running for each [`v1alpha1::KafkaCluster`]. -use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; +use std::sync::Arc; use const_format::concatcp; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::{product_image_selection::ResolvedProductImage, rbac::build_rbac_resources}, + commons::rbac::build_rbac_resources, crd::listener, kube::{ Resource, - api::{DynamicObject, ObjectMeta}, + api::DynamicObject, core::{DeserializeGuard, error_boundary}, runtime::{controller::Action, reflector::ObjectRef}, }, @@ -24,24 +24,15 @@ use stackable_operator::{ compute_conditions, operations::ClusterOperationsConditionBuilder, statefulset::StatefulSetConditionBuilder, }, - v2::types::{ - kubernetes::{NamespaceName, Uid}, - operator::ClusterName, - }, }; use strum::{EnumDiscriminants, IntoStaticStr}; -pub(crate) mod build; -mod dereference; -mod validate; - use crate::{ + controller::{build, dereference, validate}, crd::{ - APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, - authorization::KafkaAuthorizationConfig, + APP_NAME, KafkaClusterStatus, OPERATOR_NAME, listener::get_kafka_listener_config, - role::{AnyConfig, AnyConfigOverrides, KafkaRole}, - security::KafkaTlsSecurity, + role::{AnyConfig, KafkaRole}, v1alpha1, }, operations::pdb::add_pdbs, @@ -235,106 +226,6 @@ impl ReconcilerError for Error { } } -pub type RoleGroupName = String; - -/// The validated cluster. Carries everything the build steps need, resolved once -/// here so downstream code never re-derives it or touches the raw spec. -/// -/// The cluster identity (`name`, `namespace`, `uid`) is captured here so that owner -/// references for child objects can be built straight from this struct (via its -/// [`Resource`] impl) without threading the raw [`v1alpha1::KafkaCluster`] around. -/// This mirrors the hive-/opensearch-operator's `ValidatedCluster`. -pub struct ValidatedCluster { - /// `ObjectMeta` carrying `name`, `namespace` and `uid`, so this struct can act as the - /// owner [`Resource`] for child objects. - metadata: ObjectMeta, - pub name: ClusterName, - pub namespace: NamespaceName, - pub image: ResolvedProductImage, - pub cluster_config: ValidatedClusterConfig, - pub role_group_configs: BTreeMap>, -} - -impl ValidatedCluster { - pub fn new( - name: ClusterName, - namespace: NamespaceName, - uid: Uid, - image: ResolvedProductImage, - cluster_config: ValidatedClusterConfig, - role_group_configs: BTreeMap>, - ) -> Self { - Self { - metadata: ObjectMeta { - name: Some(name.to_string()), - namespace: Some(namespace.to_string()), - uid: Some(uid.to_string()), - ..ObjectMeta::default() - }, - name, - namespace, - image, - cluster_config, - role_group_configs, - } - } -} - -/// Cluster-wide settings resolved during validation and dereferencing. -/// -/// Everything the build steps need is resolved here so they never have to read the -/// raw [`v1alpha1::KafkaCluster`] spec. -pub struct ValidatedClusterConfig { - pub kafka_security: KafkaTlsSecurity, - pub authorization_config: Option, - pub pod_descriptors: Vec, - pub metadata_manager: MetadataManager, -} - -/// Lets [`ValidatedCluster`] act as the owner [`Resource`] for child objects, so owner -/// references are built from it (via the captured `metadata`) rather than the raw CR. -impl Resource for ValidatedCluster { - type DynamicType = ::DynamicType; - type Scope = ::Scope; - - fn kind(dt: &Self::DynamicType) -> Cow<'_, str> { - v1alpha1::KafkaCluster::kind(dt) - } - - fn group(dt: &Self::DynamicType) -> Cow<'_, str> { - v1alpha1::KafkaCluster::group(dt) - } - - fn version(dt: &Self::DynamicType) -> Cow<'_, str> { - v1alpha1::KafkaCluster::version(dt) - } - - fn plural(dt: &Self::DynamicType) -> Cow<'_, str> { - v1alpha1::KafkaCluster::plural(dt) - } - - fn meta(&self) -> &ObjectMeta { - &self.metadata - } - - fn meta_mut(&mut self) -> &mut ObjectMeta { - &mut self.metadata - } -} - -/// A validated, merged Kafka role-group config. -/// -/// The merged config fragment is wrapped in [`AnyConfig`] and the merged -/// `configOverrides` in [`AnyConfigOverrides`], so a single role-agnostic type -/// carries both broker and controller role groups (their concrete config and -/// override types differ). Produced via the local-`framework` -/// [`with_validated_config`](crate::framework::role_utils::with_validated_config). -pub type ValidatedRoleGroupConfig = crate::framework::role_utils::RoleGroupConfig< - AnyConfig, - stackable_operator::role_utils::JavaCommonConfig, - AnyConfigOverrides, ->; - pub async fn reconcile_kafka( kafka: Arc>, ctx: Arc, @@ -427,7 +318,6 @@ pub async fn reconcile_kafka( .context(InvalidKafkaListenersSnafu)?; let rg_configmap = build::config_map::build_rolegroup_config_map( - kafka, &validated_cluster, &rolegroup_ref, validated_rg, diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 21b2f466..fd1e1b9a 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -35,8 +35,8 @@ use stackable_operator::{ }; use crate::{ - controller::KAFKA_FULL_CONTROLLER_NAME, crd::{KafkaCluster, KafkaClusterVersion, OPERATOR_NAME, v1alpha1}, + kafka_controller::KAFKA_FULL_CONTROLLER_NAME, webhooks::conversion::create_webhook_server, }; @@ -44,6 +44,7 @@ mod config; mod controller; mod crd; mod framework; +mod kafka_controller; mod kerberos; mod operations; mod resource; @@ -176,9 +177,9 @@ async fn main() -> anyhow::Result<()> { ) .graceful_shutdown_on(sigterm_watcher.handle()) .run( - controller::reconcile_kafka, - controller::error_policy, - Arc::new(controller::Ctx { + kafka_controller::reconcile_kafka, + kafka_controller::error_policy, + Arc::new(kafka_controller::Ctx { client: client.clone(), operator_environment, }), diff --git a/rust/operator-binary/src/operations/pdb.rs b/rust/operator-binary/src/operations/pdb.rs index e42888ca..18f46dc7 100644 --- a/rust/operator-binary/src/operations/pdb.rs +++ b/rust/operator-binary/src/operations/pdb.rs @@ -5,8 +5,8 @@ use stackable_operator::{ }; use crate::{ - controller::KAFKA_CONTROLLER_NAME, crd::{APP_NAME, OPERATOR_NAME, role::KafkaRole, v1alpha1}, + kafka_controller::KAFKA_CONTROLLER_NAME, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/resource/listener.rs index f85245ec..d22a2a96 100644 --- a/rust/operator-binary/src/resource/listener.rs +++ b/rust/operator-binary/src/resource/listener.rs @@ -4,8 +4,9 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, build_recommended_labels}, + controller::ValidatedCluster, crd::{role::broker::BrokerConfig, security::KafkaTlsSecurity, v1alpha1}, + kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/resource/service.rs b/rust/operator-binary/src/resource/service.rs index 5b3802d3..8f4fa0e3 100644 --- a/rust/operator-binary/src/resource/service.rs +++ b/rust/operator-binary/src/resource/service.rs @@ -8,8 +8,9 @@ use stackable_operator::{ }; use crate::{ - controller::{KAFKA_CONTROLLER_NAME, ValidatedCluster, build_recommended_labels}, + controller::ValidatedCluster, crd::{APP_NAME, METRICS_PORT, METRICS_PORT_NAME, security::KafkaTlsSecurity, v1alpha1}, + kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 330331af..3df261a2 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -46,10 +46,7 @@ use crate::{ }, node_id_hasher::node_id_hash32_offset, }, - controller::{ - KAFKA_CONTROLLER_NAME, MAX_KAFKA_LOG_FILES_SIZE, ValidatedCluster, - ValidatedRoleGroupConfig, build_recommended_labels, - }, + controller::{ValidatedCluster, ValidatedRoleGroupConfig}, crd::{ self, APP_NAME, BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, @@ -63,6 +60,7 @@ use crate::{ security::KafkaTlsSecurity, v1alpha1, }, + kafka_controller::{KAFKA_CONTROLLER_NAME, MAX_KAFKA_LOG_FILES_SIZE, build_recommended_labels}, kerberos::add_kerberos_pod_config, operations::graceful_shutdown::add_graceful_shutdown_config, }; From c72aa851188375feb40782ace47f53d2c7e36263 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Thu, 11 Jun 2026 08:49:00 +0200 Subject: [PATCH 29/47] refactor: make better use of ValidatedCluster in property file builders --- .../src/controller/build/config_map.rs | 28 ++++--------------- .../build/properties/broker_properties.rs | 21 ++++++-------- .../build/properties/controller_properties.rs | 13 ++++----- rust/operator-binary/src/controller/mod.rs | 14 ++++++++++ 4 files changed, 33 insertions(+), 43 deletions(-) diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 919f3a66..3db8337e 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -13,8 +13,7 @@ use crate::{ build::properties::logging::role_group_config_map_data, }, crd::{ - ConfigFileName, MetadataManager, STACKABLE_LISTENER_BOOTSTRAP_DIR, - STACKABLE_LISTENER_BROKER_DIR, + ConfigFileName, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, listener::{KafkaListenerConfig, node_address_cmd}, role::AnyConfig, v1alpha1, @@ -63,7 +62,8 @@ pub fn build_rolegroup_config_map( validated_rg: &ValidatedRoleGroupConfig, listener_config: &KafkaListenerConfig, ) -> Result { - let kafka_security = &validated_cluster.cluster_config.kafka_security; + let cluster_config = &validated_cluster.cluster_config; + let kafka_security = &cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let kafka_config_file_name = validated_rg.config.config_file_name().to_string(); let config_overrides = validated_rg @@ -72,36 +72,20 @@ pub fn build_rolegroup_config_map( .overrides .clone(); - let opa_connect = validated_cluster - .cluster_config - .authorization_config - .as_ref() - .map(|auth_config| auth_config.opa_connect.clone()); - - let kraft_mode = validated_cluster.cluster_config.metadata_manager == MetadataManager::KRaft; - - if kraft_mode && validated_cluster.cluster_config.pod_descriptors.is_empty() { + if cluster_config.is_kraft_mode() && cluster_config.pod_descriptors.is_empty() { return NoKraftControllersFoundSnafu.fail(); } let kafka_config = match &validated_rg.config { AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( - kafka_security, + cluster_config, listener_config, - &validated_cluster.cluster_config.pod_descriptors, - opa_connect.as_deref(), - kraft_mode, - validated_cluster - .cluster_config - .disable_broker_id_generation, config_overrides, ), AnyConfig::Controller(_) => { crate::controller::build::properties::controller_properties::build( - kafka_security, + cluster_config, listener_config, - &validated_cluster.cluster_config.pod_descriptors, - kraft_mode, config_overrides, ) } diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs index 44840363..700e0822 100644 --- a/rust/operator-binary/src/controller/build/properties/broker_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -2,29 +2,24 @@ use std::collections::BTreeMap; use super::kraft_controllers; use crate::{ + controller::ValidatedClusterConfig, crd::{ - KafkaPodDescriptor, listener::{KafkaListenerConfig, KafkaListenerName}, role::{ KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, }, - security::KafkaTlsSecurity, }, operations::graceful_shutdown::graceful_shutdown_config_properties, }; pub fn build( - kafka_security: &KafkaTlsSecurity, + cluster_config: &ValidatedClusterConfig, listener_config: &KafkaListenerConfig, - pod_descriptors: &[KafkaPodDescriptor], - opa_connect_string: Option<&str>, - kraft_mode: bool, - disable_broker_id_generation: bool, overrides: BTreeMap, ) -> BTreeMap { - let kraft_controllers = kraft_controllers(pod_descriptors); + let kraft_controllers = kraft_controllers(&cluster_config.pod_descriptors); let mut result = BTreeMap::from([ ( @@ -46,7 +41,7 @@ pub fn build( ), ]); - if kraft_mode { + if cluster_config.is_kraft_mode() { let kraft_controllers = kraft_controllers.join(","); // Running in KRaft mode @@ -79,7 +74,7 @@ pub fn build( // so we disable automatic id generation. // This check ensures that existing clusters running in ZooKeeper mode do not // suddenly break after the introduction of this change. - if disable_broker_id_generation { + if cluster_config.disable_broker_id_generation { result.extend([ ( "broker.id.generation.enable".to_string(), @@ -91,7 +86,7 @@ pub fn build( } // Enable OPA authorization - if opa_connect_string.is_some() { + if let Some(opa_connect_string) = cluster_config.opa_connect() { result.extend([ ( "authorizer.class.name".to_string(), @@ -103,12 +98,12 @@ pub fn build( ), ( "opa.authorizer.url".to_string(), - opa_connect_string.unwrap_or_default().to_string(), + opa_connect_string.to_string(), ), ]); } - result.extend(kafka_security.broker_config_settings()); + result.extend(cluster_config.kafka_security.broker_config_settings()); result.extend(graceful_shutdown_config_properties()); result.extend(overrides); diff --git a/rust/operator-binary/src/controller/build/properties/controller_properties.rs b/rust/operator-binary/src/controller/build/properties/controller_properties.rs index ba825aec..289d0cb2 100644 --- a/rust/operator-binary/src/controller/build/properties/controller_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -2,26 +2,23 @@ use std::collections::BTreeMap; use super::kraft_controllers; use crate::{ + controller::ValidatedClusterConfig, crd::{ - KafkaPodDescriptor, listener::{KafkaListenerConfig, KafkaListenerName}, role::{ KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, }, - security::KafkaTlsSecurity, }, operations::graceful_shutdown::graceful_shutdown_config_properties, }; pub fn build( - kafka_security: &KafkaTlsSecurity, + cluster_config: &ValidatedClusterConfig, listener_config: &KafkaListenerConfig, - pod_descriptors: &[KafkaPodDescriptor], - kraft_mode: bool, overrides: BTreeMap, ) -> BTreeMap { - let kraft_controllers = kraft_controllers(pod_descriptors).join(","); + let kraft_controllers = kraft_controllers(&cluster_config.pod_descriptors).join(","); let mut result = BTreeMap::from([ ( @@ -58,14 +55,14 @@ pub fn build( // The ZooKeeper connection is needed for migration from ZooKeeper to KRaft mode. // It is not needed once the controller is fully running in KRaft mode. - if !kraft_mode { + if !cluster_config.is_kraft_mode() { result.insert( "zookeeper.connect".to_string(), "${env:ZOOKEEPER}".to_string(), ); } - result.extend(kafka_security.controller_config_settings()); + result.extend(cluster_config.kafka_security.controller_config_settings()); result.extend(graceful_shutdown_config_properties()); result.extend(overrides); diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs index 98ffb4f8..2c907057 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller/mod.rs @@ -92,6 +92,20 @@ pub struct ValidatedClusterConfig { pub disable_broker_id_generation: bool, } +impl ValidatedClusterConfig { + /// Whether the cluster runs in KRaft mode (as opposed to ZooKeeper mode). + pub fn is_kraft_mode(&self) -> bool { + self.metadata_manager == MetadataManager::KRaft + } + + /// The OPA connect string, if OPA authorization is configured. + pub fn opa_connect(&self) -> Option<&str> { + self.authorization_config + .as_ref() + .map(|auth_config| auth_config.opa_connect.as_str()) + } +} + /// Lets [`ValidatedCluster`] act as the owner [`Resource`] for child objects, so owner /// references are built from it (via the captured `metadata`) rather than the raw CR. impl Resource for ValidatedCluster { From f580049730adf5718cf89647e7cb5da12b4a89e6 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:14:43 +0200 Subject: [PATCH 30/47] Update rust/operator-binary/src/crd/mod.rs Co-authored-by: maltesander --- rust/operator-binary/src/crd/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 01a38ffd..630595f6 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -245,9 +245,6 @@ pub mod versioned { pub broker_id_pod_config_map_name: Option, } - // Uses the v2 KeyValueConfigOverrides (plain string values) to match trino/hdfs. - // Derives `Merge` so role/role-group overrides combine via the shared merge logic; - // resolution into flat maps happens in controller/validate.rs. #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Merge, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaBrokerConfigOverrides { From 18df1437145552c4009851dd5e9774441360195b Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:16:53 +0200 Subject: [PATCH 31/47] Apply suggestions from code review Co-authored-by: maltesander --- rust/operator-binary/src/controller/build/discovery.rs | 1 - rust/operator-binary/src/crd/config_file.rs | 1 - rust/operator-binary/src/crd/role/broker.rs | 1 - rust/operator-binary/src/crd/role/controller.rs | 1 - 4 files changed, 4 deletions(-) diff --git a/rust/operator-binary/src/controller/build/discovery.rs b/rust/operator-binary/src/controller/build/discovery.rs index 598cb20e..ef78a7e8 100644 --- a/rust/operator-binary/src/controller/build/discovery.rs +++ b/rust/operator-binary/src/controller/build/discovery.rs @@ -65,7 +65,6 @@ pub fn build_discovery_configmap( .metadata( ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(validated_cluster.name.to_string()) .ownerreference_from_resource(validated_cluster, None, Some(true)) .with_context(|_| ObjectMissingMetadataForOwnerRefSnafu { kafka: cluster_object_ref(validated_cluster), diff --git a/rust/operator-binary/src/crd/config_file.rs b/rust/operator-binary/src/crd/config_file.rs index 1801be9d..aec47d39 100644 --- a/rust/operator-binary/src/crd/config_file.rs +++ b/rust/operator-binary/src/crd/config_file.rs @@ -3,7 +3,6 @@ //! A single source of truth for the on-disk file names, used by the config-map //! builder, the per-file property builders, the JVM/command builders and //! [`AnyConfig::config_file_name`](crate::crd::role::AnyConfig::config_file_name). -//! Mirrors the hive-operator's `ConfigFileName`. /// The names of the Kafka config files assembled into the rolegroup `ConfigMap`. #[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 67396876..97d09613 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -93,4 +93,3 @@ impl BrokerConfig { } } -// KAFKA_CLUSTER_ID injection moved to controller/validate.rs::collect_broker_role_group_overrides. diff --git a/rust/operator-binary/src/crd/role/controller.rs b/rust/operator-binary/src/crd/role/controller.rs index 75f93cdb..7e83c770 100644 --- a/rust/operator-binary/src/crd/role/controller.rs +++ b/rust/operator-binary/src/crd/role/controller.rs @@ -84,4 +84,3 @@ impl ControllerConfig { } } -// KAFKA_CLUSTER_ID injection moved to controller/validate.rs::collect_controller_role_group_overrides. From f3a1ae12e9ab9e78a5b3f180b985ce16007fe451 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 11 Jun 2026 09:42:58 +0200 Subject: [PATCH 32/47] linting --- rust/operator-binary/src/crd/role/broker.rs | 1 - rust/operator-binary/src/crd/role/controller.rs | 1 - rust/operator-binary/src/kafka_controller.rs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 97d09613..2024d519 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -92,4 +92,3 @@ impl BrokerConfig { } } } - diff --git a/rust/operator-binary/src/crd/role/controller.rs b/rust/operator-binary/src/crd/role/controller.rs index 7e83c770..fbaf898c 100644 --- a/rust/operator-binary/src/crd/role/controller.rs +++ b/rust/operator-binary/src/crd/role/controller.rs @@ -83,4 +83,3 @@ impl ControllerConfig { } } } - diff --git a/rust/operator-binary/src/kafka_controller.rs b/rust/operator-binary/src/kafka_controller.rs index a1c0bf54..e1186e10 100644 --- a/rust/operator-binary/src/kafka_controller.rs +++ b/rust/operator-binary/src/kafka_controller.rs @@ -55,7 +55,7 @@ pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { /// Build recommended values for labels. /// /// Generic over the owner `T` so the owner can be either the raw `KafkaCluster` or the -/// [`ValidatedCluster`] (which also implements `Resource`). +/// `ValidatedCluster` (which also implements `Resource`). pub fn build_recommended_labels<'a, T>( owner: &'a T, controller_name: &'a str, From c3a263e4db39379f9e4e24a682746ba888bf2dab Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 11 Jun 2026 09:44:04 +0200 Subject: [PATCH 33/47] cleaned up comment --- rust/operator-binary/src/controller/validate.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 5c5edc06..973a8ce1 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -254,10 +254,7 @@ where /// Injects the operator-managed `KAFKA_CLUSTER_ID` into the merged env overrides, /// but only when the user has not already set it via `envOverrides` (user value -/// wins, preserving product-config's old precedence). -/// -/// `KAFKA_CLUSTER_ID` injection moved here from the now-removed -/// `crd::role::*::Configuration::compute_env`. +/// wins). fn inject_cluster_id(env_overrides: EnvVarSet, cluster_id: Option<&str>) -> Result { let Some(cluster_id) = cluster_id else { return Ok(env_overrides); From f0e6cbb70f58eb69441ed24af0e72e58ee55741f Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Thu, 11 Jun 2026 21:46:04 +0200 Subject: [PATCH 34/47] refactor: switch to v2 JavaCommonConfig & remove framework module --- Cargo.lock | 266 ++++++------ Cargo.nix | 390 ++++++++---------- rust/operator-binary/src/config/jvm.rs | 115 ++---- rust/operator-binary/src/controller/mod.rs | 83 +++- .../src/controller/validate.rs | 88 ++-- rust/operator-binary/src/crd/affinity.rs | 26 +- rust/operator-binary/src/crd/mod.rs | 4 +- rust/operator-binary/src/crd/role/broker.rs | 2 +- .../src/crd/role/controller.rs | 2 +- rust/operator-binary/src/crd/role/mod.rs | 58 +-- rust/operator-binary/src/framework.rs | 11 - .../src/framework/role_utils.rs | 152 ------- rust/operator-binary/src/main.rs | 1 - .../src/resource/statefulset.rs | 34 +- 14 files changed, 482 insertions(+), 750 deletions(-) delete mode 100644 rust/operator-binary/src/framework.rs delete mode 100644 rust/operator-binary/src/framework/role_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 92a70017..1a4cdc09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,9 +163,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -265,9 +265,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" dependencies = [ "chrono", "git2", @@ -290,9 +290,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" @@ -302,9 +302,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.61" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -320,9 +320,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -613,9 +613,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -676,9 +676,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -918,9 +918,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -975,15 +975,14 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.4" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" dependencies = [ "bitflags", "libc", "libgit2-sys", "log", - "url", ] [[package]] @@ -1017,9 +1016,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1047,9 +1046,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1079,9 +1078,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1130,9 +1129,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1328,9 +1327,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1343,7 +1342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", ] [[package]] @@ -1361,16 +1360,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1405,9 +1394,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1415,14 +1404,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -1456,27 +1445,26 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] [[package]] name = "json-patch" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" dependencies = [ "jsonptr", "schemars", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -1518,7 +1506,7 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "darling", "regex", @@ -1665,9 +1653,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", @@ -1683,9 +1671,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", @@ -1710,9 +1698,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "matchers" @@ -1731,9 +1719,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mime" @@ -1753,9 +1741,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1789,9 +1777,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2056,18 +2044,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -2191,9 +2179,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -2201,9 +2189,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", @@ -2214,9 +2202,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -2325,9 +2313,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -2348,9 +2336,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -2474,9 +2462,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -2489,9 +2477,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2677,9 +2665,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2757,9 +2745,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -2864,9 +2852,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2897,7 +2885,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackable-certs" version = "0.4.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "const-oid", "ecdsa", @@ -2942,7 +2930,7 @@ dependencies = [ [[package]] name = "stackable-operator" version = "0.111.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "base64", "clap", @@ -2986,7 +2974,7 @@ dependencies = [ [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "darling", "proc-macro2", @@ -2997,7 +2985,7 @@ dependencies = [ [[package]] name = "stackable-shared" version = "0.1.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "jiff", "k8s-openapi", @@ -3014,7 +3002,7 @@ dependencies = [ [[package]] name = "stackable-telemetry" version = "0.6.4" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "axum", "clap", @@ -3038,7 +3026,7 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "kube", "schemars", @@ -3052,7 +3040,7 @@ dependencies = [ [[package]] name = "stackable-versioned-macros" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "convert_case", "convert_case_extras", @@ -3070,7 +3058,7 @@ dependencies = [ [[package]] name = "stackable-webhook" version = "0.9.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#451088f77acee6c3d296754698260256c250ecb2" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" dependencies = [ "arc-swap", "async-trait", @@ -3293,9 +3281,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3365,9 +3353,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", "toml_datetime", @@ -3386,9 +3374,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "base64", @@ -3413,9 +3401,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -3424,9 +3412,9 @@ dependencies = [ [[package]] name = "tonic-types" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a875a902255423d34c1f20838ab374126db8eb41625b7947a1d54113b0b7399" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" dependencies = [ "prost", "prost-types", @@ -3454,9 +3442,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "base64", "bitflags", @@ -3464,13 +3452,13 @@ dependencies = [ "futures-util", "http", "http-body", - "iri-string", "mime", "pin-project-lite", "tower", "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -3597,9 +3585,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3615,9 +3603,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -3664,9 +3652,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.2" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "js-sys", "wasm-bindgen", @@ -3716,9 +3704,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" dependencies = [ "cfg-if", "once_cell", @@ -3729,9 +3717,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" dependencies = [ "js-sys", "wasm-bindgen", @@ -3739,9 +3727,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3749,9 +3737,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" dependencies = [ "bumpalo", "proc-macro2", @@ -3762,18 +3750,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" dependencies = [ "js-sys", "wasm-bindgen", @@ -3932,9 +3920,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -3973,9 +3961,9 @@ checksum = "636f85e5ca6488e96401b61eb7de54f4e44755c988af0f52cf90230c312a1a89" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3996,18 +3984,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -4016,9 +4004,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/Cargo.nix b/Cargo.nix index 43e426e5..a35cdcc9 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -489,9 +489,9 @@ rec { }; "autocfg" = rec { crateName = "autocfg"; - version = "1.5.0"; + version = "1.5.1"; edition = "2015"; - sha256 = "1s77f98id9l4af4alklmzq46f21c980v13z2r1pcxx6bqgw0d1n0"; + sha256 = "0lqasy5i30flcgih1b50kvsk6z32g09r1q4ql7q81pj6228jy0zj"; authors = [ "Josh Stone " ]; @@ -866,9 +866,9 @@ rec { }; "bitflags" = rec { crateName = "bitflags"; - version = "2.11.1"; + version = "2.13.0"; edition = "2021"; - sha256 = "1cvqijg3rvwgis20a66vfdxannjsxfy5fgjqkaq3l13gyfcj4lf4"; + sha256 = "1y239gpvl061rfvav7jds8mjs42kmwi39is7yx5d1qw3hvp8nf5l"; authors = [ "The Rust Project Developers" ]; @@ -898,9 +898,9 @@ rec { }; "built" = rec { crateName = "built"; - version = "0.8.0"; - edition = "2021"; - sha256 = "0r5f08lpjsr6j5ajkbmd0ymfmajpq8ddbfvi8ji8rx48y88qzbgl"; + version = "0.8.1"; + edition = "2024"; + sha256 = "1saq332pd6g3svvc9ah8myjpfvgqlzl2ksb1ypp3976kjcfm63jw"; authors = [ "Lukas Lueg " ]; @@ -924,15 +924,16 @@ rec { "chrono" = [ "dep:chrono" ]; "dependency-tree" = [ "cargo-lock/dependency-tree" ]; "git2" = [ "dep:git2" ]; + "gix" = [ "dep:gix" ]; "semver" = [ "dep:semver" ]; }; resolvedDefaultFeatures = [ "chrono" "git2" ]; }; "bumpalo" = rec { crateName = "bumpalo"; - version = "3.20.2"; + version = "3.20.3"; edition = "2021"; - sha256 = "1jrgxlff76k9glam0akhwpil2fr1w32gbjdf5hpipc7ld2c7h82x"; + sha256 = "0jc6va3nwcqikm7chnpdv1s87my3gs2j7g1sc7g3k91brg3arxbj"; authors = [ "Nick Fitzgerald " ]; @@ -961,9 +962,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.2.61"; + version = "1.2.63"; edition = "2018"; - sha256 = "0vawvnrrsmi8dygavq3wx085cmlp10sp3fhld5842rlqkqsr0vfi"; + sha256 = "0zy2bqc4nvj6bv2cipx4h4bn65wf1zqf1fw1hsh64mmvg1hh2vjm"; authors = [ "Alex Crichton " ]; @@ -1011,9 +1012,9 @@ rec { }; "chrono" = rec { crateName = "chrono"; - version = "0.4.44"; + version = "0.4.45"; edition = "2021"; - sha256 = "1c64mk9a235271j5g3v4zrzqqmd43vp9vki7vqfllpqf5rd0fwy6"; + sha256 = "09rkcgk6is2sdhqs9142zv8xqnj8ryx8m9hknllqwyv9wxi9x9qs"; dependencies = [ { name = "iana-time-zone"; @@ -1906,9 +1907,9 @@ rec { }; "displaydoc" = rec { crateName = "displaydoc"; - version = "0.2.5"; + version = "0.2.6"; edition = "2021"; - sha256 = "1q0alair462j21iiqwrr21iabkfnb13d6x5w95lkdg21q2xrqdlp"; + sha256 = "0kyxwfbdmagd8afzb2pzja7wj8dhah7smxdsgw00iq8pa2jhmiqs"; procMacro = true; authors = [ "Jane Lusby " @@ -2108,12 +2109,9 @@ rec { }; "either" = rec { crateName = "either"; - version = "1.15.0"; + version = "1.16.0"; edition = "2021"; - sha256 = "069p1fknsmzn9llaizh77kip0pqmcwpdsykv2x30xpjyija5gis8"; - authors = [ - "bluss" - ]; + sha256 = "17k7jfbdz7k440h6lws9baz8p9zlxgb41sig3w81h80nwzsjyqli"; features = { "default" = [ "std" ]; "serde" = [ "dep:serde" ]; @@ -2817,9 +2815,9 @@ rec { }; "futures-timer" = rec { crateName = "futures-timer"; - version = "3.0.3"; + version = "3.0.4"; edition = "2018"; - sha256 = "094vw8k37djpbwv74bwf2qb7n6v6ghif4myss6smd6hgyajb127j"; + sha256 = "0s39in8ivw7g4d37pf31q02y44zd1hpfkd1pgra2slcqibdzlhxg"; libName = "futures_timer"; authors = [ "Alex Crichton " @@ -3066,9 +3064,9 @@ rec { }; "git2" = rec { crateName = "git2"; - version = "0.20.4"; - edition = "2018"; - sha256 = "0azykjpk3j6s354z23jkyq3r3pbmlw9ha1zsxkw5cnnpi1h2b23v"; + version = "0.21.0"; + edition = "2021"; + sha256 = "0bmqga9vlyx5sdlr0i28z0362s89xv9i4qcv20vvx9j54y9vzpfx"; authors = [ "Josh Triplett " "Alex Crichton " @@ -3090,17 +3088,14 @@ rec { name = "log"; packageId = "log"; } - { - name = "url"; - packageId = "url"; - } ]; features = { - "default" = [ "ssh" "https" ]; - "https" = [ "libgit2-sys/https" "openssl-sys" "openssl-probe" ]; + "cred" = [ "dep:url" ]; + "https" = [ "libgit2-sys/https" "openssl-sys" "openssl-probe" "cred" ]; "openssl-probe" = [ "dep:openssl-probe" ]; "openssl-sys" = [ "dep:openssl-sys" ]; - "ssh" = [ "libgit2-sys/ssh" ]; + "ssh" = [ "libgit2-sys/ssh" "cred" ]; + "unstable-sha256" = [ "libgit2-sys/unstable-sha256" ]; "vendored-libgit2" = [ "libgit2-sys/vendored" ]; "vendored-openssl" = [ "openssl-sys/vendored" "libgit2-sys/vendored-openssl" ]; "zlib-ng-compat" = [ "libgit2-sys/zlib-ng-compat" ]; @@ -3190,9 +3185,9 @@ rec { }; "h2" = rec { crateName = "h2"; - version = "0.4.13"; + version = "0.4.14"; edition = "2021"; - sha256 = "0m6w5gg0n0m1m5915bxrv8n4rlazhx5icknkslz719jhh4xdli1g"; + sha256 = "0cw7jk7kn2vn6f8w8ssh6gis1mljnfjxd606gvi4sjpyjayfy7qp"; authors = [ "Carl Lerche " "Sean McArthur " @@ -3303,14 +3298,11 @@ rec { }; resolvedDefaultFeatures = [ "allocator-api2" "default" "default-hasher" "equivalent" "inline-more" "raw-entry" ]; }; - "hashbrown 0.17.0" = rec { + "hashbrown 0.17.1" = rec { crateName = "hashbrown"; - version = "0.17.0"; + version = "0.17.1"; edition = "2024"; - sha256 = "0l8gvcz80lvinb7x22h53cqbi2y1fm603y2jhhh9qwygvkb7sijg"; - authors = [ - "Amanieu d'Antras " - ]; + sha256 = "0jmqz7i4yl6cm7rbn0i2ffkfrmwi6xkmzkaldr2v8bcsx2v0jngd"; features = { "alloc" = [ "dep:alloc" ]; "allocator-api2" = [ "dep:allocator-api2" ]; @@ -3385,9 +3377,9 @@ rec { }; "http" = rec { crateName = "http"; - version = "1.4.0"; + version = "1.4.2"; edition = "2021"; - sha256 = "06iind4cwsj1d6q8c2xgq8i2wka4ps74kmws24gsi1bzdlw2mfp3"; + sha256 = "09b4p8fiivkg7wm0b59fyrn1jkm7px298ci7zb9igz6n647gaw39"; authors = [ "Alex Crichton " "Carl Lerche " @@ -3504,9 +3496,9 @@ rec { }; "hyper" = rec { crateName = "hyper"; - version = "1.9.0"; + version = "1.10.1"; edition = "2021"; - sha256 = "1jmwbwqcaficskg76kq402gbymbnh2z4v99xwq3l5aa6n8bg16b2"; + sha256 = "1624nwrh1ci34psqcl3q8q266kha8kd6fmqjj14qck49l59iqa2m"; authors = [ "Sean McArthur " ]; @@ -4277,9 +4269,9 @@ rec { }; "idna_adapter" = rec { crateName = "idna_adapter"; - version = "1.2.1"; - edition = "2021"; - sha256 = "0i0339pxig6mv786nkqcxnwqa87v4m94b2653f6k3aj0jmhfkjis"; + version = "1.2.2"; + edition = "2024"; + sha256 = "0557p76l8hj35r9zn1yv7c6x1c0qbrsffmg80n0yy8361ly3fs6b"; authors = [ "The rust-url developers" ]; @@ -4313,7 +4305,7 @@ rec { } { name = "hashbrown"; - packageId = "hashbrown 0.17.0"; + packageId = "hashbrown 0.17.1"; usesDefaultFeatures = false; } ]; @@ -4371,39 +4363,6 @@ rec { }; resolvedDefaultFeatures = [ "default" "std" ]; }; - "iri-string" = rec { - crateName = "iri-string"; - version = "0.7.12"; - edition = "2021"; - sha256 = "082fpx6c5ghvmqpwxaf2b268m47z2ic3prajqbmi1s1qpfj5kri5"; - libName = "iri_string"; - authors = [ - "YOSHIOKA Takuma " - ]; - dependencies = [ - { - name = "memchr"; - packageId = "memchr"; - optional = true; - usesDefaultFeatures = false; - } - { - name = "serde"; - packageId = "serde"; - optional = true; - usesDefaultFeatures = false; - features = [ "derive" ]; - } - ]; - features = { - "alloc" = [ "serde?/alloc" ]; - "default" = [ "std" ]; - "memchr" = [ "dep:memchr" ]; - "serde" = [ "dep:serde" ]; - "std" = [ "alloc" "memchr?/std" "serde?/std" ]; - }; - resolvedDefaultFeatures = [ "alloc" "default" "std" ]; - }; "is_terminal_polyfill" = rec { crateName = "is_terminal_polyfill"; version = "1.70.2"; @@ -4473,9 +4432,9 @@ rec { }; "jiff" = rec { crateName = "jiff"; - version = "0.2.24"; + version = "0.2.28"; edition = "2021"; - sha256 = "0g87al8yqp05m63dhqzi359xgsslc0grqz00nvfdyq8dcayms2zh"; + sha256 = "00lixngcc7amh2fcsxfr0z38j06lllhapz192biv1qj97q1x60s6"; authors = [ "Andrew Gallant " ]; @@ -4521,12 +4480,10 @@ rec { usesDefaultFeatures = false; } { - name = "windows-sys"; - packageId = "windows-sys 0.61.2"; + name = "windows-link"; + packageId = "windows-link"; optional = true; - usesDefaultFeatures = false; target = { target, features }: (target."windows" or false); - features = [ "Win32_Foundation" "Win32_System_Time" ]; } ]; devDependencies = [ @@ -4545,7 +4502,7 @@ rec { "static-tz" = [ "dep:jiff-static" ]; "std" = [ "alloc" "log?/std" "serde_core?/std" ]; "tz-fat" = [ "jiff-static?/tz-fat" ]; - "tz-system" = [ "std" "dep:windows-sys" ]; + "tz-system" = [ "std" "dep:windows-link" ]; "tzdb-bundle-always" = [ "dep:jiff-tzdb" "alloc" ]; "tzdb-bundle-platform" = [ "dep:jiff-tzdb-platform" "alloc" ]; "tzdb-concatenated" = [ "std" ]; @@ -4555,9 +4512,9 @@ rec { }; "jiff-static" = rec { crateName = "jiff-static"; - version = "0.2.24"; + version = "0.2.28"; edition = "2021"; - sha256 = "1mz6v0d1hd8wjgfzccgda5g9z01s1yxnyiizvahjw0pq1w1xw070"; + sha256 = "0irbhfh2f4i9w5l53jcmh6ssnhdd92wfy76978chgwnxilvk4bbq"; procMacro = true; libName = "jiff_static"; authors = [ @@ -4637,9 +4594,9 @@ rec { }; "js-sys" = rec { crateName = "js-sys"; - version = "0.3.95"; + version = "0.3.100"; edition = "2021"; - sha256 = "1jhj3kgxxgwm0cpdjiz7i2qapqr7ya9qswadmr63dhwx3lnyjr19"; + sha256 = "0qi1wjakyw2rx9wwprcfx77g3lvn1b8n6yvfhj2pgym4swh5y0pj"; libName = "js_sys"; authors = [ "The wasm-bindgen Developers" @@ -4648,7 +4605,6 @@ rec { { name = "cfg-if"; packageId = "cfg-if"; - optional = true; } { name = "futures-util"; @@ -4657,11 +4613,6 @@ rec { usesDefaultFeatures = false; features = [ "std" ]; } - { - name = "once_cell"; - packageId = "once_cell"; - usesDefaultFeatures = false; - } { name = "wasm-bindgen"; packageId = "wasm-bindgen"; @@ -4670,17 +4621,16 @@ rec { ]; features = { "default" = [ "std" "unsafe-eval" ]; - "futures" = [ "dep:cfg-if" "dep:futures-util" ]; - "futures-core-03-stream" = [ "futures" "dep:futures-core" ]; - "std" = [ "wasm-bindgen/std" ]; + "futures-core-03-stream" = [ "dep:futures-util" "dep:futures-core" ]; + "std" = [ "wasm-bindgen/std" "dep:futures-util" ]; }; - resolvedDefaultFeatures = [ "default" "futures" "std" "unsafe-eval" ]; + resolvedDefaultFeatures = [ "default" "std" "unsafe-eval" ]; }; "json-patch" = rec { crateName = "json-patch"; - version = "4.1.0"; + version = "4.2.0"; edition = "2021"; - sha256 = "147yaxmv3i4s0bdna86rgwpmqh2507fn4ighfpplaiqkw8ay807k"; + sha256 = "0wkv896d0pzq56i2kkl0giqpv117fwvhbpgs8iz85805w66l68bl"; libName = "json_patch"; authors = [ "Ivan Dubrov " @@ -4706,7 +4656,7 @@ rec { } { name = "thiserror"; - packageId = "thiserror 1.0.69"; + packageId = "thiserror 2.0.18"; } ]; devDependencies = [ @@ -4852,7 +4802,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "k8s_version"; @@ -5519,10 +5469,10 @@ rec { }; "libgit2-sys" = rec { crateName = "libgit2-sys"; - version = "0.18.3+1.9.2"; + version = "0.18.5+1.9.4"; edition = "2021"; links = "git2"; - sha256 = "11rlbyihj3k35mnkxxz4yvsnlx33a4r9srl66c5vp08pp72arcy9"; + sha256 = "18lwqnhy7qxg4iw24s1a0n7aj7qbnryry1iy0w32k4f1xbk6lp80"; libName = "libgit2_sys"; libPath = "lib.rs"; authors = [ @@ -5580,10 +5530,10 @@ rec { }; "libz-sys" = rec { crateName = "libz-sys"; - version = "1.1.28"; + version = "1.1.29"; edition = "2018"; links = "z"; - sha256 = "08hyf9v85zifl3353xc7i5wr53v9b3scri856cmphl3gaxp24fpw"; + sha256 = "1n98kqya7a7a0cxf5n5z3g13rj7a1vqxynk2xc7bja1qfxbrdg45"; libName = "libz_sys"; authors = [ "Alex Crichton " @@ -5660,9 +5610,9 @@ rec { }; "log" = rec { crateName = "log"; - version = "0.4.29"; + version = "0.4.32"; edition = "2021"; - sha256 = "15q8j9c8g5zpkcw0hnd6cf2z7fxqnvsjh3rw5mv5q10r83i34l2y"; + sha256 = "0fmdg0cxig7i4fwf1sw7fmg4d1gdbfzniawcfpwydy1q7320fgwm"; authors = [ "The Rust Project Developers" ]; @@ -5716,9 +5666,9 @@ rec { }; "memchr" = rec { crateName = "memchr"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "0y9zzxcqxvdqg6wyag7vc3h0blhdn7hkq164bxyx2vph8zs5ijpq"; + sha256 = "1n448jx01h5z2xknj6x2dhxgr8s8fb717cf6vfqj5lmhkpj7m53b"; authors = [ "Andrew Gallant " "bluss" @@ -5779,9 +5729,9 @@ rec { }; "mio" = rec { crateName = "mio"; - version = "1.2.0"; + version = "1.2.1"; edition = "2021"; - sha256 = "1hanrh4fwsfkdqdaqfidz48zz1wdix23zwn3r2x78am0garfbdsh"; + sha256 = "1nkggmrlnjs93w8rja4lvjj4aml1xqahgimv1h0p7d373kvhmg82"; authors = [ "Carl Lerche " "Thomas de Zeeuw " @@ -5917,9 +5867,9 @@ rec { }; "num-conv" = rec { crateName = "num-conv"; - version = "0.2.1"; + version = "0.2.2"; edition = "2021"; - sha256 = "0rqrr29brafaa2za352pbmhkk556n7f8z9rrkgmjp1idvdl3fry6"; + sha256 = "0hg4f9bwmy7cwpxdkm165dmkfc8jhkkayci234jsmi5ssb33j5sj"; libName = "num_conv"; authors = [ "Jacob Pratt " @@ -6879,9 +6829,9 @@ rec { }; "pin-project" = rec { crateName = "pin-project"; - version = "1.1.11"; + version = "1.1.13"; edition = "2021"; - sha256 = "05zm3y3bl83ypsr6favxvny2kys4i19jiz1y18ylrbxwsiz9qx7i"; + sha256 = "09091qp946lpmjz4yp0xil1r5v4hgc91fi19dg5csayhdqrv4ri4"; libName = "pin_project"; dependencies = [ { @@ -6893,9 +6843,9 @@ rec { }; "pin-project-internal" = rec { crateName = "pin-project-internal"; - version = "1.1.11"; + version = "1.1.13"; edition = "2021"; - sha256 = "1ik4mpb92da75inmjvxf2qm61vrnwml3x24wddvrjlqh1z9hxcnr"; + sha256 = "12rzlh07i1sdgrvzj6wgkka5bjqyvbfsl8knq6qi7g16m7q9aqy9"; procMacro = true; libName = "pin_project_internal"; dependencies = [ @@ -7217,9 +7167,9 @@ rec { }; "prost" = rec { crateName = "prost"; - version = "0.14.3"; + version = "0.14.4"; edition = "2021"; - sha256 = "0s057z9nzggzy7x4bbsiar852hg7zb81f4z4phcdb0ig99971snj"; + sha256 = "1qas5v5rap45f43v3ja0jngxrrafrkcwl0iw5a3ld1pz2rscd2jj"; authors = [ "Dan Burkert " "Lucio Franco " @@ -7246,9 +7196,9 @@ rec { }; "prost-derive" = rec { crateName = "prost-derive"; - version = "0.14.3"; + version = "0.14.4"; edition = "2021"; - sha256 = "02zvva6kb0pfvlyc4nac6gd37ncjrs8jq5scxcq4nbqkc8wh5ii7"; + sha256 = "1pqa77d7da5pf6ba3kjj7510m5cynz6902ax01ckvr0pfrgv4w5m"; procMacro = true; libName = "prost_derive"; authors = [ @@ -7284,9 +7234,9 @@ rec { }; "prost-types" = rec { crateName = "prost-types"; - version = "0.14.3"; + version = "0.14.4"; edition = "2021"; - sha256 = "1mrxrciryfgi6a0vmrgyj3g27r9hdhlgwkq71cgv3icbvg5w94c9"; + sha256 = "02ivjvc4cwl5bfgjs3l00hwlrk74z8zlg1xcgx60bww8fvf6fjgr"; libName = "prost_types"; authors = [ "Dan Burkert " @@ -7589,9 +7539,9 @@ rec { }; "regex" = rec { crateName = "regex"; - version = "1.12.3"; + version = "1.12.4"; edition = "2021"; - sha256 = "0xp2q0x7ybmpa5zlgaz00p8zswcirj9h8nry3rxxsdwi9fhm81z1"; + sha256 = "1fm6si2xpmhwqflabdqsakc0qkq718wx2ljl37nbj75fb5vjnagi"; authors = [ "The Rust Project Developers" "Andrew Gallant " @@ -7708,9 +7658,9 @@ rec { }; "regex-syntax" = rec { crateName = "regex-syntax"; - version = "0.8.10"; + version = "0.8.11"; edition = "2021"; - sha256 = "02jx311ka0daxxc7v45ikzhcl3iydjbbb0mdrpc1xgg8v7c7v2fw"; + sha256 = "1m25h5q2wp976fb9gc3dsc9l99svcvd5cri8lncb51c46ydgzxnn"; libName = "regex_syntax"; authors = [ "The Rust Project Developers" @@ -8248,9 +8198,9 @@ rec { }; "rustls" = rec { crateName = "rustls"; - version = "0.23.39"; + version = "0.23.40"; edition = "2021"; - sha256 = "03p6fkdwbdpp93dfidc4nzgmalwp3gxnv0rk421a5k3pn2612b3w"; + sha256 = "12qnv3ag4wrw7aj8jng74kgrilpjm2b1rfcjaac8h691frccv1pg"; dependencies = [ { name = "log"; @@ -8317,9 +8267,9 @@ rec { }; "rustls-native-certs" = rec { crateName = "rustls-native-certs"; - version = "0.8.3"; + version = "0.8.4"; edition = "2021"; - sha256 = "0qrajg2n90bcr3bcq6j95gjm7a9lirfkkdmjj32419dyyzan0931"; + sha256 = "0kgazl8zc1sv63qg179bz96ilzh56lzfa5k92ji7d265f4kibdfs"; libName = "rustls_native_certs"; dependencies = [ { @@ -8886,9 +8836,9 @@ rec { }; "serde_json" = rec { crateName = "serde_json"; - version = "1.0.149"; + version = "1.0.150"; edition = "2021"; - sha256 = "11jdx4vilzrjjd1dpgy67x5lgzr0laplz30dhv75lnf5ffa07z43"; + sha256 = "1ffgfhy9kndjnrz8lmy95pr758p2zk8dxv6yi99x0vkkni24w0g8"; authors = [ "Erick Tryzelaar " "David Tolnay " @@ -9129,9 +9079,9 @@ rec { }; "shlex" = rec { crateName = "shlex"; - version = "1.3.0"; - edition = "2015"; - sha256 = "0r1y6bv26c1scpxvhg2cabimrmwgbp4p3wy6syj9n0c4s3q2znhg"; + version = "2.0.1"; + edition = "2018"; + sha256 = "1fjsll1cd7d2bcpdij9kd6w62rpbc7qqzvydvs021vsmr1cxvypq"; authors = [ "comex " "Fenhl " @@ -9434,9 +9384,9 @@ rec { }; "socket2" = rec { crateName = "socket2"; - version = "0.6.3"; + version = "0.6.4"; edition = "2021"; - sha256 = "0gkjjcyn69hqhhlh5kl8byk5m0d7hyrp2aqwzbs3d33q208nwxis"; + sha256 = "0ldyp5rhba15spwxj1n94xh7sjks1398c3vwpwkxkd1087nwzlaj"; authors = [ "Alex Crichton " "Thomas de Zeeuw " @@ -9535,7 +9485,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_certs"; @@ -9726,7 +9676,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_operator"; @@ -9920,7 +9870,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; procMacro = true; @@ -9955,7 +9905,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_shared"; @@ -10036,7 +9986,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_telemetry"; @@ -10146,7 +10096,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_versioned"; @@ -10196,7 +10146,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; procMacro = true; @@ -10264,7 +10214,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "451088f77acee6c3d296754698260256c250ecb2"; + rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; }; libName = "stackable_webhook"; @@ -10909,9 +10859,9 @@ rec { }; "tokio" = rec { crateName = "tokio"; - version = "1.52.1"; + version = "1.52.3"; edition = "2021"; - sha256 = "1imw1dkkv38p66i33m5hsyk3d6prsbyrayjvqhndjvz89ybywzdn"; + sha256 = "1zpzazypkg61sw91na1m85x5s4rsjym335fwwhwm1hcs70dz1iwg"; authors = [ "Tokio Contributors " ]; @@ -11221,9 +11171,9 @@ rec { }; "toml_edit" = rec { crateName = "toml_edit"; - version = "0.25.11+spec-1.1.0"; + version = "0.25.12+spec-1.1.0"; edition = "2024"; - sha256 = "0awzffbkx33v9x4h19b5mfrwp3sn4ifr16y58sbk6j6l5v9c8n8b"; + sha256 = "1mx5paq837rjw7w51zprrjynk1vaig9yzxfqz9ac79jmd7f3w5fj"; dependencies = [ { name = "indexmap"; @@ -11276,9 +11226,9 @@ rec { }; "tonic" = rec { crateName = "tonic"; - version = "0.14.5"; - edition = "2021"; - sha256 = "1v4k7aa28m7722gz9qak2jiy7lis1ycm4fdmq63iip4m0qdcdizy"; + version = "0.14.6"; + edition = "2024"; + sha256 = "1vs5ci6z6b9xhfsnx4s8qx6bqi1zzcrxncjp71147a0gqwc5aamc"; authors = [ "Lucio Franco " ]; @@ -11405,9 +11355,9 @@ rec { }; "tonic-prost" = rec { crateName = "tonic-prost"; - version = "0.14.5"; - edition = "2021"; - sha256 = "02fkg2bv87q0yds2wz3w0s7i1x6qcgbrl00dy6ipajdapfh7clx5"; + version = "0.14.6"; + edition = "2024"; + sha256 = "184y40nf0iyzc5rg32ivgd88snv68sqy1kchynn55r1vhml9z12h"; libName = "tonic_prost"; authors = [ "Lucio Franco " @@ -11431,9 +11381,9 @@ rec { }; "tonic-types" = rec { crateName = "tonic-types"; - version = "0.14.5"; - edition = "2021"; - sha256 = "16bk1cxi2m0xgaabf98nnj7dn9j16ymkh27jq4s3shjm4a85m1ra"; + version = "0.14.6"; + edition = "2024"; + sha256 = "1s286gg71pjajny8xar0azq1w9lgz1ks3jm3pccxb0qz0q11pavk"; libName = "tonic_types"; authors = [ "Lucio Franco " @@ -11576,9 +11526,9 @@ rec { }; "tower-http" = rec { crateName = "tower-http"; - version = "0.6.8"; + version = "0.6.11"; edition = "2018"; - sha256 = "1y514jwzbyrmrkbaajpwmss4rg0mak82k16d6588w9ncaffmbrnl"; + sha256 = "0h08wjgs3hwnq11iwwzlmnabn1h4cl0fzd48svaccvqffkiggz2c"; libName = "tower_http"; authors = [ "Tower Maintainers " @@ -11612,11 +11562,6 @@ rec { packageId = "http-body"; optional = true; } - { - name = "iri-string"; - packageId = "iri-string"; - optional = true; - } { name = "mime"; packageId = "mime"; @@ -11646,6 +11591,11 @@ rec { optional = true; usesDefaultFeatures = false; } + { + name = "url"; + packageId = "url"; + optional = true; + } ]; devDependencies = [ { @@ -11667,35 +11617,33 @@ rec { } ]; features = { - "async-compression" = [ "dep:async-compression" ]; "auth" = [ "base64" "validate-request" ]; "base64" = [ "dep:base64" ]; "catch-panic" = [ "tracing" "futures-util/std" "dep:http-body" "dep:http-body-util" ]; - "compression-br" = [ "async-compression/brotli" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; - "compression-deflate" = [ "async-compression/zlib" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; + "compression-br" = [ "dep:async-compression" "async-compression?/brotli" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; + "compression-deflate" = [ "dep:async-compression" "async-compression?/zlib" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; "compression-full" = [ "compression-br" "compression-deflate" "compression-gzip" "compression-zstd" ]; - "compression-gzip" = [ "async-compression/gzip" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; - "compression-zstd" = [ "async-compression/zstd" "futures-core" "dep:http-body" "tokio-util" "tokio" ]; - "decompression-br" = [ "async-compression/brotli" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; - "decompression-deflate" = [ "async-compression/zlib" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; + "compression-gzip" = [ "dep:async-compression" "async-compression?/gzip" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; + "compression-zstd" = [ "dep:async-compression" "async-compression?/zstd" "futures-core" "dep:http-body" "tokio-util" "dep:tokio" ]; + "decompression-br" = [ "dep:async-compression" "async-compression?/brotli" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; + "decompression-deflate" = [ "dep:async-compression" "async-compression?/zlib" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; "decompression-full" = [ "decompression-br" "decompression-deflate" "decompression-gzip" "decompression-zstd" ]; - "decompression-gzip" = [ "async-compression/gzip" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; - "decompression-zstd" = [ "async-compression/zstd" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; - "follow-redirect" = [ "futures-util" "dep:http-body" "iri-string" "tower/util" ]; - "fs" = [ "futures-core" "futures-util" "dep:http-body" "dep:http-body-util" "tokio/fs" "tokio-util/io" "tokio/io-util" "dep:http-range-header" "mime_guess" "mime" "percent-encoding" "httpdate" "set-status" "futures-util/alloc" "tracing" ]; - "full" = [ "add-extension" "auth" "catch-panic" "compression-full" "cors" "decompression-full" "follow-redirect" "fs" "limit" "map-request-body" "map-response-body" "metrics" "normalize-path" "propagate-header" "redirect" "request-id" "sensitive-headers" "set-header" "set-status" "timeout" "trace" "util" "validate-request" ]; + "decompression-gzip" = [ "dep:async-compression" "async-compression?/gzip" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; + "decompression-zstd" = [ "dep:async-compression" "async-compression?/zstd" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "dep:tokio" ]; + "follow-redirect" = [ "futures-util" "dep:http-body" "dep:url" "tower/util" ]; + "fs" = [ "dep:tokio" "tokio?/fs" "tokio?/io-util" "futures-core" "futures-util" "dep:http-body" "dep:http-body-util" "tokio-util/io" "dep:http-range-header" "mime_guess" "mime" "percent-encoding" "httpdate" "set-status" "futures-util/alloc" ]; + "full" = [ "add-extension" "auth" "catch-panic" "compression-full" "cors" "decompression-full" "follow-redirect" "fs" "limit" "map-request-body" "map-response-body" "metrics" "normalize-path" "on-early-drop" "propagate-header" "redirect" "request-id" "sensitive-headers" "set-header" "set-status" "timeout" "trace" "util" "validate-request" ]; "futures-core" = [ "dep:futures-core" ]; "futures-util" = [ "dep:futures-util" ]; "httpdate" = [ "dep:httpdate" ]; - "iri-string" = [ "dep:iri-string" ]; "limit" = [ "dep:http-body" "dep:http-body-util" ]; - "metrics" = [ "dep:http-body" "tokio/time" ]; + "metrics" = [ "dep:http-body" "dep:tokio" "tokio?/time" ]; "mime" = [ "dep:mime" ]; "mime_guess" = [ "dep:mime_guess" ]; + "on-early-drop" = [ "dep:http-body" ]; "percent-encoding" = [ "dep:percent-encoding" ]; "request-id" = [ "uuid" ]; - "timeout" = [ "dep:http-body" "tokio/time" ]; - "tokio" = [ "dep:tokio" ]; + "timeout" = [ "dep:http-body" "dep:tokio" "tokio?/time" ]; "tokio-util" = [ "dep:tokio-util" ]; "tower" = [ "dep:tower" ]; "trace" = [ "dep:http-body" "tracing" ]; @@ -11704,7 +11652,7 @@ rec { "uuid" = [ "dep:uuid" ]; "validate-request" = [ "mime" ]; }; - resolvedDefaultFeatures = [ "auth" "base64" "default" "follow-redirect" "futures-util" "iri-string" "map-response-body" "mime" "tower" "trace" "tracing" "util" "validate-request" ]; + resolvedDefaultFeatures = [ "auth" "base64" "default" "follow-redirect" "futures-util" "map-response-body" "mime" "tower" "trace" "tracing" "util" "validate-request" ]; }; "tower-layer" = rec { crateName = "tower-layer"; @@ -12148,13 +12096,9 @@ rec { }; "typenum" = rec { crateName = "typenum"; - version = "1.20.0"; + version = "1.20.1"; edition = "2018"; - sha256 = "1pj35y6q11d3y55gdl6g1h2dfhmybjming0jdi9bh0bpnqm11kj0"; - authors = [ - "Paho Lurie-Gregg " - "Andre Bogus " - ]; + sha256 = "086s9ly0906kw5yw41249fba97w5zfxf03pyfwdkffvcprqfixdn"; features = { "scale-info" = [ "dep:scale-info" ]; "scale_info" = [ "scale-info/derive" ]; @@ -12187,9 +12131,9 @@ rec { }; "unicode-segmentation" = rec { crateName = "unicode-segmentation"; - version = "1.13.2"; + version = "1.13.3"; edition = "2018"; - sha256 = "135a26m4a0wj319gcw28j6a5aqvz00jmgwgmcs6szgxjf942facn"; + sha256 = "1a47zaq83p386r3baq4m018xd5q4q0grdg56i1x042dzn71x7xf6"; libName = "unicode_segmentation"; authors = [ "kwantam " @@ -12317,9 +12261,9 @@ rec { }; "uuid" = rec { crateName = "uuid"; - version = "1.23.2"; + version = "1.23.3"; edition = "2021"; - sha256 = "1xy942s4z0bi8p3441wvd4ry3hx6ry1c7s6fgrr38462xqybhn6j"; + sha256 = "1drddl03gi12vl1s3l2h371dw39plhn9wappp00v707g7h96nk8l"; authors = [ "Ashley Mannix" "Dylan DPC" @@ -12462,9 +12406,9 @@ rec { }; "wasm-bindgen" = rec { crateName = "wasm-bindgen"; - version = "0.2.118"; + version = "0.2.123"; edition = "2021"; - sha256 = "129s5r14fx4v4xrzpx2c6l860nkxpl48j50y7kl6j16bpah3iy8b"; + sha256 = "0qqmx07r597gm8lbz8qngvv0phwvpzzyfh3nl84nz9qr1jqs8m52"; libName = "wasm_bindgen"; authors = [ "The wasm-bindgen Developers" @@ -12513,9 +12457,9 @@ rec { }; "wasm-bindgen-futures" = rec { crateName = "wasm-bindgen-futures"; - version = "0.4.68"; + version = "0.4.73"; edition = "2021"; - sha256 = "1y7bq5d9fk7s9xaayx38bgs9ns35na0kpb5zw19944zvya1x6wgk"; + sha256 = "1bva12h8gdpqkp753czlxabs0s21lvgzm41brr4lhpdzz818fmjl"; libName = "wasm_bindgen_futures"; authors = [ "The wasm-bindgen Developers" @@ -12525,7 +12469,6 @@ rec { name = "js-sys"; packageId = "js-sys"; usesDefaultFeatures = false; - features = [ "futures" ]; } { name = "wasm-bindgen"; @@ -12542,9 +12485,9 @@ rec { }; "wasm-bindgen-macro" = rec { crateName = "wasm-bindgen-macro"; - version = "0.2.118"; + version = "0.2.123"; edition = "2021"; - sha256 = "1v98r8vs17cj8918qsg0xx4nlg4nxk1g0jd4nwnyrh1687w29zzf"; + sha256 = "1p50xdwmv543b52bc49vm5lcsgd9adpx647bdisg7ihfbg3hz914"; procMacro = true; libName = "wasm_bindgen_macro"; authors = [ @@ -12566,9 +12509,9 @@ rec { }; "wasm-bindgen-macro-support" = rec { crateName = "wasm-bindgen-macro-support"; - version = "0.2.118"; + version = "0.2.123"; edition = "2021"; - sha256 = "0169jr0q469hfx5zqxfyywf2h2f4aj17vn4zly02nfwqmxghc24x"; + sha256 = "0nwqyc63byl7rp9nnv45av8h85fncfmxywkvy35d9qwwkfyk93wh"; libName = "wasm_bindgen_macro_support"; authors = [ "The wasm-bindgen Developers" @@ -12602,10 +12545,10 @@ rec { }; "wasm-bindgen-shared" = rec { crateName = "wasm-bindgen-shared"; - version = "0.2.118"; + version = "0.2.123"; edition = "2021"; links = "wasm_bindgen"; - sha256 = "0ag1vvdzi4334jlzilsy14y3nyzwddf1ndn62fyhf6bg62g4vl2z"; + sha256 = "14lvjm3pzywm5c4962i6s5zmngic1knpggshnnxr9c97dihzgjvs"; libName = "wasm_bindgen_shared"; authors = [ "The wasm-bindgen Developers" @@ -12620,9 +12563,9 @@ rec { }; "web-sys" = rec { crateName = "web-sys"; - version = "0.3.95"; + version = "0.3.100"; edition = "2021"; - sha256 = "0zfr2jy5bpkkggl88i43yy37p538hg20i56kwn421yj9g6qznbag"; + sha256 = "0sffbkrpgyi1402mv4wzp9av6ky6rnb1d2m2dpf87wi7yfn7223f"; libName = "web_sys"; authors = [ "The wasm-bindgen Developers" @@ -12706,6 +12649,7 @@ rec { "CssStyleSheet" = [ "StyleSheet" ]; "CssSupportsRule" = [ "CssConditionRule" "CssGroupingRule" "CssRule" ]; "CssTransition" = [ "Animation" "EventTarget" ]; + "CssViewTransitionRule" = [ "CssRule" ]; "CustomEvent" = [ "Event" ]; "DedicatedWorkerGlobalScope" = [ "EventTarget" "WorkerGlobalScope" ]; "DelayNode" = [ "AudioNode" "EventTarget" ]; @@ -13782,7 +13726,7 @@ rec { "Win32_Web" = [ "Win32" ]; "Win32_Web_InternetExplorer" = [ "Win32_Web" ]; }; - resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Security_Authentication" "Win32_Security_Authentication_Identity" "Win32_Security_Credentials" "Win32_Security_Cryptography" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_LibraryLoader" "Win32_System_Memory" "Win32_System_Pipes" "Win32_System_SystemInformation" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_Time" "Win32_System_WindowsProgramming" "default" ]; + resolvedDefaultFeatures = [ "Wdk" "Wdk_Foundation" "Wdk_Storage" "Wdk_Storage_FileSystem" "Wdk_System" "Wdk_System_IO" "Win32" "Win32_Foundation" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Security_Authentication" "Win32_Security_Authentication_Identity" "Win32_Security_Credentials" "Win32_Security_Cryptography" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_LibraryLoader" "Win32_System_Memory" "Win32_System_Pipes" "Win32_System_SystemInformation" "Win32_System_SystemServices" "Win32_System_Threading" "Win32_System_WindowsProgramming" "default" ]; }; "windows-targets" = rec { crateName = "windows-targets"; @@ -13919,9 +13863,9 @@ rec { }; "winnow" = rec { crateName = "winnow"; - version = "1.0.2"; + version = "1.0.3"; edition = "2021"; - sha256 = "1l7xnfvlgy4da6gq5ip2bgcm8i9d0rwzaxg1p88nlw8lxy5p1q9f"; + sha256 = "1wajycd3krn6h699vydjv7hm0ll5l31p899qzpk59y2is74y34h5"; dependencies = [ { name = "memchr"; @@ -14044,9 +13988,9 @@ rec { }; "yoke" = rec { crateName = "yoke"; - version = "0.8.2"; + version = "0.8.3"; edition = "2021"; - sha256 = "1jprcs7a98a5whvfs6r3jvfh1nnfp6zyijl7y4ywmn88lzywbs5b"; + sha256 = "1xgyj6c2lxj2bp891ynmhws87c6z7yyv2li1v0ss9di40hxf57vh"; authors = [ "Manish Goregaokar " ]; @@ -14110,9 +14054,9 @@ rec { }; "zerocopy" = rec { crateName = "zerocopy"; - version = "0.8.48"; + version = "0.8.52"; edition = "2021"; - sha256 = "1sb8plax8jbrsng1jdval7bdhk7hhrx40dz3hwh074k6knzkgm7f"; + sha256 = "0gv563swc1yn3k8w3wjj07a8q293rkx99nfp3a25vzzmbycj446f"; authors = [ "Joshua Liebow-Feeser " "Jack Wrenn " @@ -14146,9 +14090,9 @@ rec { }; "zerocopy-derive" = rec { crateName = "zerocopy-derive"; - version = "0.8.48"; + version = "0.8.52"; edition = "2021"; - sha256 = "1m5s0g92cxggqc74j83k1priz24k3z93sj5gadppd20p9c4cvqvh"; + sha256 = "0c3rhsh4sd9kdym4z55zprybjkydy9y2gvw75d72aapcfa5z7rqs"; procMacro = true; libName = "zerocopy_derive"; authors = [ @@ -14181,11 +14125,11 @@ rec { }; "zerofrom" = rec { crateName = "zerofrom"; - version = "0.1.7"; + version = "0.1.8"; edition = "2021"; - sha256 = "1py40in4rirc9q8w36q67pld0zk8ssg024xhh0cncxgal7ra3yk9"; + sha256 = "0wjjdj7gdmd0iq91gzkxl7dlv0nhkk80l4bmdpzh3a1yh48mmh0f"; authors = [ - "Manish Goregaokar " + "The ICU4X Project Developers" ]; dependencies = [ { diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/config/jvm.rs index f0233b66..f6bb04d8 100644 --- a/rust/operator-binary/src/config/jvm.rs +++ b/rust/operator-binary/src/config/jvm.rs @@ -1,9 +1,7 @@ -use serde::Serialize; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ memory::{BinaryMultiple, MemoryQuantity}, - role_utils::{self, GenericRoleConfig, JavaCommonConfig, JvmArgumentOverrides, Role}, - schemars::JsonSchema, + v2::jvm_argument_overrides::JvmArgumentOverrides, }; use crate::crd::{ConfigFileName, METRICS_PORT, STACKABLE_CONFIG_DIR, role::AnyConfig}; @@ -19,20 +17,14 @@ pub enum Error { InvalidMemoryConfig { source: stackable_operator::memory::Error, }, - - #[snafu(display("failed to merge jvm argument overrides"))] - MergeJvmArgumentOverrides { source: role_utils::Error }, } -/// All JVM arguments. -fn construct_jvm_args( +/// All JVM arguments: operator-generated base args with the already-merged +/// (role <- role group) `jvmArgumentOverrides` applied on top. +fn construct_jvm_args( merged_config: &AnyConfig, - role: &Role, - role_group: &str, -) -> Result, Error> -where - ConfigOverrides: Default + JsonSchema + Serialize, -{ + jvm_argument_overrides: &JvmArgumentOverrides, +) -> Result, Error> { let heap_size = MemoryQuantity::try_from( merged_config .resources() @@ -49,7 +41,6 @@ where .context(InvalidMemoryConfigSnafu)?; let jvm_args = vec![ - // Heap settings format!("-Xmx{java_heap}"), format!("-Xms{java_heap}"), format!( @@ -61,43 +52,26 @@ where ), ]; - let operator_generated = JvmArgumentOverrides::new_with_only_additions(jvm_args); - let merged = role - .get_merged_jvm_argument_overrides(role_group, &operator_generated) - .context(MergeJvmArgumentOverridesSnafu)?; - Ok(merged - .effective_jvm_config_after_merging() - // Sorry for the clone, that's how operator-rs is currently modelled :P - .clone()) + Ok(jvm_argument_overrides.apply_to(jvm_args)) } -/// Arguments that go into `EXTRA_ARGS`, so *not* the heap settings (which you can get using -/// [`construct_heap_jvm_args`]). -pub fn construct_non_heap_jvm_args( +/// Arguments that go into `EXTRA_ARGS` (everything except heap settings). +pub fn construct_non_heap_jvm_args( merged_config: &AnyConfig, - role: &Role, - role_group: &str, -) -> Result -where - ConfigOverrides: Default + JsonSchema + Serialize, -{ - let mut jvm_args = construct_jvm_args(merged_config, role, role_group)?; + jvm_argument_overrides: &JvmArgumentOverrides, +) -> Result { + let mut jvm_args = construct_jvm_args(merged_config, jvm_argument_overrides)?; jvm_args.retain(|arg| !is_heap_jvm_argument(arg)); Ok(jvm_args.join(" ")) } -/// Arguments that go into `KAFKA_HEAP_OPTS`. -/// You can get the normal JVM arguments using [`construct_non_heap_jvm_args`]. -pub fn construct_heap_jvm_args( +/// Arguments that go into `KAFKA_HEAP_OPTS` (only the heap settings). +pub fn construct_heap_jvm_args( merged_config: &AnyConfig, - role: &Role, - role_group: &str, -) -> Result -where - ConfigOverrides: Default + JsonSchema + Serialize, -{ - let mut jvm_args = construct_jvm_args(merged_config, role, role_group)?; + jvm_argument_overrides: &JvmArgumentOverrides, +) -> Result { + let mut jvm_args = construct_jvm_args(merged_config, jvm_argument_overrides)?; jvm_args.retain(|arg| is_heap_jvm_argument(arg)); Ok(jvm_args.join(" ")) @@ -111,18 +85,25 @@ fn is_heap_jvm_argument(jvm_argument: &str) -> bool { #[cfg(test)] mod tests { - use stackable_operator::kube::ResourceExt; - use super::*; use crate::{ - crd::{ - BrokerRole, - role::{KafkaRole, broker::BrokerConfig}, - v1alpha1, - }, - framework::role_utils::with_validated_config, + controller::test_support::{minimal_kafka, validated_cluster}, + crd::role::KafkaRole, }; + /// Pulls the broker `default` role group's merged config + merged JVM overrides out of + /// a validated cluster built from the given YAML. + fn broker_default(yaml: &str) -> (AnyConfig, JvmArgumentOverrides) { + let kafka = minimal_kafka(yaml); + let validated = validated_cluster(&kafka); + let rg = validated + .role_group_configs + .get(&KafkaRole::Broker) + .and_then(|groups| groups.get("default")) + .expect("broker default role group should exist"); + (rg.config.clone(), rg.jvm_argument_overrides.clone()) + } + #[test] fn test_construct_jvm_arguments_defaults() { let input = r#" @@ -130,6 +111,8 @@ mod tests { kind: KafkaCluster metadata: name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 spec: image: productVersion: 3.9.2 @@ -140,10 +123,9 @@ mod tests { default: replicas: 1 "#; - let (kafka_role, role, merged_config) = construct_boilerplate(input); - let non_heap_jvm_args = - construct_non_heap_jvm_args(&kafka_role, &role, &merged_config).unwrap(); - let heap_jvm_args = construct_heap_jvm_args(&kafka_role, &role, &merged_config).unwrap(); + let (merged_config, jvm) = broker_default(input); + let non_heap_jvm_args = construct_non_heap_jvm_args(&merged_config, &jvm).unwrap(); + let heap_jvm_args = construct_heap_jvm_args(&merged_config, &jvm).unwrap(); assert_eq!( non_heap_jvm_args, @@ -160,6 +142,8 @@ mod tests { kind: KafkaCluster metadata: name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 spec: image: productVersion: 3.9.2 @@ -187,10 +171,9 @@ mod tests { - -Xmx40000m - -Dhttps.proxyPort=1234 "#; - let (merged_config, role, role_group) = construct_boilerplate(input); - let non_heap_jvm_args = - construct_non_heap_jvm_args(&merged_config, &role, &role_group).unwrap(); - let heap_jvm_args = construct_heap_jvm_args(&merged_config, &role, &role_group).unwrap(); + let (merged_config, jvm) = broker_default(input); + let non_heap_jvm_args = construct_non_heap_jvm_args(&merged_config, &jvm).unwrap(); + let heap_jvm_args = construct_heap_jvm_args(&merged_config, &jvm).unwrap(); assert_eq!( non_heap_jvm_args, @@ -202,18 +185,4 @@ mod tests { ); assert_eq!(heap_jvm_args, "-Xms34406m -Xmx40000m"); } - - fn construct_boilerplate(kafka_cluster: &str) -> (AnyConfig, BrokerRole, String) { - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(kafka_cluster).expect("illegal test input"); - - let role = kafka.spec.brokers.clone().unwrap(); - let role_group = role.role_groups.get("default").unwrap(); - let default_config = - BrokerConfig::default_config(&kafka.name_any(), &KafkaRole::Broker.to_string()); - let validated = with_validated_config(role_group, &role, &default_config).unwrap(); - let merged_config = AnyConfig::Broker(validated.config); - - (merged_config, role, "default".to_owned()) - } } diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs index 2c907057..6f1881c5 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller/mod.rs @@ -20,15 +20,12 @@ pub(crate) mod build; pub(crate) mod dereference; pub(crate) mod validate; -use crate::{ - crd::{ - KafkaPodDescriptor, MetadataManager, - authorization::KafkaAuthorizationConfig, - role::{AnyConfig, AnyConfigOverrides, KafkaRole}, - security::KafkaTlsSecurity, - v1alpha1, - }, - framework::role_utils::RoleGroupConfig, +use crate::crd::{ + KafkaPodDescriptor, MetadataManager, + authorization::KafkaAuthorizationConfig, + role::{AnyConfig, AnyConfigOverrides, KafkaRole}, + security::KafkaTlsSecurity, + v1alpha1, }; pub type RoleGroupName = String; @@ -140,12 +137,62 @@ impl Resource for ValidatedCluster { /// A validated, merged Kafka role-group config. /// /// The merged config fragment is wrapped in [`AnyConfig`] and the merged -/// `configOverrides` in [`AnyConfigOverrides`], so a single role-agnostic type -/// carries both broker and controller role groups (their concrete config and -/// override types differ). Produced via the local-`framework` -/// [`with_validated_config`](crate::framework::role_utils::with_validated_config). -pub type ValidatedRoleGroupConfig = RoleGroupConfig< - AnyConfig, - stackable_operator::role_utils::JavaCommonConfig, - AnyConfigOverrides, ->; +/// `configOverrides` in [`AnyConfigOverrides`], so a single role-agnostic type carries +/// both broker and controller role groups. Produced from the upstream +/// [`stackable_operator::v2::role_utils::with_validated_config`] result in +/// [`validate`](crate::controller::validate). `jvm_argument_overrides` is already merged +/// (role <- role group) at validation time and applied as-is during build. +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedRoleGroupConfig { + pub replicas: u16, + pub config: AnyConfig, + pub config_overrides: AnyConfigOverrides, + pub env_overrides: stackable_operator::v2::builder::pod::container::EnvVarSet, + pub pod_overrides: stackable_operator::k8s_openapi::api::core::v1::PodTemplateSpec, + pub jvm_argument_overrides: + stackable_operator::v2::jvm_argument_overrides::JvmArgumentOverrides, +} + +#[cfg(test)] +pub(crate) mod test_support { + use stackable_operator::{ + cli::OperatorEnvironmentOptions, + commons::networking::DomainName, + utils::{cluster_info::KubernetesClusterInfo, yaml_from_str_singleton_map}, + }; + + use super::{ValidatedCluster, dereference::DereferencedObjects, validate::validate}; + use crate::crd::{authentication::ResolvedAuthenticationClasses, v1alpha1}; + + pub fn minimal_kafka(yaml: &str) -> v1alpha1::KafkaCluster { + yaml_from_str_singleton_map(yaml).expect("invalid test KafkaCluster YAML") + } + + fn cluster_info() -> KubernetesClusterInfo { + KubernetesClusterInfo { + cluster_domain: DomainName::try_from("cluster.local").expect("valid domain"), + } + } + + fn operator_environment() -> OperatorEnvironmentOptions { + OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_owned(), + operator_service_name: "kafka-operator".to_owned(), + image_repository: "oci.example.org".to_owned(), + } + } + + /// Runs the real validate step against a minimal (auth/OPA-free) fixture. + pub fn validated_cluster(kafka: &v1alpha1::KafkaCluster) -> ValidatedCluster { + validate( + kafka, + DereferencedObjects { + authentication_classes: ResolvedAuthenticationClasses::new(Vec::new()), + authorization_config: None, + kubernetes_cluster_info: cluster_info(), + }, + &operator_environment(), + ) + .expect("validate should succeed for the test fixture") + } +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 973a8ce1..50de94ed 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -6,20 +6,18 @@ use std::{collections::BTreeMap, str::FromStr}; use serde::Serialize; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, commons::product_image_selection, config::{fragment::FromFragment, merge::Merge}, kube::ResourceExt, - role_utils::{GenericRoleConfig, JavaCommonConfig, Role}, + role_utils::{GenericRoleConfig, Role}, schemars::JsonSchema, v2::{ builder::pod::container::{self, EnvVarName, EnvVarSet}, - types::{ - kubernetes::{NamespaceName, Uid}, - operator::ClusterName, - }, + controller_utils::{get_cluster_name, get_namespace, get_uid}, + role_utils::{JavaCommonConfig, with_validated_config}, }, }; @@ -38,7 +36,6 @@ use crate::{ security::{self, KafkaTlsSecurity}, v1alpha1, }, - framework::role_utils::with_validated_config, }; /// The operator-managed env var carrying the Kafka cluster id. @@ -62,7 +59,7 @@ pub enum Error { #[snafu(display("failed to merge and validate the role group config"))] ValidateRoleGroupConfig { - source: crate::framework::role_utils::Error, + source: stackable_operator::config::fragment::ValidationError, }, #[snafu(display("invalid environment variable name"))] @@ -74,25 +71,19 @@ pub enum Error { #[snafu(display("invalid metadata manager"))] InvalidMetadataManager { source: crate::crd::Error }, - #[snafu(display("invalid cluster name"))] - InvalidClusterName { - source: stackable_operator::v2::macros::attributed_string_type::Error, + #[snafu(display("failed to resolve the cluster name"))] + ResolveClusterName { + source: stackable_operator::v2::controller_utils::Error, }, - #[snafu(display("object defines no namespace"))] - ObjectHasNoNamespace, - - #[snafu(display("invalid cluster namespace"))] - InvalidNamespace { - source: stackable_operator::v2::macros::attributed_string_type::Error, + #[snafu(display("failed to resolve the cluster namespace"))] + ResolveNamespace { + source: stackable_operator::v2::controller_utils::Error, }, - #[snafu(display("object has no uid"))] - ObjectHasNoUid, - - #[snafu(display("invalid cluster uid"))] - InvalidUid { - source: stackable_operator::v2::macros::attributed_string_type::Error, + #[snafu(display("failed to resolve the cluster uid"))] + ResolveUid { + source: stackable_operator::v2::controller_utils::Error, }, } @@ -176,10 +167,9 @@ pub fn validate( .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; - let name = ClusterName::from_str(&kafka.name_any()).context(InvalidClusterNameSnafu)?; - let namespace = NamespaceName::from_str(&kafka.namespace().context(ObjectHasNoNamespaceSnafu)?) - .context(InvalidNamespaceSnafu)?; - let uid = Uid::from_str(&kafka.uid().context(ObjectHasNoUidSnafu)?).context(InvalidUidSnafu)?; + let name = get_cluster_name(kafka).context(ResolveClusterNameSnafu)?; + let namespace = get_namespace(kafka).context(ResolveNamespaceSnafu)?; + let uid = get_uid(kafka).context(ResolveUidSnafu)?; Ok(ValidatedCluster::new( name, @@ -203,14 +193,14 @@ pub fn validate( /// Validates every role group of a role into a map keyed by role group name. /// -/// Each role group is merged and validated via the local-`framework` +/// Each role group is merged and validated via the upstream /// [`with_validated_config`], which folds the config fragment (default <- role <- -/// role group) plus the `configOverrides`, `envOverrides`, `cliOverrides` and -/// `podOverrides` (role group wins) into a single -/// [`RoleGroupConfig`](crate::framework::role_utils::RoleGroupConfig). The concrete -/// per-role validated config and overrides are wrapped into the role-agnostic -/// [`AnyConfig`]/[`AnyConfigOverrides`] via `wrap_config`/`wrap_overrides`, and the -/// operator-managed `KAFKA_CLUSTER_ID` is injected into the env overrides. +/// role group) plus the `configOverrides`, `envOverrides`, `podOverrides` and +/// `jvmArgumentOverrides` (role group wins) into a single +/// [`ValidatedRoleGroupConfig`]. The concrete per-role validated config and overrides +/// are wrapped into the role-agnostic [`AnyConfig`]/[`AnyConfigOverrides`] via +/// `wrap_config`/`wrap_overrides`, and the operator-managed `KAFKA_CLUSTER_ID` is +/// injected into the env overrides. fn validate_role_group_configs( role: &Role, default_config: Config, @@ -226,7 +216,7 @@ where role.role_groups .iter() .map(|(role_group_name, role_group)| { - let validated = with_validated_config::< + let merged = with_validated_config::< ValidatedConfig, JavaCommonConfig, Config, @@ -235,17 +225,25 @@ where >(role_group, role, &default_config) .context(ValidateRoleGroupConfigSnafu)?; - // Re-wrap the per-role validated config and overrides into the role-agnostic - // enums; the merged env/cli/pod overrides carry over unchanged, except that - // `KAFKA_CLUSTER_ID` is injected into the env overrides. + // The upstream merge returns env overrides as a HashMap. Convert to an + // EnvVarSet (validating names early), then inject KAFKA_CLUSTER_ID. + let mut env_overrides = EnvVarSet::new(); + for (name, value) in merged.config.env_overrides { + let name = EnvVarName::from_str(&name).context(InvalidEnvVarNameSnafu)?; + env_overrides = env_overrides.with_value(&name, value); + } + let env_overrides = inject_cluster_id(env_overrides, cluster_id)?; + let validated = ValidatedRoleGroupConfig { - replicas: validated.replicas, - config: wrap_config(validated.config), - config_overrides: wrap_overrides(validated.config_overrides), - env_overrides: inject_cluster_id(validated.env_overrides, cluster_id)?, - cli_overrides: validated.cli_overrides, - pod_overrides: validated.pod_overrides, - product_specific_common_config: validated.product_specific_common_config, + replicas: merged.replicas.unwrap_or(1), + config: wrap_config(merged.config.config), + config_overrides: wrap_overrides(merged.config.config_overrides), + env_overrides, + pod_overrides: merged.config.pod_overrides, + jvm_argument_overrides: merged + .config + .product_specific_common_config + .jvm_argument_overrides, }; Ok((role_group_name.clone(), validated)) }) diff --git a/rust/operator-binary/src/crd/affinity.rs b/rust/operator-binary/src/crd/affinity.rs index 51e0ae6b..77d50500 100644 --- a/rust/operator-binary/src/crd/affinity.rs +++ b/rust/operator-binary/src/crd/affinity.rs @@ -30,16 +30,11 @@ mod tests { api::core::v1::{PodAffinityTerm, PodAntiAffinity, WeightedPodAffinityTerm}, apimachinery::pkg::apis::meta::v1::LabelSelector, }, - kube::ResourceExt, }; use crate::{ - crd::{ - KafkaRole, - role::{AnyConfig, broker::BrokerConfig}, - v1alpha1, - }, - framework::role_utils::with_validated_config, + controller::test_support::{minimal_kafka, validated_cluster}, + crd::KafkaRole, }; #[rstest] @@ -50,6 +45,8 @@ mod tests { kind: KafkaCluster metadata: name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 spec: image: productVersion: 3.9.2 @@ -61,13 +58,14 @@ mod tests { replicas: 1 "#; - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(input).expect("illegal test input"); - let broker_role = kafka.spec.brokers.clone().unwrap(); - let role_group = broker_role.role_groups.get("default").unwrap(); - let default_config = BrokerConfig::default_config(&kafka.name_any(), &role.to_string()); - let validated = with_validated_config(role_group, &broker_role, &default_config).unwrap(); - let merged_config = AnyConfig::Broker(validated.config); + let kafka = minimal_kafka(input); + let validated = validated_cluster(&kafka); + let merged_config = validated + .role_group_configs + .get(&role) + .and_then(|groups| groups.get("default")) + .map(|rg| &rg.config) + .expect("role group should exist"); assert_eq!( merged_config.affinity, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 630595f6..7f173b42 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -21,11 +21,11 @@ use stackable_operator::{ config::merge::Merge, deep_merger::ObjectOverrides, kube::{CustomResource, runtime::reflector::ObjectRef}, - role_utils::{GenericRoleConfig, JavaCommonConfig, Role, RoleGroupRef}, + role_utils::{GenericRoleConfig, Role, RoleGroupRef}, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, utils::cluster_info::KubernetesClusterInfo, - v2::config_overrides::KeyValueConfigOverrides, + v2::{config_overrides::KeyValueConfigOverrides, role_utils::JavaCommonConfig}, versioned::versioned, }; use strum::{Display, EnumIter, EnumString}; diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 2024d519..977d91ac 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -34,7 +34,7 @@ pub enum BrokerContainer { Kafka, } -#[derive(Debug, Default, PartialEq, Fragment, JsonSchema)] +#[derive(Clone, Debug, Default, PartialEq, Fragment, JsonSchema)] #[fragment_attrs( derive( Clone, diff --git a/rust/operator-binary/src/crd/role/controller.rs b/rust/operator-binary/src/crd/role/controller.rs index fbaf898c..ec025eab 100644 --- a/rust/operator-binary/src/crd/role/controller.rs +++ b/rust/operator-binary/src/crd/role/controller.rs @@ -33,7 +33,7 @@ pub enum ControllerContainer { Kafka, } -#[derive(Debug, Default, PartialEq, Fragment, JsonSchema)] +#[derive(Clone, Debug, Default, PartialEq, Fragment, JsonSchema)] #[fragment_attrs( derive( Clone, diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index 41cdcf1f..96e698cf 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -18,7 +18,6 @@ use stackable_operator::{ use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; use crate::{ - config::jvm::{construct_heap_jvm_args, construct_non_heap_jvm_args}, crd::{ ConfigFileName, role::{ @@ -81,9 +80,6 @@ pub enum Error { #[snafu(display("missing role group {rolegroup:?} for role {role:?}"))] MissingRoleGroup { role: String, rolegroup: String }, - - #[snafu(display("failed to construct JVM arguments"))] - ConstructJvmArguments { source: crate::config::jvm::Error }, } #[derive( @@ -137,58 +133,6 @@ impl KafkaRole { "kafka" } - pub fn construct_non_heap_jvm_args( - &self, - merged_config: &AnyConfig, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - match self { - Self::Broker => construct_non_heap_jvm_args( - merged_config, - kafka.broker_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - Self::Controller => construct_non_heap_jvm_args( - merged_config, - kafka.controller_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - } - } - - pub fn construct_heap_jvm_args( - &self, - merged_config: &AnyConfig, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - match self { - Self::Broker => construct_heap_jvm_args( - merged_config, - kafka.broker_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - Self::Controller => construct_heap_jvm_args( - merged_config, - kafka.controller_role().with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })?, - rolegroup, - ) - .context(ConstructJvmArgumentsSnafu), - } - } - pub fn role_pod_overrides( &self, kafka: &v1alpha1::KafkaCluster, @@ -295,7 +239,7 @@ impl KafkaRole { } /// Configuration for a role and rolegroup of an unknown type. -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum AnyConfig { Broker(BrokerConfig), Controller(ControllerConfig), diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs deleted file mode 100644 index 6e28e88a..00000000 --- a/rust/operator-binary/src/framework.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Local framework helpers that mirror the work-in-progress upstream -//! `stackable_operator::v2::*` modules. -//! -//! We vendor `role_utils` because the upstream `v2::role_utils` requires -//! `CommonConfig: Merge`. Kafka (like hdfs and trino) uses `JavaCommonConfig`, -//! whose JVM-argument merge is fallible and so does not implement `Merge`. -//! -//! Follow-up: replace with `stackable_operator::v2::role_utils::*` once upstream -//! relaxes the `Merge` bound. - -pub mod role_utils; diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs deleted file mode 100644 index b4bc9a8b..00000000 --- a/rust/operator-binary/src/framework/role_utils.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Vendored variant of `stackable_operator::v2::role_utils` from the -//! `smooth-operator` branch, with simplifications appropriate for kafka-operator. -//! -//! Differences from upstream: -//! - No `cli_overrides_to_vec` helper, `ResourceNames`, or service-account helpers. -//! - The `CommonConfig` (a.k.a. `product_specific_common_config`) does NOT need to -//! implement `Merge`. Kafka uses `JavaCommonConfig`, which intentionally does not -//! implement `Merge` because its inner `JvmArgumentOverrides::try_merge` is -//! fallible (regex validation). The `RoleGroupConfig::product_specific_common_config` -//! field here simply carries the role-group level value through. -//! -//! Replace with `stackable_operator::v2::role_utils::*` once upstream relaxes the -//! `Merge` bound. - -use std::{ - collections::{BTreeMap, HashMap}, - str::FromStr, -}; - -use serde::Serialize; -use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - config::{ - fragment::{self, FromFragment}, - merge::{Merge, merge}, - }, - k8s_openapi::{DeepMerge, api::core::v1::PodTemplateSpec}, - role_utils::{Role, RoleGroup}, - schemars::JsonSchema, - v2::builder::pod::container::{self, EnvVarName, EnvVarSet}, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("failed to validate the role group config"))] - ValidateConfig { source: fragment::ValidationError }, - - #[snafu(display("invalid environment variable override name"))] - ParseEnvVarName { source: container::Error }, -} - -/// Kafka-friendly view of a validated, merged `RoleGroup`. -#[derive(Clone, Debug, PartialEq)] -pub struct RoleGroupConfig { - pub replicas: u16, - pub config: Config, - pub config_overrides: ConfigOverrides, - pub env_overrides: EnvVarSet, - pub cli_overrides: BTreeMap, - pub pod_overrides: PodTemplateSpec, - pub product_specific_common_config: CommonConfig, -} - -/// Merges and validates the `RoleGroup` with the given `role` and `default_config`. -pub fn with_validated_config( - role_group: &RoleGroup, - role: &Role, - default_config: &Config, -) -> Result, Error> -where - ValidatedConfig: FromFragment, - CommonConfig: Clone + Default + JsonSchema + Serialize, - Config: Clone + Merge, - RoleConfig: Default + JsonSchema + Serialize, - ConfigOverrides: Clone + Default + JsonSchema + Merge + Serialize, -{ - let validated_config = - validate_config(role_group, role, default_config).context(ValidateConfigSnafu)?; - Ok(RoleGroupConfig { - replicas: role_group.replicas.unwrap_or(1), - config: validated_config, - config_overrides: merged_config_overrides( - &role.config.config_overrides, - role_group.config.config_overrides.clone(), - ), - env_overrides: merged_env_overrides( - &role.config.env_overrides, - &role_group.config.env_overrides, - )?, - cli_overrides: merged_cli_overrides( - role.config.cli_overrides.clone(), - role_group.config.cli_overrides.clone(), - ), - pod_overrides: merged_pod_overrides( - role.config.pod_overrides.clone(), - role_group.config.pod_overrides.clone(), - ), - product_specific_common_config: role_group.config.product_specific_common_config.clone(), - }) -} - -fn validate_config( - role_group: &RoleGroup, - role: &Role, - default_config: &Config, -) -> Result -where - ValidatedConfig: FromFragment, - CommonConfig: Default + JsonSchema + Serialize, - Config: Clone + Merge, - RoleConfig: Default + JsonSchema + Serialize, - ConfigOverrides: Default + JsonSchema + Serialize, -{ - role_group.validate_config(role, default_config) -} - -fn merged_config_overrides( - role_config_overrides: &ConfigOverrides, - role_group_config_overrides: ConfigOverrides, -) -> ConfigOverrides -where - ConfigOverrides: Merge, -{ - merge(role_group_config_overrides, role_config_overrides) -} - -fn merged_env_overrides( - role_env_overrides: &HashMap, - role_group_env_overrides: &HashMap, -) -> Result { - // Process the role first, then the role group, so that role-group overrides win on key - // collisions (`EnvVarSet::with_value` overrides earlier entries with the same name). - let mut env_overrides = EnvVarSet::new(); - for (name, value) in role_env_overrides - .iter() - .chain(role_group_env_overrides.iter()) - { - env_overrides = env_overrides.with_value( - &EnvVarName::from_str(name).context(ParseEnvVarNameSnafu)?, - value.clone(), - ); - } - Ok(env_overrides) -} - -fn merged_cli_overrides( - role_cli_overrides: BTreeMap, - role_group_cli_overrides: BTreeMap, -) -> BTreeMap { - let mut merged = role_cli_overrides; - merged.extend(role_group_cli_overrides); - merged -} - -fn merged_pod_overrides( - role_pod_overrides: PodTemplateSpec, - role_group_pod_overrides: PodTemplateSpec, -) -> PodTemplateSpec { - let mut merged = role_pod_overrides; - merged.merge_from(role_group_pod_overrides); - merged -} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index fd1e1b9a..b5f6b884 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -43,7 +43,6 @@ use crate::{ mod config; mod controller; mod crd; -mod framework; mod kafka_controller; mod kerberos; mod operations; diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index 3df261a2..d4445dcd 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -105,7 +105,7 @@ pub enum Error { }, #[snafu(display("failed to construct JVM arguments"))] - ConstructJvmArguments { source: crate::crd::role::Error }, + ConstructJvmArguments { source: crate::config::jvm::Error }, #[snafu(display("failed to configure graceful shutdown"))] GracefulShutdown { @@ -291,15 +291,19 @@ pub fn build_broker_rolegroup_statefulset( )]) .add_env_var( "EXTRA_ARGS", - kafka_role - .construct_non_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, + crate::config::jvm::construct_non_heap_jvm_args( + merged_config, + &validated_rg.jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, ) .add_env_var( KAFKA_HEAP_OPTS, - kafka_role - .construct_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, + crate::config::jvm::construct_heap_jvm_args( + merged_config, + &validated_rg.jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, ) .add_env_var( kafka_log_opts_env_var(), @@ -657,15 +661,19 @@ pub fn build_controller_rolegroup_statefulset( .add_env_var("PRE_STOP_CONTROLLER_SLEEP_SECONDS", "10") .add_env_var( "EXTRA_ARGS", - kafka_role - .construct_non_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, + crate::config::jvm::construct_non_heap_jvm_args( + merged_config, + &validated_rg.jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, ) .add_env_var( KAFKA_HEAP_OPTS, - kafka_role - .construct_heap_jvm_args(merged_config, kafka, &rolegroup_ref.role_group) - .context(ConstructJvmArgumentsSnafu)?, + crate::config::jvm::construct_heap_jvm_args( + merged_config, + &validated_rg.jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, ) .add_env_var( kafka_log_opts_env_var(), From 43dc9e5402ba34b0ad8d82f503579a419e0e9711 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Fri, 12 Jun 2026 13:13:52 +0200 Subject: [PATCH 35/47] refactor: use v2 owerref, labels, RoleGroupName, cleanup --- rust/operator-binary/src/config/jvm.rs | 2 +- .../src/config/node_id_hasher.rs | 12 +- .../src/controller/build/config_map.rs | 91 +-- .../src/controller/build/discovery.rs | 47 +- .../build/properties/broker_properties.rs | 4 +- .../build/properties/controller_properties.rs | 4 +- .../controller/build/properties/listener.rs | 533 ++++++++++++++++++ .../controller/build/properties/logging.rs | 30 +- .../src/controller/build/properties/mod.rs | 1 + rust/operator-binary/src/controller/mod.rs | 244 +++++++- .../src/controller/validate.rs | 30 +- rust/operator-binary/src/crd/affinity.rs | 2 +- rust/operator-binary/src/crd/listener.rs | 505 +---------------- rust/operator-binary/src/crd/mod.rs | 139 +---- rust/operator-binary/src/crd/role/mod.rs | 23 +- rust/operator-binary/src/kafka_controller.rs | 94 +-- rust/operator-binary/src/resource/listener.rs | 50 +- rust/operator-binary/src/resource/service.rs | 125 ++-- .../src/resource/statefulset.rs | 166 ++---- 19 files changed, 1059 insertions(+), 1043 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/properties/listener.rs diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/config/jvm.rs index f6bb04d8..91b97255 100644 --- a/rust/operator-binary/src/config/jvm.rs +++ b/rust/operator-binary/src/config/jvm.rs @@ -99,7 +99,7 @@ mod tests { let rg = validated .role_group_configs .get(&KafkaRole::Broker) - .and_then(|groups| groups.get("default")) + .and_then(|groups| groups.get(&"default".parse().unwrap())) .expect("broker default role group should exist"); (rg.config.clone(), rg.jvm_argument_overrides.clone()) } diff --git a/rust/operator-binary/src/config/node_id_hasher.rs b/rust/operator-binary/src/config/node_id_hasher.rs index 51690a0e..a8ae5f3c 100644 --- a/rust/operator-binary/src/config/node_id_hasher.rs +++ b/rust/operator-binary/src/config/node_id_hasher.rs @@ -1,6 +1,4 @@ -use stackable_operator::role_utils::RoleGroupRef; - -use crate::crd::v1alpha1::KafkaCluster; +use crate::crd::role::KafkaRole; /// The Kafka node.id needs to be unique across the Kafka cluster. /// This function generates an integer that is stable for a given role group @@ -8,12 +6,8 @@ use crate::crd::v1alpha1::KafkaCluster; /// This integer is then added to the pod index to compute the final node.id /// The node.id is only set and used in Kraft mode. /// Warning: this is not safe from collisions. -pub fn node_id_hash32_offset(rolegroup_ref: &RoleGroupRef) -> u32 { - let hash = fnv_hash32(&format!( - "{role}-{rolegroup}", - role = rolegroup_ref.role, - rolegroup = rolegroup_ref.role_group - )); +pub fn node_id_hash32_offset(role: &KafkaRole, role_group: &str) -> u32 { + let hash = fnv_hash32(&format!("{role}-{role_group}")); let range = hash & 0x0000FFFF; // Kafka uses signed integer range * 0x00007FFF diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 3db8337e..9e71d683 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -3,65 +3,70 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, k8s_openapi::api::core::v1::ConfigMap, - role_utils::RoleGroupRef, - v2::config_file_writer::{PropertiesWriterError, to_java_properties_string}, + product_logging::framework::VECTOR_CONFIG_FILE, + v2::{ + builder::meta::ownerreference_from_resource, + config_file_writer::{PropertiesWriterError, to_java_properties_string}, + }, }; use crate::{ controller::{ - ValidatedCluster, ValidatedRoleGroupConfig, + RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, build::properties::logging::role_group_config_map_data, }, crd::{ ConfigFileName, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, listener::{KafkaListenerConfig, node_address_cmd}, role::AnyConfig, - v1alpha1, }, - kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, }; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("failed to build ConfigMap for {}", rolegroup))] + #[snafu(display("failed to build ConfigMap for role group {role_group}"))] BuildRoleGroupConfig { source: stackable_operator::builder::configmap::Error, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, }, - #[snafu(display("failed to serialize [{}] for {rolegroup}", ConfigFileName::Security))] + #[snafu(display( + "failed to serialize [{}] for role group {role_group}", + ConfigFileName::Security + ))] JvmSecurityProperties { source: PropertiesWriterError, - rolegroup: String, - }, - - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, + role_group: RoleGroupName, }, - #[snafu(display("failed to serialize config for {rolegroup}"))] + #[snafu(display("failed to serialize config for role group {role_group}"))] SerializeConfig { source: PropertiesWriterError, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, + }, + + #[snafu(display("failed to build pod descriptors"))] + BuildPodDescriptors { + source: crate::controller::PodDescriptorsError, }, #[snafu(display("no Kraft controllers found to build"))] NoKraftControllersFound, } -/// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator +/// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator. +/// +/// `vector_config` is the Vector agent config built by the caller (where a `RoleGroupRef` is +/// available); it is `None` when the Vector agent is disabled. Resource naming and labels use the +/// role (derived from `validated_rg.config`) and the typed `role_group_name`. pub fn build_rolegroup_config_map( validated_cluster: &ValidatedCluster, - rolegroup: &RoleGroupRef, + role_group_name: &RoleGroupName, validated_rg: &ValidatedRoleGroupConfig, listener_config: &KafkaListenerConfig, + vector_config: Option, ) -> Result { + let role = validated_rg.config.kafka_role(); let cluster_config = &validated_cluster.cluster_config; let kafka_security = &cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; @@ -72,7 +77,11 @@ pub fn build_rolegroup_config_map( .overrides .clone(); - if cluster_config.is_kraft_mode() && cluster_config.pod_descriptors.is_empty() { + let pod_descriptors = validated_cluster + .pod_descriptors(None) + .context(BuildPodDescriptorsSnafu)?; + + if cluster_config.is_kraft_mode() && pod_descriptors.is_empty() { return NoKraftControllersFoundSnafu.fail(); } @@ -80,12 +89,14 @@ pub fn build_rolegroup_config_map( AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( cluster_config, listener_config, + &pod_descriptors, config_overrides, ), AnyConfig::Controller(_) => { crate::controller::build::properties::controller_properties::build( cluster_config, listener_config, + &pod_descriptors, config_overrides, ) } @@ -101,24 +112,25 @@ pub fn build_rolegroup_config_map( .metadata( ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(rolegroup.object_name()) - .ownerreference_from_resource(validated_cluster, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( + .name( + validated_cluster + .resource_names(&role, role_group_name) + .role_group_config_map() + .to_string(), + ) + .ownerreference(ownerreference_from_resource( validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels(validated_cluster.recommended_labels(&role, role_group_name)) .build(), ) .add_data( kafka_config_file_name, to_java_properties_string(kafka_config.iter()).with_context(|_| { SerializeConfigSnafu { - rolegroup: rolegroup.clone(), + role_group: role_group_name.clone(), } })?, ) @@ -126,7 +138,7 @@ pub fn build_rolegroup_config_map( ConfigFileName::Security.to_string(), to_java_properties_string(jvm_sec_props.iter()).with_context(|_| { JvmSecurityPropertiesSnafu { - rolegroup: rolegroup.role_group.clone(), + role_group: role_group_name.clone(), } })?, ) @@ -139,7 +151,7 @@ pub fn build_rolegroup_config_map( .filter_map(|(k, v)| v.as_ref().map(|v| (k, v))), ) .with_context(|_| JvmSecurityPropertiesSnafu { - rolegroup: rolegroup.role_group.clone(), + role_group: role_group_name.clone(), })?, ) // This file contains the JAAS configuration for Kerberos authentication @@ -156,7 +168,6 @@ pub fn build_rolegroup_config_map( let config_data = role_group_config_map_data( &resolved_product_image.product_version, - rolegroup, &validated_rg.config, ); for (file_name, data) in config_data { @@ -165,10 +176,14 @@ pub fn build_rolegroup_config_map( } } + if let Some(vector_config) = vector_config { + cm_builder.add_data(VECTOR_CONFIG_FILE, vector_config); + } + cm_builder .build() .with_context(|_| BuildRoleGroupConfigSnafu { - rolegroup: rolegroup.clone(), + role_group: role_group_name.clone(), }) } diff --git a/rust/operator-binary/src/controller/build/discovery.rs b/rust/operator-binary/src/controller/build/discovery.rs index ef78a7e8..0c781694 100644 --- a/rust/operator-binary/src/controller/build/discovery.rs +++ b/rust/operator-binary/src/controller/build/discovery.rs @@ -1,27 +1,20 @@ -use std::num::TryFromIntError; +use std::{num::TryFromIntError, str::FromStr}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, crd::listener, k8s_openapi::api::core::v1::ConfigMap, - kube::runtime::reflector::ObjectRef, + v2::builder::meta::ownerreference_from_resource, }; use crate::{ - controller::ValidatedCluster, - crd::{role::KafkaRole, v1alpha1}, - kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, + controller::{RoleGroupName, ValidatedCluster}, + crd::role::KafkaRole, }; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("object {} is missing metadata to build owner reference", kafka))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - kafka: ObjectRef, - }, - #[snafu(display("could not find service port with name {}", port_name))] NoServicePort { port_name: String }, @@ -32,11 +25,6 @@ pub enum Error { BuildConfigMap { source: stackable_operator::builder::configmap::Error, }, - - #[snafu(display("failed to build metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, } /// Build a discovery [`ConfigMap`] containing information about how to connect to a certain @@ -46,7 +34,6 @@ pub fn build_discovery_configmap( listeners: &[listener::v1alpha1::Listener], ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; - let resolved_product_image = &validated_cluster.image; let port_name = if kafka_security.has_kerberos_enabled() { kafka_security.bootstrap_port_name() @@ -65,18 +52,18 @@ pub fn build_discovery_configmap( .metadata( ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .ownerreference_from_resource(validated_cluster, None, Some(true)) - .with_context(|_| ObjectMissingMetadataForOwnerRefSnafu { - kafka: cluster_object_ref(validated_cluster), - })? - .with_recommended_labels(&build_recommended_labels( + .ownerreference(ownerreference_from_resource( validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.product_version, - &KafkaRole::Broker.to_string(), - "discovery", + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels( + validated_cluster.recommended_labels( + &KafkaRole::Broker, + &RoleGroupName::from_str("discovery") + .expect("'discovery' is a valid role group name"), + ), + ) .build(), ) .add_data("KAFKA", bootstrap_servers) @@ -84,12 +71,6 @@ pub fn build_discovery_configmap( .context(BuildConfigMapSnafu) } -/// An [`ObjectRef`] to the owning cluster, built from the validated identity — used only for -/// error context. -fn cluster_object_ref(cluster: &ValidatedCluster) -> ObjectRef { - ObjectRef::new(cluster.name.as_ref()).within(cluster.namespace.as_ref()) -} - fn listener_hosts( listeners: &[listener::v1alpha1::Listener], port_name: &str, diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs index 700e0822..a950f352 100644 --- a/rust/operator-binary/src/controller/build/properties/broker_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -4,6 +4,7 @@ use super::kraft_controllers; use crate::{ controller::ValidatedClusterConfig, crd::{ + KafkaPodDescriptor, listener::{KafkaListenerConfig, KafkaListenerName}, role::{ KAFKA_ADVERTISED_LISTENERS, KAFKA_BROKER_ID, KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, @@ -17,9 +18,10 @@ use crate::{ pub fn build( cluster_config: &ValidatedClusterConfig, listener_config: &KafkaListenerConfig, + pod_descriptors: &[KafkaPodDescriptor], overrides: BTreeMap, ) -> BTreeMap { - let kraft_controllers = kraft_controllers(&cluster_config.pod_descriptors); + let kraft_controllers = kraft_controllers(pod_descriptors); let mut result = BTreeMap::from([ ( diff --git a/rust/operator-binary/src/controller/build/properties/controller_properties.rs b/rust/operator-binary/src/controller/build/properties/controller_properties.rs index 289d0cb2..ff7a57d1 100644 --- a/rust/operator-binary/src/controller/build/properties/controller_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -4,6 +4,7 @@ use super::kraft_controllers; use crate::{ controller::ValidatedClusterConfig, crd::{ + KafkaPodDescriptor, listener::{KafkaListenerConfig, KafkaListenerName}, role::{ KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS, KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, @@ -16,9 +17,10 @@ use crate::{ pub fn build( cluster_config: &ValidatedClusterConfig, listener_config: &KafkaListenerConfig, + pod_descriptors: &[KafkaPodDescriptor], overrides: BTreeMap, ) -> BTreeMap { - let kraft_controllers = kraft_controllers(&cluster_config.pod_descriptors).join(","); + let kraft_controllers = kraft_controllers(pod_descriptors).join(","); let mut result = BTreeMap::from([ ( diff --git a/rust/operator-binary/src/controller/build/properties/listener.rs b/rust/operator-binary/src/controller/build/properties/listener.rs new file mode 100644 index 00000000..5edd8c4d --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/listener.rs @@ -0,0 +1,533 @@ +//! Builds the Kafka listener configuration (`listeners` / `advertised.listeners` / +//! `listener.security.protocol.map`) for a role group's broker/controller properties. +//! +//! Consumes the [`ValidatedCluster`] (for the namespace and role-group resource names) instead of +//! the raw CRD. + +use std::collections::BTreeMap; + +use stackable_operator::{ + utils::cluster_info::KubernetesClusterInfo, v2::types::kubernetes::NamespaceName, +}; + +use crate::{ + controller::{RoleGroupName, ValidatedCluster}, + crd::{ + STACKABLE_LISTENER_BROKER_DIR, + listener::{ + KafkaListener, KafkaListenerConfig, KafkaListenerName, KafkaListenerProtocol, + LISTENER_LOCAL_ADDRESS, node_address_cmd, node_port_cmd, + }, + role::KafkaRole, + security::KafkaTlsSecurity, + }, +}; + +pub fn get_kafka_listener_config( + validated_cluster: &ValidatedCluster, + kafka_security: &KafkaTlsSecurity, + role: &KafkaRole, + role_group_name: &RoleGroupName, + cluster_info: &KubernetesClusterInfo, +) -> KafkaListenerConfig { + let headless_service_name = validated_cluster + .resource_names(role, role_group_name) + .headless_service_name(); + let pod_fqdn = pod_fqdn( + &validated_cluster.namespace, + headless_service_name.as_ref(), + cluster_info, + ); + let mut listeners = vec![]; + let mut advertised_listeners = vec![]; + let mut listener_security_protocol_map: BTreeMap = + BTreeMap::new(); + + // CLIENT + if kafka_security.has_kerberos_enabled() { + // 1) Kerberos and TLS authentication classes are mutually exclusive + listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: KafkaTlsSecurity::SECURE_CLIENT_PORT.to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Client, KafkaListenerProtocol::SaslSsl); + } else if kafka_security.tls_client_authentication_class().is_some() + || kafka_security.tls_server_secret_class().is_some() + { + // 2) Client listener uses TLS (possibly with authentication) + listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: kafka_security.client_port().to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Client, KafkaListenerProtocol::Ssl); + } else { + // 3) If no client auth or tls is required we expose CLIENT with PLAINTEXT + listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: KafkaTlsSecurity::CLIENT_PORT.to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Client, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Client, KafkaListenerProtocol::Plaintext); + } + + // INTERNAL / CONTROLLER + if kafka_security.has_kerberos_enabled() || kafka_security.tls_internal_secret_class().is_some() + { + // 5) & 6) Kerberos and TLS authentication classes are mutually exclusive but both require internal tls to be used + listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: pod_fqdn.to_string(), + port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Internal, KafkaListenerProtocol::Ssl); + listener_security_protocol_map + .insert(KafkaListenerName::Controller, KafkaListenerProtocol::Ssl); + } else { + // 7) If no internal tls is required we expose INTERNAL as PLAINTEXT + listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: kafka_security.internal_port().to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Internal, + host: pod_fqdn.to_string(), + port: kafka_security.internal_port().to_string(), + }); + listener_security_protocol_map.insert( + KafkaListenerName::Internal, + KafkaListenerProtocol::Plaintext, + ); + listener_security_protocol_map.insert( + KafkaListenerName::Controller, + KafkaListenerProtocol::Plaintext, + ); + } + + // BOOTSTRAP + if kafka_security.has_kerberos_enabled() { + listeners.push(KafkaListener { + name: KafkaListenerName::Bootstrap, + host: LISTENER_LOCAL_ADDRESS.to_string(), + port: kafka_security.bootstrap_port().to_string(), + }); + advertised_listeners.push(KafkaListener { + name: KafkaListenerName::Bootstrap, + host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port: node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name(), + ), + }); + listener_security_protocol_map + .insert(KafkaListenerName::Bootstrap, KafkaListenerProtocol::SaslSsl); + } + + KafkaListenerConfig { + listeners, + advertised_listeners, + listener_security_protocol_map, + } +} + +pub(crate) fn pod_fqdn( + namespace: &NamespaceName, + sts_service_name: &str, + cluster_info: &KubernetesClusterInfo, +) -> String { + format!( + "${{env:POD_NAME}}.{sts_service_name}.{namespace}.svc.{cluster_domain}", + cluster_domain = cluster_info.cluster_domain + ) +} + +#[cfg(test)] +mod tests { + use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + commons::networking::DomainName, + crd::authentication::{core, kerberos, tls}, + }; + + use super::*; + use crate::{ + controller::test_support::{minimal_kafka, validated_cluster}, + crd::authentication::ResolvedAuthenticationClasses, + }; + + fn default_cluster_info() -> KubernetesClusterInfo { + KubernetesClusterInfo { + cluster_domain: DomainName::try_from("cluster.local").unwrap(), + } + } + + #[test] + fn test_get_kafka_listeners_config() { + // The fixture only needs to resolve a `ValidatedCluster` (for the namespace and the + // `--` resource names); the listener output is driven by the + // explicit `kafka_security` below, so no authentication/TLS is configured here. + let kafka_cluster = r#" + apiVersion: kafka.stackable.tech/v1alpha1 + kind: KafkaCluster + metadata: + name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 + spec: + image: + productVersion: 3.9.2 + clusterConfig: + zookeeperConfigMapName: xyz + brokers: + roleGroups: + default: + replicas: 1 + "#; + let kafka = minimal_kafka(kafka_cluster); + let validated = validated_cluster(&kafka); + let kafka_security = KafkaTlsSecurity::new( + ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("auth-class").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Tls( + tls::v1alpha1::AuthenticationProvider { + client_cert_secret_class: Some("client-auth-secret-class".to_string()), + }, + ), + }, + }]), + "internalTls".to_string(), + Some("tls".to_string()), + None, + ); + let cluster_info = default_cluster_info(); + let role_group_name: RoleGroupName = "default".parse().unwrap(); + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::Ssl, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Ssl, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Ssl, + ) + ); + + let kafka_security = KafkaTlsSecurity::new( + ResolvedAuthenticationClasses::new(vec![]), + "tls".to_string(), + Some("tls".to_string()), + None, + ); + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::Ssl, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Ssl, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Ssl, + ) + ); + + let kafka_security = KafkaTlsSecurity::new( + ResolvedAuthenticationClasses::new(vec![]), + "".to_string(), + None, + None, + ); + + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::Plaintext, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Plaintext, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Plaintext, + ) + ); + } + + #[test] + fn test_get_kafka_kerberos_listeners_config() { + // See the comment in `test_get_kafka_listeners_config`: the fixture only resolves a + // `ValidatedCluster`; Kerberos is configured via the explicit `kafka_security` below. + let kafka_cluster = r#" + apiVersion: kafka.stackable.tech/v1alpha1 + kind: KafkaCluster + metadata: + name: simple-kafka + namespace: default + uid: 12345678-1234-1234-1234-123456789012 + spec: + image: + productVersion: 3.9.2 + clusterConfig: + zookeeperConfigMapName: xyz + brokers: + roleGroups: + default: + replicas: 1 + "#; + let kafka = minimal_kafka(kafka_cluster); + let validated = validated_cluster(&kafka); + let kafka_security = KafkaTlsSecurity::new( + ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("auth-class").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Kerberos( + kerberos::v1alpha1::AuthenticationProvider { + kerberos_secret_class: "kerberos-secret-class".to_string(), + }, + ), + }, + }]), + "tls".to_string(), + Some("tls".to_string()), + None, + ); + let cluster_info = default_cluster_info(); + let role_group_name: RoleGroupName = "default".parse().unwrap(); + let config = get_kafka_listener_config( + &validated, + &kafka_security, + &KafkaRole::Broker, + &role_group_name, + &cluster_info, + ); + + assert_eq!( + config.listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", + name = KafkaListenerName::Client, + host = LISTENER_LOCAL_ADDRESS, + port = kafka_security.client_port(), + internal_name = KafkaListenerName::Internal, + internal_host = LISTENER_LOCAL_ADDRESS, + internal_port = kafka_security.internal_port(), + bootstrap_name = KafkaListenerName::Bootstrap, + bootstrap_host = LISTENER_LOCAL_ADDRESS, + bootstrap_port = kafka_security.bootstrap_port(), + ) + ); + + assert_eq!( + config.advertised_listeners(), + format!( + "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", + name = KafkaListenerName::Client, + host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + internal_name = KafkaListenerName::Internal, + internal_host = pod_fqdn( + &validated.namespace, + validated + .resource_names(&KafkaRole::Broker, &role_group_name) + .headless_service_name() + .as_ref(), + &cluster_info + ), + internal_port = kafka_security.internal_port(), + bootstrap_name = KafkaListenerName::Bootstrap, + bootstrap_host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), + bootstrap_port = node_port_cmd( + STACKABLE_LISTENER_BROKER_DIR, + kafka_security.client_port_name() + ), + ) + ); + + assert_eq!( + config.listener_security_protocol_map(), + format!( + "{name}:{protocol},{internal_name}:{internal_protocol},{bootstrap_name}:{bootstrap_protocol},{controller_name}:{controller_protocol}", + name = KafkaListenerName::Client, + protocol = KafkaListenerProtocol::SaslSsl, + internal_name = KafkaListenerName::Internal, + internal_protocol = KafkaListenerProtocol::Ssl, + bootstrap_name = KafkaListenerName::Bootstrap, + bootstrap_protocol = KafkaListenerProtocol::SaslSsl, + controller_name = KafkaListenerName::Controller, + controller_protocol = KafkaListenerProtocol::Ssl, + ) + ); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/logging.rs b/rust/operator-binary/src/controller/build/properties/logging.rs index da198541..9834d39e 100644 --- a/rust/operator-binary/src/controller/build/properties/logging.rs +++ b/rust/operator-binary/src/controller/build/properties/logging.rs @@ -27,10 +27,12 @@ const KAFKA_LOG4J2_FILE: &str = "kafka.log4j2.xml"; const CONSOLE_CONVERSION_PATTERN_LOG4J: &str = "[%d] %p %m (%c)%n"; const CONSOLE_CONVERSION_PATTERN_LOG4J2: &str = "%d{ISO8601} %p [%t] %c - %m%n"; -/// Get the role group ConfigMap data with logging and Vector configurations +/// Get the role group ConfigMap data with the log4j/log4j2 logging configuration. +/// +/// The Vector agent config is built separately via [`build_vector_config`] (which needs a +/// [`RoleGroupRef`]) and added by the caller. pub fn role_group_config_map_data( product_version: &str, - rolegroup: &RoleGroupRef, merged_config: &AnyConfig, ) -> BTreeMap> { let container_name = match merged_config { @@ -66,6 +68,17 @@ pub fn role_group_config_map_data( } } + configs +} + +/// Builds the Vector agent config for a role group, or `None` when the Vector agent is disabled. +/// +/// Takes a v1 [`RoleGroupRef`] because the upstream `create_vector_config` still requires one; +/// this is the only remaining consumer of `RoleGroupRef` in the operator. +pub fn build_vector_config( + rolegroup: &RoleGroupRef, + merged_config: &AnyConfig, +) -> Option { let vector_log_config = merged_config.vector_logging(); let vector_log_config = if let ContainerLogConfig { choice: Some(ContainerLogConfigChoice::Automatic(log_config)), @@ -76,16 +89,9 @@ pub fn role_group_config_map_data( None }; - if merged_config.vector_logging_enabled() { - configs.insert( - product_logging::framework::VECTOR_CONFIG_FILE.to_string(), - Some(product_logging::framework::create_vector_config( - rolegroup, - vector_log_config, - )), - ); - } - configs + merged_config + .vector_logging_enabled() + .then(|| product_logging::framework::create_vector_config(rolegroup, vector_log_config)) } fn log4j_config_if_automatic( diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index 4f83b22c..232ab782 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -2,6 +2,7 @@ pub mod broker_properties; pub mod controller_properties; +pub mod listener; pub mod logging; use crate::crd::{KafkaPodDescriptor, role::KafkaRole}; diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller/mod.rs index 6f1881c5..b6f33cb1 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller/mod.rs @@ -5,14 +5,25 @@ //! touches the raw [`v1alpha1::KafkaCluster`] spec. The reconcile loop that consumes //! it lives in [`crate::kafka_controller`]. -use std::{borrow::Cow, collections::BTreeMap}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap}, + str::FromStr, +}; +use snafu::Snafu; use stackable_operator::{ - commons::product_image_selection::ResolvedProductImage, + commons::{networking::DomainName, product_image_selection::ResolvedProductImage}, kube::{Resource, api::ObjectMeta}, - v2::types::{ - kubernetes::{NamespaceName, Uid}, - operator::ClusterName, + kvp::Labels, + v2::{ + HasName, HasUid, NameIsValidLabelValue, + kvp::label::{recommended_labels, role_group_selector}, + role_group_utils::ResourceNames, + types::{ + kubernetes::{NamespaceName, Uid}, + operator::{ClusterName, ControllerName, OperatorName, ProductName, ProductVersion}, + }, }, }; @@ -20,15 +31,34 @@ pub(crate) mod build; pub(crate) mod dereference; pub(crate) mod validate; -use crate::crd::{ - KafkaPodDescriptor, MetadataManager, - authorization::KafkaAuthorizationConfig, - role::{AnyConfig, AnyConfigOverrides, KafkaRole}, - security::KafkaTlsSecurity, - v1alpha1, +/// The type-safe role-group name from stackable-operator's v2 module. Re-exported so the rest +/// of the operator can refer to it as `controller::RoleGroupName`. +pub use stackable_operator::v2::types::operator::{RoleGroupName, RoleName}; + +use crate::{ + config::node_id_hasher::node_id_hash32_offset, + crd::{ + APP_NAME, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, + authorization::KafkaAuthorizationConfig, + role::{AnyConfig, AnyConfigOverrides, KafkaRole}, + security::KafkaTlsSecurity, + v1alpha1, + }, + kafka_controller::KAFKA_CONTROLLER_NAME, }; -pub type RoleGroupName = String; +#[derive(Snafu, Debug)] +pub enum PodDescriptorsError { + #[snafu(display( + "the node id hash offset of role group {role}/{role_group} collides with {colliding_role}/{colliding_role_group}; node ids must be unique across the cluster" + ))] + KafkaNodeIdHashCollision { + role: KafkaRole, + role_group: RoleGroupName, + colliding_role: KafkaRole, + colliding_role_group: RoleGroupName, + }, +} /// The validated cluster. Carries everything the build steps need, resolved once /// here so downstream code never re-derives it or touches the raw spec. @@ -43,7 +73,15 @@ pub struct ValidatedCluster { metadata: ObjectMeta, pub name: ClusterName, pub namespace: NamespaceName, + pub uid: Uid, + /// The Kubernetes cluster domain (e.g. `cluster.local`), resolved from the operator's + /// `KubernetesClusterInfo`. Used to compute pod FQDNs in [`Self::pod_descriptors`]. + pub cluster_domain: DomainName, pub image: ResolvedProductImage, + /// The product version as a valid label value, used for the recommended + /// `app.kubernetes.io/version` label. Derived from the resolved image's app version label + /// value. + pub product_version: ProductVersion, pub cluster_config: ValidatedClusterConfig, pub role_group_configs: BTreeMap>, } @@ -53,10 +91,15 @@ impl ValidatedCluster { name: ClusterName, namespace: NamespaceName, uid: Uid, + cluster_domain: DomainName, image: ResolvedProductImage, cluster_config: ValidatedClusterConfig, role_group_configs: BTreeMap>, ) -> Self { + // `app_version_label_value` is constructed to be a valid label value, so it is also a + // valid `ProductVersion`. + let product_version = ProductVersion::from_str(&image.app_version_label_value) + .expect("the app version label value is a valid product version"); Self { metadata: ObjectMeta { name: Some(name.to_string()), @@ -66,11 +109,187 @@ impl ValidatedCluster { }, name, namespace, + uid, + cluster_domain, image, + product_version, cluster_config, role_group_configs, } } + + /// Predicts the pods of this cluster (or just `requested_kafka_role`'s pods, if given). + /// + /// Pods are predicted rather than read from the live cluster to avoid instance churn. The + /// node-id hash offsets must be unique across the whole cluster, so collisions are detected + /// across all role groups regardless of `requested_kafka_role`. + /// + /// Resource names reuse [`Self::resource_names`] (the canonical + /// `--` naming) so they stay in sync with the StatefulSet and + /// headless Service this descriptor refers to. + pub fn pod_descriptors( + &self, + requested_kafka_role: Option<&KafkaRole>, + ) -> Result, PodDescriptorsError> { + let client_port = self.cluster_config.kafka_security.client_port(); + let mut pod_descriptors = Vec::new(); + let mut seen_hashes = HashMap::::new(); + + for (role, role_groups) in &self.role_group_configs { + for (role_group_name, validated_rg) in role_groups { + let node_id_hash_offset = node_id_hash32_offset(role, role_group_name.as_ref()); + + // The node id hash offset must be unique across the cluster. + if let Some((colliding_role, colliding_role_group)) = + seen_hashes.get(&node_id_hash_offset) + { + return KafkaNodeIdHashCollisionSnafu { + role: role.clone(), + role_group: role_group_name.clone(), + colliding_role: colliding_role.clone(), + colliding_role_group: colliding_role_group.clone(), + } + .fail(); + } + seen_hashes.insert(node_id_hash_offset, (role.clone(), role_group_name.clone())); + + if requested_kafka_role.is_none() || Some(role) == requested_kafka_role { + let resource_names = self.resource_names(role, role_group_name); + let role_group_statefulset_name = + resource_names.stateful_set_name().to_string(); + let role_group_service_name = + resource_names.headless_service_name().to_string(); + for replica in 0..validated_rg.replicas { + pod_descriptors.push(KafkaPodDescriptor { + namespace: self.namespace.to_string(), + role: role.to_string(), + role_group_service_name: role_group_service_name.clone(), + role_group_statefulset_name: role_group_statefulset_name.clone(), + replica, + cluster_domain: self.cluster_domain.clone(), + node_id: node_id_hash_offset + u32::from(replica), + client_port, + }); + } + } + } + } + + Ok(pod_descriptors) + } + + /// The given [`KafkaRole`] as a type-safe [`RoleName`]. + pub fn role_name(role: &KafkaRole) -> RoleName { + RoleName::from_str(&role.to_string()).expect("a KafkaRole is a valid role name") + } + + /// Type-safe names for the resources of a given role group. + pub(crate) fn resource_names( + &self, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> ResourceNames { + ResourceNames { + cluster_name: self.name.clone(), + role_name: Self::role_name(role), + role_group_name: role_group_name.clone(), + } + } + + /// The name of the broker rolegroup's bootstrap [`Listener`](stackable_operator::crd::listener), + /// `---bootstrap`. + pub fn bootstrap_listener_name( + &self, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> String { + format!( + "{}-bootstrap", + self.resource_names(role, role_group_name) + .stateful_set_name() + ) + } + + /// Recommended labels for a role-group resource, using the given product version. + fn recommended_labels_for( + &self, + product_version: &ProductVersion, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> Labels { + recommended_labels( + self, + &product_name(), + product_version, + &operator_name(), + &controller_name(), + &Self::role_name(role), + role_group_name, + ) + } + + /// Recommended labels for a role-group resource. + pub fn recommended_labels(&self, role: &KafkaRole, role_group_name: &RoleGroupName) -> Labels { + self.recommended_labels_for(&self.product_version, role, role_group_name) + } + + /// Recommended labels without a version, for PVC templates that cannot be modified once + /// deployed. + pub fn unversioned_recommended_labels( + &self, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> Labels { + // A version value is required, and we do want to use the "recommended" format for the + // other desired labels. + let none_version = + ProductVersion::from_str("none").expect("'none' is a valid product version"); + self.recommended_labels_for(&none_version, role, role_group_name) + } + + /// Selector labels matching the pods of a role group. + pub fn role_group_selector(&self, role: &KafkaRole, role_group_name: &RoleGroupName) -> Labels { + role_group_selector( + self, + &product_name(), + &Self::role_name(role), + role_group_name, + ) + } +} + +impl HasName for ValidatedCluster { + fn to_name(&self) -> String { + self.name.to_string() + } +} + +impl HasUid for ValidatedCluster { + fn to_uid(&self) -> Uid { + self.uid.clone() + } +} + +impl NameIsValidLabelValue for ValidatedCluster { + fn to_label_value(&self) -> String { + self.name.to_label_value() + } +} + +/// The product name (`kafka`) as a type-safe label value. +fn product_name() -> ProductName { + ProductName::from_str(APP_NAME).expect("'kafka' is a valid product name") +} + +/// The operator name as a type-safe label value. +fn operator_name() -> OperatorName { + OperatorName::from_str(OPERATOR_NAME).expect("the operator name is a valid label value") +} + +/// The controller name as a type-safe label value. +fn controller_name() -> ControllerName { + ControllerName::from_str(KAFKA_CONTROLLER_NAME) + .expect("the controller name is a valid label value") } /// Cluster-wide settings resolved during validation and dereferencing. @@ -80,7 +299,6 @@ impl ValidatedCluster { pub struct ValidatedClusterConfig { pub kafka_security: KafkaTlsSecurity, pub authorization_config: Option, - pub pod_descriptors: Vec, pub metadata_manager: MetadataManager, /// Whether the operator must not generate broker ids itself, because the user diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 50de94ed..aab95bd0 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -65,9 +65,6 @@ pub enum Error { #[snafu(display("invalid environment variable name"))] InvalidEnvVarName { source: container::Error }, - #[snafu(display("failed to build pod descriptors"))] - BuildPodDescriptors { source: crate::crd::Error }, - #[snafu(display("invalid metadata manager"))] InvalidMetadataManager { source: crate::crd::Error }, @@ -85,6 +82,12 @@ pub enum Error { ResolveUid { source: stackable_operator::v2::controller_utils::Error, }, + + #[snafu(display("the role group name {role_group_name:?} is invalid"))] + ParseRoleGroupName { + source: stackable_operator::v2::macros::attributed_string_type::Error, + role_group_name: String, + }, } type Result = std::result::Result; @@ -155,14 +158,6 @@ pub fn validate( role_group_configs.insert(KafkaRole::Controller, controller_groups); } - let pod_descriptors = kafka - .pod_descriptors( - None, - &dereferenced_objects.kubernetes_cluster_info, - kafka_security.client_port(), - ) - .context(BuildPodDescriptorsSnafu)?; - let metadata_manager = kafka .effective_metadata_manager() .context(InvalidMetadataManagerSnafu)?; @@ -170,16 +165,20 @@ pub fn validate( let name = get_cluster_name(kafka).context(ResolveClusterNameSnafu)?; let namespace = get_namespace(kafka).context(ResolveNamespaceSnafu)?; let uid = get_uid(kafka).context(ResolveUidSnafu)?; + let cluster_domain = dereferenced_objects + .kubernetes_cluster_info + .cluster_domain + .clone(); Ok(ValidatedCluster::new( name, namespace, uid, + cluster_domain, image, ValidatedClusterConfig { kafka_security, authorization_config: dereferenced_objects.authorization_config, - pod_descriptors, metadata_manager, disable_broker_id_generation: kafka .spec @@ -245,7 +244,12 @@ where .product_specific_common_config .jvm_argument_overrides, }; - Ok((role_group_name.clone(), validated)) + let role_group_name = RoleGroupName::from_str(role_group_name).with_context(|_| { + ParseRoleGroupNameSnafu { + role_group_name: role_group_name.clone(), + } + })?; + Ok((role_group_name, validated)) }) .collect() } diff --git a/rust/operator-binary/src/crd/affinity.rs b/rust/operator-binary/src/crd/affinity.rs index 77d50500..ca7deebc 100644 --- a/rust/operator-binary/src/crd/affinity.rs +++ b/rust/operator-binary/src/crd/affinity.rs @@ -63,7 +63,7 @@ mod tests { let merged_config = validated .role_group_configs .get(&role) - .and_then(|groups| groups.get("default")) + .and_then(|groups| groups.get(&"default".parse().unwrap())) .map(|rg| &rg.config) .expect("role group should exist"); diff --git a/rust/operator-binary/src/crd/listener.rs b/rust/operator-binary/src/crd/listener.rs index 7451fad3..bc815751 100644 --- a/rust/operator-binary/src/crd/listener.rs +++ b/rust/operator-binary/src/crd/listener.rs @@ -3,21 +3,9 @@ use std::{ fmt::{Display, Formatter}, }; -use snafu::{OptionExt, Snafu}; -use stackable_operator::{ - kube::ResourceExt, role_utils::RoleGroupRef, utils::cluster_info::KubernetesClusterInfo, -}; -use strum::{EnumDiscriminants, EnumString}; - -use crate::crd::{STACKABLE_LISTENER_BROKER_DIR, security::KafkaTlsSecurity, v1alpha1}; +use strum::EnumString; -const LISTENER_LOCAL_ADDRESS: &str = "0.0.0.0"; - -#[derive(Snafu, Debug, EnumDiscriminants)] -pub enum KafkaListenerError { - #[snafu(display("object has no namespace"))] - ObjectHasNoNamespace, -} +pub(crate) const LISTENER_LOCAL_ADDRESS: &str = "0.0.0.0"; #[derive(strum::Display, Debug, EnumString)] pub enum KafkaListenerProtocol { @@ -127,9 +115,9 @@ impl KafkaListenerName { #[derive(Debug)] pub struct KafkaListenerConfig { - listeners: Vec, - advertised_listeners: Vec, - listener_security_protocol_map: BTreeMap, + pub(crate) listeners: Vec, + pub(crate) advertised_listeners: Vec, + pub(crate) listener_security_protocol_map: BTreeMap, } impl KafkaListenerConfig { @@ -177,10 +165,10 @@ impl KafkaListenerConfig { } #[derive(Debug)] -struct KafkaListener { - name: KafkaListenerName, - host: String, - port: String, +pub(crate) struct KafkaListener { + pub(crate) name: KafkaListenerName, + pub(crate) host: String, + pub(crate) port: String, } impl Display for KafkaListener { @@ -189,144 +177,6 @@ impl Display for KafkaListener { } } -pub fn get_kafka_listener_config( - kafka: &v1alpha1::KafkaCluster, - kafka_security: &KafkaTlsSecurity, - rolegroup_ref: &RoleGroupRef, - cluster_info: &KubernetesClusterInfo, -) -> Result { - let pod_fqdn = pod_fqdn( - kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - cluster_info, - )?; - let mut listeners = vec![]; - let mut advertised_listeners = vec![]; - let mut listener_security_protocol_map: BTreeMap = - BTreeMap::new(); - - // CLIENT - if kafka_security.has_kerberos_enabled() { - // 1) Kerberos and TLS authentication classes are mutually exclusive - listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::SECURE_CLIENT_PORT.to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Client, KafkaListenerProtocol::SaslSsl); - } else if kafka_security.tls_client_authentication_class().is_some() - || kafka_security.tls_server_secret_class().is_some() - { - // 2) Client listener uses TLS (possibly with authentication) - listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: kafka_security.client_port().to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Client, KafkaListenerProtocol::Ssl); - } else { - // 3) If no client auth or tls is required we expose CLIENT with PLAINTEXT - listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::CLIENT_PORT.to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Client, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Client, KafkaListenerProtocol::Plaintext); - } - - // INTERNAL / CONTROLLER - if kafka_security.has_kerberos_enabled() || kafka_security.tls_internal_secret_class().is_some() - { - // 5) & 6) Kerberos and TLS authentication classes are mutually exclusive but both require internal tls to be used - listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: pod_fqdn.to_string(), - port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Internal, KafkaListenerProtocol::Ssl); - listener_security_protocol_map - .insert(KafkaListenerName::Controller, KafkaListenerProtocol::Ssl); - } else { - // 7) If no internal tls is required we expose INTERNAL as PLAINTEXT - listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: kafka_security.internal_port().to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Internal, - host: pod_fqdn.to_string(), - port: kafka_security.internal_port().to_string(), - }); - listener_security_protocol_map.insert( - KafkaListenerName::Internal, - KafkaListenerProtocol::Plaintext, - ); - listener_security_protocol_map.insert( - KafkaListenerName::Controller, - KafkaListenerProtocol::Plaintext, - ); - } - - // BOOTSTRAP - if kafka_security.has_kerberos_enabled() { - listeners.push(KafkaListener { - name: KafkaListenerName::Bootstrap, - host: LISTENER_LOCAL_ADDRESS.to_string(), - port: kafka_security.bootstrap_port().to_string(), - }); - advertised_listeners.push(KafkaListener { - name: KafkaListenerName::Bootstrap, - host: node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port: node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name(), - ), - }); - listener_security_protocol_map - .insert(KafkaListenerName::Bootstrap, KafkaListenerProtocol::SaslSsl); - } - - Ok(KafkaListenerConfig { - listeners, - advertised_listeners, - listener_security_protocol_map, - }) -} - pub fn node_address_cmd_env(directory: &str) -> String { format!("$(cat {directory}/default-address/address)") } @@ -342,340 +192,3 @@ pub fn node_address_cmd(directory: &str) -> String { pub fn node_port_cmd(directory: &str, port_name: &str) -> String { format!("${{file:UTF-8:{directory}/default-address/ports/{port_name}}}") } - -pub fn pod_fqdn( - kafka: &v1alpha1::KafkaCluster, - sts_service_name: &str, - cluster_info: &KubernetesClusterInfo, -) -> Result { - Ok(format!( - "${{env:POD_NAME}}.{sts_service_name}.{namespace}.svc.{cluster_domain}", - namespace = kafka.namespace().context(ObjectHasNoNamespaceSnafu)?, - cluster_domain = cluster_info.cluster_domain - )) -} - -#[cfg(test)] -mod tests { - use stackable_operator::{ - builder::meta::ObjectMetaBuilder, - commons::networking::DomainName, - crd::authentication::{core, kerberos, tls}, - }; - - use super::*; - use crate::crd::{authentication::ResolvedAuthenticationClasses, role::KafkaRole}; - - fn default_cluster_info() -> KubernetesClusterInfo { - KubernetesClusterInfo { - cluster_domain: DomainName::try_from("cluster.local").unwrap(), - } - } - - #[test] - fn test_get_kafka_listeners_config() { - let kafka_cluster = r#" - apiVersion: kafka.stackable.tech/v1alpha1 - kind: KafkaCluster - metadata: - name: simple-kafka - namespace: default - spec: - image: - productVersion: 3.9.2 - clusterConfig: - authentication: - - authenticationClass: kafka-client-tls - tls: - internalSecretClass: internalTls - serverSecretClass: tls - zookeeperConfigMapName: xyz - "#; - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(kafka_cluster).expect("illegal test input"); - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { - metadata: ObjectMetaBuilder::new().name("auth-class").build(), - spec: core::v1alpha1::AuthenticationClassSpec { - provider: core::v1alpha1::AuthenticationClassProvider::Tls( - tls::v1alpha1::AuthenticationProvider { - client_cert_secret_class: Some("client-auth-secret-class".to_string()), - }, - ), - }, - }]), - "internalTls".to_string(), - Some("tls".to_string()), - None, - ); - let cluster_info = default_cluster_info(); - // "simple-kafka-broker-default" - let rolegroup_ref = kafka.rolegroup_ref(&KafkaRole::Broker, "default"); - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::Ssl, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Ssl, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Ssl, - ) - ); - - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![]), - "tls".to_string(), - Some("tls".to_string()), - None, - ); - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::Ssl, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Ssl, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Ssl, - ) - ); - - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![]), - "".to_string(), - None, - None, - ); - - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::Plaintext, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Plaintext, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Plaintext, - ) - ); - } - - #[test] - fn test_get_kafka_kerberos_listeners_config() { - let kafka_cluster = r#" - apiVersion: kafka.stackable.tech/v1alpha1 - kind: KafkaCluster - metadata: - name: simple-kafka - namespace: default - spec: - image: - productVersion: 3.9.2 - clusterConfig: - authentication: - - authenticationClass: kafka-kerberos - tls: - serverSecretClass: tls - zookeeperConfigMapName: xyz - "#; - let kafka: v1alpha1::KafkaCluster = - serde_yaml::from_str(kafka_cluster).expect("illegal test input"); - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { - metadata: ObjectMetaBuilder::new().name("auth-class").build(), - spec: core::v1alpha1::AuthenticationClassSpec { - provider: core::v1alpha1::AuthenticationClassProvider::Kerberos( - kerberos::v1alpha1::AuthenticationProvider { - kerberos_secret_class: "kerberos-secret-class".to_string(), - }, - ), - }, - }]), - "tls".to_string(), - Some("tls".to_string()), - None, - ); - let cluster_info = default_cluster_info(); - // "simple-kafka-broker-default" - let rolegroup_ref = kafka.rolegroup_ref(&KafkaRole::Broker, "default"); - let config = - get_kafka_listener_config(&kafka, &kafka_security, &rolegroup_ref, &cluster_info) - .unwrap(); - - assert_eq!( - config.listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", - name = KafkaListenerName::Client, - host = LISTENER_LOCAL_ADDRESS, - port = kafka_security.client_port(), - internal_name = KafkaListenerName::Internal, - internal_host = LISTENER_LOCAL_ADDRESS, - internal_port = kafka_security.internal_port(), - bootstrap_name = KafkaListenerName::Bootstrap, - bootstrap_host = LISTENER_LOCAL_ADDRESS, - bootstrap_port = kafka_security.bootstrap_port(), - ) - ); - - assert_eq!( - config.advertised_listeners(), - format!( - "{name}://{host}:{port},{internal_name}://{internal_host}:{internal_port},{bootstrap_name}://{bootstrap_host}:{bootstrap_port}", - name = KafkaListenerName::Client, - host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - internal_name = KafkaListenerName::Internal, - internal_host = pod_fqdn( - &kafka, - &rolegroup_ref.rolegroup_headless_service_name(), - &cluster_info - ) - .unwrap(), - internal_port = kafka_security.internal_port(), - bootstrap_name = KafkaListenerName::Bootstrap, - bootstrap_host = node_address_cmd(STACKABLE_LISTENER_BROKER_DIR), - bootstrap_port = node_port_cmd( - STACKABLE_LISTENER_BROKER_DIR, - kafka_security.client_port_name() - ), - ) - ); - - assert_eq!( - config.listener_security_protocol_map(), - format!( - "{name}:{protocol},{internal_name}:{internal_protocol},{bootstrap_name}:{bootstrap_protocol},{controller_name}:{controller_protocol}", - name = KafkaListenerName::Client, - protocol = KafkaListenerProtocol::SaslSsl, - internal_name = KafkaListenerName::Internal, - internal_protocol = KafkaListenerProtocol::Ssl, - bootstrap_name = KafkaListenerName::Bootstrap, - bootstrap_protocol = KafkaListenerProtocol::SaslSsl, - controller_name = KafkaListenerName::Controller, - controller_protocol = KafkaListenerProtocol::Ssl, - ) - ); - } -} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 7f173b42..a3625db0 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -7,8 +7,6 @@ pub mod role; pub mod security; pub mod tls; -use std::collections::{BTreeMap, HashMap}; - use authentication::KafkaAuthentication; pub use config_file::ConfigFileName; use serde::{Deserialize, Serialize}; @@ -24,19 +22,15 @@ use stackable_operator::{ role_utils::{GenericRoleConfig, Role, RoleGroupRef}, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, - utils::cluster_info::KubernetesClusterInfo, v2::{config_overrides::KeyValueConfigOverrides, role_utils::JavaCommonConfig}, versioned::versioned, }; use strum::{Display, EnumIter, EnumString}; -use crate::{ - config::node_id_hasher::node_id_hash32_offset, - crd::{ - authorization::KafkaAuthorization, - role::{KafkaRole, broker::BrokerConfigFragment, controller::ControllerConfigFragment}, - tls::KafkaTls, - }, +use crate::crd::{ + authorization::KafkaAuthorization, + role::{KafkaRole, broker::BrokerConfigFragment, controller::ControllerConfigFragment}, + tls::KafkaTls, }; pub const CONTAINER_IMAGE_BASE_NAME: &str = "kafka"; @@ -75,9 +69,6 @@ pub enum Error { #[snafu(display("The Kafka role [{role}] is missing from spec"))] MissingRole { role: String }, - #[snafu(display("Object has no namespace associated"))] - NoNamespace, - #[snafu(display( "Kafka version 4 and higher requires a Kraft controller (configured via `spec.controller`)" ))] @@ -87,16 +78,6 @@ pub enum Error { "Kraft controller (`spec.controller`) and ZooKeeper (`spec.clusterConfig.zookeeperConfigMapName`) are configured. Please only choose one" ))] KraftAndZookeeperConfigured, - - #[snafu(display( - "Could not calculate 'node.id' hash offset for role '{role}' and rolegroup '{rolegroup}' which collides with role '{coliding_role}' and rolegroup '{colliding_rolegroup}'. Please try to rename one of the rolegroups." - ))] - KafkaNodeIdHashCollision { - role: KafkaRole, - rolegroup: String, - coliding_role: KafkaRole, - colliding_rolegroup: String, - }, } pub type BrokerRole = Role< @@ -332,12 +313,6 @@ impl v1alpha1::KafkaCluster { }) } - /// The name of the load-balanced Kubernetes Service providing the bootstrap address. Kafka clients will use this - /// to get a list of broker addresses and will use those to transmit data to the correct broker. - pub fn bootstrap_service_name(&self, rolegroup: &RoleGroupRef) -> String { - format!("{}-bootstrap", rolegroup.object_name()) - } - /// Metadata about a rolegroup pub fn rolegroup_ref( &self, @@ -369,91 +344,6 @@ impl v1alpha1::KafkaCluster { role: KafkaRole::Controller.to_string(), }) } - - /// List pod descriptors for a given role and all its rolegroups. - /// If no role is provided, pod descriptors for all roles (and all groups) are listed. - /// We try to predict the pods here rather than looking at the current cluster state in order to - /// avoid instance churn. - pub fn pod_descriptors( - &self, - requested_kafka_role: Option<&KafkaRole>, - cluster_info: &KubernetesClusterInfo, - client_port: u16, - ) -> Result, Error> { - let namespace = self.metadata.namespace.clone().context(NoNamespaceSnafu)?; - let mut pod_descriptors = Vec::new(); - let mut seen_hashes = HashMap::::new(); - - for current_role in KafkaRole::roles() { - let rolegroup_replicas = self.extract_rolegroup_replicas(¤t_role)?; - for (rolegroup, replicas) in rolegroup_replicas { - let rolegroup_ref = self.rolegroup_ref(¤t_role, &rolegroup); - let node_id_hash_offset = node_id_hash32_offset(&rolegroup_ref); - - // check collisions - match seen_hashes.get(&node_id_hash_offset) { - Some((coliding_role, coliding_rolegroup)) => { - return KafkaNodeIdHashCollisionSnafu { - role: current_role.clone(), - rolegroup: rolegroup.clone(), - coliding_role: coliding_role.clone(), - colliding_rolegroup: coliding_rolegroup.to_string(), - } - .fail(); - } - None => { - seen_hashes.insert(node_id_hash_offset, (current_role.clone(), rolegroup)) - } - }; - - // If no specific role is requested, or the current role matches the requested one, add pod descriptors - if requested_kafka_role.is_none() || Some(¤t_role) == requested_kafka_role { - for replica in 0..replicas { - pod_descriptors.push(KafkaPodDescriptor { - namespace: namespace.clone(), - role: current_role.to_string(), - role_group_service_name: rolegroup_ref - .rolegroup_headless_service_name(), - role_group_statefulset_name: rolegroup_ref.object_name(), - replica, - cluster_domain: cluster_info.cluster_domain.clone(), - node_id: node_id_hash_offset + u32::from(replica), - client_port, - }); - } - } - } - } - - Ok(pod_descriptors) - } - - fn extract_rolegroup_replicas( - &self, - kafka_role: &KafkaRole, - ) -> Result, Error> { - Ok(match kafka_role { - KafkaRole::Broker => self - .broker_role() - .iter() - .flat_map(|role| &role.role_groups) - .flat_map(|(rolegroup_name, rolegroup)| { - std::iter::once((rolegroup_name.to_string(), rolegroup.replicas.unwrap_or(0))) - }) - // Order rolegroups consistently, to avoid spurious downstream rewrites - .collect::>(), - - KafkaRole::Controller => self - .controller_role() - .iter() - .flat_map(|role| &role.role_groups) - .flat_map(|(rolegroup_name, rolegroup)| { - std::iter::once((rolegroup_name.to_string(), rolegroup.replicas.unwrap_or(0))) - }) - // Order rolegroups consistently, to avoid spurious downstream rewrites - .collect::>(), - }) - } } /// Reference to a single `Pod` that is a component of a [`KafkaCluster`] @@ -461,12 +351,12 @@ impl v1alpha1::KafkaCluster { /// Used for service discovery. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct KafkaPodDescriptor { - namespace: String, - role_group_statefulset_name: String, - role_group_service_name: String, - replica: u16, - cluster_domain: DomainName, - node_id: u32, + pub(crate) namespace: String, + pub(crate) role_group_statefulset_name: String, + pub(crate) role_group_service_name: String, + pub(crate) replica: u16, + pub(crate) cluster_domain: DomainName, + pub(crate) node_id: u32, pub role: String, pub client_port: u16, } @@ -505,15 +395,6 @@ impl KafkaPodDescriptor { fqdn = self.fqdn(), ) } - - pub fn as_quorum_voter(&self) -> String { - format!( - "{node_id}@{fqdn}:{port}", - node_id = self.node_id, - port = self.client_port, - fqdn = self.fqdn(), - ) - } } #[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index 96e698cf..b6bed671 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -9,9 +9,7 @@ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::resources::{NoRuntimeLimits, Resources}, k8s_openapi::api::core::v1::PodTemplateSpec, - kube::runtime::reflector::ObjectRef, product_logging::spec::ContainerLogConfig, - role_utils::RoleGroupRef, schemars::{self, JsonSchema}, v2::config_overrides::KeyValueConfigOverrides, }; @@ -114,19 +112,6 @@ impl KafkaRole { roles } - /// Metadata about a rolegroup - pub fn rolegroup_ref( - &self, - kafka: &v1alpha1::KafkaCluster, - group_name: impl Into, - ) -> RoleGroupRef { - RoleGroupRef { - cluster: ObjectRef::from_obj(kafka), - role: self.to_string(), - role_group: group_name.into(), - } - } - /// A Kerberos principal has three parts, with the form username/fully.qualified.domain.name@YOUR-REALM.COM. /// but is similar to HBase). pub fn kerberos_service_name(&self) -> &'static str { @@ -257,6 +242,14 @@ impl Deref for AnyConfig { } impl AnyConfig { + /// The [`KafkaRole`] this config belongs to. + pub fn kafka_role(&self) -> KafkaRole { + match self { + AnyConfig::Broker(_) => KafkaRole::Broker, + AnyConfig::Controller(_) => KafkaRole::Controller, + } + } + pub fn resources(&self) -> &Resources { match self { AnyConfig::Broker(broker_config) => &broker_config.resources, diff --git a/rust/operator-binary/src/kafka_controller.rs b/rust/operator-binary/src/kafka_controller.rs index e1186e10..2fc2d007 100644 --- a/rust/operator-binary/src/kafka_controller.rs +++ b/rust/operator-binary/src/kafka_controller.rs @@ -15,7 +15,6 @@ use stackable_operator::{ core::{DeserializeGuard, error_boundary}, runtime::{controller::Action, reflector::ObjectRef}, }, - kvp::ObjectLabels, logging::controller::ReconcilerError, memory::{BinaryMultiple, MemoryQuantity}, role_utils::{GenericRoleConfig, RoleGroupRef}, @@ -28,10 +27,11 @@ use stackable_operator::{ use strum::{EnumDiscriminants, IntoStaticStr}; use crate::{ - controller::{build, dereference, validate}, + controller::{ + build, build::properties::listener::get_kafka_listener_config, dereference, validate, + }, crd::{ APP_NAME, KafkaClusterStatus, OPERATOR_NAME, - listener::get_kafka_listener_config, role::{AnyConfig, KafkaRole}, v1alpha1, }, @@ -52,28 +52,6 @@ pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { unit: BinaryMultiple::Mebi, }; -/// Build recommended values for labels. -/// -/// Generic over the owner `T` so the owner can be either the raw `KafkaCluster` or the -/// `ValidatedCluster` (which also implements `Resource`). -pub fn build_recommended_labels<'a, T>( - owner: &'a T, - controller_name: &'a str, - app_version: &'a str, - role: &'a str, - role_group: &'a str, -) -> ObjectLabels<'a, T> { - ObjectLabels { - owner, - app_name: APP_NAME, - app_version, - operator_name: OPERATOR_NAME, - controller_name, - role, - role_group, - } -} - pub struct Ctx { pub client: stackable_operator::client::Client, pub operator_environment: OperatorEnvironmentOptions, @@ -89,11 +67,6 @@ pub enum Error { #[snafu(display("failed to validate cluster"))] ValidateCluster { source: validate::Error }, - #[snafu(display("invalid kafka listeners"))] - InvalidKafkaListeners { - source: crate::crd::listener::KafkaListenerError, - }, - #[snafu(display("failed to apply bootstrap Listener"))] ApplyBootstrapListener { source: stackable_operator::cluster_resources::Error, @@ -180,16 +153,6 @@ pub enum Error { BuildConfigMap { source: crate::controller::build::config_map::Error, }, - - #[snafu(display("failed to build service"))] - BuildService { - source: crate::resource::service::Error, - }, - - #[snafu(display("failed to build listener"))] - BuildListener { - source: crate::resource::listener::Error, - }, } type Result = std::result::Result; @@ -219,9 +182,6 @@ impl ReconcilerError for Error { Error::InvalidKafkaCluster { .. } => None, Error::BuildStatefulset { .. } => None, Error::BuildConfigMap { .. } => None, - Error::BuildService { .. } => None, - Error::BuildListener { .. } => None, - Error::InvalidKafkaListeners { .. } => None, } } } @@ -292,36 +252,39 @@ pub async fn reconcile_kafka( for (kafka_role, rg_map) in &validated_cluster.role_group_configs { for (rolegroup_name, validated_rg) in rg_map { - let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name); + // The Vector log-aggregation config still consumes a v1 `RoleGroupRef`; it is built + // here and used only for that. All other identification uses the typed `kafka_role` / + // `rolegroup_name` (and `ValidatedCluster::resource_names`). + let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name.to_string()); + let vector_config = build::properties::logging::build_vector_config( + &rolegroup_ref, + &validated_rg.config, + ); let rg_headless_service = build_rolegroup_headless_service( &validated_cluster, - &validated_cluster.image, - &rolegroup_ref, + kafka_role, + rolegroup_name, &validated_cluster.cluster_config.kafka_security, - ) - .context(BuildServiceSnafu)?; + ); - let rg_metrics_service = build_rolegroup_metrics_service( - &validated_cluster, - &validated_cluster.image, - &rolegroup_ref, - ) - .context(BuildServiceSnafu)?; + let rg_metrics_service = + build_rolegroup_metrics_service(&validated_cluster, kafka_role, rolegroup_name); let kafka_listeners = get_kafka_listener_config( - kafka, + &validated_cluster, &validated_cluster.cluster_config.kafka_security, - &rolegroup_ref, + kafka_role, + rolegroup_name, &client.kubernetes_cluster_info, - ) - .context(InvalidKafkaListenersSnafu)?; + ); let rg_configmap = build::config_map::build_rolegroup_config_map( &validated_cluster, - &rolegroup_ref, + rolegroup_name, validated_rg, &kafka_listeners, + vector_config, ) .context(BuildConfigMapSnafu)?; @@ -329,33 +292,30 @@ pub async fn reconcile_kafka( KafkaRole::Broker => build_broker_rolegroup_statefulset( kafka, kafka_role, + rolegroup_name, &validated_cluster, - &rolegroup_ref, validated_rg, &rbac_sa, - &client.kubernetes_cluster_info, ) .context(BuildStatefulsetSnafu)?, KafkaRole::Controller => build_controller_rolegroup_statefulset( kafka, kafka_role, + rolegroup_name, &validated_cluster, - &rolegroup_ref, validated_rg, &rbac_sa, - &client.kubernetes_cluster_info, ) .context(BuildStatefulsetSnafu)?, }; if let AnyConfig::Broker(broker_config) = &validated_rg.config { let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( - kafka, &validated_cluster, - &rolegroup_ref, + kafka_role, + rolegroup_name, broker_config, - ) - .context(BuildListenerSnafu)?; + ); bootstrap_listeners.push( cluster_resources .add(client, rg_bootstrap_listener) diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/resource/listener.rs index d22a2a96..00b131ab 100644 --- a/rust/operator-binary/src/resource/listener.rs +++ b/rust/operator-binary/src/resource/listener.rs @@ -1,53 +1,37 @@ -use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::meta::ObjectMetaBuilder, crd::listener, role_utils::RoleGroupRef, + builder::meta::ObjectMetaBuilder, crd::listener, + v2::builder::meta::ownerreference_from_resource, }; use crate::{ - controller::ValidatedCluster, - crd::{role::broker::BrokerConfig, security::KafkaTlsSecurity, v1alpha1}, - kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, -}; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, + controller::{RoleGroupName, ValidatedCluster}, + crd::{ + role::{KafkaRole, broker::BrokerConfig}, + security::KafkaTlsSecurity, }, -} +}; /// Kafka clients will use the load-balanced bootstrap listener to get a list of broker addresses and will use those to /// transmit data to the correct broker. // TODO (@NickLarsenNZ): Move shared functionality to stackable-operator pub fn build_broker_rolegroup_bootstrap_listener( - kafka: &v1alpha1::KafkaCluster, validated_cluster: &ValidatedCluster, - rolegroup: &RoleGroupRef, + role: &KafkaRole, + role_group_name: &RoleGroupName, merged_config: &BrokerConfig, -) -> Result { +) -> listener::v1alpha1::Listener { let kafka_security = &validated_cluster.cluster_config.kafka_security; - let resolved_product_image = &validated_cluster.image; - Ok(listener::v1alpha1::Listener { + listener::v1alpha1::Listener { metadata: ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(kafka.bootstrap_service_name(rolegroup)) - .ownerreference_from_resource(validated_cluster, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( + .name(validated_cluster.bootstrap_listener_name(role, role_group_name)) + .ownerreference(ownerreference_from_resource( validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) .build(), spec: listener::v1alpha1::ListenerSpec { class_name: Some(merged_config.bootstrap_listener_class.clone()), @@ -55,7 +39,7 @@ pub fn build_broker_rolegroup_bootstrap_listener( ..listener::v1alpha1::ListenerSpec::default() }, status: None, - }) + } } fn bootstrap_listener_ports( diff --git a/rust/operator-binary/src/resource/service.rs b/rust/operator-binary/src/resource/service.rs index 8f4fa0e3..4ba8406d 100644 --- a/rust/operator-binary/src/resource/service.rs +++ b/rust/operator-binary/src/resource/service.rs @@ -1,100 +1,75 @@ -use snafu::{ResultExt, Snafu}; use stackable_operator::{ builder::meta::ObjectMetaBuilder, - commons::product_image_selection::ResolvedProductImage, k8s_openapi::api::core::v1::{Service, ServicePort, ServiceSpec}, kvp::{Annotations, Labels}, - role_utils::RoleGroupRef, + v2::builder::meta::ownerreference_from_resource, }; use crate::{ - controller::ValidatedCluster, - crd::{APP_NAME, METRICS_PORT, METRICS_PORT_NAME, security::KafkaTlsSecurity, v1alpha1}, - kafka_controller::{KAFKA_CONTROLLER_NAME, build_recommended_labels}, + controller::{RoleGroupName, ValidatedCluster}, + crd::{METRICS_PORT, METRICS_PORT_NAME, role::KafkaRole, security::KafkaTlsSecurity}, }; -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - - #[snafu(display("failed to build Labels"))] - LabelBuild { - source: stackable_operator::kvp::LabelError, - }, - - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, -} - /// The rolegroup [`Service`] is a headless service that allows direct access to the instances of a certain rolegroup /// /// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing. pub fn build_rolegroup_headless_service( validated_cluster: &ValidatedCluster, - resolved_product_image: &ResolvedProductImage, - rolegroup: &RoleGroupRef, + role: &KafkaRole, + role_group_name: &RoleGroupName, kafka_security: &KafkaTlsSecurity, -) -> Result { - Ok(Service { +) -> Service { + Service { metadata: ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(rolegroup.rolegroup_headless_service_name()) - .ownerreference_from_resource(validated_cluster, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( + .name( + validated_cluster + .resource_names(role, role_group_name) + .headless_service_name() + .to_string(), + ) + .ownerreference(ownerreference_from_resource( validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) .build(), spec: Some(ServiceSpec { cluster_ip: Some("None".to_string()), ports: Some(headless_ports(kafka_security)), selector: Some( - Labels::role_group_selector( - validated_cluster, - APP_NAME, - &rolegroup.role, - &rolegroup.role_group, - ) - .context(LabelBuildSnafu)? - .into(), + validated_cluster + .role_group_selector(role, role_group_name) + .into(), ), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() }), status: None, - }) + } } /// The rolegroup metrics [`Service`] is a service that exposes metrics and a prometheus scraping label pub fn build_rolegroup_metrics_service( validated_cluster: &ValidatedCluster, - resolved_product_image: &ResolvedProductImage, - rolegroup: &RoleGroupRef, -) -> Result { - let metrics_service = Service { + role: &KafkaRole, + role_group_name: &RoleGroupName, +) -> Service { + Service { metadata: ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(rolegroup.rolegroup_metrics_service_name()) - .ownerreference_from_resource(validated_cluster, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( + .name(metrics_service_name( + validated_cluster, + role, + role_group_name, + )) + .ownerreference(ownerreference_from_resource( validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup.role, - &rolegroup.role_group, + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) .with_labels(prometheus_labels()) .with_annotations(prometheus_annotations()) .build(), @@ -104,21 +79,33 @@ pub fn build_rolegroup_metrics_service( cluster_ip: Some("None".to_string()), ports: Some(metrics_ports()), selector: Some( - Labels::role_group_selector( - validated_cluster, - APP_NAME, - &rolegroup.role, - &rolegroup.role_group, - ) - .context(LabelBuildSnafu)? - .into(), + validated_cluster + .role_group_selector(role, role_group_name) + .into(), ), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() }), status: None, - }; - Ok(metrics_service) + } +} + +/// The metrics [`Service`] name, `---metrics`. +/// +/// [`ResourceNames`](stackable_operator::v2::role_group_utils::ResourceNames) has no metrics +/// service helper, so the `-metrics` suffix is appended to the qualified role-group name (which is +/// also the StatefulSet name). +fn metrics_service_name( + validated_cluster: &ValidatedCluster, + role: &KafkaRole, + role_group_name: &RoleGroupName, +) -> String { + format!( + "{qualified}-metrics", + qualified = validated_cluster + .resource_names(role, role_group_name) + .stateful_set_name() + ) } fn metrics_ports() -> Vec { diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/resource/statefulset.rs index d4445dcd..3be9cf6f 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/resource/statefulset.rs @@ -26,7 +26,6 @@ use stackable_operator::{ apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, }, kube::ResourceExt, - kvp::Labels, product_logging::{ self, spec::{ @@ -34,8 +33,7 @@ use stackable_operator::{ CustomContainerLogConfig, }, }, - role_utils::RoleGroupRef, - utils::cluster_info::KubernetesClusterInfo, + v2::builder::meta::ownerreference_from_resource, }; use crate::{ @@ -46,9 +44,9 @@ use crate::{ }, node_id_hasher::node_id_hash32_offset, }, - controller::{ValidatedCluster, ValidatedRoleGroupConfig}, + controller::{RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig}, crd::{ - self, APP_NAME, BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, + self, BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, MetadataManager, STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, @@ -60,7 +58,7 @@ use crate::{ security::KafkaTlsSecurity, v1alpha1, }, - kafka_controller::{KAFKA_CONTROLLER_NAME, MAX_KAFKA_LOG_FILES_SIZE, build_recommended_labels}, + kafka_controller::MAX_KAFKA_LOG_FILES_SIZE, kerberos::add_kerberos_pod_config, operations::graceful_shutdown::add_graceful_shutdown_config, }; @@ -97,7 +95,9 @@ pub enum Error { }, #[snafu(display("failed to build pod descriptors"))] - BuildPodDescriptors { source: crate::crd::Error }, + BuildPodDescriptors { + source: crate::controller::PodDescriptorsError, + }, #[snafu(display("failed to configure logging"))] ConfigureLogging { @@ -118,27 +118,12 @@ pub enum Error { source: stackable_operator::builder::pod::container::Error, }, - #[snafu(display("failed to build Labels"))] - LabelBuild { - source: stackable_operator::kvp::LabelError, - }, - #[snafu(display("failed to merge pod overrides"))] MergePodOverrides { source: crd::role::Error }, - #[snafu(display("failed to build Metadata"))] - MetadataBuild { - source: stackable_operator::builder::meta::Error, - }, - #[snafu(display("missing secret lifetime"))] MissingSecretLifetime, - #[snafu(display("object is missing metadata to build owner reference"))] - ObjectMissingMetadataForOwnerRef { - source: stackable_operator::builder::meta::Error, - }, - #[snafu(display("failed to retrieve rolegroup replicas"))] RoleGroupReplicas { source: crd::role::Error }, @@ -153,34 +138,19 @@ pub enum Error { pub fn build_broker_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, + role_group_name: &RoleGroupName, validated_cluster: &ValidatedCluster, - rolegroup_ref: &RoleGroupRef, validated_rg: &ValidatedRoleGroupConfig, service_account: &ServiceAccount, - cluster_info: &KubernetesClusterInfo, ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.config; - let recommended_object_labels = build_recommended_labels( - validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ); - let recommended_labels = - Labels::recommended(&recommended_object_labels).context(LabelBuildSnafu)?; + let resource_names = validated_cluster.resource_names(kafka_role, role_group_name); + let recommended_labels = validated_cluster.recommended_labels(kafka_role, role_group_name); // Used for PVC templates that cannot be modified once they are deployed - let unversioned_recommended_labels = Labels::recommended(&build_recommended_labels( - validated_cluster, - KAFKA_CONTROLLER_NAME, - // A version value is required, and we do want to use the "recommended" format for the other desired labels - "none", - &rolegroup_ref.role, - &rolegroup_ref.role_group, - )) - .context(LabelBuildSnafu)?; + let unversioned_recommended_labels = + validated_cluster.unversioned_recommended_labels(kafka_role, role_group_name); let kcat_prober_container_name = BrokerContainer::KcatProber.to_string(); let mut cb_kcat_prober = @@ -216,7 +186,9 @@ pub fn build_broker_rolegroup_statefulset( // main broker listener is an ephemeral PVC instead pvcs.push( ListenerOperatorVolumeSourceBuilder::new( - &ListenerReference::ListenerName(kafka.bootstrap_service_name(rolegroup_ref)), + &ListenerReference::ListenerName( + validated_cluster.bootstrap_listener_name(kafka_role, role_group_name), + ), &unversioned_recommended_labels, ) .build_pvc(LISTENER_BOOTSTRAP_VOLUME_NAME) @@ -279,12 +251,8 @@ pub fn build_broker_rolegroup_statefulset( .args(vec![broker_kafka_container_commands( metadata_manager == MetadataManager::KRaft, // we need controller pods - kafka - .pod_descriptors( - Some(&KafkaRole::Controller), - cluster_info, - kafka_security.client_port(), - ) + validated_cluster + .pod_descriptors(Some(&KafkaRole::Controller)) .context(BuildPodDescriptorsSnafu)?, kafka_security, &resolved_product_image.product_version, @@ -316,7 +284,7 @@ pub fn build_broker_rolegroup_statefulset( ) .add_env_var( KAFKA_NODE_ID_OFFSET, - node_id_hash32_offset(rolegroup_ref).to_string(), + node_id_hash32_offset(kafka_role, role_group_name.as_ref()).to_string(), ) .add_env_var( "KAFKA_CLIENT_PORT".to_string(), @@ -402,15 +370,14 @@ pub fn build_broker_rolegroup_statefulset( pod_builder .add_volume( VolumeBuilder::new("log-config") - .with_config_map(rolegroup_ref.object_name()) + .with_config_map(resource_names.role_group_config_map().to_string()) .build(), ) .context(AddVolumeSnafu)?; } let metadata = ObjectMetaBuilder::new() - .with_recommended_labels(&recommended_object_labels) - .context(MetadataBuildSnafu)? + .with_labels(recommended_labels.clone()) .build(); if let Some(listener_class) = merged_config.listener_class() { @@ -447,7 +414,7 @@ pub fn build_broker_rolegroup_statefulset( .add_volume(Volume { name: "config".to_string(), config_map: Some(ConfigMapVolumeSource { - name: rolegroup_ref.object_name(), + name: resource_names.role_group_config_map().to_string(), ..ConfigMapVolumeSource::default() }), ..Volume::default() @@ -506,46 +473,37 @@ pub fn build_broker_rolegroup_statefulset( ); pod_template.merge_from( kafka_role - .role_group_pod_overrides(kafka, &rolegroup_ref.role_group) + .role_group_pod_overrides(kafka, role_group_name.as_ref()) .context(MergePodOverridesSnafu)?, ); Ok(StatefulSet { metadata: ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(rolegroup_ref.object_name()) - .ownerreference_from_resource(validated_cluster, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( + .name(resource_names.stateful_set_name().to_string()) + .ownerreference(ownerreference_from_resource( validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels(recommended_labels.clone()) .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) .build(), spec: Some(StatefulSetSpec { pod_management_policy: Some("Parallel".to_string()), replicas: kafka_role - .replicas(kafka, &rolegroup_ref.role_group) + .replicas(kafka, role_group_name.as_ref()) .context(RoleGroupReplicasSnafu)? .map(i32::from), selector: LabelSelector { match_labels: Some( - Labels::role_group_selector( - kafka, - APP_NAME, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ) - .context(LabelBuildSnafu)? - .into(), + validated_cluster + .role_group_selector(kafka_role, role_group_name) + .into(), ), ..LabelSelector::default() }, - service_name: Some(rolegroup_ref.rolegroup_headless_service_name()), + service_name: Some(resource_names.headless_service_name().to_string()), template: pod_template, volume_claim_templates: Some(pvcs), ..StatefulSetSpec::default() @@ -558,22 +516,16 @@ pub fn build_broker_rolegroup_statefulset( pub fn build_controller_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, + role_group_name: &RoleGroupName, validated_cluster: &ValidatedCluster, - rolegroup_ref: &RoleGroupRef, validated_rg: &ValidatedRoleGroupConfig, service_account: &ServiceAccount, - cluster_info: &KubernetesClusterInfo, ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.config; - let recommended_object_labels = build_recommended_labels( - validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ); + let resource_names = validated_cluster.resource_names(kafka_role, role_group_name); + let recommended_labels = validated_cluster.recommended_labels(kafka_role, role_group_name); let kafka_container_name = ControllerContainer::Kafka.to_string(); let mut cb_kafka = @@ -611,13 +563,13 @@ pub fn build_controller_rolegroup_statefulset( env.push(EnvVar { name: "ROLEGROUP_HEADLESS_SERVICE_NAME".to_string(), - value: Some(rolegroup_ref.rolegroup_headless_service_name()), + value: Some(resource_names.headless_service_name().to_string()), ..EnvVar::default() }); env.push(EnvVar { name: "CLUSTER_DOMAIN".to_string(), - value: Some(cluster_info.cluster_domain.to_string()), + value: Some(validated_cluster.cluster_domain.to_string()), ..EnvVar::default() }); @@ -653,8 +605,8 @@ pub fn build_controller_rolegroup_statefulset( "-c".to_string(), ]) .args(vec![controller_kafka_container_command( - kafka - .pod_descriptors(Some(kafka_role), cluster_info, kafka_security.client_port()) + validated_cluster + .pod_descriptors(Some(kafka_role)) .context(BuildPodDescriptorsSnafu)?, &resolved_product_image.product_version, )]) @@ -686,7 +638,7 @@ pub fn build_controller_rolegroup_statefulset( ) .add_env_var( KAFKA_NODE_ID_OFFSET, - node_id_hash32_offset(rolegroup_ref).to_string(), + node_id_hash32_offset(kafka_role, role_group_name.as_ref()).to_string(), ) .add_env_vars(env) .add_container_ports(container_ports(kafka_security)) @@ -739,15 +691,14 @@ pub fn build_controller_rolegroup_statefulset( pod_builder .add_volume( VolumeBuilder::new("log-config") - .with_config_map(rolegroup_ref.object_name()) + .with_config_map(resource_names.role_group_config_map().to_string()) .build(), ) .context(AddVolumeSnafu)?; } let metadata = ObjectMetaBuilder::new() - .with_recommended_labels(&recommended_object_labels) - .context(MetadataBuildSnafu)? + .with_labels(recommended_labels.clone()) .build(); // Add TLS related volumes and volume mounts @@ -773,7 +724,7 @@ pub fn build_controller_rolegroup_statefulset( .add_volume(Volume { name: "config".to_string(), config_map: Some(ConfigMapVolumeSource { - name: rolegroup_ref.object_name(), + name: resource_names.role_group_config_map().to_string(), ..ConfigMapVolumeSource::default() }), ..Volume::default() @@ -828,24 +779,20 @@ pub fn build_controller_rolegroup_statefulset( ); pod_template.merge_from( kafka_role - .role_group_pod_overrides(kafka, &rolegroup_ref.role_group) + .role_group_pod_overrides(kafka, role_group_name.as_ref()) .context(MergePodOverridesSnafu)?, ); Ok(StatefulSet { metadata: ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(rolegroup_ref.object_name()) - .ownerreference_from_resource(validated_cluster, None, Some(true)) - .context(ObjectMissingMetadataForOwnerRefSnafu)? - .with_recommended_labels(&build_recommended_labels( + .name(resource_names.stateful_set_name().to_string()) + .ownerreference(ownerreference_from_resource( validated_cluster, - KAFKA_CONTROLLER_NAME, - &resolved_product_image.app_version_label_value, - &rolegroup_ref.role, - &rolegroup_ref.role_group, + None, + Some(true), )) - .context(MetadataBuildSnafu)? + .with_labels(recommended_labels.clone()) .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) .build(), spec: Some(StatefulSetSpec { @@ -855,23 +802,18 @@ pub fn build_controller_rolegroup_statefulset( ..StatefulSetUpdateStrategy::default() }), replicas: kafka_role - .replicas(kafka, &rolegroup_ref.role_group) + .replicas(kafka, role_group_name.as_ref()) .context(RoleGroupReplicasSnafu)? .map(i32::from), selector: LabelSelector { match_labels: Some( - Labels::role_group_selector( - kafka, - APP_NAME, - &rolegroup_ref.role, - &rolegroup_ref.role_group, - ) - .context(LabelBuildSnafu)? - .into(), + validated_cluster + .role_group_selector(kafka_role, role_group_name) + .into(), ), ..LabelSelector::default() }, - service_name: Some(rolegroup_ref.rolegroup_headless_service_name()), + service_name: Some(resource_names.headless_service_name().to_string()), template: pod_template, volume_claim_templates: Some(merged_config.resources().storage.build_pvcs()), ..StatefulSetSpec::default() From e3302d9920edbf9a7d79c7226281e392eb30cd86 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Fri, 12 Jun 2026 13:53:47 +0200 Subject: [PATCH 36/47] fix(tests): reduce logging and resources rolgegroups length --- tests/templates/kuttl/logging/04-assert.yaml | 4 ++-- .../kuttl/logging/04-install-kafka.yaml.j2 | 4 ++-- .../kuttl/logging/90-shutdown-kafka.yaml | 2 +- .../kafka-vector-aggregator-values.yaml.j2 | 8 +++---- .../kuttl/smoke-kraft/30-assert.yaml.j2 | 24 +++++++++---------- .../smoke-kraft/30-install-kafka.yaml.j2 | 8 +++---- .../kafka-vector-aggregator-values.yaml.j2 | 16 ++++++------- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/templates/kuttl/logging/04-assert.yaml b/tests/templates/kuttl/logging/04-assert.yaml index e445cb01..8371ed91 100644 --- a/tests/templates/kuttl/logging/04-assert.yaml +++ b/tests/templates/kuttl/logging/04-assert.yaml @@ -6,7 +6,7 @@ timeout: 600 apiVersion: apps/v1 kind: StatefulSet metadata: - name: test-kafka-broker-automatic-log-config + name: test-kafka-broker-automatic-log status: readyReplicas: 1 replicas: 1 @@ -14,7 +14,7 @@ status: apiVersion: apps/v1 kind: StatefulSet metadata: - name: test-kafka-broker-custom-log-config + name: test-kafka-broker-custom-log status: readyReplicas: 1 replicas: 1 diff --git a/tests/templates/kuttl/logging/04-install-kafka.yaml.j2 b/tests/templates/kuttl/logging/04-install-kafka.yaml.j2 index 42588924..8f3e6dab 100644 --- a/tests/templates/kuttl/logging/04-install-kafka.yaml.j2 +++ b/tests/templates/kuttl/logging/04-install-kafka.yaml.j2 @@ -53,7 +53,7 @@ spec: zookeeperConfigMapName: test-kafka-znode brokers: roleGroups: - automatic-log-config: + automatic-log: replicas: 1 config: logging: @@ -86,7 +86,7 @@ spec: - name: prepared-logs configMap: name: prepared-logs - custom-log-config: + custom-log: replicas: 1 config: logging: diff --git a/tests/templates/kuttl/logging/90-shutdown-kafka.yaml b/tests/templates/kuttl/logging/90-shutdown-kafka.yaml index c072e08c..ec77e308 100644 --- a/tests/templates/kuttl/logging/90-shutdown-kafka.yaml +++ b/tests/templates/kuttl/logging/90-shutdown-kafka.yaml @@ -6,6 +6,6 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: - script: | - kubectl patch kafkacluster test-kafka -n $NAMESPACE --type merge -p '{"spec":{"brokers":{"roleGroups":{"automatic-log-config":{"replicas":0}, "custom-log-config":{"replicas":0}}}}}' + kubectl patch kafkacluster test-kafka -n $NAMESPACE --type merge -p '{"spec":{"brokers":{"roleGroups":{"automatic-log":{"replicas":0}, "custom-log":{"replicas":0}}}}}' - script: | kubectl wait --for=delete pod -l app.kubernetes.io/instance=test-kafka -n $NAMESPACE --timeout=300s diff --git a/tests/templates/kuttl/logging/kafka-vector-aggregator-values.yaml.j2 b/tests/templates/kuttl/logging/kafka-vector-aggregator-values.yaml.j2 index f30e142e..15a49f43 100644 --- a/tests/templates/kuttl/logging/kafka-vector-aggregator-values.yaml.j2 +++ b/tests/templates/kuttl/logging/kafka-vector-aggregator-values.yaml.j2 @@ -28,25 +28,25 @@ customConfig: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-automatic-log-config-0" && + .pod == "test-kafka-broker-automatic-log-0" && .container == "kafka" filteredAutomaticLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-automatic-log-config-0" && + .pod == "test-kafka-broker-automatic-log-0" && .container == "vector" filteredCustomLogConfigBrokerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-config-0" && + .pod == "test-kafka-broker-custom-log-0" && .container == "kafka" filteredCustomLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-config-0" && + .pod == "test-kafka-broker-custom-log-0" && .container == "vector" filteredInvalidEvents: type: filter diff --git a/tests/templates/kuttl/smoke-kraft/30-assert.yaml.j2 b/tests/templates/kuttl/smoke-kraft/30-assert.yaml.j2 index c93294b3..14ef3091 100644 --- a/tests/templates/kuttl/smoke-kraft/30-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke-kraft/30-assert.yaml.j2 @@ -17,7 +17,7 @@ status: apiVersion: apps/v1 kind: StatefulSet metadata: - name: test-kafka-broker-automatic-log-config + name: test-kafka-broker-automatic-log generation: 1 # There should be no unneeded Pod restarts labels: restarter.stackable.tech/enabled: "true" @@ -28,7 +28,7 @@ status: apiVersion: apps/v1 kind: StatefulSet metadata: - name: test-kafka-broker-custom-log-config + name: test-kafka-broker-custom-log generation: 1 # There should be no unneeded Pod restarts labels: restarter.stackable.tech/enabled: "true" @@ -39,7 +39,7 @@ status: apiVersion: apps/v1 kind: StatefulSet metadata: - name: test-kafka-controller-automatic-log-config + name: test-kafka-controller-automatic-log generation: 1 # There should be no unneeded Pod restarts labels: restarter.stackable.tech/enabled: "true" @@ -50,7 +50,7 @@ status: apiVersion: apps/v1 kind: StatefulSet metadata: - name: test-kafka-controller-custom-log-config + name: test-kafka-controller-custom-log generation: 1 # There should be no unneeded Pod restarts labels: restarter.stackable.tech/enabled: "true" @@ -83,7 +83,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-broker-automatic-log-config-headless + name: test-kafka-broker-automatic-log-headless spec: ports: - name: kafka-tls @@ -94,7 +94,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-broker-automatic-log-config-metrics + name: test-kafka-broker-automatic-log-metrics spec: ports: - name: metrics @@ -105,7 +105,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-broker-custom-log-config-headless + name: test-kafka-broker-custom-log-headless spec: ports: - name: kafka-tls @@ -116,7 +116,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-broker-custom-log-config-metrics + name: test-kafka-broker-custom-log-metrics spec: ports: - name: metrics @@ -127,7 +127,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-automatic-log-config-headless + name: test-kafka-controller-automatic-log-headless spec: ports: - name: kafka-tls @@ -138,7 +138,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-automatic-log-config-metrics + name: test-kafka-controller-automatic-log-metrics spec: ports: - name: metrics @@ -149,7 +149,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-custom-log-config-headless + name: test-kafka-controller-custom-log-headless spec: ports: - name: kafka-tls @@ -160,7 +160,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-custom-log-config-metrics + name: test-kafka-controller-custom-log-metrics spec: ports: - name: metrics diff --git a/tests/templates/kuttl/smoke-kraft/30-install-kafka.yaml.j2 b/tests/templates/kuttl/smoke-kraft/30-install-kafka.yaml.j2 index 95d85da6..0a1eeb06 100644 --- a/tests/templates/kuttl/smoke-kraft/30-install-kafka.yaml.j2 +++ b/tests/templates/kuttl/smoke-kraft/30-install-kafka.yaml.j2 @@ -84,7 +84,7 @@ spec: enableVectorAgent: true requestedSecretLifetime: 7d roleGroups: - automatic-log-config: + automatic-log: replicas: 1 config: logging: @@ -117,7 +117,7 @@ spec: - name: prepared-logs configMap: name: prepared-logs - custom-log-config: + custom-log: replicas: 1 config: logging: @@ -157,7 +157,7 @@ spec: cpu: 300m limits: cpu: 1100m - automatic-log-config: + automatic-log: replicas: 1 config: logging: @@ -190,7 +190,7 @@ spec: - name: prepared-logs configMap: name: prepared-logs - custom-log-config: + custom-log: replicas: 1 config: logging: diff --git a/tests/templates/kuttl/smoke-kraft/kafka-vector-aggregator-values.yaml.j2 b/tests/templates/kuttl/smoke-kraft/kafka-vector-aggregator-values.yaml.j2 index 67eed310..cda90a7b 100644 --- a/tests/templates/kuttl/smoke-kraft/kafka-vector-aggregator-values.yaml.j2 +++ b/tests/templates/kuttl/smoke-kraft/kafka-vector-aggregator-values.yaml.j2 @@ -36,49 +36,49 @@ customConfig: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-automatic-log-config-0" && + .pod == "test-kafka-broker-automatic-log-0" && .container == "kafka" filteredAutomaticLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-automatic-log-config-0" && + .pod == "test-kafka-broker-automatic-log-0" && .container == "vector" filteredCustomLogConfigBrokerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-config-0" && + .pod == "test-kafka-broker-custom-log-0" && .container == "kafka" filteredCustomLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-config-0" && + .pod == "test-kafka-broker-custom-log-0" && .container == "vector" filteredAutomaticLogConfigControllerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-automatic-log-config-0" && + .pod == "test-kafka-controller-automatic-log-0" && .container == "kafka" filteredAutomaticLogConfigControllerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-automatic-log-config-0" && + .pod == "test-kafka-controller-automatic-log-0" && .container == "vector" filteredCustomLogConfigControllerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-custom-log-config-0" && + .pod == "test-kafka-controller-custom-log-0" && .container == "kafka" filteredCustomLogConfigControllerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-custom-log-config-0" && + .pod == "test-kafka-controller-custom-log-0" && .container == "vector" filteredInvalidEvents: type: filter From 77f3f97f6aa91940bfaee9eb86170e2e456b9c0f Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Fri, 12 Jun 2026 13:54:03 +0200 Subject: [PATCH 37/47] chore: regenerate --- Cargo.nix | 18 +++++++++--------- crate-hashes.json | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.nix b/Cargo.nix index a35cdcc9..1de0fef7 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -4803,7 +4803,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "k8s_version"; authors = [ @@ -9486,7 +9486,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_certs"; authors = [ @@ -9677,7 +9677,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_operator"; authors = [ @@ -9871,7 +9871,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -9906,7 +9906,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_shared"; authors = [ @@ -9987,7 +9987,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_telemetry"; authors = [ @@ -10097,7 +10097,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_versioned"; authors = [ @@ -10147,7 +10147,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10215,7 +10215,7 @@ rec { src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; - sha256 = "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b"; + sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_webhook"; authors = [ diff --git a/crate-hashes.json b/crate-hashes.json index c9a6e6a9..a658a742 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -1,12 +1,12 @@ { - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", - "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "1ifdpn0jvrf3xbgqldqxrq9ig1dc34d4fip7qxn38526k8004p4b", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3", "git+https://github.com/stackabletech/product-config.git?tag=0.8.0#product-config@0.8.0": "1dz70kapm2wdqcr7ndyjji0lhsl98bsq95gnb2lw487wf6yr7987" } \ No newline at end of file From 398300435341e8eceb5a9fe6e7d2ef2ee4757439 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Fri, 12 Jun 2026 14:00:08 +0200 Subject: [PATCH 38/47] fix: security properties defaults --- .../src/controller/build/config_map.rs | 13 +++- .../src/controller/build/properties/mod.rs | 1 + .../build/properties/security_properties.rs | 76 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/properties/security_properties.rs diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/config_map.rs index 9e71d683..75d4c055 100644 --- a/rust/operator-binary/src/controller/build/config_map.rs +++ b/rust/operator-binary/src/controller/build/config_map.rs @@ -102,10 +102,15 @@ pub fn build_rolegroup_config_map( } }; - let jvm_sec_props = &validated_rg - .config_overrides - .security_properties() - .overrides; + // The `networkaddress.cache.*` defaults are always emitted (with user `security.properties` + // overrides winning); see `security_properties::build`. + let jvm_sec_props = crate::controller::build::properties::security_properties::build( + validated_rg + .config_overrides + .security_properties() + .overrides + .clone(), + ); let mut cm_builder = ConfigMapBuilder::new(); cm_builder diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index 232ab782..7d2c611b 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -4,6 +4,7 @@ pub mod broker_properties; pub mod controller_properties; pub mod listener; pub mod logging; +pub mod security_properties; use crate::crd::{KafkaPodDescriptor, role::KafkaRole}; diff --git a/rust/operator-binary/src/controller/build/properties/security_properties.rs b/rust/operator-binary/src/controller/build/properties/security_properties.rs new file mode 100644 index 00000000..be9ab791 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/security_properties.rs @@ -0,0 +1,76 @@ +//! Builder for `security.properties` (the JVM security properties file). +//! +//! `networkaddress.cache.ttl` and `networkaddress.cache.negative.ttl` are `required` +//! properties with recommended values, so they are always emitted. + +use std::collections::BTreeMap; + +const NETWORKADDRESS_CACHE_TTL: &str = "networkaddress.cache.ttl"; +const NETWORKADDRESS_CACHE_NEGATIVE_TTL: &str = "networkaddress.cache.negative.ttl"; + +const DEFAULT_NETWORKADDRESS_CACHE_TTL: &str = "30"; +const DEFAULT_NETWORKADDRESS_CACHE_NEGATIVE_TTL: &str = "0"; + +/// Build the `security.properties` key/value pairs. +/// +/// `overrides` are the resolved user overrides for `security.properties` +/// (highest precedence). +pub fn build(overrides: BTreeMap) -> BTreeMap { + let mut props: BTreeMap = BTreeMap::new(); + + // 1. Defaults (recommended values for the required properties). + props.insert( + NETWORKADDRESS_CACHE_TTL.to_string(), + DEFAULT_NETWORKADDRESS_CACHE_TTL.to_string(), + ); + props.insert( + NETWORKADDRESS_CACHE_NEGATIVE_TTL.to_string(), + DEFAULT_NETWORKADDRESS_CACHE_NEGATIVE_TTL.to_string(), + ); + + // 2. User overrides (highest precedence). + for (k, v) in overrides { + props.insert(k, v); + } + + props +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_present_without_overrides() { + let props = build(BTreeMap::new()); + assert_eq!( + props.get("networkaddress.cache.ttl"), + Some(&"30".to_string()) + ); + assert_eq!( + props.get("networkaddress.cache.negative.ttl"), + Some(&"0".to_string()) + ); + } + + #[test] + fn user_override_wins() { + let overrides = [("networkaddress.cache.ttl".to_string(), "60".to_string())] + .into_iter() + .collect(); + let props = build(overrides); + assert_eq!( + props.get("networkaddress.cache.ttl"), + Some(&"60".to_string()) + ); + } + + #[test] + fn extra_user_override_key_is_added() { + let overrides = [("custom.security.prop".to_string(), "x".to_string())] + .into_iter() + .collect(); + let props = build(overrides); + assert_eq!(props.get("custom.security.prop"), Some(&"x".to_string())); + } +} From 5d74a838f67d69a7be43c3441179564c2233bac8 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 20:27:27 +0200 Subject: [PATCH 39/47] refactor: move resources to builder --- rust/operator-binary/src/config/mod.rs | 3 - .../src/{controller/mod.rs => controller.rs} | 414 +++++++++++++++++- .../{config => controller/build}/command.rs | 0 .../build}/graceful_shutdown.rs | 0 .../src/{config => controller/build}/jvm.rs | 0 .../src/{ => controller/build}/kerberos.rs | 0 .../src/controller/build/mod.rs | 7 +- .../build/properties/broker_properties.rs | 5 +- .../build/properties/controller_properties.rs | 5 +- .../controller/build/properties/logging.rs | 17 +- .../build/{ => resource}/config_map.rs | 0 .../build/{ => resource}/discovery.rs | 0 .../build}/resource/listener.rs | 0 .../src/controller/build/resource/mod.rs | 8 + .../build/resource}/pdb.rs | 2 +- .../build}/resource/service.rs | 0 .../build}/resource/statefulset.rs | 38 +- .../{config => controller}/node_id_hasher.rs | 0 rust/operator-binary/src/kafka_controller.rs | 408 ----------------- rust/operator-binary/src/main.rs | 13 +- rust/operator-binary/src/operations/mod.rs | 2 - rust/operator-binary/src/resource/mod.rs | 3 - 22 files changed, 461 insertions(+), 464 deletions(-) delete mode 100644 rust/operator-binary/src/config/mod.rs rename rust/operator-binary/src/{controller/mod.rs => controller.rs} (51%) rename rust/operator-binary/src/{config => controller/build}/command.rs (100%) rename rust/operator-binary/src/{operations => controller/build}/graceful_shutdown.rs (100%) rename rust/operator-binary/src/{config => controller/build}/jvm.rs (100%) rename rust/operator-binary/src/{ => controller/build}/kerberos.rs (100%) rename rust/operator-binary/src/controller/build/{ => resource}/config_map.rs (100%) rename rust/operator-binary/src/controller/build/{ => resource}/discovery.rs (100%) rename rust/operator-binary/src/{ => controller/build}/resource/listener.rs (100%) create mode 100644 rust/operator-binary/src/controller/build/resource/mod.rs rename rust/operator-binary/src/{operations => controller/build/resource}/pdb.rs (97%) rename rust/operator-binary/src/{ => controller/build}/resource/service.rs (100%) rename rust/operator-binary/src/{ => controller/build}/resource/statefulset.rs (96%) rename rust/operator-binary/src/{config => controller}/node_id_hasher.rs (100%) delete mode 100644 rust/operator-binary/src/kafka_controller.rs delete mode 100644 rust/operator-binary/src/operations/mod.rs delete mode 100644 rust/operator-binary/src/resource/mod.rs diff --git a/rust/operator-binary/src/config/mod.rs b/rust/operator-binary/src/config/mod.rs deleted file mode 100644 index ae92b3c2..00000000 --- a/rust/operator-binary/src/config/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod command; -pub mod jvm; -pub mod node_id_hasher; diff --git a/rust/operator-binary/src/controller/mod.rs b/rust/operator-binary/src/controller.rs similarity index 51% rename from rust/operator-binary/src/controller/mod.rs rename to rust/operator-binary/src/controller.rs index b6f33cb1..242f1128 100644 --- a/rust/operator-binary/src/controller/mod.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,21 +1,41 @@ -//! The validated cluster model and the steps that produce it. +//! The validated cluster model, the reconcile loop, and the steps that produce it. //! //! [`ValidatedCluster`] carries everything the build steps need, resolved once during //! [`validate`] (after [`dereference`]) so downstream code never re-derives it or -//! touches the raw [`v1alpha1::KafkaCluster`] spec. The reconcile loop that consumes -//! it lives in [`crate::kafka_controller`]. +//! touches the raw [`v1alpha1::KafkaCluster`] spec. [`reconcile_kafka`] consumes it to +//! build (under [`build`]) and apply the child resources. use std::{ borrow::Cow, collections::{BTreeMap, HashMap}, str::FromStr, + sync::Arc, }; -use snafu::Snafu; +use const_format::concatcp; +use snafu::{ResultExt, Snafu}; use stackable_operator::{ - commons::{networking::DomainName, product_image_selection::ResolvedProductImage}, - kube::{Resource, api::ObjectMeta}, + cli::OperatorEnvironmentOptions, + cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, + commons::{ + networking::DomainName, product_image_selection::ResolvedProductImage, + rbac::build_rbac_resources, + }, + crd::listener, + kube::{ + Resource, + api::{DynamicObject, ObjectMeta}, + core::{DeserializeGuard, error_boundary}, + runtime::{controller::Action, reflector::ObjectRef}, + }, kvp::Labels, + logging::controller::ReconcilerError, + role_utils::{GenericRoleConfig, RoleGroupRef}, + shared::time::Duration, + status::condition::{ + compute_conditions, operations::ClusterOperationsConditionBuilder, + statefulset::StatefulSetConditionBuilder, + }, v2::{ HasName, HasUid, NameIsValidLabelValue, kvp::label::{recommended_labels, role_group_selector}, @@ -26,9 +46,11 @@ use stackable_operator::{ }, }, }; +use strum::{EnumDiscriminants, IntoStaticStr}; pub(crate) mod build; pub(crate) mod dereference; +pub(crate) mod node_id_hasher; pub(crate) mod validate; /// The type-safe role-group name from stackable-operator's v2 module. Re-exported so the rest @@ -36,17 +58,32 @@ pub(crate) mod validate; pub use stackable_operator::v2::types::operator::{RoleGroupName, RoleName}; use crate::{ - config::node_id_hasher::node_id_hash32_offset, + controller::{ + build::{ + properties::listener::get_kafka_listener_config, + resource::{ + listener::build_broker_rolegroup_bootstrap_listener, + pdb::add_pdbs, + service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, + statefulset::{ + build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset, + }, + }, + }, + node_id_hasher::node_id_hash32_offset, + }, crd::{ - APP_NAME, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, + APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, authorization::KafkaAuthorizationConfig, role::{AnyConfig, AnyConfigOverrides, KafkaRole}, security::KafkaTlsSecurity, v1alpha1, }, - kafka_controller::KAFKA_CONTROLLER_NAME, }; +pub const KAFKA_CONTROLLER_NAME: &str = "kafkacluster"; +pub const KAFKA_FULL_CONTROLLER_NAME: &str = concatcp!(KAFKA_CONTROLLER_NAME, '.', OPERATOR_NAME); + #[derive(Snafu, Debug)] pub enum PodDescriptorsError { #[snafu(display( @@ -371,6 +408,365 @@ pub struct ValidatedRoleGroupConfig { stackable_operator::v2::jvm_argument_overrides::JvmArgumentOverrides, } +pub struct Ctx { + pub client: stackable_operator::client::Client, + pub operator_environment: OperatorEnvironmentOptions, +} + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +#[allow(clippy::enum_variant_names)] +pub enum Error { + #[snafu(display("failed to dereference resources"))] + Dereference { source: dereference::Error }, + + #[snafu(display("failed to validate cluster"))] + ValidateCluster { source: validate::Error }, + + #[snafu(display("failed to apply bootstrap Listener"))] + ApplyBootstrapListener { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to apply Service for {}", rolegroup))] + ApplyRoleGroupService { + source: stackable_operator::cluster_resources::Error, + rolegroup: RoleGroupRef, + }, + + #[snafu(display("failed to apply ConfigMap for {}", rolegroup))] + ApplyRoleGroupConfig { + source: stackable_operator::cluster_resources::Error, + rolegroup: RoleGroupRef, + }, + + #[snafu(display("failed to apply StatefulSet for {}", rolegroup))] + ApplyRoleGroupStatefulSet { + source: stackable_operator::cluster_resources::Error, + rolegroup: RoleGroupRef, + }, + + #[snafu(display("failed to build discovery ConfigMap"))] + BuildDiscoveryConfig { + source: build::resource::discovery::Error, + }, + + #[snafu(display("failed to apply discovery ConfigMap"))] + ApplyDiscoveryConfig { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to delete orphaned resources"))] + DeleteOrphans { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to create cluster resources"))] + CreateClusterResources { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to patch service account"))] + ApplyServiceAccount { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to patch role binding"))] + ApplyRoleBinding { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to update status"))] + ApplyStatus { + source: stackable_operator::client::Error, + }, + + #[snafu(display("failed to build RBAC resources"))] + BuildRbacResources { + source: stackable_operator::commons::rbac::Error, + }, + + #[snafu(display("failed to create PodDisruptionBudget"))] + FailedToCreatePdb { + source: crate::controller::build::resource::pdb::Error, + }, + + #[snafu(display("failed to get required Labels"))] + GetRequiredLabels { + source: + stackable_operator::kvp::KeyValuePairError, + }, + + #[snafu(display("KafkaCluster object is invalid"))] + InvalidKafkaCluster { + source: error_boundary::InvalidObject, + }, + + #[snafu(display("failed to build statefulset"))] + BuildStatefulset { + source: crate::controller::build::resource::statefulset::Error, + }, + + #[snafu(display("failed to build configmap"))] + BuildConfigMap { + source: crate::controller::build::resource::config_map::Error, + }, +} +type Result = std::result::Result; + +impl ReconcilerError for Error { + fn category(&self) -> &'static str { + ErrorDiscriminants::from(self).into() + } + + fn secondary_object(&self) -> Option> { + match self { + Error::Dereference { .. } => None, + Error::ValidateCluster { .. } => None, + Error::ApplyBootstrapListener { .. } => None, + Error::ApplyRoleGroupService { .. } => None, + Error::ApplyRoleGroupConfig { .. } => None, + Error::ApplyRoleGroupStatefulSet { .. } => None, + Error::BuildDiscoveryConfig { .. } => None, + Error::ApplyDiscoveryConfig { .. } => None, + Error::DeleteOrphans { .. } => None, + Error::CreateClusterResources { .. } => None, + Error::ApplyServiceAccount { .. } => None, + Error::ApplyRoleBinding { .. } => None, + Error::ApplyStatus { .. } => None, + Error::BuildRbacResources { .. } => None, + Error::FailedToCreatePdb { .. } => None, + Error::GetRequiredLabels { .. } => None, + Error::InvalidKafkaCluster { .. } => None, + Error::BuildStatefulset { .. } => None, + Error::BuildConfigMap { .. } => None, + } + } +} + +pub async fn reconcile_kafka( + kafka: Arc>, + ctx: Arc, +) -> Result { + tracing::info!("Starting reconcile"); + + let kafka = kafka + .0 + .as_ref() + .map_err(error_boundary::InvalidObject::clone) + .context(InvalidKafkaClusterSnafu)?; + + let client = &ctx.client; + + // dereference (client required) + let dereferenced_objects = dereference::dereference(client, kafka) + .await + .context(DereferenceSnafu)?; + + // validate (no client required) + let validated_cluster = + validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) + .context(ValidateClusterSnafu)?; + + let mut cluster_resources = ClusterResources::new( + APP_NAME, + OPERATOR_NAME, + KAFKA_CONTROLLER_NAME, + &validated_cluster.object_ref(&()), + ClusterResourceApplyStrategy::from(&kafka.spec.cluster_operation), + &kafka.spec.object_overrides, + ) + .context(CreateClusterResourcesSnafu)?; + + tracing::debug!( + kerberos_enabled = validated_cluster.cluster_config.kafka_security.has_kerberos_enabled(), + kerberos_secret_class = ?validated_cluster.cluster_config.kafka_security.kerberos_secret_class(), + tls_enabled = validated_cluster.cluster_config.kafka_security.tls_enabled(), + tls_client_authentication_class = ?validated_cluster.cluster_config.kafka_security.tls_client_authentication_class(), + "The following security settings are used" + ); + + let mut ss_cond_builder = StatefulSetConditionBuilder::default(); + + let (rbac_sa, rbac_rolebinding) = build_rbac_resources( + kafka, + APP_NAME, + cluster_resources + .get_required_labels() + .context(GetRequiredLabelsSnafu)?, + ) + .context(BuildRbacResourcesSnafu)?; + + let rbac_sa = cluster_resources + .add(client, rbac_sa.clone()) + .await + .context(ApplyServiceAccountSnafu)?; + cluster_resources + .add(client, rbac_rolebinding) + .await + .context(ApplyRoleBindingSnafu)?; + + let mut bootstrap_listeners = Vec::::new(); + + for (kafka_role, rg_map) in &validated_cluster.role_group_configs { + for (rolegroup_name, validated_rg) in rg_map { + // The Vector log-aggregation config still consumes a v1 `RoleGroupRef`; it is built + // here and used only for that. All other identification uses the typed `kafka_role` / + // `rolegroup_name` (and `ValidatedCluster::resource_names`). + let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name.to_string()); + let vector_config = build::properties::logging::build_vector_config( + &rolegroup_ref, + &validated_rg.config, + ); + + let rg_headless_service = build_rolegroup_headless_service( + &validated_cluster, + kafka_role, + rolegroup_name, + &validated_cluster.cluster_config.kafka_security, + ); + + let rg_metrics_service = + build_rolegroup_metrics_service(&validated_cluster, kafka_role, rolegroup_name); + + let kafka_listeners = get_kafka_listener_config( + &validated_cluster, + &validated_cluster.cluster_config.kafka_security, + kafka_role, + rolegroup_name, + &client.kubernetes_cluster_info, + ); + + let rg_configmap = build::resource::config_map::build_rolegroup_config_map( + &validated_cluster, + rolegroup_name, + validated_rg, + &kafka_listeners, + vector_config, + ) + .context(BuildConfigMapSnafu)?; + + let rg_statefulset = match kafka_role { + KafkaRole::Broker => build_broker_rolegroup_statefulset( + kafka, + kafka_role, + rolegroup_name, + &validated_cluster, + validated_rg, + &rbac_sa, + ) + .context(BuildStatefulsetSnafu)?, + KafkaRole::Controller => build_controller_rolegroup_statefulset( + kafka, + kafka_role, + rolegroup_name, + &validated_cluster, + validated_rg, + &rbac_sa, + ) + .context(BuildStatefulsetSnafu)?, + }; + + if let AnyConfig::Broker(broker_config) = &validated_rg.config { + let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( + &validated_cluster, + kafka_role, + rolegroup_name, + broker_config, + ); + bootstrap_listeners.push( + cluster_resources + .add(client, rg_bootstrap_listener) + .await + .context(ApplyBootstrapListenerSnafu)?, + ); + } + + cluster_resources + .add(client, rg_headless_service) + .await + .with_context(|_| ApplyRoleGroupServiceSnafu { + rolegroup: rolegroup_ref.clone(), + })?; + cluster_resources + .add(client, rg_metrics_service) + .await + .with_context(|_| ApplyRoleGroupServiceSnafu { + rolegroup: rolegroup_ref.clone(), + })?; + cluster_resources + .add(client, rg_configmap) + .await + .with_context(|_| ApplyRoleGroupConfigSnafu { + rolegroup: rolegroup_ref.clone(), + })?; + + // Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts + // to prevent unnecessary Pod restarts. + // See https://github.com/stackabletech/commons-operator/issues/111 for details. + ss_cond_builder.add( + cluster_resources + .add(client, rg_statefulset) + .await + .with_context(|_| ApplyRoleGroupStatefulSetSnafu { + rolegroup: rolegroup_ref.clone(), + })?, + ); + } + + let role_cfg = kafka.role_config(kafka_role); + if let Some(GenericRoleConfig { + pod_disruption_budget: pdb, + }) = role_cfg + { + add_pdbs(pdb, kafka, kafka_role, client, &mut cluster_resources) + .await + .context(FailedToCreatePdbSnafu)?; + } + } + + let discovery_cm = build::resource::discovery::build_discovery_configmap( + &validated_cluster, + &bootstrap_listeners, + ) + .context(BuildDiscoveryConfigSnafu)?; + + cluster_resources + .add(client, discovery_cm) + .await + .context(ApplyDiscoveryConfigSnafu)?; + + let cluster_operation_cond_builder = + ClusterOperationsConditionBuilder::new(&kafka.spec.cluster_operation); + + let status = KafkaClusterStatus { + conditions: compute_conditions(kafka, &[&ss_cond_builder, &cluster_operation_cond_builder]), + }; + + cluster_resources + .delete_orphaned_resources(client) + .await + .context(DeleteOrphansSnafu)?; + + client + .apply_patch_status(OPERATOR_NAME, kafka, &status) + .await + .context(ApplyStatusSnafu)?; + + Ok(Action::await_change()) +} + +pub fn error_policy( + _obj: Arc>, + error: &Error, + _ctx: Arc, +) -> Action { + match error { + Error::InvalidKafkaCluster { .. } => Action::await_change(), + _ => Action::requeue(*Duration::from_secs(5)), + } +} + #[cfg(test)] pub(crate) mod test_support { use stackable_operator::{ diff --git a/rust/operator-binary/src/config/command.rs b/rust/operator-binary/src/controller/build/command.rs similarity index 100% rename from rust/operator-binary/src/config/command.rs rename to rust/operator-binary/src/controller/build/command.rs diff --git a/rust/operator-binary/src/operations/graceful_shutdown.rs b/rust/operator-binary/src/controller/build/graceful_shutdown.rs similarity index 100% rename from rust/operator-binary/src/operations/graceful_shutdown.rs rename to rust/operator-binary/src/controller/build/graceful_shutdown.rs diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/controller/build/jvm.rs similarity index 100% rename from rust/operator-binary/src/config/jvm.rs rename to rust/operator-binary/src/controller/build/jvm.rs diff --git a/rust/operator-binary/src/kerberos.rs b/rust/operator-binary/src/controller/build/kerberos.rs similarity index 100% rename from rust/operator-binary/src/kerberos.rs rename to rust/operator-binary/src/controller/build/kerberos.rs diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index 0cbab809..5b1e0bbc 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -1,5 +1,8 @@ //! Builders that assemble Kubernetes resources for kafka rolegroups. -pub mod config_map; -pub mod discovery; +pub mod command; +pub mod graceful_shutdown; +pub mod jvm; +pub mod kerberos; pub mod properties; +pub mod resource; diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs index a950f352..b09e071b 100644 --- a/rust/operator-binary/src/controller/build/properties/broker_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -2,7 +2,9 @@ use std::collections::BTreeMap; use super::kraft_controllers; use crate::{ - controller::ValidatedClusterConfig, + controller::{ + ValidatedClusterConfig, build::graceful_shutdown::graceful_shutdown_config_properties, + }, crd::{ KafkaPodDescriptor, listener::{KafkaListenerConfig, KafkaListenerName}, @@ -12,7 +14,6 @@ use crate::{ KAFKA_PROCESS_ROLES, KafkaRole, }, }, - operations::graceful_shutdown::graceful_shutdown_config_properties, }; pub fn build( diff --git a/rust/operator-binary/src/controller/build/properties/controller_properties.rs b/rust/operator-binary/src/controller/build/properties/controller_properties.rs index ff7a57d1..93fce3c6 100644 --- a/rust/operator-binary/src/controller/build/properties/controller_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -2,7 +2,9 @@ use std::collections::BTreeMap; use super::kraft_controllers; use crate::{ - controller::ValidatedClusterConfig, + controller::{ + ValidatedClusterConfig, build::graceful_shutdown::graceful_shutdown_config_properties, + }, crd::{ KafkaPodDescriptor, listener::{KafkaListenerConfig, KafkaListenerName}, @@ -11,7 +13,6 @@ use crate::{ KAFKA_LISTENERS, KAFKA_LOG_DIRS, KAFKA_NODE_ID, KAFKA_PROCESS_ROLES, KafkaRole, }, }, - operations::graceful_shutdown::graceful_shutdown_config_properties, }; pub fn build( diff --git a/rust/operator-binary/src/controller/build/properties/logging.rs b/rust/operator-binary/src/controller/build/properties/logging.rs index 9834d39e..ac83f665 100644 --- a/rust/operator-binary/src/controller/build/properties/logging.rs +++ b/rust/operator-binary/src/controller/build/properties/logging.rs @@ -12,13 +12,16 @@ use stackable_operator::{ role_utils::RoleGroupRef, }; -use crate::{ - crd::{ - ConfigFileName, STACKABLE_LOG_DIR, - role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, - v1alpha1, - }, - kafka_controller::MAX_KAFKA_LOG_FILES_SIZE, +use crate::crd::{ + ConfigFileName, STACKABLE_LOG_DIR, + role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, + v1alpha1, +}; + +/// The maximum size of a single Kafka log file before it is rotated. +pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { + value: 10.0, + unit: BinaryMultiple::Mebi, }; const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; diff --git a/rust/operator-binary/src/controller/build/config_map.rs b/rust/operator-binary/src/controller/build/resource/config_map.rs similarity index 100% rename from rust/operator-binary/src/controller/build/config_map.rs rename to rust/operator-binary/src/controller/build/resource/config_map.rs diff --git a/rust/operator-binary/src/controller/build/discovery.rs b/rust/operator-binary/src/controller/build/resource/discovery.rs similarity index 100% rename from rust/operator-binary/src/controller/build/discovery.rs rename to rust/operator-binary/src/controller/build/resource/discovery.rs diff --git a/rust/operator-binary/src/resource/listener.rs b/rust/operator-binary/src/controller/build/resource/listener.rs similarity index 100% rename from rust/operator-binary/src/resource/listener.rs rename to rust/operator-binary/src/controller/build/resource/listener.rs diff --git a/rust/operator-binary/src/controller/build/resource/mod.rs b/rust/operator-binary/src/controller/build/resource/mod.rs new file mode 100644 index 00000000..37b2b92f --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/mod.rs @@ -0,0 +1,8 @@ +//! Builders that assemble Kubernetes resources for kafka rolegroups. + +pub mod config_map; +pub mod discovery; +pub mod listener; +pub mod pdb; +pub mod service; +pub mod statefulset; diff --git a/rust/operator-binary/src/operations/pdb.rs b/rust/operator-binary/src/controller/build/resource/pdb.rs similarity index 97% rename from rust/operator-binary/src/operations/pdb.rs rename to rust/operator-binary/src/controller/build/resource/pdb.rs index 18f46dc7..e42888ca 100644 --- a/rust/operator-binary/src/operations/pdb.rs +++ b/rust/operator-binary/src/controller/build/resource/pdb.rs @@ -5,8 +5,8 @@ use stackable_operator::{ }; use crate::{ + controller::KAFKA_CONTROLLER_NAME, crd::{APP_NAME, OPERATOR_NAME, role::KafkaRole, v1alpha1}, - kafka_controller::KAFKA_CONTROLLER_NAME, }; #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/resource/service.rs b/rust/operator-binary/src/controller/build/resource/service.rs similarity index 100% rename from rust/operator-binary/src/resource/service.rs rename to rust/operator-binary/src/controller/build/resource/service.rs diff --git a/rust/operator-binary/src/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs similarity index 96% rename from rust/operator-binary/src/resource/statefulset.rs rename to rust/operator-binary/src/controller/build/resource/statefulset.rs index 3be9cf6f..6bbd000c 100644 --- a/rust/operator-binary/src/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -37,14 +37,19 @@ use stackable_operator::{ }; use crate::{ - config::{ - command::{ - broker_kafka_container_commands, controller_kafka_container_command, kafka_log_opts, - kafka_log_opts_env_var, + controller::{ + RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, + build::{ + command::{ + broker_kafka_container_commands, controller_kafka_container_command, + kafka_log_opts, kafka_log_opts_env_var, + }, + graceful_shutdown::add_graceful_shutdown_config, + kerberos::add_kerberos_pod_config, + properties::logging::MAX_KAFKA_LOG_FILES_SIZE, }, node_id_hasher::node_id_hash32_offset, }, - controller::{RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig}, crd::{ self, BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, @@ -58,9 +63,6 @@ use crate::{ security::KafkaTlsSecurity, v1alpha1, }, - kafka_controller::MAX_KAFKA_LOG_FILES_SIZE, - kerberos::add_kerberos_pod_config, - operations::graceful_shutdown::add_graceful_shutdown_config, }; #[derive(Snafu, Debug)] @@ -69,7 +71,9 @@ pub enum Error { InvalidMetadataManager { source: crate::crd::Error }, #[snafu(display("failed to add kerberos config"))] - AddKerberosConfig { source: crate::kerberos::Error }, + AddKerberosConfig { + source: crate::controller::build::kerberos::Error, + }, #[snafu(display("failed to add listener volume"))] AddListenerVolume { @@ -105,11 +109,13 @@ pub enum Error { }, #[snafu(display("failed to construct JVM arguments"))] - ConstructJvmArguments { source: crate::config::jvm::Error }, + ConstructJvmArguments { + source: crate::controller::build::jvm::Error, + }, #[snafu(display("failed to configure graceful shutdown"))] GracefulShutdown { - source: crate::operations::graceful_shutdown::Error, + source: crate::controller::build::graceful_shutdown::Error, }, #[snafu(display("invalid Container name [{name}]"))] @@ -134,7 +140,7 @@ pub enum Error { /// The broker rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. /// /// The [`Pod`](`stackable_operator::k8s_openapi::api::core::v1::Pod`)s are accessible through the corresponding -/// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_headless_service`](`crate::resource::service::build_rolegroup_headless_service`). +/// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_headless_service`](`crate::controller::build::resource::service::build_rolegroup_headless_service`). pub fn build_broker_rolegroup_statefulset( kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, @@ -259,7 +265,7 @@ pub fn build_broker_rolegroup_statefulset( )]) .add_env_var( "EXTRA_ARGS", - crate::config::jvm::construct_non_heap_jvm_args( + crate::controller::build::jvm::construct_non_heap_jvm_args( merged_config, &validated_rg.jvm_argument_overrides, ) @@ -267,7 +273,7 @@ pub fn build_broker_rolegroup_statefulset( ) .add_env_var( KAFKA_HEAP_OPTS, - crate::config::jvm::construct_heap_jvm_args( + crate::controller::build::jvm::construct_heap_jvm_args( merged_config, &validated_rg.jvm_argument_overrides, ) @@ -613,7 +619,7 @@ pub fn build_controller_rolegroup_statefulset( .add_env_var("PRE_STOP_CONTROLLER_SLEEP_SECONDS", "10") .add_env_var( "EXTRA_ARGS", - crate::config::jvm::construct_non_heap_jvm_args( + crate::controller::build::jvm::construct_non_heap_jvm_args( merged_config, &validated_rg.jvm_argument_overrides, ) @@ -621,7 +627,7 @@ pub fn build_controller_rolegroup_statefulset( ) .add_env_var( KAFKA_HEAP_OPTS, - crate::config::jvm::construct_heap_jvm_args( + crate::controller::build::jvm::construct_heap_jvm_args( merged_config, &validated_rg.jvm_argument_overrides, ) diff --git a/rust/operator-binary/src/config/node_id_hasher.rs b/rust/operator-binary/src/controller/node_id_hasher.rs similarity index 100% rename from rust/operator-binary/src/config/node_id_hasher.rs rename to rust/operator-binary/src/controller/node_id_hasher.rs diff --git a/rust/operator-binary/src/kafka_controller.rs b/rust/operator-binary/src/kafka_controller.rs deleted file mode 100644 index 2fc2d007..00000000 --- a/rust/operator-binary/src/kafka_controller.rs +++ /dev/null @@ -1,408 +0,0 @@ -//! Ensures that `Pod`s are configured and running for each [`v1alpha1::KafkaCluster`]. - -use std::sync::Arc; - -use const_format::concatcp; -use snafu::{ResultExt, Snafu}; -use stackable_operator::{ - cli::OperatorEnvironmentOptions, - cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - commons::rbac::build_rbac_resources, - crd::listener, - kube::{ - Resource, - api::DynamicObject, - core::{DeserializeGuard, error_boundary}, - runtime::{controller::Action, reflector::ObjectRef}, - }, - logging::controller::ReconcilerError, - memory::{BinaryMultiple, MemoryQuantity}, - role_utils::{GenericRoleConfig, RoleGroupRef}, - shared::time::Duration, - status::condition::{ - compute_conditions, operations::ClusterOperationsConditionBuilder, - statefulset::StatefulSetConditionBuilder, - }, -}; -use strum::{EnumDiscriminants, IntoStaticStr}; - -use crate::{ - controller::{ - build, build::properties::listener::get_kafka_listener_config, dereference, validate, - }, - crd::{ - APP_NAME, KafkaClusterStatus, OPERATOR_NAME, - role::{AnyConfig, KafkaRole}, - v1alpha1, - }, - operations::pdb::add_pdbs, - resource::{ - listener::build_broker_rolegroup_bootstrap_listener, - service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, - statefulset::{build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset}, - }, -}; - -pub const KAFKA_CONTROLLER_NAME: &str = "kafkacluster"; -pub const KAFKA_FULL_CONTROLLER_NAME: &str = concatcp!(KAFKA_CONTROLLER_NAME, '.', OPERATOR_NAME); - -/// The maximum size of a single Kafka log file before it is rotated. -pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { - value: 10.0, - unit: BinaryMultiple::Mebi, -}; - -pub struct Ctx { - pub client: stackable_operator::client::Client, - pub operator_environment: OperatorEnvironmentOptions, -} - -#[derive(Snafu, Debug, EnumDiscriminants)] -#[strum_discriminants(derive(IntoStaticStr))] -#[allow(clippy::enum_variant_names)] -pub enum Error { - #[snafu(display("failed to dereference resources"))] - Dereference { source: dereference::Error }, - - #[snafu(display("failed to validate cluster"))] - ValidateCluster { source: validate::Error }, - - #[snafu(display("failed to apply bootstrap Listener"))] - ApplyBootstrapListener { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to apply Service for {}", rolegroup))] - ApplyRoleGroupService { - source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("failed to apply ConfigMap for {}", rolegroup))] - ApplyRoleGroupConfig { - source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("failed to apply StatefulSet for {}", rolegroup))] - ApplyRoleGroupStatefulSet { - source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, - }, - - #[snafu(display("failed to build discovery ConfigMap"))] - BuildDiscoveryConfig { source: build::discovery::Error }, - - #[snafu(display("failed to apply discovery ConfigMap"))] - ApplyDiscoveryConfig { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to delete orphaned resources"))] - DeleteOrphans { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to create cluster resources"))] - CreateClusterResources { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to patch service account"))] - ApplyServiceAccount { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to patch role binding"))] - ApplyRoleBinding { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to update status"))] - ApplyStatus { - source: stackable_operator::client::Error, - }, - - #[snafu(display("failed to build RBAC resources"))] - BuildRbacResources { - source: stackable_operator::commons::rbac::Error, - }, - - #[snafu(display("failed to create PodDisruptionBudget"))] - FailedToCreatePdb { - source: crate::operations::pdb::Error, - }, - - #[snafu(display("failed to get required Labels"))] - GetRequiredLabels { - source: - stackable_operator::kvp::KeyValuePairError, - }, - - #[snafu(display("KafkaCluster object is invalid"))] - InvalidKafkaCluster { - source: error_boundary::InvalidObject, - }, - - #[snafu(display("failed to build statefulset"))] - BuildStatefulset { - source: crate::resource::statefulset::Error, - }, - - #[snafu(display("failed to build configmap"))] - BuildConfigMap { - source: crate::controller::build::config_map::Error, - }, -} -type Result = std::result::Result; - -impl ReconcilerError for Error { - fn category(&self) -> &'static str { - ErrorDiscriminants::from(self).into() - } - - fn secondary_object(&self) -> Option> { - match self { - Error::Dereference { .. } => None, - Error::ValidateCluster { .. } => None, - Error::ApplyBootstrapListener { .. } => None, - Error::ApplyRoleGroupService { .. } => None, - Error::ApplyRoleGroupConfig { .. } => None, - Error::ApplyRoleGroupStatefulSet { .. } => None, - Error::BuildDiscoveryConfig { .. } => None, - Error::ApplyDiscoveryConfig { .. } => None, - Error::DeleteOrphans { .. } => None, - Error::CreateClusterResources { .. } => None, - Error::ApplyServiceAccount { .. } => None, - Error::ApplyRoleBinding { .. } => None, - Error::ApplyStatus { .. } => None, - Error::BuildRbacResources { .. } => None, - Error::FailedToCreatePdb { .. } => None, - Error::GetRequiredLabels { .. } => None, - Error::InvalidKafkaCluster { .. } => None, - Error::BuildStatefulset { .. } => None, - Error::BuildConfigMap { .. } => None, - } - } -} - -pub async fn reconcile_kafka( - kafka: Arc>, - ctx: Arc, -) -> Result { - tracing::info!("Starting reconcile"); - - let kafka = kafka - .0 - .as_ref() - .map_err(error_boundary::InvalidObject::clone) - .context(InvalidKafkaClusterSnafu)?; - - let client = &ctx.client; - - // dereference (client required) - let dereferenced_objects = dereference::dereference(client, kafka) - .await - .context(DereferenceSnafu)?; - - // validate (no client required) - let validated_cluster = - validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) - .context(ValidateClusterSnafu)?; - - let mut cluster_resources = ClusterResources::new( - APP_NAME, - OPERATOR_NAME, - KAFKA_CONTROLLER_NAME, - &validated_cluster.object_ref(&()), - ClusterResourceApplyStrategy::from(&kafka.spec.cluster_operation), - &kafka.spec.object_overrides, - ) - .context(CreateClusterResourcesSnafu)?; - - tracing::debug!( - kerberos_enabled = validated_cluster.cluster_config.kafka_security.has_kerberos_enabled(), - kerberos_secret_class = ?validated_cluster.cluster_config.kafka_security.kerberos_secret_class(), - tls_enabled = validated_cluster.cluster_config.kafka_security.tls_enabled(), - tls_client_authentication_class = ?validated_cluster.cluster_config.kafka_security.tls_client_authentication_class(), - "The following security settings are used" - ); - - let mut ss_cond_builder = StatefulSetConditionBuilder::default(); - - let (rbac_sa, rbac_rolebinding) = build_rbac_resources( - kafka, - APP_NAME, - cluster_resources - .get_required_labels() - .context(GetRequiredLabelsSnafu)?, - ) - .context(BuildRbacResourcesSnafu)?; - - let rbac_sa = cluster_resources - .add(client, rbac_sa.clone()) - .await - .context(ApplyServiceAccountSnafu)?; - cluster_resources - .add(client, rbac_rolebinding) - .await - .context(ApplyRoleBindingSnafu)?; - - let mut bootstrap_listeners = Vec::::new(); - - for (kafka_role, rg_map) in &validated_cluster.role_group_configs { - for (rolegroup_name, validated_rg) in rg_map { - // The Vector log-aggregation config still consumes a v1 `RoleGroupRef`; it is built - // here and used only for that. All other identification uses the typed `kafka_role` / - // `rolegroup_name` (and `ValidatedCluster::resource_names`). - let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name.to_string()); - let vector_config = build::properties::logging::build_vector_config( - &rolegroup_ref, - &validated_rg.config, - ); - - let rg_headless_service = build_rolegroup_headless_service( - &validated_cluster, - kafka_role, - rolegroup_name, - &validated_cluster.cluster_config.kafka_security, - ); - - let rg_metrics_service = - build_rolegroup_metrics_service(&validated_cluster, kafka_role, rolegroup_name); - - let kafka_listeners = get_kafka_listener_config( - &validated_cluster, - &validated_cluster.cluster_config.kafka_security, - kafka_role, - rolegroup_name, - &client.kubernetes_cluster_info, - ); - - let rg_configmap = build::config_map::build_rolegroup_config_map( - &validated_cluster, - rolegroup_name, - validated_rg, - &kafka_listeners, - vector_config, - ) - .context(BuildConfigMapSnafu)?; - - let rg_statefulset = match kafka_role { - KafkaRole::Broker => build_broker_rolegroup_statefulset( - kafka, - kafka_role, - rolegroup_name, - &validated_cluster, - validated_rg, - &rbac_sa, - ) - .context(BuildStatefulsetSnafu)?, - KafkaRole::Controller => build_controller_rolegroup_statefulset( - kafka, - kafka_role, - rolegroup_name, - &validated_cluster, - validated_rg, - &rbac_sa, - ) - .context(BuildStatefulsetSnafu)?, - }; - - if let AnyConfig::Broker(broker_config) = &validated_rg.config { - let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( - &validated_cluster, - kafka_role, - rolegroup_name, - broker_config, - ); - bootstrap_listeners.push( - cluster_resources - .add(client, rg_bootstrap_listener) - .await - .context(ApplyBootstrapListenerSnafu)?, - ); - } - - cluster_resources - .add(client, rg_headless_service) - .await - .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: rolegroup_ref.clone(), - })?; - cluster_resources - .add(client, rg_metrics_service) - .await - .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: rolegroup_ref.clone(), - })?; - cluster_resources - .add(client, rg_configmap) - .await - .with_context(|_| ApplyRoleGroupConfigSnafu { - rolegroup: rolegroup_ref.clone(), - })?; - - // Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts - // to prevent unnecessary Pod restarts. - // See https://github.com/stackabletech/commons-operator/issues/111 for details. - ss_cond_builder.add( - cluster_resources - .add(client, rg_statefulset) - .await - .with_context(|_| ApplyRoleGroupStatefulSetSnafu { - rolegroup: rolegroup_ref.clone(), - })?, - ); - } - - let role_cfg = kafka.role_config(kafka_role); - if let Some(GenericRoleConfig { - pod_disruption_budget: pdb, - }) = role_cfg - { - add_pdbs(pdb, kafka, kafka_role, client, &mut cluster_resources) - .await - .context(FailedToCreatePdbSnafu)?; - } - } - - let discovery_cm = - build::discovery::build_discovery_configmap(&validated_cluster, &bootstrap_listeners) - .context(BuildDiscoveryConfigSnafu)?; - - cluster_resources - .add(client, discovery_cm) - .await - .context(ApplyDiscoveryConfigSnafu)?; - - let cluster_operation_cond_builder = - ClusterOperationsConditionBuilder::new(&kafka.spec.cluster_operation); - - let status = KafkaClusterStatus { - conditions: compute_conditions(kafka, &[&ss_cond_builder, &cluster_operation_cond_builder]), - }; - - cluster_resources - .delete_orphaned_resources(client) - .await - .context(DeleteOrphansSnafu)?; - - client - .apply_patch_status(OPERATOR_NAME, kafka, &status) - .await - .context(ApplyStatusSnafu)?; - - Ok(Action::await_change()) -} - -pub fn error_policy( - _obj: Arc>, - error: &Error, - _ctx: Arc, -) -> Action { - match error { - Error::InvalidKafkaCluster { .. } => Action::await_change(), - _ => Action::requeue(*Duration::from_secs(5)), - } -} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index b5f6b884..7516ad96 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -35,18 +35,13 @@ use stackable_operator::{ }; use crate::{ + controller::KAFKA_FULL_CONTROLLER_NAME, crd::{KafkaCluster, KafkaClusterVersion, OPERATOR_NAME, v1alpha1}, - kafka_controller::KAFKA_FULL_CONTROLLER_NAME, webhooks::conversion::create_webhook_server, }; -mod config; mod controller; mod crd; -mod kafka_controller; -mod kerberos; -mod operations; -mod resource; mod webhooks; mod built_info { @@ -176,9 +171,9 @@ async fn main() -> anyhow::Result<()> { ) .graceful_shutdown_on(sigterm_watcher.handle()) .run( - kafka_controller::reconcile_kafka, - kafka_controller::error_policy, - Arc::new(kafka_controller::Ctx { + controller::reconcile_kafka, + controller::error_policy, + Arc::new(controller::Ctx { client: client.clone(), operator_environment, }), diff --git a/rust/operator-binary/src/operations/mod.rs b/rust/operator-binary/src/operations/mod.rs deleted file mode 100644 index 92ca2ec7..00000000 --- a/rust/operator-binary/src/operations/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod graceful_shutdown; -pub mod pdb; diff --git a/rust/operator-binary/src/resource/mod.rs b/rust/operator-binary/src/resource/mod.rs deleted file mode 100644 index 514d0adb..00000000 --- a/rust/operator-binary/src/resource/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod listener; -pub mod service; -pub mod statefulset; From cedfb41d3a65aca0768f767a668096de74d1f798 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 20:33:15 +0200 Subject: [PATCH 40/47] fix: consolidate merging --- .../controller/build/resource/statefulset.rs | 27 ++------ rust/operator-binary/src/crd/role/mod.rs | 66 ------------------- 2 files changed, 4 insertions(+), 89 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 6bbd000c..6e37f33a 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -124,9 +124,6 @@ pub enum Error { source: stackable_operator::builder::pod::container::Error, }, - #[snafu(display("failed to merge pod overrides"))] - MergePodOverrides { source: crd::role::Error }, - #[snafu(display("missing secret lifetime"))] MissingSecretLifetime, @@ -472,16 +469,8 @@ pub fn build_broker_rolegroup_statefulset( // Don't run kcat pod as PID 1, to ensure that default signal handlers apply pod_template_spec.share_process_namespace = Some(true); - pod_template.merge_from( - kafka_role - .role_pod_overrides(kafka) - .context(MergePodOverridesSnafu)?, - ); - pod_template.merge_from( - kafka_role - .role_group_pod_overrides(kafka, role_group_name.as_ref()) - .context(MergePodOverridesSnafu)?, - ); + // Pod overrides were already merged (role <- role group) during validation. + pod_template.merge_from(validated_rg.pod_overrides.clone()); Ok(StatefulSet { metadata: ObjectMetaBuilder::new() @@ -778,16 +767,8 @@ pub fn build_controller_rolegroup_statefulset( let mut pod_template = pod_builder.build_template(); - pod_template.merge_from( - kafka_role - .role_pod_overrides(kafka) - .context(MergePodOverridesSnafu)?, - ); - pod_template.merge_from( - kafka_role - .role_group_pod_overrides(kafka, role_group_name.as_ref()) - .context(MergePodOverridesSnafu)?, - ); + // Pod overrides were already merged (role <- role group) during validation. + pod_template.merge_from(validated_rg.pod_overrides.clone()); Ok(StatefulSet { metadata: ObjectMetaBuilder::new() diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index b6bed671..8b81f1af 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::resources::{NoRuntimeLimits, Resources}, - k8s_openapi::api::core::v1::PodTemplateSpec, product_logging::spec::ContainerLogConfig, schemars::{self, JsonSchema}, v2::config_overrides::KeyValueConfigOverrides, @@ -118,71 +117,6 @@ impl KafkaRole { "kafka" } - pub fn role_pod_overrides( - &self, - kafka: &v1alpha1::KafkaCluster, - ) -> Result { - let pod_overrides = match self { - Self::Broker => kafka - .broker_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .config - .pod_overrides - .clone(), - Self::Controller => kafka - .controller_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .config - .pod_overrides - .clone(), - }; - - Ok(pod_overrides) - } - - pub fn role_group_pod_overrides( - &self, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result { - let pod_overrides = match self { - Self::Broker => kafka - .broker_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .pod_overrides - .clone(), - Self::Controller => kafka - .controller_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .config - .pod_overrides - .clone(), - }; - - Ok(pod_overrides) - } - pub fn replicas( &self, kafka: &v1alpha1::KafkaCluster, From 9d3411d85beb7d91613a6911b254e64acc574655 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 20:41:31 +0200 Subject: [PATCH 41/47] refactor: share common sts functionality --- .../controller/build/resource/statefulset.rs | 394 +++++++++--------- 1 file changed, 194 insertions(+), 200 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 6e37f33a..8461796a 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -12,6 +12,7 @@ use stackable_operator::{ volume::{ListenerOperatorVolumeSourceBuilder, ListenerReference, VolumeBuilder}, }, }, + commons::product_image_selection::ResolvedProductImage, constants::RESTART_CONTROLLER_ENABLED_LABEL, k8s_openapi::{ DeepMerge, @@ -33,7 +34,10 @@ use stackable_operator::{ CustomContainerLogConfig, }, }, - v2::builder::meta::ownerreference_from_resource, + v2::{ + builder::meta::ownerreference_from_resource, jvm_argument_overrides::JvmArgumentOverrides, + role_group_utils::ResourceNames, + }, }; use crate::{ @@ -57,7 +61,7 @@ use crate::{ STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, role::{ - KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, + AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, controller::ControllerContainer, }, security::KafkaTlsSecurity, @@ -259,36 +263,18 @@ pub fn build_broker_rolegroup_statefulset( .context(BuildPodDescriptorsSnafu)?, kafka_security, &resolved_product_image.product_version, - )]) - .add_env_var( - "EXTRA_ARGS", - crate::controller::build::jvm::construct_non_heap_jvm_args( - merged_config, - &validated_rg.jvm_argument_overrides, - ) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - KAFKA_HEAP_OPTS, - crate::controller::build::jvm::construct_heap_jvm_args( - merged_config, - &validated_rg.jvm_argument_overrides, - ) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - kafka_log_opts_env_var(), - kafka_log_opts(&resolved_product_image.product_version), - ) - // Needed for the `containerdebug` process to log it's tracing information to. - .add_env_var( - "CONTAINERDEBUG_LOG_DIRECTORY", - format!("{STACKABLE_LOG_DIR}/containerdebug"), - ) - .add_env_var( - KAFKA_NODE_ID_OFFSET, - node_id_hash32_offset(kafka_role, role_group_name.as_ref()).to_string(), - ) + )]); + + add_common_kafka_env( + &mut cb_kafka, + merged_config, + &validated_rg.jvm_argument_overrides, + resolved_product_image, + kafka_role, + role_group_name, + )?; + + cb_kafka .add_env_var( "KAFKA_CLIENT_PORT".to_string(), kafka_security.client_port().to_string(), @@ -355,29 +341,7 @@ pub fn build_broker_rolegroup_statefulset( ..Probe::default() }); - if let ContainerLogConfig { - choice: - Some(ContainerLogConfigChoice::Custom(CustomContainerLogConfig { - custom: ConfigMapLogConfig { config_map }, - })), - } = &*merged_config.kafka_logging() - { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(config_map) - .build(), - ) - .context(AddVolumeSnafu)?; - } else { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(resource_names.role_group_config_map().to_string()) - .build(), - ) - .context(AddVolumeSnafu)?; - } + add_log_config_volume(&mut pod_builder, merged_config, &resource_names)?; let metadata = ObjectMetaBuilder::new() .with_labels(recommended_labels.clone()) @@ -413,53 +377,16 @@ pub fn build_broker_rolegroup_statefulset( .image_pull_secrets_from_product_image(resolved_product_image) .add_container(cb_kafka.build()) .add_container(cb_kcat_prober.build()) - .affinity(&merged_config.affinity) - .add_volume(Volume { - name: "config".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: resource_names.role_group_config_map().to_string(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)? - // bootstrap volume is a persistent volume template instead, to keep addresses persistent - .add_empty_dir_volume( - "log", - Some(product_logging::framework::calculate_log_volume_size_limit( - &[MAX_KAFKA_LOG_FILES_SIZE], - )), - ) - .context(AddVolumeSnafu)? - .service_account_name(service_account.name_any()) - .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); + .affinity(&merged_config.affinity); - // Add vector container after kafka container to keep the defaulting into kafka container - if merged_config.vector_logging_enabled() { - match &kafka.spec.cluster_config.vector_aggregator_config_map_name { - Some(vector_aggregator_config_map_name) => { - pod_builder.add_container( - product_logging::framework::vector_container( - resolved_product_image, - "config", - "log", - Some(&*merged_config.vector_logging()), - ResourceRequirementsBuilder::new() - .with_cpu_request("250m") - .with_cpu_limit("500m") - .with_memory_request("128Mi") - .with_memory_limit("128Mi") - .build(), - vector_aggregator_config_map_name, - ) - .context(ConfigureLoggingSnafu)?, - ); - } - None => { - VectorAggregatorConfigMapMissingSnafu.fail()?; - } - } - } + add_common_pod_config(&mut pod_builder, &resource_names, service_account)?; + + add_vector_container( + &mut pod_builder, + kafka, + resolved_product_image, + merged_config, + )?; add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; @@ -605,36 +532,18 @@ pub fn build_controller_rolegroup_statefulset( .context(BuildPodDescriptorsSnafu)?, &resolved_product_image.product_version, )]) - .add_env_var("PRE_STOP_CONTROLLER_SLEEP_SECONDS", "10") - .add_env_var( - "EXTRA_ARGS", - crate::controller::build::jvm::construct_non_heap_jvm_args( - merged_config, - &validated_rg.jvm_argument_overrides, - ) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - KAFKA_HEAP_OPTS, - crate::controller::build::jvm::construct_heap_jvm_args( - merged_config, - &validated_rg.jvm_argument_overrides, - ) - .context(ConstructJvmArgumentsSnafu)?, - ) - .add_env_var( - kafka_log_opts_env_var(), - kafka_log_opts(&resolved_product_image.product_version), - ) - // Needed for the `containerdebug` process to log it's tracing information to. - .add_env_var( - "CONTAINERDEBUG_LOG_DIRECTORY", - format!("{STACKABLE_LOG_DIR}/containerdebug"), - ) - .add_env_var( - KAFKA_NODE_ID_OFFSET, - node_id_hash32_offset(kafka_role, role_group_name.as_ref()).to_string(), - ) + .add_env_var("PRE_STOP_CONTROLLER_SLEEP_SECONDS", "10"); + + add_common_kafka_env( + &mut cb_kafka, + merged_config, + &validated_rg.jvm_argument_overrides, + resolved_product_image, + kafka_role, + role_group_name, + )?; + + cb_kafka .add_env_vars(env) .add_container_ports(container_ports(kafka_security)) .add_volume_mount(LOG_DIRS_VOLUME_NAME, STACKABLE_DATA_DIR) @@ -668,29 +577,7 @@ pub fn build_controller_rolegroup_statefulset( ..Probe::default() }); - if let ContainerLogConfig { - choice: - Some(ContainerLogConfigChoice::Custom(CustomContainerLogConfig { - custom: ConfigMapLogConfig { config_map }, - })), - } = &*merged_config.kafka_logging() - { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(config_map) - .build(), - ) - .context(AddVolumeSnafu)?; - } else { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(resource_names.role_group_config_map().to_string()) - .build(), - ) - .context(AddVolumeSnafu)?; - } + add_log_config_volume(&mut pod_builder, merged_config, &resource_names)?; let metadata = ObjectMetaBuilder::new() .with_labels(recommended_labels.clone()) @@ -715,53 +602,16 @@ pub fn build_controller_rolegroup_statefulset( .metadata(metadata) .image_pull_secrets_from_product_image(resolved_product_image) .add_container(kafka_container) - .affinity(&merged_config.affinity) - .add_volume(Volume { - name: "config".to_string(), - config_map: Some(ConfigMapVolumeSource { - name: resource_names.role_group_config_map().to_string(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }) - .context(AddVolumeSnafu)? - // bootstrap volume is a persistent volume template instead, to keep addresses persistent - .add_empty_dir_volume( - "log", - Some(product_logging::framework::calculate_log_volume_size_limit( - &[MAX_KAFKA_LOG_FILES_SIZE], - )), - ) - .context(AddVolumeSnafu)? - .service_account_name(service_account.name_any()) - .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); + .affinity(&merged_config.affinity); - // Add vector container after kafka container to keep the defaulting into kafka container - if merged_config.vector_logging_enabled() { - match &kafka.spec.cluster_config.vector_aggregator_config_map_name { - Some(vector_aggregator_config_map_name) => { - pod_builder.add_container( - product_logging::framework::vector_container( - resolved_product_image, - "config", - "log", - Some(&*merged_config.vector_logging()), - ResourceRequirementsBuilder::new() - .with_cpu_request("250m") - .with_cpu_limit("500m") - .with_memory_request("128Mi") - .with_memory_limit("128Mi") - .build(), - vector_aggregator_config_map_name, - ) - .context(ConfigureLoggingSnafu)?, - ); - } - None => { - VectorAggregatorConfigMapMissingSnafu.fail()?; - } - } - } + add_common_pod_config(&mut pod_builder, &resource_names, service_account)?; + + add_vector_container( + &mut pod_builder, + kafka, + resolved_product_image, + merged_config, + )?; add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; @@ -835,3 +685,147 @@ fn container_ports(kafka_security: &KafkaTlsSecurity) -> Vec { } ports } + +/// Adds the env vars that the broker and controller Kafka containers share: the JVM +/// arguments, log options, the `containerdebug` log directory and the node-id offset. +fn add_common_kafka_env( + cb_kafka: &mut ContainerBuilder, + merged_config: &AnyConfig, + jvm_argument_overrides: &JvmArgumentOverrides, + resolved_product_image: &ResolvedProductImage, + kafka_role: &KafkaRole, + role_group_name: &RoleGroupName, +) -> Result<(), Error> { + cb_kafka + .add_env_var( + "EXTRA_ARGS", + crate::controller::build::jvm::construct_non_heap_jvm_args( + merged_config, + jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, + ) + .add_env_var( + KAFKA_HEAP_OPTS, + crate::controller::build::jvm::construct_heap_jvm_args( + merged_config, + jvm_argument_overrides, + ) + .context(ConstructJvmArgumentsSnafu)?, + ) + .add_env_var( + kafka_log_opts_env_var(), + kafka_log_opts(&resolved_product_image.product_version), + ) + // Needed for the `containerdebug` process to log it's tracing information to. + .add_env_var( + "CONTAINERDEBUG_LOG_DIRECTORY", + format!("{STACKABLE_LOG_DIR}/containerdebug"), + ) + .add_env_var( + KAFKA_NODE_ID_OFFSET, + node_id_hash32_offset(kafka_role, role_group_name.as_ref()).to_string(), + ); + Ok(()) +} + +/// Adds the `log-config` volume, sourced either from the user-supplied custom log config +/// `ConfigMap` or the rolegroup `ConfigMap`. +fn add_log_config_volume( + pod_builder: &mut PodBuilder, + merged_config: &AnyConfig, + resource_names: &ResourceNames, +) -> Result<(), Error> { + if let ContainerLogConfig { + choice: + Some(ContainerLogConfigChoice::Custom(CustomContainerLogConfig { + custom: ConfigMapLogConfig { config_map }, + })), + } = &*merged_config.kafka_logging() + { + pod_builder + .add_volume( + VolumeBuilder::new("log-config") + .with_config_map(config_map) + .build(), + ) + .context(AddVolumeSnafu)?; + } else { + pod_builder + .add_volume( + VolumeBuilder::new("log-config") + .with_config_map(resource_names.role_group_config_map().to_string()) + .build(), + ) + .context(AddVolumeSnafu)?; + } + Ok(()) +} + +/// Adds the `config` volume, the `log` emptyDir, the service account and the pod security +/// context that the broker and controller pods share. +fn add_common_pod_config( + pod_builder: &mut PodBuilder, + resource_names: &ResourceNames, + service_account: &ServiceAccount, +) -> Result<(), Error> { + pod_builder + .add_volume(Volume { + name: "config".to_string(), + config_map: Some(ConfigMapVolumeSource { + name: resource_names.role_group_config_map().to_string(), + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }) + .context(AddVolumeSnafu)? + .add_empty_dir_volume( + "log", + Some(product_logging::framework::calculate_log_volume_size_limit( + &[MAX_KAFKA_LOG_FILES_SIZE], + )), + ) + .context(AddVolumeSnafu)? + .service_account_name(service_account.name_any()) + .security_context(PodSecurityContextBuilder::new().fs_group(1000).build()); + Ok(()) +} + +/// Adds the Vector log-aggregation sidecar container, when Vector logging is enabled. +/// +/// Errors if Vector logging is enabled but no Vector aggregator discovery `ConfigMap` is +/// configured on the cluster. +fn add_vector_container( + pod_builder: &mut PodBuilder, + kafka: &v1alpha1::KafkaCluster, + resolved_product_image: &ResolvedProductImage, + merged_config: &AnyConfig, +) -> Result<(), Error> { + // Add vector container after kafka container to keep the defaulting into kafka container + if merged_config.vector_logging_enabled() { + match &kafka.spec.cluster_config.vector_aggregator_config_map_name { + Some(vector_aggregator_config_map_name) => { + pod_builder.add_container( + product_logging::framework::vector_container( + resolved_product_image, + "config", + "log", + Some(&*merged_config.vector_logging()), + ResourceRequirementsBuilder::new() + .with_cpu_request("250m") + .with_cpu_limit("500m") + .with_memory_request("128Mi") + .with_memory_limit("128Mi") + .build(), + vector_aggregator_config_map_name, + ) + .context(ConfigureLoggingSnafu)?, + ); + } + None => { + VectorAggregatorConfigMapMissingSnafu.fail()?; + } + } + } + Ok(()) +} From f14e1383c3f42268ae7d4c18e143b72f0f32d150 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 20:47:33 +0200 Subject: [PATCH 42/47] refactor: move ConfigFileName enum --- .../src/controller/build/command.rs | 6 +- .../src/controller/build/jvm.rs | 3 +- .../controller/build/properties/logging.rs | 3 +- .../src/controller/build/properties/mod.rs | 62 ++++++++++++++++++- .../controller/build/resource/config_map.rs | 8 ++- rust/operator-binary/src/crd/config_file.rs | 50 --------------- rust/operator-binary/src/crd/mod.rs | 2 - rust/operator-binary/src/crd/role/mod.rs | 18 ++---- 8 files changed, 77 insertions(+), 75 deletions(-) delete mode 100644 rust/operator-binary/src/crd/config_file.rs diff --git a/rust/operator-binary/src/controller/build/command.rs b/rust/operator-binary/src/controller/build/command.rs index 7cdc5738..ea7d0500 100644 --- a/rust/operator-binary/src/controller/build/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -6,10 +6,10 @@ use stackable_operator::{ utils::COMMON_BASH_TRAP_FUNCTIONS, }; +use super::properties::ConfigFileName; use crate::crd::{ - BROKER_ID_POD_MAP_DIR, ConfigFileName, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, - STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, - security::KafkaTlsSecurity, + BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, + STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, security::KafkaTlsSecurity, }; /// The JVM options selecting the Kafka log4j/log4j2 config file. Kafka 3.x uses log4j, diff --git a/rust/operator-binary/src/controller/build/jvm.rs b/rust/operator-binary/src/controller/build/jvm.rs index 91b97255..adcdefc1 100644 --- a/rust/operator-binary/src/controller/build/jvm.rs +++ b/rust/operator-binary/src/controller/build/jvm.rs @@ -4,7 +4,8 @@ use stackable_operator::{ v2::jvm_argument_overrides::JvmArgumentOverrides, }; -use crate::crd::{ConfigFileName, METRICS_PORT, STACKABLE_CONFIG_DIR, role::AnyConfig}; +use super::properties::ConfigFileName; +use crate::crd::{METRICS_PORT, STACKABLE_CONFIG_DIR, role::AnyConfig}; const JAVA_HEAP_FACTOR: f32 = 0.8; diff --git a/rust/operator-binary/src/controller/build/properties/logging.rs b/rust/operator-binary/src/controller/build/properties/logging.rs index ac83f665..01221355 100644 --- a/rust/operator-binary/src/controller/build/properties/logging.rs +++ b/rust/operator-binary/src/controller/build/properties/logging.rs @@ -12,8 +12,9 @@ use stackable_operator::{ role_utils::RoleGroupRef, }; +use super::ConfigFileName; use crate::crd::{ - ConfigFileName, STACKABLE_LOG_DIR, + STACKABLE_LOG_DIR, role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, v1alpha1, }; diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index 7d2c611b..5c45ef52 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -6,7 +6,45 @@ pub mod listener; pub mod logging; pub mod security_properties; -use crate::crd::{KafkaPodDescriptor, role::KafkaRole}; +use crate::crd::{ + KafkaPodDescriptor, + role::{AnyConfig, KafkaRole}, +}; + +/// The names of the config files assembled into the rolegroup `ConfigMap`. +/// +/// A single source of truth for the on-disk file names, used by the config-map +/// builder, the per-file property builders and the JVM/command builders. +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] +pub enum ConfigFileName { + #[strum(serialize = "broker.properties")] + BrokerProperties, + #[strum(serialize = "controller.properties")] + ControllerProperties, + #[strum(serialize = "security.properties")] + Security, + #[strum(serialize = "client.properties")] + Client, + /// JAAS configuration for Kerberos authentication. It has the `.properties` + /// extension but is not a Java properties file. + #[strum(serialize = "jaas.properties")] + Jaas, + /// Used by Kafka 3.x. + #[strum(serialize = "log4j.properties")] + Log4j, + /// Used by Kafka 4.0 and later. + #[strum(serialize = "log4j2.properties")] + Log4j2, +} + +/// The product config-file name for a role group, derived from its role +/// (`broker.properties` for brokers, `controller.properties` for controllers). +pub fn config_file_name(config: &AnyConfig) -> ConfigFileName { + match config { + AnyConfig::Broker(_) => ConfigFileName::BrokerProperties, + AnyConfig::Controller(_) => ConfigFileName::ControllerProperties, + } +} pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Vec { pod_descriptors @@ -21,3 +59,25 @@ pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Vec>() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_names_match_the_kafka_on_disk_names() { + assert_eq!( + ConfigFileName::BrokerProperties.to_string(), + "broker.properties" + ); + assert_eq!( + ConfigFileName::ControllerProperties.to_string(), + "controller.properties" + ); + assert_eq!(ConfigFileName::Security.to_string(), "security.properties"); + assert_eq!(ConfigFileName::Client.to_string(), "client.properties"); + assert_eq!(ConfigFileName::Jaas.to_string(), "jaas.properties"); + assert_eq!(ConfigFileName::Log4j.to_string(), "log4j.properties"); + assert_eq!(ConfigFileName::Log4j2.to_string(), "log4j2.properties"); + } +} diff --git a/rust/operator-binary/src/controller/build/resource/config_map.rs b/rust/operator-binary/src/controller/build/resource/config_map.rs index 75d4c055..43846eb5 100644 --- a/rust/operator-binary/src/controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -13,10 +13,12 @@ use stackable_operator::{ use crate::{ controller::{ RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, - build::properties::logging::role_group_config_map_data, + build::properties::{ + ConfigFileName, config_file_name, logging::role_group_config_map_data, + }, }, crd::{ - ConfigFileName, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, + STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, listener::{KafkaListenerConfig, node_address_cmd}, role::AnyConfig, }, @@ -70,7 +72,7 @@ pub fn build_rolegroup_config_map( let cluster_config = &validated_cluster.cluster_config; let kafka_security = &cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; - let kafka_config_file_name = validated_rg.config.config_file_name().to_string(); + let kafka_config_file_name = config_file_name(&validated_rg.config).to_string(); let config_overrides = validated_rg .config_overrides .config_file_overrides() diff --git a/rust/operator-binary/src/crd/config_file.rs b/rust/operator-binary/src/crd/config_file.rs deleted file mode 100644 index aec47d39..00000000 --- a/rust/operator-binary/src/crd/config_file.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! The names of the config files assembled into the rolegroup `ConfigMap`. -//! -//! A single source of truth for the on-disk file names, used by the config-map -//! builder, the per-file property builders, the JVM/command builders and -//! [`AnyConfig::config_file_name`](crate::crd::role::AnyConfig::config_file_name). - -/// The names of the Kafka config files assembled into the rolegroup `ConfigMap`. -#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] -pub enum ConfigFileName { - #[strum(serialize = "broker.properties")] - BrokerProperties, - #[strum(serialize = "controller.properties")] - ControllerProperties, - #[strum(serialize = "security.properties")] - Security, - #[strum(serialize = "client.properties")] - Client, - /// JAAS configuration for Kerberos authentication. It has the `.properties` - /// extension but is not a Java properties file. - #[strum(serialize = "jaas.properties")] - Jaas, - /// Used by Kafka 3.x. - #[strum(serialize = "log4j.properties")] - Log4j, - /// Used by Kafka 4.0 and later. - #[strum(serialize = "log4j2.properties")] - Log4j2, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn file_names_match_the_kafka_on_disk_names() { - assert_eq!( - ConfigFileName::BrokerProperties.to_string(), - "broker.properties" - ); - assert_eq!( - ConfigFileName::ControllerProperties.to_string(), - "controller.properties" - ); - assert_eq!(ConfigFileName::Security.to_string(), "security.properties"); - assert_eq!(ConfigFileName::Client.to_string(), "client.properties"); - assert_eq!(ConfigFileName::Jaas.to_string(), "jaas.properties"); - assert_eq!(ConfigFileName::Log4j.to_string(), "log4j.properties"); - assert_eq!(ConfigFileName::Log4j2.to_string(), "log4j2.properties"); - } -} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index a3625db0..4cf07d0f 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,14 +1,12 @@ pub mod affinity; pub mod authentication; pub mod authorization; -pub mod config_file; pub mod listener; pub mod role; pub mod security; pub mod tls; use authentication::KafkaAuthentication; -pub use config_file::ConfigFileName; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, Snafu}; use stackable_operator::{ diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index 8b81f1af..c8c3b392 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -15,13 +15,10 @@ use stackable_operator::{ use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; use crate::{ - crd::{ - ConfigFileName, - role::{ - broker::BrokerConfig, - commons::{CommonConfig, Storage}, - controller::ControllerConfig, - }, + crd::role::{ + broker::BrokerConfig, + commons::{CommonConfig, Storage}, + controller::ControllerConfig, }, v1alpha1, }; @@ -228,13 +225,6 @@ impl AnyConfig { AnyConfig::Controller(_) => None, } } - - pub fn config_file_name(&self) -> ConfigFileName { - match self { - AnyConfig::Broker(_) => ConfigFileName::BrokerProperties, - AnyConfig::Controller(_) => ConfigFileName::ControllerProperties, - } - } } /// Merged role/role-group `configOverrides` for a role group of an unknown type. From bbaee25d583f48a8c0ce3e12d82267cfa20789f7 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 20:55:00 +0200 Subject: [PATCH 43/47] refactor: cleanup security properties --- .../src/controller/build/command.rs | 2 +- .../controller/build/properties/logging.rs | 2 +- .../src/controller/build/properties/mod.rs | 10 + rust/operator-binary/src/crd/security.rs | 316 ++++++------------ 4 files changed, 118 insertions(+), 212 deletions(-) diff --git a/rust/operator-binary/src/controller/build/command.rs b/rust/operator-binary/src/controller/build/command.rs index ea7d0500..49cb2110 100644 --- a/rust/operator-binary/src/controller/build/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -15,7 +15,7 @@ use crate::crd::{ /// The JVM options selecting the Kafka log4j/log4j2 config file. Kafka 3.x uses log4j, /// Kafka 4.0 and higher use log4j2. pub fn kafka_log_opts(product_version: &str) -> String { - if product_version.starts_with("3.") { + if super::properties::uses_legacy_log4j(product_version) { format!( "-Dlog4j.configuration=file:{STACKABLE_LOG_CONFIG_DIR}/{log4j}", log4j = ConfigFileName::Log4j diff --git a/rust/operator-binary/src/controller/build/properties/logging.rs b/rust/operator-binary/src/controller/build/properties/logging.rs index 01221355..648d9018 100644 --- a/rust/operator-binary/src/controller/build/properties/logging.rs +++ b/rust/operator-binary/src/controller/build/properties/logging.rs @@ -47,7 +47,7 @@ pub fn role_group_config_map_data( let mut configs: BTreeMap> = BTreeMap::new(); // Starting with Kafka 4.0, log4j2 is used instead of log4j. - match product_version.starts_with("3.") { + match super::uses_legacy_log4j(product_version) { true => { configs.insert( ConfigFileName::Log4j.to_string(), diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index 5c45ef52..fbc514f4 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -46,6 +46,16 @@ pub fn config_file_name(config: &AnyConfig) -> ConfigFileName { } } +/// Whether the given Kafka version uses the legacy log4j logging framework. +/// +/// Kafka 3.x uses log4j ([`ConfigFileName::Log4j`]); Kafka 4.0 and later use log4j2 +/// ([`ConfigFileName::Log4j2`]). This is the single source of truth for that decision, +/// used both when rendering the log config file and when selecting the JVM option that +/// points at it. +pub fn uses_legacy_log4j(product_version: &str) -> bool { + product_version.starts_with("3.") +} + pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Vec { pod_descriptors .iter() diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/crd/security.rs index 334aeda8..da504c27 100644 --- a/rust/operator-binary/src/crd/security.rs +++ b/rust/operator-binary/src/crd/security.rs @@ -86,7 +86,10 @@ impl KafkaTlsSecurity { pub const SECURE_CLIENT_PORT_NAME: &'static str = "kafka-tls"; pub const SECURE_INTERNAL_PORT: u16 = 19093; // - TLS global + const KEYSTORE_P12_FILE_NAME: &'static str = "keystore.p12"; const SSL_STORE_PASSWORD: &'static str = ""; + const SSL_STORE_TYPE_PKCS12: &'static str = "PKCS12"; + const TRUSTSTORE_P12_FILE_NAME: &'static str = "truststore.p12"; const STACKABLE_TLS_KAFKA_INTERNAL_DIR: &'static str = "/stackable/tls-kafka-internal"; const STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME: &'static str = "tls-kafka-internal"; const STACKABLE_TLS_KAFKA_SERVER_DIR: &'static str = "/stackable/tls-kafka-server"; @@ -180,9 +183,10 @@ impl KafkaTlsSecurity { pub fn copy_opa_tls_cert_command(&self) -> String { match self.has_opa_tls_enabled() { true => format!( - "keytool -importcert -file {opa_mount_path}/ca.crt -keystore {tls_dir}/truststore.p12 -storepass '{tls_password}' -alias opa-ca -noprompt", + "keytool -importcert -file {opa_mount_path}/ca.crt -keystore {tls_dir}/{truststore} -storepass '{tls_password}' -alias opa-ca -noprompt", opa_mount_path = Self::OPA_TLS_MOUNT_PATH, tls_dir = Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, + truststore = Self::TRUSTSTORE_P12_FILE_NAME, tls_password = Self::SSL_STORE_PASSWORD, ), false => "".to_string(), @@ -339,33 +343,7 @@ impl KafkaTlsSecurity { Some(KafkaListenerProtocol::Ssl.to_string()), )); props.push(("ssl.client.auth".to_string(), Some("required".to_string()))); - props.push(("ssl.keystore.type".to_string(), Some("PKCS12".to_string()))); - props.push(( - "ssl.keystore.location".to_string(), - Some(format!( - "{}/keystore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.keystore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); - props.push(( - "ssl.truststore.type".to_string(), - Some("PKCS12".to_string()), - )); - props.push(( - "ssl.truststore.location".to_string(), - Some(format!( - "{}/truststore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.truststore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); + Self::push_client_ssl_stores(&mut props, Self::STACKABLE_TLS_KAFKA_SERVER_DIR); } else if self.has_kerberos_enabled() { // TODO: to make this configuration file usable out of the box the operator needs to be // refactored to write out Java jaas files instead of passing command line parameters @@ -376,33 +354,7 @@ impl KafkaTlsSecurity { "security.protocol".to_string(), Some(KafkaListenerProtocol::SaslSsl.to_string()), )); - props.push(("ssl.keystore.type".to_string(), Some("PKCS12".to_string()))); - props.push(( - "ssl.keystore.location".to_string(), - Some(format!( - "{}/keystore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.keystore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); - props.push(( - "ssl.truststore.type".to_string(), - Some("PKCS12".to_string()), - )); - props.push(( - "ssl.truststore.location".to_string(), - Some(format!( - "{}/truststore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.truststore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); + Self::push_client_ssl_stores(&mut props, Self::STACKABLE_TLS_KAFKA_SERVER_DIR); props.push(( "sasl.enabled.mechanisms".to_string(), Some("GSSAPI".to_string()), @@ -427,21 +379,7 @@ impl KafkaTlsSecurity { "security.protocol".to_string(), Some(KafkaListenerProtocol::Ssl.to_string()), )); - props.push(( - "ssl.truststore.type".to_string(), - Some("PKCS12".to_string()), - )); - props.push(( - "ssl.truststore.location".to_string(), - Some(format!( - "{}/truststore.p12", - Self::STACKABLE_TLS_KAFKA_SERVER_DIR - )), - )); - props.push(( - "ssl.truststore.password".to_string(), - Some(Self::SSL_STORE_PASSWORD.to_string()), - )); + Self::push_client_ssl_truststore(&mut props, Self::STACKABLE_TLS_KAFKA_SERVER_DIR); } else { props.push(( "security.protocol".to_string(), @@ -573,6 +511,74 @@ impl KafkaTlsSecurity { Ok(()) } + /// Inserts the `listener..ssl.{keystore,truststore}.{location,password,type}` + /// settings for `listener`, pointing at the PKCS12 stores under `dir`. + fn insert_listener_ssl_stores( + config: &mut BTreeMap, + listener: &KafkaListenerName, + dir: &str, + ) { + config.insert( + listener.listener_ssl_keystore_location(), + format!("{dir}/{}", Self::KEYSTORE_P12_FILE_NAME), + ); + config.insert( + listener.listener_ssl_keystore_password(), + Self::SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + listener.listener_ssl_keystore_type(), + Self::SSL_STORE_TYPE_PKCS12.to_string(), + ); + config.insert( + listener.listener_ssl_truststore_location(), + format!("{dir}/{}", Self::TRUSTSTORE_P12_FILE_NAME), + ); + config.insert( + listener.listener_ssl_truststore_password(), + Self::SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + listener.listener_ssl_truststore_type(), + Self::SSL_STORE_TYPE_PKCS12.to_string(), + ); + } + + /// Pushes the client-side `ssl.keystore.*` and `ssl.truststore.*` properties, pointing + /// at the PKCS12 stores under `dir`. + fn push_client_ssl_stores(props: &mut Vec<(String, Option)>, dir: &str) { + props.push(( + "ssl.keystore.type".to_string(), + Some(Self::SSL_STORE_TYPE_PKCS12.to_string()), + )); + props.push(( + "ssl.keystore.location".to_string(), + Some(format!("{dir}/{}", Self::KEYSTORE_P12_FILE_NAME)), + )); + props.push(( + "ssl.keystore.password".to_string(), + Some(Self::SSL_STORE_PASSWORD.to_string()), + )); + Self::push_client_ssl_truststore(props, dir); + } + + /// Pushes the client-side `ssl.truststore.*` properties, pointing at the PKCS12 + /// truststore under `dir`. + fn push_client_ssl_truststore(props: &mut Vec<(String, Option)>, dir: &str) { + props.push(( + "ssl.truststore.type".to_string(), + Some(Self::SSL_STORE_TYPE_PKCS12.to_string()), + )); + props.push(( + "ssl.truststore.location".to_string(), + Some(format!("{dir}/{}", Self::TRUSTSTORE_P12_FILE_NAME)), + )); + props.push(( + "ssl.truststore.password".to_string(), + Some(Self::SSL_STORE_PASSWORD.to_string()), + )); + } + /// Returns required Kafka configuration settings for the `broker.properties` file /// depending on the tls and authentication settings. pub fn broker_config_settings(&self) -> BTreeMap { @@ -584,29 +590,10 @@ impl KafkaTlsSecurity { if self.tls_client_authentication_class().is_some() || self.tls_server_secret_class().is_some() { - config.insert( - KafkaListenerName::Client.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Client.listener_ssl_truststore_type(), - "PKCS12".to_string(), + Self::insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Client, + Self::STACKABLE_TLS_KAFKA_SERVER_DIR, ); if self.tls_client_authentication_class().is_some() { // client auth required @@ -619,29 +606,10 @@ impl KafkaTlsSecurity { if self.has_kerberos_enabled() { // Bootstrap - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_SERVER_DIR), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Bootstrap.listener_ssl_truststore_type(), - "PKCS12".to_string(), + Self::insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Bootstrap, + Self::STACKABLE_TLS_KAFKA_SERVER_DIR, ); config.insert("sasl.enabled.mechanisms".to_string(), "GSSAPI".to_string()); config.insert( @@ -658,54 +626,16 @@ impl KafkaTlsSecurity { // Internal TLS if self.tls_internal_secret_class().is_some() { // BROKERS - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_type(), - "PKCS12".to_string(), + Self::insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Internal, + Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, ); // CONTROLLERS - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_type(), - "PKCS12".to_string(), + Self::insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Controller, + Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, ); // client auth required config.insert( @@ -718,7 +648,11 @@ impl KafkaTlsSecurity { if self.opa_secret_class.is_some() { config.insert( "opa.authorizer.truststore.path".to_string(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), + format!( + "{}/{}", + Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, + Self::TRUSTSTORE_P12_FILE_NAME + ), ); config.insert( "opa.authorizer.truststore.password".to_string(), @@ -726,7 +660,7 @@ impl KafkaTlsSecurity { ); config.insert( "opa.authorizer.truststore.type".to_string(), - "PKCS12".to_string(), + Self::SSL_STORE_TYPE_PKCS12.to_string(), ); } @@ -747,56 +681,18 @@ impl KafkaTlsSecurity { if self.tls_client_authentication_class().is_some() || self.tls_internal_secret_class().is_some() { - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Controller.listener_ssl_truststore_type(), - "PKCS12".to_string(), + Self::insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Controller, + Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, ); // The TLS properties for the internal broker listener are needed by the Kraft controllers // too during metadata migration from ZooKeeper to Kraft mode. - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_location(), - format!("{}/keystore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_keystore_type(), - "PKCS12".to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_location(), - format!("{}/truststore.p12", Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_password(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - KafkaListenerName::Internal.listener_ssl_truststore_type(), - "PKCS12".to_string(), + Self::insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Internal, + Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, ); // We set either client tls with authentication or client tls without authentication // If authentication is explicitly required we do not want to have any other CAs to From dc1ea69861be4eec244de8e190c094cd601516ca Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 21:07:59 +0200 Subject: [PATCH 44/47] refactor: adapt pdb to build instead of mutate --- rust/operator-binary/src/controller.rs | 46 ++++++++------- .../src/controller/build/resource/pdb.rs | 56 ++++++------------- .../controller/build/resource/statefulset.rs | 42 +++++++------- 3 files changed, 58 insertions(+), 86 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 242f1128..b8bb0185 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -16,7 +16,7 @@ use const_format::concatcp; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, - cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, + cluster_resources::ClusterResourceApplyStrategy, commons::{ networking::DomainName, product_image_selection::ResolvedProductImage, rbac::build_rbac_resources, @@ -38,6 +38,7 @@ use stackable_operator::{ }, v2::{ HasName, HasUid, NameIsValidLabelValue, + cluster_resources::cluster_resources_new, kvp::label::{recommended_labels, role_group_selector}, role_group_utils::ResourceNames, types::{ @@ -63,7 +64,7 @@ use crate::{ properties::listener::get_kafka_listener_config, resource::{ listener::build_broker_rolegroup_bootstrap_listener, - pdb::add_pdbs, + pdb::build_pdb, service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, statefulset::{ build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset, @@ -314,17 +315,17 @@ impl NameIsValidLabelValue for ValidatedCluster { } /// The product name (`kafka`) as a type-safe label value. -fn product_name() -> ProductName { +pub(crate) fn product_name() -> ProductName { ProductName::from_str(APP_NAME).expect("'kafka' is a valid product name") } /// The operator name as a type-safe label value. -fn operator_name() -> OperatorName { +pub(crate) fn operator_name() -> OperatorName { OperatorName::from_str(OPERATOR_NAME).expect("the operator name is a valid label value") } /// The controller name as a type-safe label value. -fn controller_name() -> ControllerName { +pub(crate) fn controller_name() -> ControllerName { ControllerName::from_str(KAFKA_CONTROLLER_NAME) .expect("the controller name is a valid label value") } @@ -461,11 +462,6 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to create cluster resources"))] - CreateClusterResources { - source: stackable_operator::cluster_resources::Error, - }, - #[snafu(display("failed to patch service account"))] ApplyServiceAccount { source: stackable_operator::cluster_resources::Error, @@ -486,9 +482,9 @@ pub enum Error { source: stackable_operator::commons::rbac::Error, }, - #[snafu(display("failed to create PodDisruptionBudget"))] - FailedToCreatePdb { - source: crate::controller::build::resource::pdb::Error, + #[snafu(display("failed to apply PodDisruptionBudget"))] + ApplyPdb { + source: stackable_operator::cluster_resources::Error, }, #[snafu(display("failed to get required Labels"))] @@ -530,12 +526,11 @@ impl ReconcilerError for Error { Error::BuildDiscoveryConfig { .. } => None, Error::ApplyDiscoveryConfig { .. } => None, Error::DeleteOrphans { .. } => None, - Error::CreateClusterResources { .. } => None, Error::ApplyServiceAccount { .. } => None, Error::ApplyRoleBinding { .. } => None, Error::ApplyStatus { .. } => None, Error::BuildRbacResources { .. } => None, - Error::FailedToCreatePdb { .. } => None, + Error::ApplyPdb { .. } => None, Error::GetRequiredLabels { .. } => None, Error::InvalidKafkaCluster { .. } => None, Error::BuildStatefulset { .. } => None, @@ -568,15 +563,16 @@ pub async fn reconcile_kafka( validate::validate(kafka, dereferenced_objects, &ctx.operator_environment) .context(ValidateClusterSnafu)?; - let mut cluster_resources = ClusterResources::new( - APP_NAME, - OPERATOR_NAME, - KAFKA_CONTROLLER_NAME, - &validated_cluster.object_ref(&()), + let mut cluster_resources = cluster_resources_new( + &product_name(), + &operator_name(), + &controller_name(), + &validated_cluster.name, + &validated_cluster.namespace, + &validated_cluster.uid, ClusterResourceApplyStrategy::from(&kafka.spec.cluster_operation), &kafka.spec.object_overrides, - ) - .context(CreateClusterResourcesSnafu)?; + ); tracing::debug!( kerberos_enabled = validated_cluster.cluster_config.kafka_security.has_kerberos_enabled(), @@ -718,10 +714,12 @@ pub async fn reconcile_kafka( if let Some(GenericRoleConfig { pod_disruption_budget: pdb, }) = role_cfg + && let Some(pdb) = build_pdb(pdb, &validated_cluster, kafka_role) { - add_pdbs(pdb, kafka, kafka_role, client, &mut cluster_resources) + cluster_resources + .add(client, pdb) .await - .context(FailedToCreatePdbSnafu)?; + .context(ApplyPdbSnafu)?; } } diff --git a/rust/operator-binary/src/controller/build/resource/pdb.rs b/rust/operator-binary/src/controller/build/resource/pdb.rs index e42888ca..29bd473e 100644 --- a/rust/operator-binary/src/controller/build/resource/pdb.rs +++ b/rust/operator-binary/src/controller/build/resource/pdb.rs @@ -1,61 +1,37 @@ -use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::pdb::PodDisruptionBudgetBuilder, client::Client, cluster_resources::ClusterResources, - commons::pdb::PdbConfig, kube::ResourceExt, + commons::pdb::PdbConfig, k8s_openapi::api::policy::v1::PodDisruptionBudget, + v2::builder::pdb::pod_disruption_budget_builder_with_role, }; use crate::{ - controller::KAFKA_CONTROLLER_NAME, - crd::{APP_NAME, OPERATOR_NAME, role::KafkaRole, v1alpha1}, + controller::{ValidatedCluster, controller_name, operator_name, product_name}, + crd::role::KafkaRole, }; -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("Cannot create PodDisruptionBudget for role [{role}]"))] - CreatePdb { - source: stackable_operator::builder::pdb::Error, - role: String, - }, - #[snafu(display("Cannot apply PodDisruptionBudget [{name}]"))] - ApplyPdb { - source: stackable_operator::cluster_resources::Error, - name: String, - }, -} - -pub async fn add_pdbs( +/// Builds the [`PodDisruptionBudget`] for the given `role`, or `None` if PDBs are disabled. +pub fn build_pdb( pdb: &PdbConfig, - kafka: &v1alpha1::KafkaCluster, + validated_cluster: &ValidatedCluster, role: &KafkaRole, - client: &Client, - cluster_resources: &mut ClusterResources<'_>, -) -> Result<(), Error> { +) -> Option { if !pdb.enabled { - return Ok(()); + return None; } let max_unavailable = pdb.max_unavailable.unwrap_or(match role { KafkaRole::Broker => max_unavailable_brokers(), KafkaRole::Controller => max_unavailable_controllers(), }); - let pdb = PodDisruptionBudgetBuilder::new_with_role( - kafka, - APP_NAME, - &role.to_string(), - OPERATOR_NAME, - KAFKA_CONTROLLER_NAME, + let pdb = pod_disruption_budget_builder_with_role( + validated_cluster, + &product_name(), + &ValidatedCluster::role_name(role), + &operator_name(), + &controller_name(), ) - .with_context(|_| CreatePdbSnafu { - role: role.to_string(), - })? .with_max_unavailable(max_unavailable) .build(); - let pdb_name = pdb.name_any(); - cluster_resources - .add(client, pdb) - .await - .with_context(|_| ApplyPdbSnafu { name: pdb_name })?; - Ok(()) + Some(pdb) } fn max_unavailable_brokers() -> u16 { diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 8461796a..f241891f 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -1,15 +1,12 @@ -use std::ops::Deref; +use std::{ops::Deref, str::FromStr}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ builder::{ meta::ObjectMetaBuilder, pod::{ - PodBuilder, - container::ContainerBuilder, - resources::ResourceRequirementsBuilder, - security::PodSecurityContextBuilder, - volume::{ListenerOperatorVolumeSourceBuilder, ListenerReference, VolumeBuilder}, + PodBuilder, container::ContainerBuilder, resources::ResourceRequirementsBuilder, + security::PodSecurityContextBuilder, volume::VolumeBuilder, }, }, commons::product_image_selection::ResolvedProductImage, @@ -35,8 +32,13 @@ use stackable_operator::{ }, }, v2::{ - builder::meta::ownerreference_from_resource, jvm_argument_overrides::JvmArgumentOverrides, + builder::{ + meta::ownerreference_from_resource, + pod::volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, + }, + jvm_argument_overrides::JvmArgumentOverrides, role_group_utils::ResourceNames, + types::kubernetes::{ListenerName, PersistentVolumeClaimName}, }, }; @@ -97,11 +99,6 @@ pub enum Error { source: stackable_operator::builder::pod::Error, }, - #[snafu(display("failed to build bootstrap listener pvc"))] - BuildBootstrapListenerPvc { - source: stackable_operator::builder::pod::volume::ListenerOperatorVolumeSourceBuilderError, - }, - #[snafu(display("failed to build pod descriptors"))] BuildPodDescriptors { source: crate::controller::PodDescriptorsError, @@ -191,16 +188,17 @@ pub fn build_broker_rolegroup_statefulset( // bootstrap listener should be persistent, // main broker listener is an ephemeral PVC instead - pvcs.push( - ListenerOperatorVolumeSourceBuilder::new( - &ListenerReference::ListenerName( - validated_cluster.bootstrap_listener_name(kafka_role, role_group_name), - ), - &unversioned_recommended_labels, - ) - .build_pvc(LISTENER_BOOTSTRAP_VOLUME_NAME) - .context(BuildBootstrapListenerPvcSnafu)?, - ); + let bootstrap_listener_name = ListenerName::from_str( + &validated_cluster.bootstrap_listener_name(kafka_role, role_group_name), + ) + .expect("the bootstrap listener name is a valid Listener name"); + let bootstrap_pvc_name = PersistentVolumeClaimName::from_str(LISTENER_BOOTSTRAP_VOLUME_NAME) + .expect("the bootstrap listener volume name is a valid PVC name"); + pvcs.push(listener_operator_volume_source_builder_build_pvc( + &ListenerReference::Listener(bootstrap_listener_name), + &unversioned_recommended_labels, + &bootstrap_pvc_name, + )); if kafka_security.has_kerberos_enabled() { add_kerberos_pod_config( From a7a76863071f9ab0bc9b7b21c353dee1c77b7501 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 21:18:13 +0200 Subject: [PATCH 45/47] refactor: use ValidatedCluster in statefulset --- rust/operator-binary/src/controller.rs | 25 +++++++-- .../build/properties/broker_properties.rs | 2 +- .../controller/build/resource/statefulset.rs | 56 ++++++++----------- .../src/controller/validate.rs | 10 +++- rust/operator-binary/src/crd/role/mod.rs | 52 ----------------- 5 files changed, 50 insertions(+), 95 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index b8bb0185..1e50f31a 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -339,10 +339,19 @@ pub struct ValidatedClusterConfig { pub authorization_config: Option, pub metadata_manager: MetadataManager, - /// Whether the operator must not generate broker ids itself, because the user - /// supplied a `broker_id_pod_config_map_name`. Resolved from the raw spec during - /// validation so the config-map builder never has to read it. - pub disable_broker_id_generation: bool, + /// The discovery `ConfigMap` providing the ZooKeeper connection string, if the cluster + /// is connected to a ZooKeeper ensemble. Resolved from the raw spec during validation so + /// the build steps never have to read it. + pub zookeeper_config_map_name: Option, + + /// The `ConfigMap` mapping pods to broker ids, if the user supplied one. Resolved from the + /// raw spec during validation so the build steps never have to read it. + pub broker_id_pod_config_map_name: Option, + + /// The discovery `ConfigMap` providing the Vector aggregator address, if Vector log + /// aggregation is configured. Resolved from the raw spec during validation so the build + /// steps never have to read it. + pub vector_aggregator_config_map_name: Option, } impl ValidatedClusterConfig { @@ -351,6 +360,12 @@ impl ValidatedClusterConfig { self.metadata_manager == MetadataManager::KRaft } + /// Whether the operator must not generate broker ids itself, because the user supplied a + /// `broker_id_pod_config_map_name`. + pub fn disable_broker_id_generation(&self) -> bool { + self.broker_id_pod_config_map_name.is_some() + } + /// The OPA connect string, if OPA authorization is configured. pub fn opa_connect(&self) -> Option<&str> { self.authorization_config @@ -644,7 +659,6 @@ pub async fn reconcile_kafka( let rg_statefulset = match kafka_role { KafkaRole::Broker => build_broker_rolegroup_statefulset( - kafka, kafka_role, rolegroup_name, &validated_cluster, @@ -653,7 +667,6 @@ pub async fn reconcile_kafka( ) .context(BuildStatefulsetSnafu)?, KafkaRole::Controller => build_controller_rolegroup_statefulset( - kafka, kafka_role, rolegroup_name, &validated_cluster, diff --git a/rust/operator-binary/src/controller/build/properties/broker_properties.rs b/rust/operator-binary/src/controller/build/properties/broker_properties.rs index b09e071b..d8abf628 100644 --- a/rust/operator-binary/src/controller/build/properties/broker_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -77,7 +77,7 @@ pub fn build( // so we disable automatic id generation. // This check ensures that existing clusters running in ZooKeeper mode do not // suddenly break after the introduction of this change. - if cluster_config.disable_broker_id_generation { + if cluster_config.disable_broker_id_generation() { result.extend([ ( "broker.id.generation.enable".to_string(), diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index f241891f..136421e4 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -57,25 +57,20 @@ use crate::{ node_id_hasher::node_id_hash32_offset, }, crd::{ - self, BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, + BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, METRICS_PORT, METRICS_PORT_NAME, - MetadataManager, STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, - STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, - STACKABLE_LOG_DIR, + STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, STACKABLE_LISTENER_BOOTSTRAP_DIR, + STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, role::{ AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, controller::ControllerContainer, }, security::KafkaTlsSecurity, - v1alpha1, }, }; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("invalid metadata manager"))] - InvalidMetadataManager { source: crate::crd::Error }, - #[snafu(display("failed to add kerberos config"))] AddKerberosConfig { source: crate::controller::build::kerberos::Error, @@ -128,9 +123,6 @@ pub enum Error { #[snafu(display("missing secret lifetime"))] MissingSecretLifetime, - #[snafu(display("failed to retrieve rolegroup replicas"))] - RoleGroupReplicas { source: crd::role::Error }, - #[snafu(display("vector agent is enabled but vector aggregator ConfigMap is missing"))] VectorAggregatorConfigMapMissing, } @@ -140,7 +132,6 @@ pub enum Error { /// The [`Pod`](`stackable_operator::k8s_openapi::api::core::v1::Pod`)s are accessible through the corresponding /// [`Service`](`stackable_operator::k8s_openapi::api::core::v1::Service`) from [`build_rolegroup_headless_service`](`crate::controller::build::resource::service::build_rolegroup_headless_service`). pub fn build_broker_rolegroup_statefulset( - kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, role_group_name: &RoleGroupName, validated_cluster: &ValidatedCluster, @@ -213,7 +204,9 @@ pub fn build_broker_rolegroup_statefulset( let mut env = Vec::::from(validated_rg.env_overrides.clone()); - if let Some(zookeeper_config_map_name) = &kafka.spec.cluster_config.zookeeper_config_map_name { + if let Some(zookeeper_config_map_name) = + &validated_cluster.cluster_config.zookeeper_config_map_name + { env.push(EnvVar { name: "ZOOKEEPER".to_string(), value_from: Some(EnvVarSource { @@ -240,10 +233,6 @@ pub fn build_broker_rolegroup_statefulset( ..EnvVar::default() }); - let metadata_manager = kafka - .effective_metadata_manager() - .context(InvalidMetadataManagerSnafu)?; - cb_kafka .image_from_product_image(resolved_product_image) .command(vec![ @@ -254,7 +243,7 @@ pub fn build_broker_rolegroup_statefulset( "-c".to_string(), ]) .args(vec![broker_kafka_container_commands( - metadata_manager == MetadataManager::KRaft, + validated_cluster.cluster_config.is_kraft_mode(), // we need controller pods validated_cluster .pod_descriptors(Some(&KafkaRole::Controller)) @@ -355,8 +344,9 @@ pub fn build_broker_rolegroup_statefulset( .context(AddListenerVolumeSnafu)?; } - if let Some(broker_id_config_map_name) = - &kafka.spec.cluster_config.broker_id_pod_config_map_name + if let Some(broker_id_config_map_name) = &validated_cluster + .cluster_config + .broker_id_pod_config_map_name { pod_builder .add_volume( @@ -381,7 +371,7 @@ pub fn build_broker_rolegroup_statefulset( add_vector_container( &mut pod_builder, - kafka, + validated_cluster, resolved_product_image, merged_config, )?; @@ -411,10 +401,7 @@ pub fn build_broker_rolegroup_statefulset( .build(), spec: Some(StatefulSetSpec { pod_management_policy: Some("Parallel".to_string()), - replicas: kafka_role - .replicas(kafka, role_group_name.as_ref()) - .context(RoleGroupReplicasSnafu)? - .map(i32::from), + replicas: Some(i32::from(validated_rg.replicas)), selector: LabelSelector { match_labels: Some( validated_cluster @@ -434,7 +421,6 @@ pub fn build_broker_rolegroup_statefulset( /// The controller rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. pub fn build_controller_rolegroup_statefulset( - kafka: &v1alpha1::KafkaCluster, kafka_role: &KafkaRole, role_group_name: &RoleGroupName, validated_cluster: &ValidatedCluster, @@ -500,7 +486,9 @@ pub fn build_controller_rolegroup_statefulset( }); // Controllers need the ZooKeeper connection string for migration - if let Some(zookeeper_config_map_name) = &kafka.spec.cluster_config.zookeeper_config_map_name { + if let Some(zookeeper_config_map_name) = + &validated_cluster.cluster_config.zookeeper_config_map_name + { env.push(EnvVar { name: "ZOOKEEPER".to_string(), value_from: Some(EnvVarSource { @@ -606,7 +594,7 @@ pub fn build_controller_rolegroup_statefulset( add_vector_container( &mut pod_builder, - kafka, + validated_cluster, resolved_product_image, merged_config, )?; @@ -636,10 +624,7 @@ pub fn build_controller_rolegroup_statefulset( type_: Some("RollingUpdate".to_string()), ..StatefulSetUpdateStrategy::default() }), - replicas: kafka_role - .replicas(kafka, role_group_name.as_ref()) - .context(RoleGroupReplicasSnafu)? - .map(i32::from), + replicas: Some(i32::from(validated_rg.replicas)), selector: LabelSelector { match_labels: Some( validated_cluster @@ -795,13 +780,16 @@ fn add_common_pod_config( /// configured on the cluster. fn add_vector_container( pod_builder: &mut PodBuilder, - kafka: &v1alpha1::KafkaCluster, + validated_cluster: &ValidatedCluster, resolved_product_image: &ResolvedProductImage, merged_config: &AnyConfig, ) -> Result<(), Error> { // Add vector container after kafka container to keep the defaulting into kafka container if merged_config.vector_logging_enabled() { - match &kafka.spec.cluster_config.vector_aggregator_config_map_name { + match &validated_cluster + .cluster_config + .vector_aggregator_config_map_name + { Some(vector_aggregator_config_map_name) => { pod_builder.add_container( product_logging::framework::vector_container( diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index aab95bd0..a7fa2a7c 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -180,11 +180,17 @@ pub fn validate( kafka_security, authorization_config: dereferenced_objects.authorization_config, metadata_manager, - disable_broker_id_generation: kafka + zookeeper_config_map_name: kafka.spec.cluster_config.zookeeper_config_map_name.clone(), + broker_id_pod_config_map_name: kafka .spec .cluster_config .broker_id_pod_config_map_name - .is_some(), + .clone(), + vector_aggregator_config_map_name: kafka + .spec + .cluster_config + .vector_aggregator_config_map_name + .clone(), }, role_group_configs, )) diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index c8c3b392..1cac599c 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -5,7 +5,6 @@ pub mod controller; use std::{borrow::Cow, ops::Deref}; use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::resources::{NoRuntimeLimits, Resources}, product_logging::spec::ContainerLogConfig, @@ -64,18 +63,6 @@ pub const KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: &str = "listener.security.protoc /// For example: localhost:9092,localhost:9093,localhost:9094. pub const KAFKA_CONTROLLER_QUORUM_BOOTSTRAP_SERVERS: &str = "controller.quorum.bootstrap.servers"; -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("the Kafka role [{role}] is missing from spec"))] - MissingRole { - source: crate::crd::Error, - role: String, - }, - - #[snafu(display("missing role group {rolegroup:?} for role {role:?}"))] - MissingRoleGroup { role: String, rolegroup: String }, -} - #[derive( Clone, Debug, @@ -113,45 +100,6 @@ impl KafkaRole { pub fn kerberos_service_name(&self) -> &'static str { "kafka" } - - pub fn replicas( - &self, - kafka: &v1alpha1::KafkaCluster, - rolegroup: &str, - ) -> Result, Error> { - let replicas = match self { - Self::Broker => { - kafka - .broker_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .replicas - } - Self::Controller => { - kafka - .controller_role() - .with_context(|_| MissingRoleSnafu { - role: self.to_string(), - })? - .role_groups - .get(rolegroup) - .with_context(|| MissingRoleGroupSnafu { - role: self.to_string(), - rolegroup: rolegroup.to_string(), - })? - .replicas - } - }; - - Ok(replicas) - } } /// Configuration for a role and rolegroup of an unknown type. From 52915544a6342ab18bbcb3a1d83311197d387b7f Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sat, 13 Jun 2026 22:17:54 +0200 Subject: [PATCH 46/47] refactor: adapt logging to v2, add vector test --- rust/operator-binary/src/controller.rs | 24 +- .../src/controller/build/properties/mod.rs | 2 +- .../{logging.rs => product_logging/mod.rs} | 55 +-- .../properties/product_logging/test-vector.sh | 11 + .../product_logging/vector-test.yaml | 136 +++++++ .../properties/product_logging/vector.yaml | 274 ++++++++++++++ .../controller/build/resource/config_map.rs | 2 +- .../controller/build/resource/statefulset.rs | 143 +++----- .../src/controller/validate.rs | 130 ++++++- rust/operator-binary/src/crd/role/mod.rs | 20 - tests/templates/kuttl/smoke/33-assert.yaml.j2 | 26 ++ tests/templates/kuttl/smoke/34-assert.yaml.j2 | 343 +----------------- 12 files changed, 681 insertions(+), 485 deletions(-) rename rust/operator-binary/src/controller/build/properties/{logging.rs => product_logging/mod.rs} (77%) create mode 100755 rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh create mode 100644 rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml create mode 100644 rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 1e50f31a..41282993 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -347,11 +347,6 @@ pub struct ValidatedClusterConfig { /// The `ConfigMap` mapping pods to broker ids, if the user supplied one. Resolved from the /// raw spec during validation so the build steps never have to read it. pub broker_id_pod_config_map_name: Option, - - /// The discovery `ConfigMap` providing the Vector aggregator address, if Vector log - /// aggregation is configured. Resolved from the raw spec during validation so the build - /// steps never have to read it. - pub vector_aggregator_config_map_name: Option, } impl ValidatedClusterConfig { @@ -422,6 +417,8 @@ pub struct ValidatedRoleGroupConfig { pub pod_overrides: stackable_operator::k8s_openapi::api::core::v1::PodTemplateSpec, pub jvm_argument_overrides: stackable_operator::v2::jvm_argument_overrides::JvmArgumentOverrides, + /// Validated logging configuration (derived from `config.logging` during validation). + pub logging: validate::ValidatedLogging, } pub struct Ctx { @@ -621,14 +618,17 @@ pub async fn reconcile_kafka( for (kafka_role, rg_map) in &validated_cluster.role_group_configs { for (rolegroup_name, validated_rg) in rg_map { - // The Vector log-aggregation config still consumes a v1 `RoleGroupRef`; it is built - // here and used only for that. All other identification uses the typed `kafka_role` / - // `rolegroup_name` (and `ValidatedCluster::resource_names`). + // `rolegroup_ref` is a v1 `RoleGroupRef` retained only for the error context of the + // per-rolegroup apply calls below. All other identification uses the typed + // `kafka_role` / `rolegroup_name` (and `ValidatedCluster::resource_names`). let rolegroup_ref = kafka.rolegroup_ref(kafka_role, rolegroup_name.to_string()); - let vector_config = build::properties::logging::build_vector_config( - &rolegroup_ref, - &validated_rg.config, - ); + // The Vector agent config is the static `vector.yaml`, added to the rolegroup + // ConfigMap only when the Vector agent is enabled (resolved during validation). + let vector_config = validated_rg + .logging + .vector_container + .is_some() + .then(build::properties::product_logging::vector_config_file_content); let rg_headless_service = build_rolegroup_headless_service( &validated_cluster, diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index fbc514f4..c0b530fe 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -3,7 +3,7 @@ pub mod broker_properties; pub mod controller_properties; pub mod listener; -pub mod logging; +pub mod product_logging; pub mod security_properties; use crate::crd::{ diff --git a/rust/operator-binary/src/controller/build/properties/logging.rs b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs similarity index 77% rename from rust/operator-binary/src/controller/build/properties/logging.rs rename to rust/operator-binary/src/controller/build/properties/product_logging/mod.rs index 648d9018..e50f1e2c 100644 --- a/rust/operator-binary/src/controller/build/properties/logging.rs +++ b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs @@ -9,14 +9,12 @@ use stackable_operator::{ self, spec::{ContainerLogConfig, ContainerLogConfigChoice}, }, - role_utils::RoleGroupRef, }; use super::ConfigFileName; use crate::crd::{ STACKABLE_LOG_DIR, role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, - v1alpha1, }; /// The maximum size of a single Kafka log file before it is rotated. @@ -28,6 +26,18 @@ pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { const KAFKA_LOG4J_FILE: &str = "kafka.log4j.xml"; const KAFKA_LOG4J2_FILE: &str = "kafka.log4j2.xml"; +/// The static, env-driven Vector agent configuration (`vector.yaml`). +/// +/// The v2 [`vector_container`](stackable_operator::v2::product_logging::framework::vector_container) +/// mounts this file and supplies its `${...}` values (`LOG_DIR`, `DATA_DIR`, `NAMESPACE`, +/// `CLUSTER_NAME`, `ROLE_NAME`, `ROLE_GROUP_NAME`, `VECTOR_AGGREGATOR_ADDRESS`) as env vars. +const VECTOR_CONFIG: &str = include_str!("vector.yaml"); + +/// Returns the Vector agent config (`vector.yaml`) content. +pub fn vector_config_file_content() -> String { + VECTOR_CONFIG.to_owned() +} + const CONSOLE_CONVERSION_PATTERN_LOG4J: &str = "[%d] %p %m (%c)%n"; const CONSOLE_CONVERSION_PATTERN_LOG4J2: &str = "%d{ISO8601} %p [%t] %c - %m%n"; @@ -75,29 +85,6 @@ pub fn role_group_config_map_data( configs } -/// Builds the Vector agent config for a role group, or `None` when the Vector agent is disabled. -/// -/// Takes a v1 [`RoleGroupRef`] because the upstream `create_vector_config` still requires one; -/// this is the only remaining consumer of `RoleGroupRef` in the operator. -pub fn build_vector_config( - rolegroup: &RoleGroupRef, - merged_config: &AnyConfig, -) -> Option { - let vector_log_config = merged_config.vector_logging(); - let vector_log_config = if let ContainerLogConfig { - choice: Some(ContainerLogConfigChoice::Automatic(log_config)), - } = &*vector_log_config - { - Some(log_config) - } else { - None - }; - - merged_config - .vector_logging_enabled() - .then(|| product_logging::framework::create_vector_config(rolegroup, vector_log_config)) -} - fn log4j_config_if_automatic( log_config: Option>, container_name: impl Display, @@ -147,3 +134,21 @@ fn log4j2_config_if_automatic( None } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vector_config_file_content() { + let content = vector_config_file_content(); + assert!(!content.is_empty()); + // The two Kafka log formats must be handled ... + assert!(content.contains("files_log4j")); + assert!(content.contains("files_log4j2")); + // ... while the non-Kafka sources were removed. + assert!(!content.contains("files_stdout")); + assert!(!content.contains("files_tracing_rs")); + assert!(!content.contains("files_opa_json")); + } +} diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh b/rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh new file mode 100755 index 00000000..d7535667 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/test-vector.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +DATA_DIR=/stackable/log/_vector-state \ +LOG_DIR=/stackable/log \ +NAMESPACE=default \ +CLUSTER_NAME=kafka \ +ROLE_NAME=broker \ +ROLE_GROUP_NAME=default \ +VECTOR_AGGREGATOR_ADDRESS=vector-aggregator \ +VECTOR_FILE_LOG_LEVEL=info \ +vector test vector.yaml vector-test.yaml diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml b/rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml new file mode 100644 index 00000000..c62a3807 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/vector-test.yaml @@ -0,0 +1,136 @@ +# Run tests with `./test-vector.sh` +# +# A downside of these test cases is that they compare the whole event and that the message can +# contain source code positions in vector.yaml, e.g. "function call error for \"parse_xml\" at +# (584:643)". Please adapt the tests if you change VRL code in vector.yaml. +--- +tests: + - name: Test log4j XML log entry without throwable + inputs: + - type: log + insert_at: processed_files_log4j + log_fields: + file: /stackable/log/kafka/kafka.log4j.xml + message: > + started + (kafka.server.KafkaServer) + pod: kafka-broker-default-0 + source_type: file + timestamp: 2025-10-02T09:27:29.473487331Z + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "kafka", + "container": "kafka", + "file": "kafka.log4j.xml", + "level": "INFO", + "logger": "kafka.server.KafkaServer", + "message": "started (kafka.server.KafkaServer)", + "namespace": "default", + "pod": "kafka-broker-default-0", + "role": "broker", + "roleGroup": "default", + "timestamp": t'2025-10-02T09:27:28.582Z' + } + + assert_eq!(expected_log_event, .) + - name: Test log4j2 XML log entry without stacktrace + inputs: + - type: log + insert_at: processed_files_log4j2 + log_fields: + file: /stackable/log/kafka/kafka.log4j2.xml + message: > + started + (kafka.server.KafkaServer) + pod: kafka-broker-default-0 + source_type: file + timestamp: 2025-10-02T09:27:29.473487331Z + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "kafka", + "container": "kafka", + "file": "kafka.log4j2.xml", + "level": "INFO", + "logger": "kafka.server.KafkaServer", + "message": "started (kafka.server.KafkaServer)", + "namespace": "default", + "pod": "kafka-broker-default-0", + "role": "broker", + "roleGroup": "default", + "timestamp": t'2025-10-02T09:27:28.582Z' + } + + assert_eq!(expected_log_event, .) + - name: Test Vector internal logs + inputs: + - type: log + insert_at: filtered_logs_vector + log_fields: + arch: x86_64 + message: Vector has started. + metadata: + kind: event + level: INFO + module_path: vector::internal_events::process + target: vector + pid: 14 + pod: kafka-broker-default-0 + source_type: internal_logs + timestamp: 2025-10-02T09:46:14.479381097Z + version: 0.49.0 + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "arch": "x86_64", + "cluster": "kafka", + "container": "vector", + "level": "INFO", + "logger": "vector::internal_events::process", + "message": "Vector has started.", + "namespace": "default", + "pod": "kafka-broker-default-0", + "role": "broker", + "roleGroup": "default", + "timestamp": "2025-10-02T09:46:14.479381097Z", + "version": "0.49.0" + } + + assert_eq!(expected_log_event, .) + - name: Test Vector internal log level filtering - INFO passes + inputs: + - type: log + insert_at: filtered_logs_vector + log_fields: + metadata: + level: INFO + outputs: + - extract_from: filtered_logs_vector + conditions: + - type: vrl + source: | + assert_eq!("INFO", .metadata.level) + - name: Test Vector internal log level filtering - DEBUG dropped + inputs: + - type: log + insert_at: filtered_logs_vector + log_fields: + metadata: + level: DEBUG + no_outputs_from: + - filtered_logs_vector diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml b/rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml new file mode 100644 index 00000000..4226a675 --- /dev/null +++ b/rust/operator-binary/src/controller/build/properties/product_logging/vector.yaml @@ -0,0 +1,274 @@ +--- +data_dir: ${DATA_DIR} + +log_schema: + host_key: pod + +sources: + vector: + type: internal_logs + + files_log4j: + type: file + include: + - ${LOG_DIR}/*/*.log4j.xml + line_delimiter: "\r\n" + multiline: + mode: halt_before + start_pattern: ^" + raw_message + "" + parsed_event, err = parse_xml(wrapped_xml_event) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + root = object!(parsed_event.root) + if !is_object(root.event) { + error = "Parsed event contains no \"event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if keys(root) != ["event"] { + .errors = push(.errors, "Parsed event contains multiple tags: " + join!(keys(root), ", ")) + } + event = object!(root.event) + + epoch_milliseconds, err = to_int(event.@timestamp) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "Time not parsable, using current time instead: " + err) + } + } else { + .errors = push(.errors, "Timestamp not found, using current time instead.") + } + + .logger, err = string(event.@logger) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } + + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } + + message, err = string(event.message) + if err != null || is_empty(message) { + .errors = push(.errors, "Message not found.") + } + throwable = string(event.throwable) ?? "" + .message = join!(compact([message, throwable]), "\n") + } + } + + processed_files_log4j2: + inputs: + - files_log4j2 + type: remap + source: | + raw_message = string!(.message) + + .timestamp = now() + .logger = "" + .level = "INFO" + .message = "" + .errors = [] + + event = {} + parsed_event, err = parse_xml(raw_message) + if err != null { + error = "XML not parsable: " + err + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + if !is_object(parsed_event.Event) { + error = "Parsed event contains no \"Event\" tag." + .errors = push(.errors, error) + log(error, level: "warn") + .message = raw_message + } else { + event = object!(parsed_event.Event) + + tag_instant_valid = false + instant, err = object(event.Instant) + if err == null { + epoch_nanoseconds, err = to_int(instant.@epochSecond) * 1_000_000_000 + to_int(instant.@nanoOfSecond) + if err == null && epoch_nanoseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_nanoseconds, "nanoseconds") + if err == null { + .timestamp = converted_timestamp + tag_instant_valid = true + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } else { + .errors = push(.errors, "Instant invalid, trying property timeMillis instead: " + err) + } + } + if !tag_instant_valid { + epoch_milliseconds, err = to_int(event.@timeMillis) + if err == null && epoch_milliseconds != 0 { + converted_timestamp, err = from_unix_timestamp(epoch_milliseconds, "milliseconds") + if err == null { + .timestamp = converted_timestamp + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } else { + .errors = push(.errors, "timeMillis not parsable, using current time instead: " + err) + } + } + + .logger, err = string(event.@loggerName) + if err != null || is_empty(.logger) { + .errors = push(.errors, "Logger not found.") + } + + level, err = string(event.@level) + if err != null { + .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") + } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") + } else { + .level = level + } + + exception = null + thrown = event.Thrown + if is_object(thrown) { + exception = "Exception" + thread, err = string(event.@thread) + if err == null && !is_empty(thread) { + exception = exception + " in thread \"" + thread + "\"" + } + thrown_name, err = string(thrown.@name) + if err == null && !is_empty(exception) { + exception = exception + " " + thrown_name + } + message = string(thrown.@localizedMessage) ?? + string(thrown.@message) ?? + "" + if !is_empty(message) { + exception = exception + ": " + message + } + stacktrace_items = array(thrown.ExtendedStackTrace.ExtendedStackTraceItem) ?? [] + stacktrace = "" + for_each(stacktrace_items) -> |_index, value| { + stacktrace = stacktrace + " " + class = string(value.@class) ?? "" + method = string(value.@method) ?? "" + if !is_empty(class) && !is_empty(method) { + stacktrace = stacktrace + "at " + class + "." + method + } + file = string(value.@file) ?? "" + line = string(value.@line) ?? "" + if !is_empty(file) && !is_empty(line) { + stacktrace = stacktrace + "(" + file + ":" + line + ")" + } + exact = to_bool(value.@exact) ?? false + location = string(value.@location) ?? "" + version = string(value.@version) ?? "" + if !is_empty(location) && !is_empty(version) { + stacktrace = stacktrace + " " + if !exact { + stacktrace = stacktrace + "~" + } + stacktrace = stacktrace + "[" + location + ":" + version + "]" + } + stacktrace = stacktrace + "\n" + } + if stacktrace != "" { + exception = exception + "\n" + stacktrace + } + } + + message, err = string(event.Message) + if err != null || is_empty(message) { + message = null + .errors = push(.errors, "Message not found.") + } + .message = join!(compact([message, exception]), "\n") + } + } + + extended_logs_files: + inputs: + - processed_files_* + type: remap + source: | + del(.source_type) + if .errors == [] { + del(.errors) + } + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') + + filtered_logs_vector: + inputs: + - vector + type: filter + condition: '!includes(["TRACE", "DEBUG"], .metadata.level)' + + extended_logs_vector: + inputs: + - filtered_logs_vector + type: remap + source: | + .container = "vector" + .level = .metadata.level + .logger = .metadata.module_path + if exists(.file) { .processed_file = del(.file) } + del(.metadata) + del(.pid) + del(.source_type) + + extended_logs: + inputs: + - extended_logs_* + type: remap + source: | + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" + +sinks: + aggregator: + inputs: + - extended_logs + type: vector + address: ${VECTOR_AGGREGATOR_ADDRESS} diff --git a/rust/operator-binary/src/controller/build/resource/config_map.rs b/rust/operator-binary/src/controller/build/resource/config_map.rs index 43846eb5..88ea7cd5 100644 --- a/rust/operator-binary/src/controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -14,7 +14,7 @@ use crate::{ controller::{ RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, build::properties::{ - ConfigFileName, config_file_name, logging::role_group_config_map_data, + ConfigFileName, config_file_name, product_logging::role_group_config_map_data, }, }, crd::{ diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 136421e4..880099e9 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -24,21 +24,19 @@ use stackable_operator::{ apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, }, kube::ResourceExt, - product_logging::{ - self, - spec::{ - ConfigMapLogConfig, ContainerLogConfig, ContainerLogConfigChoice, - CustomContainerLogConfig, - }, - }, + product_logging, v2::{ builder::{ meta::ownerreference_from_resource, - pod::volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, + pod::{ + container::EnvVarSet, + volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, + }, }, jvm_argument_overrides::JvmArgumentOverrides, + product_logging::framework::{ValidatedContainerLogConfigChoice, vector_container}, role_group_utils::ResourceNames, - types::kubernetes::{ListenerName, PersistentVolumeClaimName}, + types::kubernetes::{ContainerName, ListenerName, PersistentVolumeClaimName, VolumeName}, }, }; @@ -52,9 +50,10 @@ use crate::{ }, graceful_shutdown::add_graceful_shutdown_config, kerberos::add_kerberos_pod_config, - properties::logging::MAX_KAFKA_LOG_FILES_SIZE, + properties::product_logging::MAX_KAFKA_LOG_FILES_SIZE, }, node_id_hasher::node_id_hash32_offset, + validate::ValidatedLogging, }, crd::{ BROKER_ID_POD_MAP_DIR, KAFKA_HEAP_OPTS, LISTENER_BOOTSTRAP_VOLUME_NAME, @@ -69,6 +68,12 @@ use crate::{ }, }; +stackable_operator::constant!(VECTOR_CONTAINER_NAME: ContainerName = "vector"); +// The Vector container reads its `vector.yaml` from the `config` volume (the rolegroup +// ConfigMap) and tails product logs from the `log` volume. +stackable_operator::constant!(VECTOR_CONFIG_VOLUME_NAME: VolumeName = "config"); +stackable_operator::constant!(VECTOR_LOG_VOLUME_NAME: VolumeName = "log"); + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("failed to add kerberos config"))] @@ -99,11 +104,6 @@ pub enum Error { source: crate::controller::PodDescriptorsError, }, - #[snafu(display("failed to configure logging"))] - ConfigureLogging { - source: stackable_operator::product_logging::framework::LoggingError, - }, - #[snafu(display("failed to construct JVM arguments"))] ConstructJvmArguments { source: crate::controller::build::jvm::Error, @@ -122,9 +122,6 @@ pub enum Error { #[snafu(display("missing secret lifetime"))] MissingSecretLifetime, - - #[snafu(display("vector agent is enabled but vector aggregator ConfigMap is missing"))] - VectorAggregatorConfigMapMissing, } /// The broker rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator. @@ -328,7 +325,7 @@ pub fn build_broker_rolegroup_statefulset( ..Probe::default() }); - add_log_config_volume(&mut pod_builder, merged_config, &resource_names)?; + add_log_config_volume(&mut pod_builder, &validated_rg.logging, &resource_names)?; let metadata = ObjectMetaBuilder::new() .with_labels(recommended_labels.clone()) @@ -371,10 +368,10 @@ pub fn build_broker_rolegroup_statefulset( add_vector_container( &mut pod_builder, - validated_cluster, + &validated_rg.logging, resolved_product_image, - merged_config, - )?; + &resource_names, + ); add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; @@ -563,7 +560,7 @@ pub fn build_controller_rolegroup_statefulset( ..Probe::default() }); - add_log_config_volume(&mut pod_builder, merged_config, &resource_names)?; + add_log_config_volume(&mut pod_builder, &validated_rg.logging, &resource_names)?; let metadata = ObjectMetaBuilder::new() .with_labels(recommended_labels.clone()) @@ -594,10 +591,10 @@ pub fn build_controller_rolegroup_statefulset( add_vector_container( &mut pod_builder, - validated_cluster, + &validated_rg.logging, resolved_product_image, - merged_config, - )?; + &resource_names, + ); add_graceful_shutdown_config(merged_config, &mut pod_builder).context(GracefulShutdownSnafu)?; @@ -713,35 +710,26 @@ fn add_common_kafka_env( } /// Adds the `log-config` volume, sourced either from the user-supplied custom log config -/// `ConfigMap` or the rolegroup `ConfigMap`. +/// `ConfigMap` or the rolegroup `ConfigMap` (which carries the operator-generated config). +/// Branches on the *validated* Kafka-container logging choice. fn add_log_config_volume( pod_builder: &mut PodBuilder, - merged_config: &AnyConfig, + logging: &ValidatedLogging, resource_names: &ResourceNames, ) -> Result<(), Error> { - if let ContainerLogConfig { - choice: - Some(ContainerLogConfigChoice::Custom(CustomContainerLogConfig { - custom: ConfigMapLogConfig { config_map }, - })), - } = &*merged_config.kafka_logging() - { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(config_map) - .build(), - ) - .context(AddVolumeSnafu)?; - } else { - pod_builder - .add_volume( - VolumeBuilder::new("log-config") - .with_config_map(resource_names.role_group_config_map().to_string()) - .build(), - ) - .context(AddVolumeSnafu)?; - } + let config_map = match &logging.kafka_container { + ValidatedContainerLogConfigChoice::Custom(config_map_name) => config_map_name.to_string(), + ValidatedContainerLogConfigChoice::Automatic(_) => { + resource_names.role_group_config_map().to_string() + } + }; + pod_builder + .add_volume( + VolumeBuilder::new("log-config") + .with_config_map(config_map) + .build(), + ) + .context(AddVolumeSnafu)?; Ok(()) } @@ -774,44 +762,29 @@ fn add_common_pod_config( Ok(()) } -/// Adds the Vector log-aggregation sidecar container, when Vector logging is enabled. +/// Adds the v2 Vector log-aggregation sidecar container, when the Vector agent is enabled. /// -/// Errors if Vector logging is enabled but no Vector aggregator discovery `ConfigMap` is -/// configured on the cluster. +/// Whether Vector is enabled, the per-container log config and the (validated) aggregator +/// discovery `ConfigMap` name are resolved up-front in +/// [`ValidatedLogging`](crate::controller::validate::ValidatedLogging). The container mounts the +/// static `vector.yaml` from the `config` volume and is driven by the env vars the v2 +/// [`vector_container`] sets. fn add_vector_container( pod_builder: &mut PodBuilder, - validated_cluster: &ValidatedCluster, + logging: &ValidatedLogging, resolved_product_image: &ResolvedProductImage, - merged_config: &AnyConfig, -) -> Result<(), Error> { + resource_names: &ResourceNames, +) { // Add vector container after kafka container to keep the defaulting into kafka container - if merged_config.vector_logging_enabled() { - match &validated_cluster - .cluster_config - .vector_aggregator_config_map_name - { - Some(vector_aggregator_config_map_name) => { - pod_builder.add_container( - product_logging::framework::vector_container( - resolved_product_image, - "config", - "log", - Some(&*merged_config.vector_logging()), - ResourceRequirementsBuilder::new() - .with_cpu_request("250m") - .with_cpu_limit("500m") - .with_memory_request("128Mi") - .with_memory_limit("128Mi") - .build(), - vector_aggregator_config_map_name, - ) - .context(ConfigureLoggingSnafu)?, - ); - } - None => { - VectorAggregatorConfigMapMissingSnafu.fail()?; - } - } + if let Some(vector_container_log_config) = &logging.vector_container { + pod_builder.add_container(vector_container( + &VECTOR_CONTAINER_NAME, + resolved_product_image, + vector_container_log_config, + resource_names, + &VECTOR_CONFIG_VOLUME_NAME, + &VECTOR_LOG_VOLUME_NAME, + EnvVarSet::new(), + )); } - Ok(()) } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index a7fa2a7c..969b10e3 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -6,18 +6,24 @@ use std::{collections::BTreeMap, str::FromStr}; use serde::Serialize; -use snafu::{ResultExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, commons::product_image_selection, config::{fragment::FromFragment, merge::Merge}, kube::ResourceExt, + product_logging::spec::Logging, role_utils::{GenericRoleConfig, Role}, schemars::JsonSchema, v2::{ builder::pod::container::{self, EnvVarName, EnvVarSet}, controller_utils::{get_cluster_name, get_namespace, get_uid}, + product_logging::framework::{ + ValidatedContainerLogConfigChoice, VectorContainerLogConfig, + validate_logging_configuration_for_container, + }, role_utils::{JavaCommonConfig, with_validated_config}, + types::kubernetes::ConfigMapName, }, }; @@ -30,8 +36,9 @@ use crate::{ self, CONTAINER_IMAGE_BASE_NAME, authentication::{self}, role::{ - AnyConfig, AnyConfigOverrides, KafkaRole, broker::BrokerConfig, - controller::ControllerConfig, + AnyConfig, AnyConfigOverrides, KafkaRole, + broker::{BrokerConfig, BrokerContainer}, + controller::{ControllerConfig, ControllerContainer}, }, security::{self, KafkaTlsSecurity}, v1alpha1, @@ -88,6 +95,96 @@ pub enum Error { source: stackable_operator::v2::macros::attributed_string_type::Error, role_group_name: String, }, + + #[snafu(display("failed to validate the logging configuration"))] + ValidateLoggingConfig { + source: stackable_operator::v2::product_logging::framework::Error, + }, + + #[snafu(display( + "the Vector aggregator discovery ConfigMap name is required when the Vector agent is enabled" + ))] + MissingVectorAggregatorConfigMapName, + + #[snafu(display("the Vector aggregator discovery ConfigMap name is invalid"))] + ParseVectorAggregatorConfigMapName { + source: stackable_operator::v2::macros::attributed_string_type::Error, + }, +} + +/// Validated logging configuration for a Kafka role group's Kafka and (optional) Vector +/// containers. +/// +/// Produced up-front by [`validate_logging`] (mirroring the hive-/opensearch-operator) so that +/// an invalid custom log `ConfigMap` name or a missing Vector aggregator discovery `ConfigMap` +/// name fails reconciliation during validation rather than at resource-build time. +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedLogging { + pub kafka_container: ValidatedContainerLogConfigChoice, + pub vector_container: Option, +} + +/// Validates the logging configuration for a role group's Kafka and (optional) Vector container. +/// +/// `vector_aggregator_config_map_name` is the discovery `ConfigMap` name of the Vector +/// aggregator; it is required (and was validated into a [`ConfigMapName`]) only when the Vector +/// agent is enabled. Generic over the role's container enum so it serves both broker and +/// controller role groups. +fn validate_logging( + logging: &Logging, + kafka_container: C, + vector_container: C, + vector_aggregator_config_map_name: &Option, +) -> Result +where + C: Clone + std::fmt::Display + Ord, +{ + let kafka_container = validate_logging_configuration_for_container(logging, &kafka_container) + .context(ValidateLoggingConfigSnafu)?; + + let vector_container = if logging.enable_vector_agent { + let vector_aggregator_config_map_name = vector_aggregator_config_map_name + .clone() + .context(MissingVectorAggregatorConfigMapNameSnafu)?; + Some(VectorContainerLogConfig { + log_config: validate_logging_configuration_for_container(logging, &vector_container) + .context(ValidateLoggingConfigSnafu)?, + vector_aggregator_config_map_name, + }) + } else { + None + }; + + Ok(ValidatedLogging { + kafka_container, + vector_container, + }) +} + +/// Validates a broker role group's logging configuration. +fn validate_broker_logging( + config: &BrokerConfig, + vector_aggregator_config_map_name: &Option, +) -> Result { + validate_logging( + &config.logging, + BrokerContainer::Kafka, + BrokerContainer::Vector, + vector_aggregator_config_map_name, + ) +} + +/// Validates a controller role group's logging configuration. +fn validate_controller_logging( + config: &ControllerConfig, + vector_aggregator_config_map_name: &Option, +) -> Result { + validate_logging( + &config.logging, + ControllerContainer::Kafka, + ControllerContainer::Vector, + vector_aggregator_config_map_name, + ) } type Result = std::result::Result; @@ -127,6 +224,18 @@ pub fn validate( let cluster_id = kafka.cluster_id(); + // The Vector aggregator discovery ConfigMap name, validated up-front so an invalid name + // fails reconciliation here rather than at resource-build time. It is only required (per + // role group) when the Vector agent is enabled; see [`validate_logging`]. + let vector_aggregator_config_map_name = kafka + .spec + .cluster_config + .vector_aggregator_config_map_name + .as_deref() + .map(ConfigMapName::from_str) + .transpose() + .context(ParseVectorAggregatorConfigMapNameSnafu)?; + let mut role_group_configs: BTreeMap< KafkaRole, BTreeMap, @@ -142,6 +251,8 @@ pub fn validate( cluster_id, AnyConfig::Broker, AnyConfigOverrides::Broker, + validate_broker_logging, + &vector_aggregator_config_map_name, )?; role_group_configs.insert(KafkaRole::Broker, broker_groups); @@ -154,6 +265,8 @@ pub fn validate( cluster_id, AnyConfig::Controller, AnyConfigOverrides::Controller, + validate_controller_logging, + &vector_aggregator_config_map_name, )?; role_group_configs.insert(KafkaRole::Controller, controller_groups); } @@ -186,11 +299,6 @@ pub fn validate( .cluster_config .broker_id_pod_config_map_name .clone(), - vector_aggregator_config_map_name: kafka - .spec - .cluster_config - .vector_aggregator_config_map_name - .clone(), }, role_group_configs, )) @@ -212,6 +320,8 @@ fn validate_role_group_configs( cluster_id: Option<&str>, wrap_config: fn(ValidatedConfig) -> AnyConfig, wrap_overrides: fn(ConfigOverrides) -> AnyConfigOverrides, + validate_logging: fn(&ValidatedConfig, &Option) -> Result, + vector_aggregator_config_map_name: &Option, ) -> Result> where Config: Clone + Merge, @@ -239,6 +349,9 @@ where } let env_overrides = inject_cluster_id(env_overrides, cluster_id)?; + let logging = + validate_logging(&merged.config.config, vector_aggregator_config_map_name)?; + let validated = ValidatedRoleGroupConfig { replicas: merged.replicas.unwrap_or(1), config: wrap_config(merged.config.config), @@ -249,6 +362,7 @@ where .config .product_specific_common_config .jvm_argument_overrides, + logging, }; let role_group_name = RoleGroupName::from_str(role_group_name).with_context(|_| { ParseRoleGroupNameSnafu { diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index 1cac599c..be1d21e3 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -147,26 +147,6 @@ impl AnyConfig { } } - pub fn vector_logging(&'_ self) -> Cow<'_, ContainerLogConfig> { - match &self { - AnyConfig::Broker(broker_config) => broker_config - .logging - .for_container(&broker::BrokerContainer::Vector), - AnyConfig::Controller(controller_config) => controller_config - .logging - .for_container(&controller::ControllerContainer::Vector), - } - } - - pub fn vector_logging_enabled(&self) -> bool { - match self { - AnyConfig::Broker(broker_config) => broker_config.logging.enable_vector_agent, - AnyConfig::Controller(controller_config) => { - controller_config.logging.enable_vector_agent - } - } - } - pub fn listener_class(&self) -> Option<&String> { match self { AnyConfig::Broker(broker_config) => Some(&broker_config.broker_listener_class), diff --git a/tests/templates/kuttl/smoke/33-assert.yaml.j2 b/tests/templates/kuttl/smoke/33-assert.yaml.j2 index 1ed5fe65..70678faa 100644 --- a/tests/templates/kuttl/smoke/33-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/33-assert.yaml.j2 @@ -74,6 +74,32 @@ spec: memory: 128Mi {% if vector_enabled %} - name: vector + env: + - name: CLUSTER_NAME + value: test-kafka + - name: DATA_DIR + value: /stackable/log/_vector-state + - name: LOG_DIR + value: /stackable/log + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: ROLE_GROUP_NAME + value: default + - name: ROLE_NAME + value: broker + - name: VECTOR_AGGREGATOR_ADDRESS + valueFrom: + configMapKeyRef: + key: ADDRESS + name: vector-aggregator-discovery + - name: VECTOR_CONFIG_YAML + value: /stackable/config/vector.yaml + - name: VECTOR_FILE_LOG_LEVEL + value: info + - name: VECTOR_LOG + value: info resources: limits: cpu: 500m diff --git a/tests/templates/kuttl/smoke/34-assert.yaml.j2 b/tests/templates/kuttl/smoke/34-assert.yaml.j2 index 299f2a81..e6164a7e 100644 --- a/tests/templates/kuttl/smoke/34-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/34-assert.yaml.j2 @@ -100,7 +100,8 @@ commands: networkaddress.cache.ttl=30 {% if lookup('env', 'VECTOR_AGGREGATOR') %} vector.yaml: | - data_dir: /stackable/log/_vector-state + --- + data_dir: ${DATA_DIR} log_schema: host_key: pod @@ -109,20 +110,10 @@ commands: vector: type: internal_logs - files_stdout: - type: file - include: - - /stackable/log/*/*.stdout.log - - files_stderr: - type: file - include: - - /stackable/log/*/*.stderr.log - files_log4j: type: file include: - - /stackable/log/*/*.log4j.xml + - ${LOG_DIR}/*/*.log4j.xml line_delimiter: "\r\n" multiline: mode: halt_before @@ -133,194 +124,10 @@ commands: files_log4j2: type: file include: - - /stackable/log/*/*.log4j2.xml + - ${LOG_DIR}/*/*.log4j2.xml line_delimiter: "\r\n" - files_py: - type: file - include: - - /stackable/log/*/*.py.json - - files_airlift: - type: file - include: - - /stackable/log/*/*.airlift.json - - files_tracing_rs: - type: file - include: - - /stackable/log/*/*.tracing-rs.json - - files_opa_json: - type: file - include: - - /stackable/log/opa/current - - /stackable/log/opa/test - transforms: - processed_files_tracing_rs: - inputs: - - files_tracing_rs - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - timestamp_string, err = string(event.timestamp) - if err == null { - parsed_timestamp, err = parse_timestamp(timestamp_string, "%+") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Timestamp not parsable, trying current time instead: " + err) - } - } - - .logger, err = string(event.target) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger/target not found.") - } - - level, err = string(event.level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], upcase(level)) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = upcase(level) - } - - fields, err = object(event.fields) - if err != null { - .errors = push(.errors, "Fields are not an object.") - } - - .message, err = string(fields.message) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - - del(fields.message) - - other_fields = encode_key_value(fields, field_delimiter: "\n") - .message = join!(compact([.message, other_fields]), "\n\n") - } - - processed_files_opa_json: - inputs: - - files_opa_json - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - property_timestamp_valid = false - timestamp_string, err = string(event.timestamp) - if err == null { - parsed_timestamp, err = parse_timestamp(timestamp_string, "%Y-%m-%dT%H:%M:%S.%fZ") - if err == null { - .timestamp = parsed_timestamp - property_timestamp_valid = true - } else { - .errors = push(.errors, "Timestamp not parsable, trying property time instead: " + err) - } - } - if !property_timestamp_valid { - time_string, err = string(event.time) - if err == null { - parsed_timestamp, err = parse_timestamp(time_string, "%Y-%m-%dT%H:%M:%SZ") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Time not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "Time not found, using current time instead.") - } - } - - .logger, err = string(event.logger) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } - - level, err = string(event.level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], upcase(level)) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = upcase(level) - } - - .message, err = string(event.msg) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - - del(event.time) - del(event.timestamp) - del(event.level) - del(event.msg) - - other_fields = encode_key_value(event, field_delimiter: "\n") - .message = join!(compact([.message, other_fields]), "\n\n") - } - - processed_files_stdout: - inputs: - - files_stdout - type: remap - source: | - .logger = "ROOT" - .level = "INFO" - - processed_files_stderr: - inputs: - - files_stderr - type: remap - source: | - .logger = "ROOT" - .level = "ERROR" - processed_files_log4j: inputs: - files_log4j @@ -521,136 +328,6 @@ commands: } } - processed_files_py: - inputs: - - files_py - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - asctime, err = string(event.asctime) - if err == null { - parsed_timestamp, err = parse_timestamp(asctime, "%F %T,%3f") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Timestamp not parsable, using current time instead: "+ err) - } - } else { - .errors = push(.errors, "Timestamp not found, using current time instead.") - } - - .logger, err = string(event.name) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } - - level, err = string(event.levelname) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if level == "DEBUG" { - .level = "DEBUG" - } else if level == "INFO" { - .level = "INFO" - } else if level == "WARNING" { - .level = "WARN" - } else if level == "ERROR" { - .level = "ERROR" - } else if level == "CRITICAL" { - .level = "FATAL" - } else { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } - - .message, err = string(event.message) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - } - - processed_files_airlift: - inputs: - - files_airlift - type: remap - source: | - raw_message = string!(.message) - - .timestamp = now() - .logger = "" - .level = "INFO" - .message = "" - .errors = [] - - parsed_event, err = parse_json(raw_message) - if err != null { - error = "JSON not parsable: " + err - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else if !is_object(parsed_event) { - error = "Parsed event is not a JSON object." - .errors = push(.errors, error) - log(error, level: "warn") - .message = raw_message - } else { - event = object!(parsed_event) - - timestamp_string, err = string(event.timestamp) - if err == null { - parsed_timestamp, err = parse_timestamp(timestamp_string, "%Y-%m-%dT%H:%M:%S.%fZ") - if err == null { - .timestamp = parsed_timestamp - } else { - .errors = push(.errors, "Timestamp not parsable, using current time instead: " + err) - } - } else { - .errors = push(.errors, "Timestamp not found, using current time instead.") - } - - .logger, err = string(event.logger) - if err != null || is_empty(.logger) { - .errors = push(.errors, "Logger not found.") - } - - level, err = string(event.level) - if err != null { - .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { - .level = level - } - - .thread = string(parsed_event.thread) ?? null - - .message, err = string(event.message) - if err != null || is_empty(.message) { - .errors = push(.errors, "Message not found.") - } - stacktrace = string(event.stackTrace) ?? "" - .message = join!(compact([.message, stacktrace]), "\n\n") - } - extended_logs_files: inputs: - processed_files_* @@ -660,7 +337,7 @@ commands: if .errors == [] { del(.errors) } - . |= parse_regex!(.file, r'^/stackable/log/(?P.*?)/(?P.*?)$') + . |= parse_regex!(.file, r'^${LOG_DIR}/(?P.*?)/(?P.*?)$') filtered_logs_vector: inputs: @@ -686,17 +363,17 @@ commands: - extended_logs_* type: remap source: | - .namespace = "__NAMESPACE__" - .cluster = "test-kafka" - .role = "broker" - .roleGroup = "default" + .namespace = "${NAMESPACE}" + .cluster = "${CLUSTER_NAME}" + .role = "${ROLE_NAME}" + .roleGroup = "${ROLE_GROUP_NAME}" sinks: aggregator: inputs: - extended_logs type: vector - address: $VECTOR_AGGREGATOR_ADDRESS + address: ${VECTOR_AGGREGATOR_ADDRESS} {% endif %} YAMLEOF ) From e7428d6a87f1d3f4ce752036a1e30fb8f057039c Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Sun, 14 Jun 2026 16:31:45 +0200 Subject: [PATCH 47/47] chore: fix fmt --- rust/operator-binary/src/crd/security.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/crd/security.rs index da504c27..cd83aadd 100644 --- a/rust/operator-binary/src/crd/security.rs +++ b/rust/operator-binary/src/crd/security.rs @@ -78,6 +78,8 @@ impl KafkaTlsSecurity { pub const INTERNAL_PORT: u16 = 19092; // - TLS internal const INTER_BROKER_LISTENER_NAME: &'static str = "inter.broker.listener.name"; + // - TLS global + const KEYSTORE_P12_FILE_NAME: &'static str = "keystore.p12"; const OPA_TLS_MOUNT_PATH: &str = "/stackable/tls-opa"; // opa const OPA_TLS_VOLUME_NAME: &str = "tls-opa"; @@ -85,11 +87,8 @@ impl KafkaTlsSecurity { pub const SECURE_CLIENT_PORT: u16 = 9093; pub const SECURE_CLIENT_PORT_NAME: &'static str = "kafka-tls"; pub const SECURE_INTERNAL_PORT: u16 = 19093; - // - TLS global - const KEYSTORE_P12_FILE_NAME: &'static str = "keystore.p12"; const SSL_STORE_PASSWORD: &'static str = ""; const SSL_STORE_TYPE_PKCS12: &'static str = "PKCS12"; - const TRUSTSTORE_P12_FILE_NAME: &'static str = "truststore.p12"; const STACKABLE_TLS_KAFKA_INTERNAL_DIR: &'static str = "/stackable/tls-kafka-internal"; const STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME: &'static str = "tls-kafka-internal"; const STACKABLE_TLS_KAFKA_SERVER_DIR: &'static str = "/stackable/tls-kafka-server"; @@ -97,6 +96,7 @@ impl KafkaTlsSecurity { // directories const STACKABLE_TLS_KCAT_DIR: &'static str = "/stackable/tls-kcat"; const STACKABLE_TLS_KCAT_VOLUME_NAME: &'static str = "tls-kcat"; + const TRUSTSTORE_P12_FILE_NAME: &'static str = "truststore.p12"; #[cfg(test)] pub fn new(