From 9225150069f9b5e3db108e31f31700e268ee99f0 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Wed, 3 Jun 2026 17:41:06 +0200 Subject: [PATCH 01/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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( From 31a4dbcd16ff8da2054548e7b7a59c3aa7db3d20 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Mon, 15 Jun 2026 21:26:26 +0200 Subject: [PATCH 48/66] chore: bump dependencies --- Cargo.lock | 91 ++++++++++++++++++++--------------------- Cargo.nix | 116 +++++++++++++++++++++++++---------------------------- 2 files changed, 98 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a4cdc09..88162bc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,9 +302,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.63" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "jobserver", @@ -574,9 +574,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_more" @@ -1016,9 +1013,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1445,9 +1442,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", "futures-util", @@ -1506,7 +1503,7 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "darling", "regex", @@ -1719,9 +1716,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -2783,9 +2780,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "snafu" @@ -2885,7 +2882,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackable-certs" version = "0.4.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "const-oid", "ecdsa", @@ -2930,7 +2927,7 @@ dependencies = [ [[package]] name = "stackable-operator" version = "0.111.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "base64", "clap", @@ -2955,6 +2952,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha2", "snafu 0.9.1", "stackable-operator-derive", "stackable-shared", @@ -2974,7 +2972,7 @@ dependencies = [ [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "darling", "proc-macro2", @@ -2985,7 +2983,7 @@ dependencies = [ [[package]] name = "stackable-shared" version = "0.1.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "jiff", "k8s-openapi", @@ -3002,7 +3000,7 @@ dependencies = [ [[package]] name = "stackable-telemetry" version = "0.6.4" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "axum", "clap", @@ -3026,7 +3024,7 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "kube", "schemars", @@ -3040,7 +3038,7 @@ dependencies = [ [[package]] name = "stackable-versioned-macros" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "convert_case", "convert_case_extras", @@ -3058,7 +3056,7 @@ dependencies = [ [[package]] name = "stackable-webhook" version = "0.9.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#1e8099fd157b06f27d93854b0838f67871448c4e" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" dependencies = [ "arc-swap", "async-trait", @@ -3219,12 +3217,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -3234,15 +3231,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -3695,18 +3692,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -3717,9 +3714,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.73" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ "js-sys", "wasm-bindgen", @@ -3727,9 +3724,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3737,9 +3734,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", @@ -3750,18 +3747,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4025,18 +4022,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.nix b/Cargo.nix index 1de0fef7..9a82f154 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -962,9 +962,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.2.63"; + version = "1.2.64"; edition = "2018"; - sha256 = "0zy2bqc4nvj6bv2cipx4h4bn65wf1zqf1fw1hsh64mmvg1hh2vjm"; + sha256 = "07shcd8faxw7csz13m3cg2mj6i8z07pqs960k181pscbjpyqgn6s"; authors = [ "Alex Crichton " ]; @@ -1740,14 +1740,6 @@ rec { authors = [ "Jacob Pratt " ]; - dependencies = [ - { - name = "powerfmt"; - packageId = "powerfmt"; - optional = true; - usesDefaultFeatures = false; - } - ]; features = { "macros" = [ "dep:deranged-macros" ]; "num" = [ "dep:num-traits" ]; @@ -1759,7 +1751,7 @@ rec { "rand09" = [ "dep:rand09" ]; "serde" = [ "dep:serde_core" ]; }; - resolvedDefaultFeatures = [ "default" "powerfmt" ]; + resolvedDefaultFeatures = [ "default" ]; }; "derive_more" = rec { crateName = "derive_more"; @@ -3185,9 +3177,9 @@ rec { }; "h2" = rec { crateName = "h2"; - version = "0.4.14"; + version = "0.4.15"; edition = "2021"; - sha256 = "0cw7jk7kn2vn6f8w8ssh6gis1mljnfjxd606gvi4sjpyjayfy7qp"; + sha256 = "0mgilh1g8gydcchqi6acs5l6j0gwg5jwpa64sj4b3ncb9v497c3c"; authors = [ "Carl Lerche " "Sean McArthur " @@ -4594,9 +4586,9 @@ rec { }; "js-sys" = rec { crateName = "js-sys"; - version = "0.3.100"; + version = "0.3.102"; edition = "2021"; - sha256 = "0qi1wjakyw2rx9wwprcfx77g3lvn1b8n6yvfhj2pgym4swh5y0pj"; + sha256 = "0cgxklnyrfpzvf32cvdl3x5d070kfsv7ykdxfl3yizwdjqq4rl03"; libName = "js_sys"; authors = [ "The wasm-bindgen Developers" @@ -4802,7 +4794,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "k8s_version"; @@ -5666,9 +5658,9 @@ rec { }; "memchr" = rec { crateName = "memchr"; - version = "2.8.1"; + version = "2.8.2"; edition = "2021"; - sha256 = "1n448jx01h5z2xknj6x2dhxgr8s8fb717cf6vfqj5lmhkpj7m53b"; + sha256 = "1i33wr49pcz2sbd12nds3n9fszay8kq5bk78gwciz462mcs49448"; authors = [ "Andrew Gallant " "bluss" @@ -9176,9 +9168,9 @@ rec { }; "smallvec" = rec { crateName = "smallvec"; - version = "1.15.1"; + version = "1.15.2"; edition = "2018"; - sha256 = "00xxdxxpgyq5vjnpljvkmy99xij5rxgh913ii1v16kzynnivgcb7"; + sha256 = "143wzbqf6vgapdp2z4qpl0yvlqcn17s8cnk8m28rqly808zsdmlf"; authors = [ "The Servo Project Developers" ]; @@ -9485,7 +9477,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_certs"; @@ -9676,7 +9668,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_operator"; @@ -9786,6 +9778,11 @@ rec { name = "serde_yaml"; packageId = "serde_yaml"; } + { + name = "sha2"; + packageId = "sha2"; + features = [ "oid" ]; + } { name = "snafu"; packageId = "snafu 0.9.1"; @@ -9870,7 +9867,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; procMacro = true; @@ -9905,7 +9902,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_shared"; @@ -9986,7 +9983,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_telemetry"; @@ -10096,7 +10093,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_versioned"; @@ -10146,7 +10143,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; procMacro = true; @@ -10214,7 +10211,7 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "1e8099fd157b06f27d93854b0838f67871448c4e"; + rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; }; libName = "stackable_webhook"; @@ -10654,9 +10651,9 @@ rec { }; "time" = rec { crateName = "time"; - version = "0.3.47"; + version = "0.3.49"; edition = "2024"; - sha256 = "0b7g9ly2iabrlgizliz6v5x23yq5d6bpp0mqz6407z1s526d8fvl"; + sha256 = "0sc4dgw6g187gvz5qj9iqqk2ashqzvdwi664b2183gbvsk1566ki"; authors = [ "Jacob Pratt " "Time contributors" @@ -10665,12 +10662,6 @@ rec { { name = "deranged"; packageId = "deranged"; - features = [ "powerfmt" ]; - } - { - name = "itoa"; - packageId = "itoa"; - optional = true; } { name = "num-conv"; @@ -10710,13 +10701,14 @@ rec { features = { "alloc" = [ "serde_core?/alloc" ]; "default" = [ "std" ]; - "formatting" = [ "dep:itoa" "std" "time-macros?/formatting" ]; + "formatting" = [ "std" "time-macros?/formatting" ]; "large-dates" = [ "time-core/large-dates" "time-macros?/large-dates" ]; "local-offset" = [ "std" "dep:libc" "dep:num_threads" ]; "macros" = [ "dep:time-macros" ]; "parsing" = [ "time-macros?/parsing" ]; "quickcheck" = [ "dep:quickcheck" "alloc" "deranged/quickcheck" ]; - "rand" = [ "rand08" "rand09" ]; + "rand" = [ "rand08" "rand09" "rand010" ]; + "rand010" = [ "dep:rand010" "deranged/rand010" ]; "rand08" = [ "dep:rand08" "deranged/rand08" ]; "rand09" = [ "dep:rand09" "deranged/rand09" ]; "serde" = [ "dep:serde_core" "time-macros?/serde" "deranged/serde" ]; @@ -10729,9 +10721,9 @@ rec { }; "time-core" = rec { crateName = "time-core"; - version = "0.1.8"; + version = "0.1.9"; edition = "2024"; - sha256 = "1jidl426mw48i7hjj4hs9vxgd9lwqq4vyalm4q8d7y4iwz7y353n"; + sha256 = "028ix0ax7ixp1h1k5zsqwgw85w6y1q32irslma7ci6ddd5kr074y"; libName = "time_core"; authors = [ "Jacob Pratt " @@ -10742,9 +10734,9 @@ rec { }; "time-macros" = rec { crateName = "time-macros"; - version = "0.2.27"; + version = "0.2.29"; edition = "2024"; - sha256 = "058ja265waq275wxvnfwavbz9r1hd4dgwpfn7a1a9a70l32y8w1f"; + sha256 = "0zf1ycfikg93ijf00qnprk801khqnqqga1zp0adbp73sfaim5iki"; procMacro = true; libName = "time_macros"; authors = [ @@ -12386,9 +12378,9 @@ rec { }; "wasip2" = rec { crateName = "wasip2"; - version = "1.0.3+wasi-0.2.9"; + version = "1.0.4+wasi-0.2.12"; edition = "2021"; - sha256 = "1mi3w855dz99xzjqc4aa8c9q5b6z1y5c963pkk4cvmr6vdr4c1i0"; + sha256 = "11wl7lqwq4pbmlmzr6n7bwz0hzy1z6sxc4554bkmrr86w4vznzmn"; dependencies = [ { name = "wit-bindgen"; @@ -12406,9 +12398,9 @@ rec { }; "wasm-bindgen" = rec { crateName = "wasm-bindgen"; - version = "0.2.123"; + version = "0.2.125"; edition = "2021"; - sha256 = "0qqmx07r597gm8lbz8qngvv0phwvpzzyfh3nl84nz9qr1jqs8m52"; + sha256 = "06nakz7nfy0ymyp7a27wfbjwx69659i12117hkgddkiv2iwkznwd"; libName = "wasm_bindgen"; authors = [ "The wasm-bindgen Developers" @@ -12457,9 +12449,9 @@ rec { }; "wasm-bindgen-futures" = rec { crateName = "wasm-bindgen-futures"; - version = "0.4.73"; + version = "0.4.75"; edition = "2021"; - sha256 = "1bva12h8gdpqkp753czlxabs0s21lvgzm41brr4lhpdzz818fmjl"; + sha256 = "104jssshr6cm5hmkn6c66mbkyxgaaphng6c17g0dmj7jhk918fsh"; libName = "wasm_bindgen_futures"; authors = [ "The wasm-bindgen Developers" @@ -12485,9 +12477,9 @@ rec { }; "wasm-bindgen-macro" = rec { crateName = "wasm-bindgen-macro"; - version = "0.2.123"; + version = "0.2.125"; edition = "2021"; - sha256 = "1p50xdwmv543b52bc49vm5lcsgd9adpx647bdisg7ihfbg3hz914"; + sha256 = "0g9w68dwcs4ylm5kxf7schi0kjdfarhc9qlnf8arxc9zn62a28af"; procMacro = true; libName = "wasm_bindgen_macro"; authors = [ @@ -12509,9 +12501,9 @@ rec { }; "wasm-bindgen-macro-support" = rec { crateName = "wasm-bindgen-macro-support"; - version = "0.2.123"; + version = "0.2.125"; edition = "2021"; - sha256 = "0nwqyc63byl7rp9nnv45av8h85fncfmxywkvy35d9qwwkfyk93wh"; + sha256 = "1gayzdx5iwl8gllh7ys79wg9cf4iyasl9hrzzhh5m4xx6nfgvkpy"; libName = "wasm_bindgen_macro_support"; authors = [ "The wasm-bindgen Developers" @@ -12545,10 +12537,10 @@ rec { }; "wasm-bindgen-shared" = rec { crateName = "wasm-bindgen-shared"; - version = "0.2.123"; + version = "0.2.125"; edition = "2021"; links = "wasm_bindgen"; - sha256 = "14lvjm3pzywm5c4962i6s5zmngic1knpggshnnxr9c97dihzgjvs"; + sha256 = "07w7fy5qa14ys3p8v2p84h98yqinw713smibz9v7apcspd29x4r3"; libName = "wasm_bindgen_shared"; authors = [ "The wasm-bindgen Developers" @@ -12563,9 +12555,9 @@ rec { }; "web-sys" = rec { crateName = "web-sys"; - version = "0.3.100"; + version = "0.3.102"; edition = "2021"; - sha256 = "0sffbkrpgyi1402mv4wzp9av6ky6rnb1d2m2dpf87wi7yfn7223f"; + sha256 = "0786aybrnwsgdmcynhc2k5ii291a02rq9zk054j35csyvxr0lhx6"; libName = "web_sys"; authors = [ "The wasm-bindgen Developers" @@ -14178,9 +14170,9 @@ rec { }; "zeroize" = rec { crateName = "zeroize"; - version = "1.8.2"; - edition = "2021"; - sha256 = "1l48zxgcv34d7kjskr610zqsm6j2b4fcr2vfh9jm9j1jgvk58wdr"; + version = "1.9.0"; + edition = "2024"; + sha256 = "0kpnij2v1ig6g2mhc0bnci0lrdfdhiq40afbc0fahajqc9jiag71"; authors = [ "The RustCrypto Project Developers" ]; @@ -14202,9 +14194,9 @@ rec { }; "zeroize_derive" = rec { crateName = "zeroize_derive"; - version = "1.4.3"; - edition = "2021"; - sha256 = "0bl5vd1lz27p4z336nximg5wrlw5j7jc8fxh7iv6r1wrhhav99c5"; + version = "1.5.0"; + edition = "2024"; + sha256 = "0a7kq8srk81pn23xqn7c9jw1jpnfy41ffn802x1zrqqgpdf6al1w"; procMacro = true; authors = [ "The RustCrypto Project Developers" From 5c9d391fbebd30adf7b5855d51a9cf37b04303f8 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Mon, 15 Jun 2026 21:42:22 +0200 Subject: [PATCH 49/66] refactor: use ConfigMapName, SecretName, ListenerName in CRD --- extra/crds.yaml | 29 ++++++++++++++++ rust/operator-binary/src/controller.rs | 6 ++-- .../controller/build/properties/listener.rs | 20 +++++------ .../src/controller/build/resource/listener.rs | 2 +- .../controller/build/resource/statefulset.rs | 2 +- .../src/controller/validate.rs | 16 +++------ rust/operator-binary/src/crd/authorization.rs | 15 +++++++-- rust/operator-binary/src/crd/mod.rs | 33 +++++++++++-------- rust/operator-binary/src/crd/role/broker.rs | 19 ++++++++--- rust/operator-binary/src/crd/role/mod.rs | 4 +-- rust/operator-binary/src/crd/security.rs | 33 +++++++++---------- rust/operator-binary/src/crd/tls.rs | 26 +++++++++++---- rust/operator-binary/src/main.rs | 11 +++++-- 13 files changed, 138 insertions(+), 78 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index 6177a6e5..109c5672 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -79,11 +79,17 @@ spec: type: object bootstrapListenerClass: description: The ListenerClass used for bootstrapping new clients. Should use a stable ListenerClass to avoid unnecessary client restarts (such as `cluster-internal` or `external-stable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string brokerListenerClass: description: The ListenerClass used for connecting to brokers. Should use a direct connection ListenerClass to minimize cost and minimize performance overhead (such as `cluster-internal` or `external-unstable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string gracefulShutdownTimeout: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. @@ -608,11 +614,17 @@ spec: type: object bootstrapListenerClass: description: The ListenerClass used for bootstrapping new clients. Should use a stable ListenerClass to avoid unnecessary client restarts (such as `cluster-internal` or `external-stable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string brokerListenerClass: description: The ListenerClass used for connecting to brokers. Should use a direct connection ListenerClass to minimize cost and minimize performance overhead (such as `cluster-internal` or `external-unstable`). + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string gracefulShutdownTimeout: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. @@ -1153,7 +1165,10 @@ spec: ``` This is necessary when migrating from ZooKeeper to Kraft mode to retain existing broker IDs because previously broker ids were generated by Kafka and not the operator. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string metadataManager: description: |- @@ -1204,6 +1219,11 @@ spec: - Which ca.crt to use when validating the other brokers Defaults to `tls` + Set to `null` to disable internal TLS (resulting in a plaintext cluster). + maxLength: 253 + minLength: 1 + nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string serverSecretClass: default: tls @@ -1214,7 +1234,10 @@ spec: - Which cert the servers should use to authenticate themselves against the client Defaults to `tls`. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: object vectorAggregatorConfigMapName: @@ -1223,7 +1246,10 @@ spec: It must contain the key `ADDRESS` with the address of the Vector aggregator. Follow the [logging tutorial](https://docs.stackable.tech/home/nightly/tutorials/logging-vector-aggregator) to learn how to configure log aggregation with Vector. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string zookeeperConfigMapName: description: |- @@ -1232,7 +1258,10 @@ spec: to deploy a ZooKeeper cluster, this will simply be the name of your ZookeeperCluster resource. This can only be used up to Kafka version 3.9.x. Since Kafka 4.0.0, ZooKeeper support was dropped. Please use the 'controller' role instead. + maxLength: 253 + minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: object clusterOperation: diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 41282993..87c00148 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -42,7 +42,7 @@ use stackable_operator::{ kvp::label::{recommended_labels, role_group_selector}, role_group_utils::ResourceNames, types::{ - kubernetes::{NamespaceName, Uid}, + kubernetes::{ConfigMapName, NamespaceName, Uid}, operator::{ClusterName, ControllerName, OperatorName, ProductName, ProductVersion}, }, }, @@ -342,11 +342,11 @@ pub struct ValidatedClusterConfig { /// 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, + 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, + pub broker_id_pod_config_map_name: Option, } impl ValidatedClusterConfig { diff --git a/rust/operator-binary/src/controller/build/properties/listener.rs b/rust/operator-binary/src/controller/build/properties/listener.rs index 5edd8c4d..47112beb 100644 --- a/rust/operator-binary/src/controller/build/properties/listener.rs +++ b/rust/operator-binary/src/controller/build/properties/listener.rs @@ -231,8 +231,8 @@ mod tests { ), }, }]), - "internalTls".to_string(), - Some("tls".to_string()), + Some("internal-tls".parse().unwrap()), + Some("tls".parse().unwrap()), None, ); let cluster_info = default_cluster_info(); @@ -296,8 +296,8 @@ mod tests { let kafka_security = KafkaTlsSecurity::new( ResolvedAuthenticationClasses::new(vec![]), - "tls".to_string(), - Some("tls".to_string()), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), None, ); let config = get_kafka_listener_config( @@ -357,12 +357,8 @@ mod tests { ) ); - let kafka_security = KafkaTlsSecurity::new( - ResolvedAuthenticationClasses::new(vec![]), - "".to_string(), - None, - None, - ); + let kafka_security = + KafkaTlsSecurity::new(ResolvedAuthenticationClasses::new(vec![]), None, None, None); let config = get_kafka_listener_config( &validated, @@ -456,8 +452,8 @@ mod tests { ), }, }]), - "tls".to_string(), - Some("tls".to_string()), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), None, ); let cluster_info = default_cluster_info(); diff --git a/rust/operator-binary/src/controller/build/resource/listener.rs b/rust/operator-binary/src/controller/build/resource/listener.rs index 00b131ab..b13e1de6 100644 --- a/rust/operator-binary/src/controller/build/resource/listener.rs +++ b/rust/operator-binary/src/controller/build/resource/listener.rs @@ -34,7 +34,7 @@ pub fn build_broker_rolegroup_bootstrap_listener( .with_labels(validated_cluster.recommended_labels(role, role_group_name)) .build(), spec: listener::v1alpha1::ListenerSpec { - class_name: Some(merged_config.bootstrap_listener_class.clone()), + class_name: Some(merged_config.bootstrap_listener_class.to_string()), ports: Some(bootstrap_listener_ports(kafka_security)), ..listener::v1alpha1::ListenerSpec::default() }, diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 880099e9..ad47d2e0 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -335,7 +335,7 @@ pub fn build_broker_rolegroup_statefulset( pod_builder .add_listener_volume_by_listener_class( LISTENER_BROKER_VOLUME_NAME, - listener_class, + listener_class.as_ref(), &recommended_labels, ) .context(AddListenerVolumeSnafu)?; diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 969b10e3..2aa50213 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -105,11 +105,6 @@ pub enum Error { "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 @@ -224,17 +219,14 @@ 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`]. + // The Vector aggregator discovery ConfigMap name. Validity is enforced by the `ConfigMapName` + // type on the CRD field. 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)?; + .clone(); let mut role_group_configs: BTreeMap< KafkaRole, diff --git a/rust/operator-binary/src/crd/authorization.rs b/rust/operator-binary/src/crd/authorization.rs index e94ea533..829879a5 100644 --- a/rust/operator-binary/src/crd/authorization.rs +++ b/rust/operator-binary/src/crd/authorization.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ @@ -5,6 +7,7 @@ use stackable_operator::{ commons::opa::{OpaApiVersion, OpaConfig}, k8s_openapi::api::core::v1::ConfigMap, schemars::{self, JsonSchema}, + v2::types::kubernetes::SecretClassName, }; use crate::crd::v1alpha1; @@ -23,6 +26,11 @@ pub enum Error { source: stackable_operator::commons::opa::Error, }, + #[snafu(display("the OPA TLS secret class name (key `OPA_SECRET_CLASS`) is invalid"))] + ParseOpaSecretClass { + source: stackable_operator::v2::macros::attributed_string_type::Error, + }, + #[snafu(display("object defines no namespace"))] ObjectHasNoNamespace, } @@ -36,7 +44,7 @@ pub struct KafkaAuthorization { pub struct KafkaAuthorizationConfig { pub opa_connect: String, - pub secret_class: Option, + pub secret_class: Option, } impl KafkaAuthorization { @@ -60,7 +68,10 @@ impl KafkaAuthorization { namespace, })? .data - .and_then(|mut data| data.remove("OPA_SECRET_CLASS")); + .and_then(|mut data| data.remove("OPA_SECRET_CLASS")) + .map(|secret_class| SecretClassName::from_str(&secret_class)) + .transpose() + .context(ParseOpaSecretClassSnafu)?; let opa_connect = opa .full_document_url_from_config_map(client, kafka, Some("allow"), &OpaApiVersion::V1) .await diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 4cf07d0f..f4b1685c 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -20,7 +20,10 @@ use stackable_operator::{ role_utils::{GenericRoleConfig, Role, RoleGroupRef}, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, - v2::{config_overrides::KeyValueConfigOverrides, role_utils::JavaCommonConfig}, + v2::{ + config_overrides::KeyValueConfigOverrides, role_utils::JavaCommonConfig, + types::kubernetes::ConfigMapName, + }, versioned::versioned, }; use strum::{Display, EnumIter, EnumString}; @@ -163,14 +166,14 @@ pub mod versioned { /// Follow the [logging tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/logging-vector-aggregator) /// to learn how to configure log aggregation with Vector. #[serde(skip_serializing_if = "Option::is_none")] - pub vector_aggregator_config_map_name: Option, + pub vector_aggregator_config_map_name: Option, /// Provide the name of the ZooKeeper [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery) /// here. When using the [Stackable operator for Apache ZooKeeper](DOCS_BASE_URL_PLACEHOLDER/zookeeper/) /// to deploy a ZooKeeper cluster, this will simply be the name of your ZookeeperCluster resource. /// This can only be used up to Kafka version 3.9.x. Since Kafka 4.0.0, ZooKeeper support was dropped. /// Please use the 'controller' role instead. - pub zookeeper_config_map_name: Option, + pub zookeeper_config_map_name: Option, /// Metadata manager to use for the Kafka cluster. /// @@ -221,7 +224,7 @@ pub mod versioned { /// because previously broker ids were generated by Kafka and not the operator. /// #[serde(skip_serializing_if = "Option::is_none")] - pub broker_id_pod_config_map_name: Option, + pub broker_id_pod_config_map_name: Option, } #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Merge, PartialEq, Serialize)] @@ -424,11 +427,13 @@ pub enum MetadataManager { #[cfg(test)] mod tests { use rstest::rstest; - use stackable_operator::versioned::test_utils::RoundtripTestData; + use stackable_operator::{ + v2::types::kubernetes::SecretClassName, versioned::test_utils::RoundtripTestData, + }; use super::*; - fn get_server_secret_class(kafka: &v1alpha1::KafkaCluster) -> Option { + fn get_server_secret_class(kafka: &v1alpha1::KafkaCluster) -> Option { kafka .spec .cluster_config @@ -437,7 +442,7 @@ mod tests { .and_then(|tls| tls.server_secret_class.clone()) } - fn get_internal_secret_class(kafka: &v1alpha1::KafkaCluster) -> String { + fn get_internal_secret_class(kafka: &v1alpha1::KafkaCluster) -> Option { kafka .spec .cluster_config @@ -486,8 +491,8 @@ mod tests { let kafka: v1alpha1::KafkaCluster = serde_yaml::from_str(input).expect("illegal test input"); assert_eq!( - get_server_secret_class(&kafka).unwrap(), - "simple-kafka-server-tls".to_string() + get_server_secret_class(&kafka).unwrap().to_string(), + "simple-kafka-server-tls" ); assert_eq!( get_internal_secret_class(&kafka), @@ -533,8 +538,8 @@ mod tests { serde_yaml::from_str(input).expect("illegal test input"); assert_eq!(get_server_secret_class(&kafka), tls::server_tls_default()); assert_eq!( - get_internal_secret_class(&kafka), - "simple-kafka-internal-tls".to_string() + get_internal_secret_class(&kafka).map(|s| s.to_string()), + Some("simple-kafka-internal-tls".to_string()) ); } @@ -576,8 +581,8 @@ mod tests { serde_yaml::from_str(input).expect("illegal test input"); assert_eq!(get_server_secret_class(&kafka), tls::server_tls_default()); assert_eq!( - get_internal_secret_class(&kafka), - "simple-kafka-internal-tls".to_string() + get_internal_secret_class(&kafka).map(|s| s.to_string()), + Some("simple-kafka-internal-tls".to_string()) ); let input = r#" @@ -596,7 +601,7 @@ mod tests { let kafka: v1alpha1::KafkaCluster = serde_yaml::from_str(input).expect("illegal test input"); assert_eq!( - get_server_secret_class(&kafka), + get_server_secret_class(&kafka).map(|s| s.to_string()), Some("simple-kafka-server-tls".to_string()) ); assert_eq!( diff --git a/rust/operator-binary/src/crd/role/broker.rs b/rust/operator-binary/src/crd/role/broker.rs index 977d91ac..c1eafa34 100644 --- a/rust/operator-binary/src/crd/role/broker.rs +++ b/rust/operator-binary/src/crd/role/broker.rs @@ -8,6 +8,7 @@ use stackable_operator::{ k8s_openapi::apimachinery::pkg::api::resource::Quantity, product_logging::{self, spec::Logging}, schemars::{self, JsonSchema}, + v2::types::kubernetes::ListenerClassName, }; use strum::{Display, EnumIter}; @@ -34,7 +35,7 @@ pub enum BrokerContainer { Kafka, } -#[derive(Clone, Debug, Default, PartialEq, Fragment, JsonSchema)] +#[derive(Clone, Debug, PartialEq, Fragment, JsonSchema)] #[fragment_attrs( derive( Clone, @@ -53,10 +54,10 @@ pub struct BrokerConfig { pub common_config: CommonConfig, /// The ListenerClass used for bootstrapping new clients. Should use a stable ListenerClass to avoid unnecessary client restarts (such as `cluster-internal` or `external-stable`). - pub bootstrap_listener_class: String, + pub bootstrap_listener_class: ListenerClassName, /// The ListenerClass used for connecting to brokers. Should use a direct connection ListenerClass to minimize cost and minimize performance overhead (such as `cluster-internal` or `external-unstable`). - pub broker_listener_class: String, + pub broker_listener_class: ListenerClassName, #[fragment_attrs(serde(default))] pub logging: Logging, @@ -69,8 +70,16 @@ impl BrokerConfig { pub fn default_config(cluster_name: &str, role: &str) -> BrokerConfigFragment { BrokerConfigFragment { common_config: CommonConfig::default_config(cluster_name, role), - bootstrap_listener_class: Some("cluster-internal".to_string()), - broker_listener_class: Some("cluster-internal".to_string()), + bootstrap_listener_class: Some( + "cluster-internal" + .parse() + .expect("\"cluster-internal\" is a valid listener class name"), + ), + broker_listener_class: Some( + "cluster-internal" + .parse() + .expect("\"cluster-internal\" is a valid listener class name"), + ), logging: product_logging::spec::default_logging(), resources: ResourcesFragment { cpu: CpuLimitsFragment { diff --git a/rust/operator-binary/src/crd/role/mod.rs b/rust/operator-binary/src/crd/role/mod.rs index be1d21e3..d607a3b9 100644 --- a/rust/operator-binary/src/crd/role/mod.rs +++ b/rust/operator-binary/src/crd/role/mod.rs @@ -9,7 +9,7 @@ use stackable_operator::{ commons::resources::{NoRuntimeLimits, Resources}, product_logging::spec::ContainerLogConfig, schemars::{self, JsonSchema}, - v2::config_overrides::KeyValueConfigOverrides, + v2::{config_overrides::KeyValueConfigOverrides, types::kubernetes::ListenerClassName}, }; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; @@ -147,7 +147,7 @@ impl AnyConfig { } } - pub fn listener_class(&self) -> Option<&String> { + pub fn listener_class(&self) -> Option<&ListenerClassName> { match self { AnyConfig::Broker(broker_config) => Some(&broker_config.broker_listener_class), AnyConfig::Controller(_) => None, diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/crd/security.rs index cd83aadd..7bd15bab 100644 --- a/rust/operator-binary/src/crd/security.rs +++ b/rust/operator-binary/src/crd/security.rs @@ -20,6 +20,7 @@ use stackable_operator::{ crd::authentication::core, k8s_openapi::api::core::v1::Volume, shared::time::Duration, + v2::types::kubernetes::SecretClassName, }; use super::listener::KafkaListenerProtocol; @@ -59,9 +60,9 @@ pub enum Error { /// Helper struct combining TLS settings for server and internal with the resolved AuthenticationClasses pub struct KafkaTlsSecurity { resolved_authentication_classes: ResolvedAuthenticationClasses, - internal_secret_class: String, - server_secret_class: Option, - opa_secret_class: Option, + internal_secret_class: Option, + server_secret_class: Option, + opa_secret_class: Option, } impl KafkaTlsSecurity { @@ -101,9 +102,9 @@ impl KafkaTlsSecurity { #[cfg(test)] pub fn new( resolved_authentication_classes: ResolvedAuthenticationClasses, - internal_secret_class: String, - server_secret_class: Option, - opa_secret_class: Option, + internal_secret_class: Option, + server_secret_class: Option, + opa_secret_class: Option, ) -> Self { Self { resolved_authentication_classes, @@ -120,7 +121,7 @@ impl KafkaTlsSecurity { pub fn new_from_kafka_cluster( kafka: &v1alpha1::KafkaCluster, resolved_authentication_classes: ResolvedAuthenticationClasses, - opa_secret_class: Option, + opa_secret_class: Option, ) -> Self { KafkaTlsSecurity { resolved_authentication_classes, @@ -154,7 +155,7 @@ impl KafkaTlsSecurity { /// Retrieve an optional TLS secret class for external client -> server communications. pub fn tls_server_secret_class(&self) -> Option<&str> { - self.server_secret_class.as_deref() + self.server_secret_class.as_ref().map(|s| s.as_ref()) } /// Retrieve an optional TLS `AuthenticationClass`. @@ -163,13 +164,11 @@ impl KafkaTlsSecurity { .get_tls_authentication_class() } - /// Retrieve the mandatory internal `SecretClass`. + /// Retrieve the optional internal `SecretClass`. + /// + /// Returns `None` when internal TLS is disabled (a plaintext cluster). pub fn tls_internal_secret_class(&self) -> Option<&str> { - if !self.internal_secret_class.is_empty() { - Some(self.internal_secret_class.as_str()) - } else { - None - } + self.internal_secret_class.as_ref().map(|s| s.as_ref()) } pub fn has_kerberos_enabled(&self) -> bool { @@ -724,16 +723,16 @@ impl KafkaTlsSecurity { } /// Returns the `SecretClass` provided in a `AuthenticationClass` for TLS. - fn get_tls_secret_class(&self) -> Option<&String> { + fn get_tls_secret_class(&self) -> Option<&str> { self.resolved_authentication_classes .get_tls_authentication_class() .and_then(|auth_class| match &auth_class.spec.provider { core::v1alpha1::AuthenticationClassProvider::Tls(tls) => { - tls.client_cert_secret_class.as_ref() + tls.client_cert_secret_class.as_deref() } _ => None, }) - .or(self.server_secret_class.as_ref()) + .or_else(|| self.server_secret_class.as_ref().map(|s| s.as_ref())) } /// Creates ephemeral volumes to mount the `SecretClass` into the Pods for kcat client diff --git a/rust/operator-binary/src/crd/tls.rs b/rust/operator-binary/src/crd/tls.rs index 94843601..d899eef6 100644 --- a/rust/operator-binary/src/crd/tls.rs +++ b/rust/operator-binary/src/crd/tls.rs @@ -1,5 +1,10 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; -use stackable_operator::schemars::{self, JsonSchema}; +use stackable_operator::{ + schemars::{self, JsonSchema}, + v2::types::kubernetes::SecretClassName, +}; const TLS_DEFAULT_SECRET_CLASS: &str = "tls"; @@ -13,8 +18,9 @@ pub struct KafkaTls { /// - Which ca.crt to use when validating the other brokers /// /// Defaults to `tls` + /// Set to `null` to disable internal TLS (resulting in a plaintext cluster). #[serde(default = "internal_tls_default")] - pub internal_secret_class: String, + pub internal_secret_class: Option, /// The [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass.html) to use for /// client connections. This setting controls: /// - If TLS encryption is used at all @@ -25,7 +31,7 @@ pub struct KafkaTls { default = "server_tls_default", skip_serializing_if = "Option::is_none" )] - pub server_secret_class: Option, + pub server_secret_class: Option, } /// Default TLS settings. @@ -37,12 +43,18 @@ pub fn default_kafka_tls() -> Option { }) } +/// The `tls` default secret class as a typed name. +fn default_secret_class() -> SecretClassName { + SecretClassName::from_str(TLS_DEFAULT_SECRET_CLASS) + .expect("the default secret class name is valid") +} + /// Helper methods to provide defaults in the CRDs and tests -pub fn internal_tls_default() -> String { - TLS_DEFAULT_SECRET_CLASS.into() +pub fn internal_tls_default() -> Option { + Some(default_secret_class()) } /// Helper methods to provide defaults in the CRDs and tests -pub fn server_tls_default() -> Option { - Some(TLS_DEFAULT_SECRET_CLASS.into()) +pub fn server_tls_default() -> Option { + Some(default_secret_class()) } diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 7516ad96..7a97394a 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -217,9 +217,16 @@ fn references_config_map( return false; }; - kafka.spec.cluster_config.zookeeper_config_map_name == Some(config_map.name_any()) + let config_map_name = config_map.name_any(); + + kafka + .spec + .cluster_config + .zookeeper_config_map_name + .as_ref() + .is_some_and(|name| name.to_string() == config_map_name) || match &kafka.spec.cluster_config.authorization.opa { - Some(opa_config) => opa_config.config_map_name == config_map.name_any(), + Some(opa_config) => opa_config.config_map_name == config_map_name, None => false, } } From 74dbc4663b72841d540a994d0388713ddd7e4837 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Mon, 15 Jun 2026 21:45:01 +0200 Subject: [PATCH 50/66] refactor: use v2 metrics service helper --- .../src/controller/build/resource/service.rs | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/service.rs b/rust/operator-binary/src/controller/build/resource/service.rs index 4ba8406d..4531ef2a 100644 --- a/rust/operator-binary/src/controller/build/resource/service.rs +++ b/rust/operator-binary/src/controller/build/resource/service.rs @@ -59,11 +59,12 @@ pub fn build_rolegroup_metrics_service( Service { metadata: ObjectMetaBuilder::new() .name_and_namespace(validated_cluster) - .name(metrics_service_name( - validated_cluster, - role, - role_group_name, - )) + .name( + validated_cluster + .resource_names(role, role_group_name) + .metrics_service_name() + .to_string(), + ) .ownerreference(ownerreference_from_resource( validated_cluster, None, @@ -90,24 +91,6 @@ pub fn build_rolegroup_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 { vec![ServicePort { name: Some(METRICS_PORT_NAME.to_string()), From 882eb003724871f569b93a9d02a0c46d01a7d13b Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Mon, 15 Jun 2026 21:51:23 +0200 Subject: [PATCH 51/66] refactor: consolidate rbac resources --- rust/operator-binary/src/controller.rs | 25 ++---- .../src/controller/build/resource/mod.rs | 1 + .../src/controller/build/resource/rbac.rs | 81 +++++++++++++++++++ 3 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/resource/rbac.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 87c00148..6494c4d9 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -17,10 +17,7 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::ClusterResourceApplyStrategy, - commons::{ - networking::DomainName, product_image_selection::ResolvedProductImage, - rbac::build_rbac_resources, - }, + commons::{networking::DomainName, product_image_selection::ResolvedProductImage}, crd::listener, kube::{ Resource, @@ -65,6 +62,7 @@ use crate::{ resource::{ listener::build_broker_rolegroup_bootstrap_listener, pdb::build_pdb, + rbac::{build_rbac_role_binding, build_rbac_service_account}, service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, statefulset::{ build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset, @@ -489,11 +487,6 @@ pub enum Error { source: stackable_operator::client::Error, }, - #[snafu(display("failed to build RBAC resources"))] - BuildRbacResources { - source: stackable_operator::commons::rbac::Error, - }, - #[snafu(display("failed to apply PodDisruptionBudget"))] ApplyPdb { source: stackable_operator::cluster_resources::Error, @@ -541,7 +534,6 @@ impl ReconcilerError for Error { Error::ApplyServiceAccount { .. } => None, Error::ApplyRoleBinding { .. } => None, Error::ApplyStatus { .. } => None, - Error::BuildRbacResources { .. } => None, Error::ApplyPdb { .. } => None, Error::GetRequiredLabels { .. } => None, Error::InvalidKafkaCluster { .. } => None, @@ -596,14 +588,11 @@ pub async fn reconcile_kafka( 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 required_labels = cluster_resources + .get_required_labels() + .context(GetRequiredLabelsSnafu)?; + let rbac_sa = build_rbac_service_account(&validated_cluster, required_labels.clone()); + let rbac_rolebinding = build_rbac_role_binding(&validated_cluster, required_labels); let rbac_sa = cluster_resources .add(client, rbac_sa.clone()) diff --git a/rust/operator-binary/src/controller/build/resource/mod.rs b/rust/operator-binary/src/controller/build/resource/mod.rs index 37b2b92f..2d0977ad 100644 --- a/rust/operator-binary/src/controller/build/resource/mod.rs +++ b/rust/operator-binary/src/controller/build/resource/mod.rs @@ -4,5 +4,6 @@ pub mod config_map; pub mod discovery; pub mod listener; pub mod pdb; +pub mod rbac; pub mod service; pub mod statefulset; diff --git a/rust/operator-binary/src/controller/build/resource/rbac.rs b/rust/operator-binary/src/controller/build/resource/rbac.rs new file mode 100644 index 00000000..fcbacdfe --- /dev/null +++ b/rust/operator-binary/src/controller/build/resource/rbac.rs @@ -0,0 +1,81 @@ +//! Builds the cluster-wide RBAC resources (`ServiceAccount` and `RoleBinding`). +//! +//! The names come from the v2 [`ResourceNames`](stackable_operator::v2::role_utils::ResourceNames) +//! and are identical to the previously used `commons::rbac::build_rbac_resources` +//! (`-serviceaccount`, `-rolebinding`, `-clusterrole`), so switching to +//! this builder does not rename any RBAC objects. + +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + k8s_openapi::api::{ + core::v1::ServiceAccount, + rbac::v1::{RoleBinding, RoleRef, Subject}, + }, + kvp::Labels, + v2::{builder::meta::ownerreference_from_resource, role_utils::ResourceNames}, +}; + +use crate::controller::{ValidatedCluster, product_name}; + +/// Type-safe RBAC resource names for this cluster. +fn rbac_resource_names(validated_cluster: &ValidatedCluster) -> ResourceNames { + ResourceNames { + cluster_name: validated_cluster.name.clone(), + product_name: product_name(), + } +} + +/// Builds the [`ServiceAccount`] shared by all role groups, named `-serviceaccount`. +pub fn build_rbac_service_account( + validated_cluster: &ValidatedCluster, + labels: Labels, +) -> ServiceAccount { + let resource_names = rbac_resource_names(validated_cluster); + + ServiceAccount { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name(resource_names.service_account_name().to_string()) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(labels) + .build(), + ..ServiceAccount::default() + } +} + +/// Builds the [`RoleBinding`] (named `-rolebinding`) that binds the +/// [`ServiceAccount`] to the `-clusterrole` `ClusterRole`. +pub fn build_rbac_role_binding( + validated_cluster: &ValidatedCluster, + labels: Labels, +) -> RoleBinding { + let resource_names = rbac_resource_names(validated_cluster); + + RoleBinding { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(validated_cluster) + .name(resource_names.role_binding_name().to_string()) + .ownerreference(ownerreference_from_resource( + validated_cluster, + None, + Some(true), + )) + .with_labels(labels) + .build(), + role_ref: RoleRef { + api_group: "rbac.authorization.k8s.io".to_string(), + kind: "ClusterRole".to_string(), + name: resource_names.cluster_role_name().to_string(), + }, + subjects: Some(vec![Subject { + kind: "ServiceAccount".to_string(), + name: resource_names.service_account_name().to_string(), + namespace: Some(validated_cluster.namespace.to_string()), + ..Subject::default() + }]), + } +} From b3a02e1132ed0cc7ba45ea4b9b41b64415bb5afc Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Mon, 15 Jun 2026 22:01:42 +0200 Subject: [PATCH 52/66] refactor: use v2 Port --- rust/operator-binary/src/controller.rs | 12 +++++----- .../src/controller/build/properties/mod.rs | 2 +- rust/operator-binary/src/crd/mod.rs | 22 +++++++++++-------- rust/operator-binary/src/crd/security.rs | 20 ++++++++--------- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 6494c4d9..03d521f0 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -191,20 +191,18 @@ impl ValidatedCluster { 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(); + let role_group_statefulset_name = resource_names.stateful_set_name(); + let role_group_service_name = resource_names.headless_service_name(); for replica in 0..validated_rg.replicas { pod_descriptors.push(KafkaPodDescriptor { - namespace: self.namespace.to_string(), - role: role.to_string(), + namespace: self.namespace.clone(), 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, + role: role.clone(), + client_port: client_port.clone(), }); } } diff --git a/rust/operator-binary/src/controller/build/properties/mod.rs b/rust/operator-binary/src/controller/build/properties/mod.rs index c0b530fe..bedf3db8 100644 --- a/rust/operator-binary/src/controller/build/properties/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/mod.rs @@ -59,7 +59,7 @@ pub fn uses_legacy_log4j(product_version: &str) -> bool { pub(crate) fn kraft_controllers(pod_descriptors: &[KafkaPodDescriptor]) -> Vec { pod_descriptors .iter() - .filter(|pd| pd.role == KafkaRole::Controller.to_string()) + .filter(|pd| pd.role == KafkaRole::Controller) .map(|desc| { format!( "{fqdn}:{client_port}", diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index f4b1685c..59e15893 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -21,8 +21,12 @@ use stackable_operator::{ schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, v2::{ - config_overrides::KeyValueConfigOverrides, role_utils::JavaCommonConfig, - types::kubernetes::ConfigMapName, + config_overrides::KeyValueConfigOverrides, + role_utils::JavaCommonConfig, + types::{ + common::Port, + kubernetes::{ConfigMapName, NamespaceName, ServiceName, StatefulSetName}, + }, }, versioned::versioned, }; @@ -40,7 +44,7 @@ pub const OPERATOR_NAME: &str = "kafka.stackable.tech"; pub const FIELD_MANAGER: &str = "kafka-operator"; // metrics pub const METRICS_PORT_NAME: &str = "metrics"; -pub const METRICS_PORT: u16 = 9606; +pub const METRICS_PORT: Port = Port(9606); // env vars pub const KAFKA_HEAP_OPTS: &str = "KAFKA_HEAP_OPTS"; // server_properties @@ -350,16 +354,16 @@ impl v1alpha1::KafkaCluster { /// Reference to a single `Pod` that is a component of a [`KafkaCluster`] /// /// Used for service discovery. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq)] pub struct KafkaPodDescriptor { - pub(crate) namespace: String, - pub(crate) role_group_statefulset_name: String, - pub(crate) role_group_service_name: String, + pub(crate) namespace: NamespaceName, + pub(crate) role_group_statefulset_name: StatefulSetName, + pub(crate) role_group_service_name: ServiceName, pub(crate) replica: u16, pub(crate) cluster_domain: DomainName, pub(crate) node_id: u32, - pub role: String, - pub client_port: u16, + pub role: KafkaRole, + pub client_port: Port, } impl KafkaPodDescriptor { diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/crd/security.rs index 7bd15bab..4a5d5086 100644 --- a/rust/operator-binary/src/crd/security.rs +++ b/rust/operator-binary/src/crd/security.rs @@ -20,7 +20,7 @@ use stackable_operator::{ crd::authentication::core, k8s_openapi::api::core::v1::Volume, shared::time::Duration, - v2::types::kubernetes::SecretClassName, + v2::types::{common::Port, kubernetes::SecretClassName}, }; use super::listener::KafkaListenerProtocol; @@ -66,17 +66,17 @@ pub struct KafkaTlsSecurity { } impl KafkaTlsSecurity { - pub const BOOTSTRAP_PORT: u16 = 9094; + pub const BOOTSTRAP_PORT: Port = Port(9094); // bootstrap: we will have a single named port with different values for // secure (9095) and insecure (9094). The bootstrap listener is needed to // be able to expose principals for both the broker and bootstrap in the // JAAS configuration, so that clients can use both. pub const BOOTSTRAP_PORT_NAME: &'static str = "bootstrap"; - pub const CLIENT_PORT: u16 = 9092; + pub const CLIENT_PORT: Port = Port(9092); // ports pub const CLIENT_PORT_NAME: &'static str = "kafka"; // internal - pub const INTERNAL_PORT: u16 = 19092; + pub const INTERNAL_PORT: Port = Port(19092); // - TLS internal const INTER_BROKER_LISTENER_NAME: &'static str = "inter.broker.listener.name"; // - TLS global @@ -84,10 +84,10 @@ impl KafkaTlsSecurity { const OPA_TLS_MOUNT_PATH: &str = "/stackable/tls-opa"; // opa const OPA_TLS_VOLUME_NAME: &str = "tls-opa"; - pub const SECURE_BOOTSTRAP_PORT: u16 = 9095; - pub const SECURE_CLIENT_PORT: u16 = 9093; + pub const SECURE_BOOTSTRAP_PORT: Port = Port(9095); + pub const SECURE_CLIENT_PORT: Port = Port(9093); pub const SECURE_CLIENT_PORT_NAME: &'static str = "kafka-tls"; - pub const SECURE_INTERNAL_PORT: u16 = 19093; + pub const SECURE_INTERNAL_PORT: Port = Port(19093); const SSL_STORE_PASSWORD: &'static str = ""; const SSL_STORE_TYPE_PKCS12: &'static str = "PKCS12"; const STACKABLE_TLS_KAFKA_INTERNAL_DIR: &'static str = "/stackable/tls-kafka-internal"; @@ -222,7 +222,7 @@ impl KafkaTlsSecurity { } /// Return the Kafka (secure) client port depending on tls or authentication settings. - pub fn client_port(&self) -> u16 { + pub fn client_port(&self) -> Port { if self.tls_enabled() { Self::SECURE_CLIENT_PORT } else { @@ -230,7 +230,7 @@ impl KafkaTlsSecurity { } } - pub fn bootstrap_port(&self) -> u16 { + pub fn bootstrap_port(&self) -> Port { if self.tls_enabled() { Self::SECURE_BOOTSTRAP_PORT } else { @@ -252,7 +252,7 @@ impl KafkaTlsSecurity { } /// Return the Kafka (secure) internal port depending on tls settings. - pub fn internal_port(&self) -> u16 { + pub fn internal_port(&self) -> Port { if self.tls_internal_secret_class().is_some() { Self::SECURE_INTERNAL_PORT } else { From fb5564418c317582738b0dfaa9457309512e5644 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 16 Jun 2026 18:58:49 +0200 Subject: [PATCH 53/66] fix: move to stackable-operator STACKABLE_LOG_DIR --- rust/operator-binary/src/controller/build/command.rs | 3 ++- .../src/controller/build/properties/product_logging/mod.rs | 6 ++---- .../src/controller/build/resource/statefulset.rs | 6 ++++-- rust/operator-binary/src/crd/mod.rs | 1 - 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rust/operator-binary/src/controller/build/command.rs b/rust/operator-binary/src/controller/build/command.rs index 49cb2110..caff517a 100644 --- a/rust/operator-binary/src/controller/build/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -4,12 +4,13 @@ use stackable_operator::{ create_vector_shutdown_file_command, remove_vector_shutdown_file_command, }, utils::COMMON_BASH_TRAP_FUNCTIONS, + v2::product_logging::framework::STACKABLE_LOG_DIR, }; use super::properties::ConfigFileName; use crate::crd::{ BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, - STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, security::KafkaTlsSecurity, + STACKABLE_LOG_CONFIG_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/properties/product_logging/mod.rs b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs index e50f1e2c..908dca9e 100644 --- a/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs @@ -9,13 +9,11 @@ use stackable_operator::{ self, spec::{ContainerLogConfig, ContainerLogConfigChoice}, }, + v2::product_logging::framework::STACKABLE_LOG_DIR, }; use super::ConfigFileName; -use crate::crd::{ - STACKABLE_LOG_DIR, - role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}, -}; +use crate::crd::role::{AnyConfig, broker::BrokerContainer, controller::ControllerContainer}; /// The maximum size of a single Kafka log file before it is rotated. pub const MAX_KAFKA_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index ad47d2e0..7fc1873d 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -34,7 +34,9 @@ use stackable_operator::{ }, }, jvm_argument_overrides::JvmArgumentOverrides, - product_logging::framework::{ValidatedContainerLogConfigChoice, vector_container}, + product_logging::framework::{ + STACKABLE_LOG_DIR, ValidatedContainerLogConfigChoice, vector_container, + }, role_group_utils::ResourceNames, types::kubernetes::{ContainerName, ListenerName, PersistentVolumeClaimName, VolumeName}, }, @@ -59,7 +61,7 @@ use crate::{ 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, STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, STACKABLE_LISTENER_BOOTSTRAP_DIR, - STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR, + STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, role::{ AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, controller::ControllerContainer, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 59e15893..9eec16bb 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -60,7 +60,6 @@ pub const STACKABLE_CONFIG_DIR: &str = "/stackable/config"; 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"; From 3cebee33136b8f63ece62f535f5005e251a5c21d Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 16 Jun 2026 19:04:31 +0200 Subject: [PATCH 54/66] fix: remove hardcoded volume names --- .../controller/build/resource/statefulset.rs | 31 ++++++++++--------- rust/operator-binary/src/crd/mod.rs | 4 +++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 7fc1873d..5647a3d8 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -58,10 +58,11 @@ use crate::{ validate::ValidatedLogging, }, crd::{ - 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, - STACKABLE_CONFIG_DIR, STACKABLE_DATA_DIR, STACKABLE_LISTENER_BOOTSTRAP_DIR, - STACKABLE_LISTENER_BROKER_DIR, STACKABLE_LOG_CONFIG_DIR, + BROKER_ID_POD_MAP_DIR, BROKER_ID_POD_MAP_DIR_NAME, KAFKA_HEAP_OPTS, + LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, LOG_DIRS_VOLUME_NAME, + METRICS_PORT, METRICS_PORT_NAME, STACKABLE_CONFIG_DIR, STACKABLE_CONFIG_DIR_NAME, + STACKABLE_DATA_DIR, STACKABLE_LISTENER_BOOTSTRAP_DIR, STACKABLE_LISTENER_BROKER_DIR, + STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_CONFIG_DIR_NAME, STACKABLE_LOG_DIR_NAME, role::{ AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, controller::ControllerContainer, @@ -269,7 +270,7 @@ pub fn build_broker_rolegroup_statefulset( .add_container_ports(container_ports(kafka_security)) .add_volume_mount(LOG_DIRS_VOLUME_NAME, STACKABLE_DATA_DIR) .context(AddVolumeMountSnafu)? - .add_volume_mount("config", STACKABLE_CONFIG_DIR) + .add_volume_mount(STACKABLE_CONFIG_DIR_NAME, STACKABLE_CONFIG_DIR) .context(AddVolumeMountSnafu)? .add_volume_mount( LISTENER_BOOTSTRAP_VOLUME_NAME, @@ -278,9 +279,9 @@ pub fn build_broker_rolegroup_statefulset( .context(AddVolumeMountSnafu)? .add_volume_mount(LISTENER_BROKER_VOLUME_NAME, STACKABLE_LISTENER_BROKER_DIR) .context(AddVolumeMountSnafu)? - .add_volume_mount("log-config", STACKABLE_LOG_CONFIG_DIR) + .add_volume_mount(STACKABLE_LOG_CONFIG_DIR_NAME, STACKABLE_LOG_CONFIG_DIR) .context(AddVolumeMountSnafu)? - .add_volume_mount("log", STACKABLE_LOG_DIR) + .add_volume_mount(STACKABLE_LOG_DIR_NAME, STACKABLE_LOG_DIR) .context(AddVolumeMountSnafu)? .resources(merged_config.resources().clone().into()); @@ -349,13 +350,13 @@ pub fn build_broker_rolegroup_statefulset( { pod_builder .add_volume( - VolumeBuilder::new("broker-id-pod-map-dir") + VolumeBuilder::new(BROKER_ID_POD_MAP_DIR_NAME) .with_config_map(broker_id_config_map_name) .build(), ) .context(AddVolumeSnafu)?; cb_kafka - .add_volume_mount("broker-id-pod-map-dir", BROKER_ID_POD_MAP_DIR) + .add_volume_mount(BROKER_ID_POD_MAP_DIR_NAME, BROKER_ID_POD_MAP_DIR) .context(AddVolumeMountSnafu)?; } @@ -533,11 +534,11 @@ pub fn build_controller_rolegroup_statefulset( .add_container_ports(container_ports(kafka_security)) .add_volume_mount(LOG_DIRS_VOLUME_NAME, STACKABLE_DATA_DIR) .context(AddVolumeMountSnafu)? - .add_volume_mount("config", STACKABLE_CONFIG_DIR) + .add_volume_mount(STACKABLE_CONFIG_DIR_NAME, STACKABLE_CONFIG_DIR) .context(AddVolumeMountSnafu)? - .add_volume_mount("log-config", STACKABLE_LOG_CONFIG_DIR) + .add_volume_mount(STACKABLE_LOG_CONFIG_DIR_NAME, STACKABLE_LOG_CONFIG_DIR) .context(AddVolumeMountSnafu)? - .add_volume_mount("log", STACKABLE_LOG_DIR) + .add_volume_mount(STACKABLE_LOG_DIR_NAME, STACKABLE_LOG_DIR) .context(AddVolumeMountSnafu)? .resources(merged_config.resources().clone().into()) // TODO: improve probes @@ -727,7 +728,7 @@ fn add_log_config_volume( }; pod_builder .add_volume( - VolumeBuilder::new("log-config") + VolumeBuilder::new(STACKABLE_LOG_CONFIG_DIR_NAME) .with_config_map(config_map) .build(), ) @@ -744,7 +745,7 @@ fn add_common_pod_config( ) -> Result<(), Error> { pod_builder .add_volume(Volume { - name: "config".to_string(), + name: STACKABLE_CONFIG_DIR_NAME.to_string(), config_map: Some(ConfigMapVolumeSource { name: resource_names.role_group_config_map().to_string(), ..ConfigMapVolumeSource::default() @@ -753,7 +754,7 @@ fn add_common_pod_config( }) .context(AddVolumeSnafu)? .add_empty_dir_volume( - "log", + STACKABLE_LOG_DIR_NAME, Some(product_logging::framework::calculate_log_volume_size_limit( &[MAX_KAFKA_LOG_FILES_SIZE], )), diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 9eec16bb..a30dbdc7 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -56,12 +56,16 @@ pub const STACKABLE_LISTENER_BROKER_DIR: &str = "/stackable/listener-broker"; pub const STACKABLE_LISTENER_BOOTSTRAP_DIR: &str = "/stackable/listener-bootstrap"; pub const STACKABLE_DATA_DIR: &str = "/stackable/data"; pub const STACKABLE_CONFIG_DIR: &str = "/stackable/config"; +pub const STACKABLE_CONFIG_DIR_NAME: &str = "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_CONFIG_DIR: &str = "/stackable/log_config"; +pub const STACKABLE_LOG_CONFIG_DIR_NAME: &str = "log-config"; +pub const STACKABLE_LOG_DIR_NAME: &str = "log"; pub const BROKER_ID_POD_MAP_DIR: &str = "/stackable/broker-id-pod-map"; +pub const BROKER_ID_POD_MAP_DIR_NAME: &str = "broker-id-pod-map-dir"; #[derive(Snafu, Debug)] pub enum Error { From 7736b5e14c03ae580b6419269fc5e341fe941b0c Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 16 Jun 2026 19:11:17 +0200 Subject: [PATCH 55/66] refactor: remove more rolegroupref references. --- rust/operator-binary/src/controller.rs | 26 ++++++++----------- .../build/properties/product_logging/mod.rs | 4 +-- .../controller/build/resource/config_map.rs | 6 ++--- rust/operator-binary/src/crd/mod.rs | 21 +-------------- 4 files changed, 17 insertions(+), 40 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 03d521f0..f31ad71b 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -27,7 +27,7 @@ use stackable_operator::{ }, kvp::Labels, logging::controller::ReconcilerError, - role_utils::{GenericRoleConfig, RoleGroupRef}, + role_utils::GenericRoleConfig, shared::time::Duration, status::condition::{ compute_conditions, operations::ClusterOperationsConditionBuilder, @@ -437,22 +437,22 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to apply Service for {}", rolegroup))] + #[snafu(display("failed to apply Service for role group {role_group}"))] ApplyRoleGroupService { source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, }, - #[snafu(display("failed to apply ConfigMap for {}", rolegroup))] + #[snafu(display("failed to apply ConfigMap for role group {role_group}"))] ApplyRoleGroupConfig { source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, }, - #[snafu(display("failed to apply StatefulSet for {}", rolegroup))] + #[snafu(display("failed to apply StatefulSet for role group {role_group}"))] ApplyRoleGroupStatefulSet { source: stackable_operator::cluster_resources::Error, - rolegroup: RoleGroupRef, + role_group: RoleGroupName, }, #[snafu(display("failed to build discovery ConfigMap"))] @@ -605,10 +605,6 @@ pub async fn reconcile_kafka( for (kafka_role, rg_map) in &validated_cluster.role_group_configs { for (rolegroup_name, validated_rg) in rg_map { - // `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()); // 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 @@ -682,19 +678,19 @@ pub async fn reconcile_kafka( .add(client, rg_headless_service) .await .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?; cluster_resources .add(client, rg_metrics_service) .await .with_context(|_| ApplyRoleGroupServiceSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?; cluster_resources .add(client, rg_configmap) .await .with_context(|_| ApplyRoleGroupConfigSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?; // Note: The StatefulSet needs to be applied after all ConfigMaps and Secrets it mounts @@ -705,7 +701,7 @@ pub async fn reconcile_kafka( .add(client, rg_statefulset) .await .with_context(|_| ApplyRoleGroupStatefulSetSnafu { - rolegroup: rolegroup_ref.clone(), + role_group: rolegroup_name.clone(), })?, ); } diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs index 908dca9e..ded68638 100644 --- a/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs @@ -41,8 +41,8 @@ const CONSOLE_CONVERSION_PATTERN_LOG4J2: &str = "%d{ISO8601} %p [%t] %c - %m%n"; /// 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. +/// The Vector agent config (`vector.yaml`) is provided separately via +/// [`vector_config_file_content`] and added by the caller. pub fn role_group_config_map_data( product_version: &str, merged_config: &AnyConfig, 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 88ea7cd5..0ec3e075 100644 --- a/rust/operator-binary/src/controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -58,9 +58,9 @@ pub enum Error { /// 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`. +/// `vector_config` is the static Vector agent config (`vector.yaml`) added by the caller; 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, role_group_name: &RoleGroupName, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index a30dbdc7..5bae372f 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -17,7 +17,7 @@ use stackable_operator::{ config::merge::Merge, deep_merger::ObjectOverrides, kube::{CustomResource, runtime::reflector::ObjectRef}, - role_utils::{GenericRoleConfig, Role, RoleGroupRef}, + role_utils::{GenericRoleConfig, Role}, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, v2::{ @@ -321,19 +321,6 @@ impl v1alpha1::KafkaCluster { }) } - /// Metadata about a rolegroup - pub fn rolegroup_ref( - &self, - role: &KafkaRole, - group_name: impl Into, - ) -> RoleGroupRef { - RoleGroupRef { - cluster: ObjectRef::from_obj(self), - role: role.to_string(), - role_group: group_name.into(), - } - } - pub fn role_config(&self, role: &KafkaRole) -> Option<&GenericRoleConfig> { match role { KafkaRole::Broker => self.spec.brokers.as_ref().map(|b| &b.role_config), @@ -346,12 +333,6 @@ impl v1alpha1::KafkaCluster { role: KafkaRole::Broker.to_string(), }) } - - pub fn controller_role(&self) -> Result<&ControllerRole, Error> { - self.spec.controllers.as_ref().context(MissingRoleSnafu { - role: KafkaRole::Controller.to_string(), - }) - } } /// Reference to a single `Pod` that is a component of a [`KafkaCluster`] From 8be7586e13073aaf1a73864c0a9ab31525cf8b72 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 16 Jun 2026 19:16:35 +0200 Subject: [PATCH 56/66] refactor: introduce ValidatedRoleConfig --- rust/operator-binary/src/controller.rs | 22 ++++++++++++++----- .../src/controller/validate.rs | 22 +++++++++++++++---- rust/operator-binary/src/crd/mod.rs | 7 ------ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index f31ad71b..c631ce3d 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -27,7 +27,6 @@ use stackable_operator::{ }, kvp::Labels, logging::controller::ReconcilerError, - role_utils::GenericRoleConfig, shared::time::Duration, status::condition::{ compute_conditions, operations::ClusterOperationsConditionBuilder, @@ -119,10 +118,13 @@ pub struct ValidatedCluster { /// value. pub product_version: ProductVersion, pub cluster_config: ValidatedClusterConfig, + /// Per-role configuration (e.g. the Pod disruption budget), keyed by role. + pub role_configs: BTreeMap, pub role_group_configs: BTreeMap>, } impl ValidatedCluster { + #[allow(clippy::too_many_arguments)] pub fn new( name: ClusterName, namespace: NamespaceName, @@ -130,6 +132,7 @@ impl ValidatedCluster { cluster_domain: DomainName, image: ResolvedProductImage, cluster_config: ValidatedClusterConfig, + role_configs: BTreeMap, role_group_configs: BTreeMap>, ) -> Self { // `app_version_label_value` is constructed to be a valid label value, so it is also a @@ -150,6 +153,7 @@ impl ValidatedCluster { image, product_version, cluster_config, + role_configs, role_group_configs, } } @@ -365,6 +369,15 @@ impl ValidatedClusterConfig { } } +/// Per-role configuration extracted during validation. +/// +/// Resolved from the raw [`v1alpha1::KafkaCluster`] spec during validation so the reconcile loop +/// never has to read it. Kafka's `GenericRoleConfig` only carries the Pod disruption budget. +#[derive(Clone, Debug)] +pub struct ValidatedRoleConfig { + pub pdb: stackable_operator::commons::pdb::PdbConfig, +} + /// 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 { @@ -706,11 +719,8 @@ pub async fn reconcile_kafka( ); } - let role_cfg = kafka.role_config(kafka_role); - if let Some(GenericRoleConfig { - pod_disruption_budget: pdb, - }) = role_cfg - && let Some(pdb) = build_pdb(pdb, &validated_cluster, kafka_role) + if let Some(role_config) = validated_cluster.role_configs.get(kafka_role) + && let Some(pdb) = build_pdb(&role_config.pdb, &validated_cluster, kafka_role) { cluster_resources .add(client, pdb) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 2aa50213..5e012236 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -29,8 +29,8 @@ use stackable_operator::{ use crate::{ controller::{ - RoleGroupName, ValidatedCluster, ValidatedClusterConfig, ValidatedRoleGroupConfig, - dereference::DereferencedObjects, + RoleGroupName, ValidatedCluster, ValidatedClusterConfig, ValidatedRoleConfig, + ValidatedRoleGroupConfig, dereference::DereferencedObjects, }, crd::{ self, CONTAINER_IMAGE_BASE_NAME, @@ -228,6 +228,7 @@ pub fn validate( .vector_aggregator_config_map_name .clone(); + let mut role_configs: BTreeMap = BTreeMap::new(); let mut role_group_configs: BTreeMap< KafkaRole, BTreeMap, @@ -246,10 +247,16 @@ pub fn validate( validate_broker_logging, &vector_aggregator_config_map_name, )?; + role_configs.insert( + KafkaRole::Broker, + ValidatedRoleConfig { + pdb: broker_role.role_config.pod_disruption_budget.clone(), + }, + ); role_group_configs.insert(KafkaRole::Broker, broker_groups); - // Controllers are optional: ZooKeeper-mode clusters have none, and `controller_role()` - // errors when `controllers` is unset, which would stop their reconciliation. + // Controllers are optional: ZooKeeper-mode clusters have none, in which case they are simply + // absent from both maps and not reconciled. if let Some(controller_role) = kafka.spec.controllers.as_ref() { let controller_groups = validate_role_group_configs( controller_role, @@ -260,6 +267,12 @@ pub fn validate( validate_controller_logging, &vector_aggregator_config_map_name, )?; + role_configs.insert( + KafkaRole::Controller, + ValidatedRoleConfig { + pdb: controller_role.role_config.pod_disruption_budget.clone(), + }, + ); role_group_configs.insert(KafkaRole::Controller, controller_groups); } @@ -292,6 +305,7 @@ pub fn validate( .broker_id_pod_config_map_name .clone(), }, + role_configs, role_group_configs, )) } diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 5bae372f..791c652b 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -321,13 +321,6 @@ impl v1alpha1::KafkaCluster { }) } - pub fn role_config(&self, role: &KafkaRole) -> Option<&GenericRoleConfig> { - match role { - KafkaRole::Broker => self.spec.brokers.as_ref().map(|b| &b.role_config), - KafkaRole::Controller => self.spec.controllers.as_ref().map(|b| &b.role_config), - } - } - pub fn broker_role(&self) -> Result<&BrokerRole, Error> { self.spec.brokers.as_ref().context(MissingRoleSnafu { role: KafkaRole::Broker.to_string(), From ebcb5bc57bd5996418c79ade37fd32ed6b9d7df9 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Tue, 16 Jun 2026 19:20:54 +0200 Subject: [PATCH 57/66] fix: remove v2 references in comments --- rust/operator-binary/src/controller.rs | 3 +-- .../src/controller/build/properties/product_logging/mod.rs | 2 +- rust/operator-binary/src/controller/build/resource/rbac.rs | 2 +- .../src/controller/build/resource/statefulset.rs | 4 ++-- rust/operator-binary/src/controller/validate.rs | 6 +++--- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index c631ce3d..d4f1fd6b 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -50,7 +50,7 @@ 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 +/// The type-safe role-group name from stackable-operator. Re-exported so the rest /// of the operator can refer to it as `controller::RoleGroupName`. pub use stackable_operator::v2::types::operator::{RoleGroupName, RoleName}; @@ -101,7 +101,6 @@ pub enum PodDescriptorsError { /// 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. diff --git a/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs index ded68638..e643b197 100644 --- a/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs +++ b/rust/operator-binary/src/controller/build/properties/product_logging/mod.rs @@ -26,7 +26,7 @@ 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) +/// The [`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"); diff --git a/rust/operator-binary/src/controller/build/resource/rbac.rs b/rust/operator-binary/src/controller/build/resource/rbac.rs index fcbacdfe..175148f3 100644 --- a/rust/operator-binary/src/controller/build/resource/rbac.rs +++ b/rust/operator-binary/src/controller/build/resource/rbac.rs @@ -1,6 +1,6 @@ //! Builds the cluster-wide RBAC resources (`ServiceAccount` and `RoleBinding`). //! -//! The names come from the v2 [`ResourceNames`](stackable_operator::v2::role_utils::ResourceNames) +//! The names come from [`ResourceNames`](stackable_operator::v2::role_utils::ResourceNames) //! and are identical to the previously used `commons::rbac::build_rbac_resources` //! (`-serviceaccount`, `-rolebinding`, `-clusterrole`), so switching to //! this builder does not rename any RBAC objects. diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 5647a3d8..68600c9a 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -765,12 +765,12 @@ fn add_common_pod_config( Ok(()) } -/// Adds the v2 Vector log-aggregation sidecar container, when the Vector agent is enabled. +/// Adds the Vector log-aggregation sidecar container, when the Vector agent is enabled. /// /// 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 +/// static `vector.yaml` from the `config` volume and is driven by the env vars the /// [`vector_container`] sets. fn add_vector_container( pod_builder: &mut PodBuilder, diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 5e012236..01326393 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -110,9 +110,9 @@ pub enum 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. +/// Produced up-front by [`validate_logging`] 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, From 8cfb5f97cef70dead188c18a8f81ad0f248f280f Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 14:33:23 +0200 Subject: [PATCH 58/66] refactor: move security mode to controller, rename KafkaTlsSecurity -> ValidatedKafkaSecurity --- rust/operator-binary/src/controller.rs | 5 ++-- .../src/controller/build/command.rs | 11 +++++--- .../src/controller/build/kerberos.rs | 11 +++++--- .../controller/build/properties/listener.rs | 27 ++++++++++--------- .../src/controller/build/resource/listener.rs | 9 +++---- .../src/controller/build/resource/service.rs | 8 +++--- .../controller/build/resource/statefulset.rs | 8 +++--- .../src/{crd => controller}/security.rs | 13 ++++----- .../src/controller/validate.rs | 12 ++++++--- rust/operator-binary/src/crd/mod.rs | 1 - 10 files changed, 59 insertions(+), 46 deletions(-) rename rust/operator-binary/src/{crd => controller}/security.rs (99%) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index d4f1fd6b..3f723efb 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -48,6 +48,7 @@ use strum::{EnumDiscriminants, IntoStaticStr}; pub(crate) mod build; pub(crate) mod dereference; pub(crate) mod node_id_hasher; +pub(crate) mod security; pub(crate) mod validate; /// The type-safe role-group name from stackable-operator. Re-exported so the rest @@ -69,12 +70,12 @@ use crate::{ }, }, node_id_hasher::node_id_hash32_offset, + security::ValidatedKafkaSecurity, }, crd::{ APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, authorization::KafkaAuthorizationConfig, role::{AnyConfig, AnyConfigOverrides, KafkaRole}, - security::KafkaTlsSecurity, v1alpha1, }, }; @@ -334,7 +335,7 @@ pub(crate) fn controller_name() -> ControllerName { /// 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 kafka_security: ValidatedKafkaSecurity, pub authorization_config: Option, pub metadata_manager: MetadataManager, diff --git a/rust/operator-binary/src/controller/build/command.rs b/rust/operator-binary/src/controller/build/command.rs index caff517a..ab1a8d0b 100644 --- a/rust/operator-binary/src/controller/build/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -8,9 +8,12 @@ use stackable_operator::{ }; use super::properties::ConfigFileName; -use crate::crd::{ - BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, - STACKABLE_LOG_CONFIG_DIR, security::KafkaTlsSecurity, +use crate::{ + controller::security::ValidatedKafkaSecurity, + crd::{ + BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, + STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LOG_CONFIG_DIR, + }, }; /// The JVM options selecting the Kafka log4j/log4j2 config file. Kafka 3.x uses log4j, @@ -38,7 +41,7 @@ pub fn kafka_log_opts_env_var() -> String { pub fn broker_kafka_container_commands( kraft_mode: bool, controller_descriptors: Vec, - kafka_security: &KafkaTlsSecurity, + kafka_security: &ValidatedKafkaSecurity, product_version: &str, ) -> String { formatdoc! {" diff --git a/rust/operator-binary/src/controller/build/kerberos.rs b/rust/operator-binary/src/controller/build/kerberos.rs index 971cee5c..676c45b0 100644 --- a/rust/operator-binary/src/controller/build/kerberos.rs +++ b/rust/operator-binary/src/controller/build/kerberos.rs @@ -14,9 +14,12 @@ use stackable_operator::{ commons::secret_class::SecretClassVolumeProvisionParts, }; -use crate::crd::{ - LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_DIR, - STACKABLE_KERBEROS_KRB5_PATH, role::KafkaRole, security::KafkaTlsSecurity, +use crate::{ + controller::security::ValidatedKafkaSecurity, + crd::{ + LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_DIR, + STACKABLE_KERBEROS_KRB5_PATH, role::KafkaRole, + }, }; #[derive(Snafu, Debug)] @@ -36,7 +39,7 @@ pub enum Error { } pub fn add_kerberos_pod_config( - kafka_security: &KafkaTlsSecurity, + kafka_security: &ValidatedKafkaSecurity, role: &KafkaRole, cb_kcat_prober: &mut ContainerBuilder, cb_kafka: &mut ContainerBuilder, diff --git a/rust/operator-binary/src/controller/build/properties/listener.rs b/rust/operator-binary/src/controller/build/properties/listener.rs index 47112beb..ab8bd205 100644 --- a/rust/operator-binary/src/controller/build/properties/listener.rs +++ b/rust/operator-binary/src/controller/build/properties/listener.rs @@ -11,7 +11,7 @@ use stackable_operator::{ }; use crate::{ - controller::{RoleGroupName, ValidatedCluster}, + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, crd::{ STACKABLE_LISTENER_BROKER_DIR, listener::{ @@ -19,13 +19,12 @@ use crate::{ 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, + kafka_security: &ValidatedKafkaSecurity, role: &KafkaRole, role_group_name: &RoleGroupName, cluster_info: &KubernetesClusterInfo, @@ -49,7 +48,7 @@ pub fn get_kafka_listener_config( listeners.push(KafkaListener { name: KafkaListenerName::Client, host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::SECURE_CLIENT_PORT.to_string(), + port: ValidatedKafkaSecurity::SECURE_CLIENT_PORT.to_string(), }); advertised_listeners.push(KafkaListener { name: KafkaListenerName::Client, @@ -85,7 +84,7 @@ pub fn get_kafka_listener_config( listeners.push(KafkaListener { name: KafkaListenerName::Client, host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::CLIENT_PORT.to_string(), + port: ValidatedKafkaSecurity::CLIENT_PORT.to_string(), }); advertised_listeners.push(KafkaListener { name: KafkaListenerName::Client, @@ -106,12 +105,12 @@ pub fn get_kafka_listener_config( listeners.push(KafkaListener { name: KafkaListenerName::Internal, host: LISTENER_LOCAL_ADDRESS.to_string(), - port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), + port: ValidatedKafkaSecurity::SECURE_INTERNAL_PORT.to_string(), }); advertised_listeners.push(KafkaListener { name: KafkaListenerName::Internal, host: pod_fqdn.to_string(), - port: KafkaTlsSecurity::SECURE_INTERNAL_PORT.to_string(), + port: ValidatedKafkaSecurity::SECURE_INTERNAL_PORT.to_string(), }); listener_security_protocol_map .insert(KafkaListenerName::Internal, KafkaListenerProtocol::Ssl); @@ -220,7 +219,7 @@ mod tests { "#; let kafka = minimal_kafka(kafka_cluster); let validated = validated_cluster(&kafka); - let kafka_security = KafkaTlsSecurity::new( + let kafka_security = ValidatedKafkaSecurity::new( ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { metadata: ObjectMetaBuilder::new().name("auth-class").build(), spec: core::v1alpha1::AuthenticationClassSpec { @@ -294,7 +293,7 @@ mod tests { ) ); - let kafka_security = KafkaTlsSecurity::new( + let kafka_security = ValidatedKafkaSecurity::new( ResolvedAuthenticationClasses::new(vec![]), Some("tls".parse().unwrap()), Some("tls".parse().unwrap()), @@ -357,8 +356,12 @@ mod tests { ) ); - let kafka_security = - KafkaTlsSecurity::new(ResolvedAuthenticationClasses::new(vec![]), None, None, None); + let kafka_security = ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![]), + None, + None, + None, + ); let config = get_kafka_listener_config( &validated, @@ -441,7 +444,7 @@ mod tests { "#; let kafka = minimal_kafka(kafka_cluster); let validated = validated_cluster(&kafka); - let kafka_security = KafkaTlsSecurity::new( + let kafka_security = ValidatedKafkaSecurity::new( ResolvedAuthenticationClasses::new(vec![core::v1alpha1::AuthenticationClass { metadata: ObjectMetaBuilder::new().name("auth-class").build(), spec: core::v1alpha1::AuthenticationClassSpec { diff --git a/rust/operator-binary/src/controller/build/resource/listener.rs b/rust/operator-binary/src/controller/build/resource/listener.rs index b13e1de6..6e5adc88 100644 --- a/rust/operator-binary/src/controller/build/resource/listener.rs +++ b/rust/operator-binary/src/controller/build/resource/listener.rs @@ -4,11 +4,8 @@ use stackable_operator::{ }; use crate::{ - controller::{RoleGroupName, ValidatedCluster}, - crd::{ - role::{KafkaRole, broker::BrokerConfig}, - security::KafkaTlsSecurity, - }, + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, + crd::role::{KafkaRole, broker::BrokerConfig}, }; /// Kafka clients will use the load-balanced bootstrap listener to get a list of broker addresses and will use those to @@ -43,7 +40,7 @@ pub fn build_broker_rolegroup_bootstrap_listener( } fn bootstrap_listener_ports( - kafka_security: &KafkaTlsSecurity, + kafka_security: &ValidatedKafkaSecurity, ) -> Vec { vec![if kafka_security.has_kerberos_enabled() { listener::v1alpha1::ListenerPort { diff --git a/rust/operator-binary/src/controller/build/resource/service.rs b/rust/operator-binary/src/controller/build/resource/service.rs index 4531ef2a..60f10b16 100644 --- a/rust/operator-binary/src/controller/build/resource/service.rs +++ b/rust/operator-binary/src/controller/build/resource/service.rs @@ -6,8 +6,8 @@ use stackable_operator::{ }; use crate::{ - controller::{RoleGroupName, ValidatedCluster}, - crd::{METRICS_PORT, METRICS_PORT_NAME, role::KafkaRole, security::KafkaTlsSecurity}, + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, + crd::{METRICS_PORT, METRICS_PORT_NAME, role::KafkaRole}, }; /// The rolegroup [`Service`] is a headless service that allows direct access to the instances of a certain rolegroup @@ -17,7 +17,7 @@ pub fn build_rolegroup_headless_service( validated_cluster: &ValidatedCluster, role: &KafkaRole, role_group_name: &RoleGroupName, - kafka_security: &KafkaTlsSecurity, + kafka_security: &ValidatedKafkaSecurity, ) -> Service { Service { metadata: ObjectMetaBuilder::new() @@ -100,7 +100,7 @@ fn metrics_ports() -> Vec { }] } -fn headless_ports(kafka_security: &KafkaTlsSecurity) -> Vec { +fn headless_ports(kafka_security: &ValidatedKafkaSecurity) -> Vec { vec![ServicePort { name: Some(kafka_security.client_port_name().into()), port: kafka_security.client_port().into(), diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 68600c9a..dcc3a5ae 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -55,6 +55,7 @@ use crate::{ properties::product_logging::MAX_KAFKA_LOG_FILES_SIZE, }, node_id_hasher::node_id_hash32_offset, + security::ValidatedKafkaSecurity, validate::ValidatedLogging, }, crd::{ @@ -67,7 +68,6 @@ use crate::{ AnyConfig, KAFKA_NODE_ID_OFFSET, KafkaRole, broker::BrokerContainer, controller::ControllerContainer, }, - security::KafkaTlsSecurity, }, }; @@ -90,7 +90,9 @@ pub enum Error { }, #[snafu(display("failed to add Secret Volumes and VolumeMounts"))] - AddVolumesAndVolumeMounts { source: crate::crd::security::Error }, + AddVolumesAndVolumeMounts { + source: crate::controller::security::Error, + }, #[snafu(display("failed to add needed volumeMount"))] AddVolumeMount { @@ -643,7 +645,7 @@ pub fn build_controller_rolegroup_statefulset( } /// We only expose client HTTP / HTTPS and Metrics ports. -fn container_ports(kafka_security: &KafkaTlsSecurity) -> Vec { +fn container_ports(kafka_security: &ValidatedKafkaSecurity) -> Vec { let mut ports = vec![ ContainerPort { name: Some(METRICS_PORT_NAME.to_string()), diff --git a/rust/operator-binary/src/crd/security.rs b/rust/operator-binary/src/controller/security.rs similarity index 99% rename from rust/operator-binary/src/crd/security.rs rename to rust/operator-binary/src/controller/security.rs index 4a5d5086..154f798b 100644 --- a/rust/operator-binary/src/crd/security.rs +++ b/rust/operator-binary/src/controller/security.rs @@ -23,12 +23,13 @@ use stackable_operator::{ v2::types::{common::Port, kubernetes::SecretClassName}, }; -use super::listener::KafkaListenerProtocol; use crate::crd::{ LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LISTENER_BROKER_DIR, authentication::ResolvedAuthenticationClasses, - listener::{self, KafkaListenerName, node_address_cmd_env, node_port_cmd_env}, + listener::{ + self, KafkaListenerName, KafkaListenerProtocol, node_address_cmd_env, node_port_cmd_env, + }, role::KafkaRole, tls, v1alpha1, }; @@ -58,14 +59,14 @@ pub enum Error { } /// Helper struct combining TLS settings for server and internal with the resolved AuthenticationClasses -pub struct KafkaTlsSecurity { +pub struct ValidatedKafkaSecurity { resolved_authentication_classes: ResolvedAuthenticationClasses, internal_secret_class: Option, server_secret_class: Option, opa_secret_class: Option, } -impl KafkaTlsSecurity { +impl ValidatedKafkaSecurity { pub const BOOTSTRAP_PORT: Port = Port(9094); // bootstrap: we will have a single named port with different values for // secure (9095) and insecure (9094). The bootstrap listener is needed to @@ -114,7 +115,7 @@ impl KafkaTlsSecurity { } } - /// Build a [`KafkaTlsSecurity`] from already-resolved authentication classes. + /// Build a [`ValidatedKafkaSecurity`] from already-resolved authentication classes. /// /// The async retrieval of [`ResolvedAuthenticationClasses`] now happens in the dereference /// step of the controller; this constructor only reads TLS settings from the spec. @@ -123,7 +124,7 @@ impl KafkaTlsSecurity { resolved_authentication_classes: ResolvedAuthenticationClasses, opa_secret_class: Option, ) -> Self { - KafkaTlsSecurity { + ValidatedKafkaSecurity { resolved_authentication_classes, internal_secret_class: kafka .spec diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 01326393..252923f9 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -30,7 +30,9 @@ use stackable_operator::{ use crate::{ controller::{ RoleGroupName, ValidatedCluster, ValidatedClusterConfig, ValidatedRoleConfig, - ValidatedRoleGroupConfig, dereference::DereferencedObjects, + ValidatedRoleGroupConfig, + dereference::DereferencedObjects, + security::{self, ValidatedKafkaSecurity}, }, crd::{ self, CONTAINER_IMAGE_BASE_NAME, @@ -40,7 +42,6 @@ use crate::{ broker::{BrokerConfig, BrokerContainer}, controller::{ControllerConfig, ControllerContainer}, }, - security::{self, KafkaTlsSecurity}, v1alpha1, }, }; @@ -210,8 +211,11 @@ pub fn validate( .as_ref() .and_then(|cfg| cfg.secret_class.clone()); - let kafka_security = - KafkaTlsSecurity::new_from_kafka_cluster(kafka, authentication_classes, opa_secret_class); + let kafka_security = ValidatedKafkaSecurity::new_from_kafka_cluster( + kafka, + authentication_classes, + opa_secret_class, + ); kafka_security .validate_authentication_methods() diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 791c652b..edd553b4 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -3,7 +3,6 @@ pub mod authentication; pub mod authorization; pub mod listener; pub mod role; -pub mod security; pub mod tls; use authentication::KafkaAuthentication; From ec869a7fcb2e9af1baa2ee0c5fde676f4d32f019 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 15:32:17 +0200 Subject: [PATCH 59/66] refactor: extract build logic from KafkaSecurity --- .../src/controller/build/command.rs | 4 +- .../src/controller/build/mod.rs | 1 + .../build/properties/broker_properties.rs | 8 +- .../build/properties/controller_properties.rs | 8 +- .../controller/build/resource/config_map.rs | 10 +- .../controller/build/resource/statefulset.rs | 38 +- .../src/controller/build/security.rs | 642 ++++++++++++++++++ .../src/controller/security.rs | 642 +----------------- 8 files changed, 689 insertions(+), 664 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/security.rs diff --git a/rust/operator-binary/src/controller/build/command.rs b/rust/operator-binary/src/controller/build/command.rs index ab1a8d0b..4805a623 100644 --- a/rust/operator-binary/src/controller/build/command.rs +++ b/rust/operator-binary/src/controller/build/command.rs @@ -9,7 +9,7 @@ use stackable_operator::{ use super::properties::ConfigFileName; use crate::{ - controller::security::ValidatedKafkaSecurity, + controller::{build::security::copy_opa_tls_cert_command, security::ValidatedKafkaSecurity}, crd::{ BROKER_ID_POD_MAP_DIR, KafkaPodDescriptor, STACKABLE_CONFIG_DIR, STACKABLE_KERBEROS_KRB5_PATH, STACKABLE_LOG_CONFIG_DIR, @@ -64,7 +64,7 @@ pub fn broker_kafka_container_commands( true => format!("export KERBEROS_REALM=$(grep -oP 'default_realm = \\K.*' {STACKABLE_KERBEROS_KRB5_PATH})"), false => "".to_string(), }, - import_opa_tls_cert = kafka_security.copy_opa_tls_cert_command(), + import_opa_tls_cert = copy_opa_tls_cert_command(kafka_security), broker_start_command = broker_start_command(kraft_mode, controller_descriptors, product_version), } } diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index 5b1e0bbc..5e837150 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -6,3 +6,4 @@ pub mod jvm; pub mod kerberos; pub mod properties; pub mod resource; +pub mod security; 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 d8abf628..ec7d6957 100644 --- a/rust/operator-binary/src/controller/build/properties/broker_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/broker_properties.rs @@ -3,7 +3,11 @@ use std::collections::BTreeMap; use super::kraft_controllers; use crate::{ controller::{ - ValidatedClusterConfig, build::graceful_shutdown::graceful_shutdown_config_properties, + ValidatedClusterConfig, + build::{ + graceful_shutdown::graceful_shutdown_config_properties, + security::broker_config_settings, + }, }, crd::{ KafkaPodDescriptor, @@ -106,7 +110,7 @@ pub fn build( ]); } - result.extend(cluster_config.kafka_security.broker_config_settings()); + result.extend(broker_config_settings(&cluster_config.kafka_security)); 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 93fce3c6..4044e4f6 100644 --- a/rust/operator-binary/src/controller/build/properties/controller_properties.rs +++ b/rust/operator-binary/src/controller/build/properties/controller_properties.rs @@ -3,7 +3,11 @@ use std::collections::BTreeMap; use super::kraft_controllers; use crate::{ controller::{ - ValidatedClusterConfig, build::graceful_shutdown::graceful_shutdown_config_properties, + ValidatedClusterConfig, + build::{ + graceful_shutdown::graceful_shutdown_config_properties, + security::controller_config_settings, + }, }, crd::{ KafkaPodDescriptor, @@ -65,7 +69,7 @@ pub fn build( ); } - result.extend(cluster_config.kafka_security.controller_config_settings()); + result.extend(controller_config_settings(&cluster_config.kafka_security)); result.extend(graceful_shutdown_config_properties()); result.extend(overrides); 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 0ec3e075..551db138 100644 --- a/rust/operator-binary/src/controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -13,8 +13,11 @@ use stackable_operator::{ use crate::{ controller::{ RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, - build::properties::{ - ConfigFileName, config_file_name, product_logging::role_group_config_map_data, + build::{ + properties::{ + ConfigFileName, config_file_name, product_logging::role_group_config_map_data, + }, + security::client_properties, }, }, crd::{ @@ -152,8 +155,7 @@ pub fn build_rolegroup_config_map( .add_data( ConfigFileName::Client.to_string(), to_java_properties_string( - kafka_security - .client_properties() + client_properties(kafka_security) .iter() .filter_map(|(k, v)| v.as_ref().map(|v| (k, v))), ) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index dcc3a5ae..d332e81a 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -53,6 +53,10 @@ use crate::{ graceful_shutdown::add_graceful_shutdown_config, kerberos::add_kerberos_pod_config, properties::product_logging::MAX_KAFKA_LOG_FILES_SIZE, + security::{ + add_broker_volume_and_volume_mounts, add_controller_volume_and_volume_mounts, + kcat_prober_container_commands, + }, }, node_id_hasher::node_id_hash32_offset, security::ValidatedKafkaSecurity, @@ -91,7 +95,7 @@ pub enum Error { #[snafu(display("failed to add Secret Volumes and VolumeMounts"))] AddVolumesAndVolumeMounts { - source: crate::controller::security::Error, + source: crate::controller::build::security::Error, }, #[snafu(display("failed to add needed volumeMount"))] @@ -168,14 +172,14 @@ pub fn build_broker_rolegroup_statefulset( .deref() .requested_secret_lifetime .context(MissingSecretLifetimeSnafu)?; - kafka_security - .add_broker_volume_and_volume_mounts( - &mut pod_builder, - &mut cb_kcat_prober, - &mut cb_kafka, - &requested_secret_lifetime, - ) - .context(AddVolumesAndVolumeMountsSnafu)?; + add_broker_volume_and_volume_mounts( + kafka_security, + &mut pod_builder, + &mut cb_kcat_prober, + &mut cb_kafka, + &requested_secret_lifetime, + ) + .context(AddVolumesAndVolumeMountsSnafu)?; let mut pvcs = merged_config.resources().storage.build_pvcs(); @@ -323,7 +327,7 @@ pub fn build_broker_rolegroup_statefulset( .readiness_probe(Probe { exec: Some(ExecAction { // If the broker is able to get its fellow cluster members then it has at least completed basic registration at some point - command: Some(kafka_security.kcat_prober_container_commands()), + command: Some(kcat_prober_container_commands(kafka_security)), }), timeout_seconds: Some(5), period_seconds: Some(2), @@ -576,13 +580,13 @@ pub fn build_controller_rolegroup_statefulset( .deref() .requested_secret_lifetime .context(MissingSecretLifetimeSnafu)?; - kafka_security - .add_controller_volume_and_volume_mounts( - &mut pod_builder, - &mut cb_kafka, - &requested_secret_lifetime, - ) - .context(AddVolumesAndVolumeMountsSnafu)?; + add_controller_volume_and_volume_mounts( + kafka_security, + &mut pod_builder, + &mut cb_kafka, + &requested_secret_lifetime, + ) + .context(AddVolumesAndVolumeMountsSnafu)?; let kafka_container = cb_kafka.build(); diff --git a/rust/operator-binary/src/controller/build/security.rs b/rust/operator-binary/src/controller/build/security.rs new file mode 100644 index 00000000..add15883 --- /dev/null +++ b/rust/operator-binary/src/controller/build/security.rs @@ -0,0 +1,642 @@ +//! Build-step helpers that turn a [`ValidatedKafkaSecurity`] into Kafka configuration settings, +//! client properties, container commands and TLS volumes/mounts. +//! +//! These consume the validated security inputs and produce build artifacts; they must not perform +//! any validation themselves. +use std::collections::BTreeMap; + +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + builder::{ + self, + pod::{ + PodBuilder, + container::ContainerBuilder, + volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + }, + }, + commons::secret_class::SecretClassVolumeProvisionParts, + crd::authentication::core, + k8s_openapi::api::core::v1::Volume, + shared::time::Duration, +}; + +use crate::{ + controller::security::ValidatedKafkaSecurity, + crd::{ + LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_KRB5_PATH, + STACKABLE_LISTENER_BROKER_DIR, + listener::{ + self, KafkaListenerName, KafkaListenerProtocol, node_address_cmd_env, node_port_cmd_env, + }, + role::KafkaRole, + }, +}; + +// - TLS internal +const INTER_BROKER_LISTENER_NAME: &str = "inter.broker.listener.name"; +// - TLS global +const KEYSTORE_P12_FILE_NAME: &str = "keystore.p12"; +const OPA_TLS_MOUNT_PATH: &str = "/stackable/tls-opa"; +// opa +const OPA_TLS_VOLUME_NAME: &str = "tls-opa"; +const SSL_STORE_PASSWORD: &str = ""; +const SSL_STORE_TYPE_PKCS12: &str = "PKCS12"; +const STACKABLE_TLS_KAFKA_INTERNAL_DIR: &str = "/stackable/tls-kafka-internal"; +const STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME: &str = "tls-kafka-internal"; +const STACKABLE_TLS_KAFKA_SERVER_DIR: &str = "/stackable/tls-kafka-server"; +const STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME: &str = "tls-kafka-server"; +// directories +const STACKABLE_TLS_KCAT_DIR: &str = "/stackable/tls-kcat"; +const STACKABLE_TLS_KCAT_VOLUME_NAME: &str = "tls-kcat"; +const TRUSTSTORE_P12_FILE_NAME: &str = "truststore.p12"; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to build the secret operator Volume"))] + SecretVolumeBuild { + source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, + }, + + #[snafu(display("failed to add needed volume"))] + AddVolume { source: builder::pod::Error }, + + #[snafu(display("failed to add needed volumeMount"))] + AddVolumeMount { + source: builder::pod::container::Error, + }, + + #[snafu(display("failed to build OPA TLS certificate volume"))] + OpaTlsCertSecretClassVolumeBuild { + source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, + }, +} + +pub fn copy_opa_tls_cert_command(security: &ValidatedKafkaSecurity) -> String { + match security.opa_secret_class().is_some() { + true => format!( + "keytool -importcert -file {opa_mount_path}/ca.crt -keystore {tls_dir}/{truststore} -storepass '{tls_password}' -alias opa-ca -noprompt", + opa_mount_path = OPA_TLS_MOUNT_PATH, + tls_dir = STACKABLE_TLS_KAFKA_INTERNAL_DIR, + truststore = TRUSTSTORE_P12_FILE_NAME, + tls_password = SSL_STORE_PASSWORD, + ), + false => "".to_string(), + } +} + +/// Returns the commands for the kcat readiness probe. +pub fn kcat_prober_container_commands(security: &ValidatedKafkaSecurity) -> Vec { + let mut args = vec![]; + let port = security.client_port(); + + if security.tls_client_authentication_class().is_some() { + args.push("/stackable/kcat".to_string()); + args.push("-b".to_string()); + args.push(format!("localhost:{}", port)); + args.extend(kcat_client_auth_ssl(STACKABLE_TLS_KCAT_DIR)); + args.push("-L".to_string()); + } else if security.has_kerberos_enabled() { + let service_name = KafkaRole::Broker.kerberos_service_name(); + // here we need to specify a shell so that variable substitution will work + // see e.g. https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ExecAction.md + args.push("/bin/bash".to_string()); + args.push("-x".to_string()); + args.push("-euo".to_string()); + args.push("pipefail".to_string()); + args.push("-c".to_string()); + + // the entire command needs to be subject to the -c directive + // to prevent short-circuiting + let mut bash_args = vec![]; + bash_args.push( + format!( + "export KERBEROS_REALM=$(grep -oP 'default_realm = \\K.*' {});", + STACKABLE_KERBEROS_KRB5_PATH + ) + .to_string(), + ); + bash_args.push( + format!( + "export POD_BROKER_LISTENER_ADDRESS={};", + node_address_cmd_env(STACKABLE_LISTENER_BROKER_DIR) + ) + .to_string(), + ); + bash_args.push( + format!( + "export POD_BROKER_LISTENER_PORT={};", + node_port_cmd_env(STACKABLE_LISTENER_BROKER_DIR, security.client_port_name()) + ) + .to_string(), + ); + bash_args.push("/stackable/kcat".to_string()); + bash_args.push("-b".to_string()); + bash_args.push("$POD_BROKER_LISTENER_ADDRESS:$POD_BROKER_LISTENER_PORT".to_string()); + bash_args.extend(kcat_client_sasl_ssl(STACKABLE_TLS_KCAT_DIR, service_name)); + bash_args.push("-L".to_string()); + + args.push(bash_args.join(" ")); + } else if security.tls_server_secret_class().is_some() { + args.push("/stackable/kcat".to_string()); + args.push("-b".to_string()); + args.push(format!("localhost:{}", port)); + args.extend(kcat_client_ssl(STACKABLE_TLS_KCAT_DIR)); + args.push("-L".to_string()); + } else { + args.push("/stackable/kcat".to_string()); + args.push("-b".to_string()); + args.push(format!("localhost:{}", port)); + args.push("-L".to_string()); + } + + args +} + +/// Returns a configuration file that can be used by Kafka clients running inside the +/// Kubernetes cluster to connect to the Kafka servers. +pub fn client_properties(security: &ValidatedKafkaSecurity) -> Vec<(String, Option)> { + let mut props = vec![]; + + if security.tls_client_authentication_class().is_some() { + props.push(( + "security.protocol".to_string(), + Some(KafkaListenerProtocol::Ssl.to_string()), + )); + props.push(("ssl.client.auth".to_string(), Some("required".to_string()))); + push_client_ssl_stores(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); + } else if security.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 + // to the Kafka daemon scripts. + // This will simplify the code and the command lines lot. + // It will also make the jaas files reusable by the Kafka shell scripts. + props.push(( + "security.protocol".to_string(), + Some(KafkaListenerProtocol::SaslSsl.to_string()), + )); + push_client_ssl_stores(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); + props.push(( + "sasl.enabled.mechanisms".to_string(), + Some("GSSAPI".to_string()), + )); + props.push(( + "sasl.kerberos.service.name".to_string(), + Some(KafkaRole::Broker.kerberos_service_name().to_string()), + )); + props.push(( + "sasl.mechanism.inter.broker.protocol".to_string(), + Some("GSSAPI".to_string()), + )); + props.push(( + "sasl.jaas.config".to_string(), + Some(format!("com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true keyTab=\"{keytab}\" principal=\"{service}/{pod}@{realm}\"", + keytab="/stackable/kerberos/keytab", + service=KafkaRole::Broker.kerberos_service_name(), + pod="todo", + realm="$KERBEROS_REALM")))); + } else if security.tls_server_secret_class().is_some() { + props.push(( + "security.protocol".to_string(), + Some(KafkaListenerProtocol::Ssl.to_string()), + )); + push_client_ssl_truststore(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); + } else { + props.push(( + "security.protocol".to_string(), + Some(KafkaListenerProtocol::Plaintext.to_string()), + )); + } + + props +} + +/// Adds required volumes and volume mounts to the broker pod and container builders +/// depending on the tls and authentication settings. +pub fn add_broker_volume_and_volume_mounts( + security: &ValidatedKafkaSecurity, + pod_builder: &mut PodBuilder, + cb_kcat_prober: &mut ContainerBuilder, + cb_kafka: &mut ContainerBuilder, + requested_secret_lifetime: &Duration, +) -> Result<(), Error> { + // add tls (server or client authentication volumes) if required + if let Some(tls_server_secret_class) = tls_secret_class(security) { + // We have to mount tls pem files for kcat (the mount can be used directly) + pod_builder + .add_volume(create_kcat_tls_volume( + STACKABLE_TLS_KCAT_VOLUME_NAME, + tls_server_secret_class, + requested_secret_lifetime, + )?) + .context(AddVolumeSnafu)?; + cb_kcat_prober + .add_volume_mount(STACKABLE_TLS_KCAT_VOLUME_NAME, STACKABLE_TLS_KCAT_DIR) + .context(AddVolumeMountSnafu)?; + // Keystores fore the kafka container + pod_builder + .add_volume(create_tls_keystore_volume( + STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, + tls_server_secret_class, + requested_secret_lifetime, + )?) + .context(AddVolumeSnafu)?; + cb_kafka + .add_volume_mount( + STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, + STACKABLE_TLS_KAFKA_SERVER_DIR, + ) + .context(AddVolumeMountSnafu)?; + } + + if let Some(tls_internal_secret_class) = security.tls_internal_secret_class() { + pod_builder + .add_volume(create_tls_keystore_volume( + STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, + tls_internal_secret_class, + requested_secret_lifetime, + )?) + .context(AddVolumeSnafu)?; + cb_kafka + .add_volume_mount( + STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ) + .context(AddVolumeMountSnafu)?; + } + + if let Some(secret_class) = security.opa_secret_class() { + cb_kafka + .add_volume_mount(OPA_TLS_VOLUME_NAME, OPA_TLS_MOUNT_PATH) + .context(AddVolumeMountSnafu)?; + + pod_builder + .add_volume( + VolumeBuilder::new(OPA_TLS_VOLUME_NAME) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + secret_class, + // Only the truststore is required to connect to OPA. + SecretClassVolumeProvisionParts::Public, + ) + .build() + .context(OpaTlsCertSecretClassVolumeBuildSnafu)?, + ) + .build(), + ) + .context(AddVolumeSnafu)?; + } + + Ok(()) +} + +/// Adds required volumes and volume mounts to the controller pod and container builders +/// depending on the tls and authentication settings. +pub fn add_controller_volume_and_volume_mounts( + security: &ValidatedKafkaSecurity, + pod_builder: &mut PodBuilder, + cb_kafka: &mut ContainerBuilder, + requested_secret_lifetime: &Duration, +) -> Result<(), Error> { + if let Some(tls_internal_secret_class) = security.tls_internal_secret_class() { + pod_builder + .add_volume( + VolumeBuilder::new(STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + tls_internal_secret_class, + // Kafka needs both the public certificate and the private key for + // the internal communication. + SecretClassVolumeProvisionParts::PublicPrivate, + ) + .with_pod_scope() + .with_format(SecretFormat::TlsPkcs12) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .context(SecretVolumeBuildSnafu)?, + ) + .build(), + ) + .context(AddVolumeSnafu)?; + cb_kafka + .add_volume_mount( + STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ) + .context(AddVolumeMountSnafu)?; + } + + 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}/{}", KEYSTORE_P12_FILE_NAME), + ); + config.insert( + listener.listener_ssl_keystore_password(), + SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + listener.listener_ssl_keystore_type(), + SSL_STORE_TYPE_PKCS12.to_string(), + ); + config.insert( + listener.listener_ssl_truststore_location(), + format!("{dir}/{}", TRUSTSTORE_P12_FILE_NAME), + ); + config.insert( + listener.listener_ssl_truststore_password(), + SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + listener.listener_ssl_truststore_type(), + 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(SSL_STORE_TYPE_PKCS12.to_string()), + )); + props.push(( + "ssl.keystore.location".to_string(), + Some(format!("{dir}/{}", KEYSTORE_P12_FILE_NAME)), + )); + props.push(( + "ssl.keystore.password".to_string(), + Some(SSL_STORE_PASSWORD.to_string()), + )); + 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(SSL_STORE_TYPE_PKCS12.to_string()), + )); + props.push(( + "ssl.truststore.location".to_string(), + Some(format!("{dir}/{}", TRUSTSTORE_P12_FILE_NAME)), + )); + props.push(( + "ssl.truststore.password".to_string(), + Some(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(security: &ValidatedKafkaSecurity) -> BTreeMap { + let mut config = BTreeMap::new(); + + // 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 + // be trusted. + if security.tls_client_authentication_class().is_some() + || security.tls_server_secret_class().is_some() + { + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Client, + STACKABLE_TLS_KAFKA_SERVER_DIR, + ); + if security.tls_client_authentication_class().is_some() { + // client auth required + config.insert( + KafkaListenerName::Client.listener_ssl_client_auth(), + "required".to_string(), + ); + } + } + + if security.has_kerberos_enabled() { + // Bootstrap + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Bootstrap, + STACKABLE_TLS_KAFKA_SERVER_DIR, + ); + config.insert("sasl.enabled.mechanisms".to_string(), "GSSAPI".to_string()); + config.insert( + "sasl.kerberos.service.name".to_string(), + KafkaRole::Broker.kerberos_service_name().to_string(), + ); + config.insert( + "sasl.mechanism.inter.broker.protocol".to_string(), + "GSSAPI".to_string(), + ); + tracing::debug!("Kerberos configs added: [{:#?}]", config); + } + + // Internal TLS + if security.tls_internal_secret_class().is_some() { + // BROKERS + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Internal, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ); + // CONTROLLERS + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Controller, + STACKABLE_TLS_KAFKA_INTERNAL_DIR, + ); + // client auth required + config.insert( + KafkaListenerName::Internal.listener_ssl_client_auth(), + "required".to_string(), + ); + } + + //OPA Tls + if security.opa_secret_class().is_some() { + config.insert( + "opa.authorizer.truststore.path".to_string(), + format!( + "{}/{}", + STACKABLE_TLS_KAFKA_INTERNAL_DIR, TRUSTSTORE_P12_FILE_NAME + ), + ); + config.insert( + "opa.authorizer.truststore.password".to_string(), + SSL_STORE_PASSWORD.to_string(), + ); + config.insert( + "opa.authorizer.truststore.type".to_string(), + SSL_STORE_TYPE_PKCS12.to_string(), + ); + } + + // common + config.insert( + INTER_BROKER_LISTENER_NAME.to_string(), + listener::KafkaListenerName::Internal.to_string(), + ); + + config +} + +/// Returns required Kafka configuration settings for the `controller.properties` file +/// depending on the tls and authentication settings. +pub fn controller_config_settings(security: &ValidatedKafkaSecurity) -> BTreeMap { + let mut config = BTreeMap::new(); + + if security.tls_client_authentication_class().is_some() + || security.tls_internal_secret_class().is_some() + { + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Controller, + 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. + insert_listener_ssl_stores( + &mut config, + &KafkaListenerName::Internal, + 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 + // be trusted. + if security.tls_client_authentication_class().is_some() { + // client auth required + config.insert( + KafkaListenerName::Controller.listener_ssl_client_auth(), + "required".to_string(), + ); + } + } + + // Kerberos + if security.has_kerberos_enabled() { + config.insert("sasl.enabled.mechanisms".to_string(), "GSSAPI".to_string()); + config.insert( + "sasl.kerberos.service.name".to_string(), + KafkaRole::Controller.kerberos_service_name().to_string(), + ); + config.insert( + "sasl.mechanism.inter.broker.protocol".to_string(), + "GSSAPI".to_string(), + ); + tracing::debug!("Kerberos configs added: [{:#?}]", config); + } + + config +} + +/// Returns the `SecretClass` provided in a `AuthenticationClass` for TLS. +fn tls_secret_class(security: &ValidatedKafkaSecurity) -> Option<&str> { + security + .tls_client_authentication_class() + .and_then(|auth_class| match &auth_class.spec.provider { + core::v1alpha1::AuthenticationClassProvider::Tls(tls) => { + tls.client_cert_secret_class.as_deref() + } + _ => None, + }) + .or_else(|| security.tls_server_secret_class()) +} + +/// Creates ephemeral volumes to mount the `SecretClass` into the Pods for kcat client +fn create_kcat_tls_volume( + volume_name: &str, + secret_class_name: &str, + requested_secret_lifetime: &Duration, +) -> Result { + Ok(VolumeBuilder::new(volume_name) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + secret_class_name, + // Both the public certificate and the private key are required for the kcat + // client authentication. + SecretClassVolumeProvisionParts::PublicPrivate, + ) + .with_pod_scope() + .with_format(SecretFormat::TlsPem) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .context(SecretVolumeBuildSnafu)?, + ) + .build()) +} + +/// Creates ephemeral volumes to mount the `SecretClass` into the Pods as keystores +fn create_tls_keystore_volume( + volume_name: &str, + secret_class_name: &str, + requested_secret_lifetime: &Duration, +) -> Result { + Ok(VolumeBuilder::new(volume_name) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new( + secret_class_name, + // Both the keystore and truststore are required for keystore volume. + SecretClassVolumeProvisionParts::PublicPrivate, + ) + .with_pod_scope() + .with_listener_volume_scope(LISTENER_BROKER_VOLUME_NAME) + .with_listener_volume_scope(LISTENER_BOOTSTRAP_VOLUME_NAME) + .with_format(SecretFormat::TlsPkcs12) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .context(SecretVolumeBuildSnafu)?, + ) + .build()) +} + +fn kcat_client_auth_ssl(cert_directory: &str) -> Vec { + vec![ + "-X".to_string(), + "security.protocol=SSL".to_string(), + "-X".to_string(), + format!("ssl.key.location={cert_directory}/tls.key"), + "-X".to_string(), + format!("ssl.certificate.location={cert_directory}/tls.crt"), + "-X".to_string(), + format!("ssl.ca.location={cert_directory}/ca.crt"), + ] +} + +fn kcat_client_ssl(cert_directory: &str) -> Vec { + vec![ + "-X".to_string(), + "security.protocol=SSL".to_string(), + "-X".to_string(), + format!("ssl.ca.location={cert_directory}/ca.crt"), + ] +} + +fn kcat_client_sasl_ssl(cert_directory: &str, service_name: &str) -> Vec { + vec![ + "-X".to_string(), + "security.protocol=SASL_SSL".to_string(), + "-X".to_string(), + format!("ssl.ca.location={cert_directory}/ca.crt"), + "-X".to_string(), + "sasl.kerberos.keytab=/stackable/kerberos/keytab".to_string(), + "-X".to_string(), + "sasl.mechanism=GSSAPI".to_string(), + "-X".to_string(), + format!("sasl.kerberos.service.name={service_name}"), + "-X".to_string(), + format!( + "sasl.kerberos.principal={service_name}/$POD_BROKER_LISTENER_ADDRESS@$KERBEROS_REALM" + ), + ] +} diff --git a/rust/operator-binary/src/controller/security.rs b/rust/operator-binary/src/controller/security.rs index 154f798b..4ceb0747 100644 --- a/rust/operator-binary/src/controller/security.rs +++ b/rust/operator-binary/src/controller/security.rs @@ -4,58 +4,18 @@ //! and helper functions //! //! This is required due to overlaps between TLS encryption and e.g. mTLS authentication or Kerberos -use std::collections::BTreeMap; - -use snafu::{ResultExt, Snafu, ensure}; +use snafu::{Snafu, ensure}; use stackable_operator::{ - builder::{ - self, - pod::{ - PodBuilder, - container::ContainerBuilder, - volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, - }, - }, - commons::secret_class::SecretClassVolumeProvisionParts, crd::authentication::core, - k8s_openapi::api::core::v1::Volume, - shared::time::Duration, v2::types::{common::Port, kubernetes::SecretClassName}, }; -use crate::crd::{ - LISTENER_BOOTSTRAP_VOLUME_NAME, LISTENER_BROKER_VOLUME_NAME, STACKABLE_KERBEROS_KRB5_PATH, - STACKABLE_LISTENER_BROKER_DIR, - authentication::ResolvedAuthenticationClasses, - listener::{ - self, KafkaListenerName, KafkaListenerProtocol, node_address_cmd_env, node_port_cmd_env, - }, - role::KafkaRole, - tls, v1alpha1, -}; +use crate::crd::{authentication::ResolvedAuthenticationClasses, tls, v1alpha1}; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("failed to build the secret operator Volume"))] - SecretVolumeBuild { - source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, - }, - - #[snafu(display("failed to add needed volume"))] - AddVolume { source: builder::pod::Error }, - - #[snafu(display("failed to add needed volumeMount"))] - AddVolumeMount { - source: builder::pod::container::Error, - }, - #[snafu(display("kerberos enablement requires TLS activation"))] KerberosRequiresTls, - - #[snafu(display("failed to build OPA TLS certificate volume"))] - OpaTlsCertSecretClassVolumeBuild { - source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, - }, } /// Helper struct combining TLS settings for server and internal with the resolved AuthenticationClasses @@ -78,27 +38,10 @@ impl ValidatedKafkaSecurity { pub const CLIENT_PORT_NAME: &'static str = "kafka"; // internal pub const INTERNAL_PORT: Port = Port(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"; pub const SECURE_BOOTSTRAP_PORT: Port = Port(9095); pub const SECURE_CLIENT_PORT: Port = Port(9093); pub const SECURE_CLIENT_PORT_NAME: &'static str = "kafka-tls"; pub const SECURE_INTERNAL_PORT: Port = Port(19093); - const SSL_STORE_PASSWORD: &'static str = ""; - const SSL_STORE_TYPE_PKCS12: &'static str = "PKCS12"; - 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"; - const STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME: &'static str = "tls-kafka-server"; - // 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( @@ -176,21 +119,9 @@ impl ValidatedKafkaSecurity { self.kerberos_secret_class().is_some() } - fn has_opa_tls_enabled(&self) -> bool { - self.opa_secret_class.is_some() - } - - 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} -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(), - } + /// Retrieve the optional OPA TLS `SecretClass`. + pub fn opa_secret_class(&self) -> Option<&SecretClassName> { + self.opa_secret_class.as_ref() } pub fn kerberos_secret_class(&self) -> Option { @@ -260,567 +191,4 @@ impl ValidatedKafkaSecurity { Self::INTERNAL_PORT } } - - /// Returns the commands for the kcat readiness probe. - pub fn kcat_prober_container_commands(&self) -> Vec { - let mut args = vec![]; - let port = self.client_port(); - - if self.tls_client_authentication_class().is_some() { - args.push("/stackable/kcat".to_string()); - args.push("-b".to_string()); - args.push(format!("localhost:{}", port)); - args.extend(Self::kcat_client_auth_ssl(Self::STACKABLE_TLS_KCAT_DIR)); - args.push("-L".to_string()); - } else if self.has_kerberos_enabled() { - let service_name = KafkaRole::Broker.kerberos_service_name(); - // here we need to specify a shell so that variable substitution will work - // see e.g. https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ExecAction.md - args.push("/bin/bash".to_string()); - args.push("-x".to_string()); - args.push("-euo".to_string()); - args.push("pipefail".to_string()); - args.push("-c".to_string()); - - // the entire command needs to be subject to the -c directive - // to prevent short-circuiting - let mut bash_args = vec![]; - bash_args.push( - format!( - "export KERBEROS_REALM=$(grep -oP 'default_realm = \\K.*' {});", - STACKABLE_KERBEROS_KRB5_PATH - ) - .to_string(), - ); - bash_args.push( - format!( - "export POD_BROKER_LISTENER_ADDRESS={};", - node_address_cmd_env(STACKABLE_LISTENER_BROKER_DIR) - ) - .to_string(), - ); - bash_args.push( - format!( - "export POD_BROKER_LISTENER_PORT={};", - node_port_cmd_env(STACKABLE_LISTENER_BROKER_DIR, self.client_port_name()) - ) - .to_string(), - ); - bash_args.push("/stackable/kcat".to_string()); - bash_args.push("-b".to_string()); - bash_args.push("$POD_BROKER_LISTENER_ADDRESS:$POD_BROKER_LISTENER_PORT".to_string()); - bash_args.extend(Self::kcat_client_sasl_ssl( - Self::STACKABLE_TLS_KCAT_DIR, - service_name, - )); - bash_args.push("-L".to_string()); - - args.push(bash_args.join(" ")); - } else if self.tls_server_secret_class().is_some() { - args.push("/stackable/kcat".to_string()); - args.push("-b".to_string()); - args.push(format!("localhost:{}", port)); - args.extend(Self::kcat_client_ssl(Self::STACKABLE_TLS_KCAT_DIR)); - args.push("-L".to_string()); - } else { - args.push("/stackable/kcat".to_string()); - args.push("-b".to_string()); - args.push(format!("localhost:{}", port)); - args.push("-L".to_string()); - } - - args - } - - /// Returns a configuration file that can be used by Kafka clients running inside the - /// Kubernetes cluster to connect to the Kafka servers. - pub fn client_properties(&self) -> Vec<(String, Option)> { - let mut props = vec![]; - - if self.tls_client_authentication_class().is_some() { - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::Ssl.to_string()), - )); - props.push(("ssl.client.auth".to_string(), Some("required".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 - // to the Kafka daemon scripts. - // This will simplify the code and the command lines lot. - // It will also make the jaas files reusable by the Kafka shell scripts. - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::SaslSsl.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()), - )); - props.push(( - "sasl.kerberos.service.name".to_string(), - Some(KafkaRole::Broker.kerberos_service_name().to_string()), - )); - props.push(( - "sasl.mechanism.inter.broker.protocol".to_string(), - Some("GSSAPI".to_string()), - )); - props.push(( - "sasl.jaas.config".to_string(), - Some(format!("com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true keyTab=\"{keytab}\" principal=\"{service}/{pod}@{realm}\"", - keytab="/stackable/kerberos/keytab", - service=KafkaRole::Broker.kerberos_service_name(), - pod="todo", - realm="$KERBEROS_REALM")))); - } else if self.tls_server_secret_class().is_some() { - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::Ssl.to_string()), - )); - Self::push_client_ssl_truststore(&mut props, Self::STACKABLE_TLS_KAFKA_SERVER_DIR); - } else { - props.push(( - "security.protocol".to_string(), - Some(KafkaListenerProtocol::Plaintext.to_string()), - )); - } - - props - } - - /// Adds required volumes and volume mounts to the broker pod and container builders - /// depending on the tls and authentication settings. - pub fn add_broker_volume_and_volume_mounts( - &self, - pod_builder: &mut PodBuilder, - cb_kcat_prober: &mut ContainerBuilder, - cb_kafka: &mut ContainerBuilder, - requested_secret_lifetime: &Duration, - ) -> Result<(), Error> { - // add tls (server or client authentication volumes) if required - if let Some(tls_server_secret_class) = self.get_tls_secret_class() { - // We have to mount tls pem files for kcat (the mount can be used directly) - pod_builder - .add_volume(Self::create_kcat_tls_volume( - Self::STACKABLE_TLS_KCAT_VOLUME_NAME, - tls_server_secret_class, - requested_secret_lifetime, - )?) - .context(AddVolumeSnafu)?; - cb_kcat_prober - .add_volume_mount( - Self::STACKABLE_TLS_KCAT_VOLUME_NAME, - Self::STACKABLE_TLS_KCAT_DIR, - ) - .context(AddVolumeMountSnafu)?; - // Keystores fore the kafka container - pod_builder - .add_volume(Self::create_tls_keystore_volume( - Self::STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, - tls_server_secret_class, - requested_secret_lifetime, - )?) - .context(AddVolumeSnafu)?; - cb_kafka - .add_volume_mount( - Self::STACKABLE_TLS_KAFKA_SERVER_VOLUME_NAME, - Self::STACKABLE_TLS_KAFKA_SERVER_DIR, - ) - .context(AddVolumeMountSnafu)?; - } - - if let Some(tls_internal_secret_class) = self.tls_internal_secret_class() { - pod_builder - .add_volume(Self::create_tls_keystore_volume( - Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, - tls_internal_secret_class, - requested_secret_lifetime, - )?) - .context(AddVolumeSnafu)?; - cb_kafka - .add_volume_mount( - Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, - Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - ) - .context(AddVolumeMountSnafu)?; - } - - if let Some(secret_class) = &self.opa_secret_class { - cb_kafka - .add_volume_mount(Self::OPA_TLS_VOLUME_NAME, Self::OPA_TLS_MOUNT_PATH) - .context(AddVolumeMountSnafu)?; - - pod_builder - .add_volume( - VolumeBuilder::new(Self::OPA_TLS_VOLUME_NAME) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - secret_class, - // Only the truststore is required to connect to OPA. - SecretClassVolumeProvisionParts::Public, - ) - .build() - .context(OpaTlsCertSecretClassVolumeBuildSnafu)?, - ) - .build(), - ) - .context(AddVolumeSnafu)?; - } - - Ok(()) - } - - /// Adds required volumes and volume mounts to the controller pod and container builders - /// depending on the tls and authentication settings. - pub fn add_controller_volume_and_volume_mounts( - &self, - pod_builder: &mut PodBuilder, - cb_kafka: &mut ContainerBuilder, - requested_secret_lifetime: &Duration, - ) -> Result<(), Error> { - if let Some(tls_internal_secret_class) = self.tls_internal_secret_class() { - pod_builder - .add_volume( - VolumeBuilder::new(Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - tls_internal_secret_class, - // Kafka needs both the public certificate and the private key for - // the internal communication. - SecretClassVolumeProvisionParts::PublicPrivate, - ) - .with_pod_scope() - .with_format(SecretFormat::TlsPkcs12) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime) - .build() - .context(SecretVolumeBuildSnafu)?, - ) - .build(), - ) - .context(AddVolumeSnafu)?; - cb_kafka - .add_volume_mount( - Self::STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME, - Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - ) - .context(AddVolumeMountSnafu)?; - } - - 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 { - let mut config = BTreeMap::new(); - - // 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 - // be trusted. - if self.tls_client_authentication_class().is_some() - || self.tls_server_secret_class().is_some() - { - 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 - config.insert( - KafkaListenerName::Client.listener_ssl_client_auth(), - "required".to_string(), - ); - } - } - - if self.has_kerberos_enabled() { - // Bootstrap - 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( - "sasl.kerberos.service.name".to_string(), - KafkaRole::Broker.kerberos_service_name().to_string(), - ); - config.insert( - "sasl.mechanism.inter.broker.protocol".to_string(), - "GSSAPI".to_string(), - ); - tracing::debug!("Kerberos configs added: [{:#?}]", config); - } - - // Internal TLS - if self.tls_internal_secret_class().is_some() { - // BROKERS - Self::insert_listener_ssl_stores( - &mut config, - &KafkaListenerName::Internal, - Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - ); - // CONTROLLERS - Self::insert_listener_ssl_stores( - &mut config, - &KafkaListenerName::Controller, - Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - ); - // client auth required - config.insert( - KafkaListenerName::Internal.listener_ssl_client_auth(), - "required".to_string(), - ); - } - - //OPA Tls - if self.opa_secret_class.is_some() { - config.insert( - "opa.authorizer.truststore.path".to_string(), - format!( - "{}/{}", - Self::STACKABLE_TLS_KAFKA_INTERNAL_DIR, - Self::TRUSTSTORE_P12_FILE_NAME - ), - ); - config.insert( - "opa.authorizer.truststore.password".to_string(), - Self::SSL_STORE_PASSWORD.to_string(), - ); - config.insert( - "opa.authorizer.truststore.type".to_string(), - Self::SSL_STORE_TYPE_PKCS12.to_string(), - ); - } - - // common - config.insert( - Self::INTER_BROKER_LISTENER_NAME.to_string(), - listener::KafkaListenerName::Internal.to_string(), - ); - - config - } - - /// Returns required Kafka configuration settings for the `controller.properties` file - /// depending on the tls and authentication settings. - pub fn controller_config_settings(&self) -> BTreeMap { - let mut config = BTreeMap::new(); - - if self.tls_client_authentication_class().is_some() - || self.tls_internal_secret_class().is_some() - { - 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. - 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 - // be trusted. - if self.tls_client_authentication_class().is_some() { - // client auth required - config.insert( - KafkaListenerName::Controller.listener_ssl_client_auth(), - "required".to_string(), - ); - } - } - - // Kerberos - if self.has_kerberos_enabled() { - config.insert("sasl.enabled.mechanisms".to_string(), "GSSAPI".to_string()); - config.insert( - "sasl.kerberos.service.name".to_string(), - KafkaRole::Controller.kerberos_service_name().to_string(), - ); - config.insert( - "sasl.mechanism.inter.broker.protocol".to_string(), - "GSSAPI".to_string(), - ); - tracing::debug!("Kerberos configs added: [{:#?}]", config); - } - - config - } - - /// Returns the `SecretClass` provided in a `AuthenticationClass` for TLS. - fn get_tls_secret_class(&self) -> Option<&str> { - self.resolved_authentication_classes - .get_tls_authentication_class() - .and_then(|auth_class| match &auth_class.spec.provider { - core::v1alpha1::AuthenticationClassProvider::Tls(tls) => { - tls.client_cert_secret_class.as_deref() - } - _ => None, - }) - .or_else(|| self.server_secret_class.as_ref().map(|s| s.as_ref())) - } - - /// Creates ephemeral volumes to mount the `SecretClass` into the Pods for kcat client - fn create_kcat_tls_volume( - volume_name: &str, - secret_class_name: &str, - requested_secret_lifetime: &Duration, - ) -> Result { - Ok(VolumeBuilder::new(volume_name) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - secret_class_name, - // Both the public certificate and the private key are required for the kcat - // client authentication. - SecretClassVolumeProvisionParts::PublicPrivate, - ) - .with_pod_scope() - .with_format(SecretFormat::TlsPem) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime) - .build() - .context(SecretVolumeBuildSnafu)?, - ) - .build()) - } - - /// Creates ephemeral volumes to mount the `SecretClass` into the Pods as keystores - fn create_tls_keystore_volume( - volume_name: &str, - secret_class_name: &str, - requested_secret_lifetime: &Duration, - ) -> Result { - Ok(VolumeBuilder::new(volume_name) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new( - secret_class_name, - // Both the keystore and truststore are required for keystore volume. - SecretClassVolumeProvisionParts::PublicPrivate, - ) - .with_pod_scope() - .with_listener_volume_scope(LISTENER_BROKER_VOLUME_NAME) - .with_listener_volume_scope(LISTENER_BOOTSTRAP_VOLUME_NAME) - .with_format(SecretFormat::TlsPkcs12) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime) - .build() - .context(SecretVolumeBuildSnafu)?, - ) - .build()) - } - - fn kcat_client_auth_ssl(cert_directory: &str) -> Vec { - vec![ - "-X".to_string(), - "security.protocol=SSL".to_string(), - "-X".to_string(), - format!("ssl.key.location={cert_directory}/tls.key"), - "-X".to_string(), - format!("ssl.certificate.location={cert_directory}/tls.crt"), - "-X".to_string(), - format!("ssl.ca.location={cert_directory}/ca.crt"), - ] - } - - fn kcat_client_ssl(cert_directory: &str) -> Vec { - vec![ - "-X".to_string(), - "security.protocol=SSL".to_string(), - "-X".to_string(), - format!("ssl.ca.location={cert_directory}/ca.crt"), - ] - } - - fn kcat_client_sasl_ssl(cert_directory: &str, service_name: &str) -> Vec { - vec![ - "-X".to_string(), - "security.protocol=SASL_SSL".to_string(), - "-X".to_string(), - format!("ssl.ca.location={cert_directory}/ca.crt"), - "-X".to_string(), - "sasl.kerberos.keytab=/stackable/kerberos/keytab".to_string(), - "-X".to_string(), - "sasl.mechanism=GSSAPI".to_string(), - "-X".to_string(), - format!("sasl.kerberos.service.name={service_name}"), - "-X".to_string(), - format!( - "sasl.kerberos.principal={service_name}/$POD_BROKER_LISTENER_ADDRESS@$KERBEROS_REALM" - ), - ] - } } From 1f1cd7a386f4b0e5311f7b98374fe9157cb6f8fe Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 16:29:16 +0200 Subject: [PATCH 60/66] chore: bump dependencies --- Cargo.lock | 104 ++++++++++++++++++------------------- Cargo.nix | 130 +++++++++++++++++++++++----------------------- crate-hashes.json | 18 +++---- 3 files changed, 126 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88162bc5..175dea75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -152,7 +152,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -360,7 +360,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -520,7 +520,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -531,7 +531,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -542,7 +542,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -566,7 +566,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -593,7 +593,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -616,7 +616,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -668,7 +668,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -732,7 +732,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -898,7 +898,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1412,7 +1412,7 @@ checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1503,7 +1503,7 @@ dependencies = [ [[package]] name = "k8s-version" version = "0.1.3" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "darling", "regex", @@ -1603,7 +1603,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2026,7 +2026,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2056,7 +2056,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2194,7 +2194,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2305,7 +2305,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2444,7 +2444,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-ident", ] @@ -2548,7 +2548,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2646,7 +2646,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2657,7 +2657,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2832,7 +2832,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2844,7 +2844,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2882,7 +2882,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackable-certs" version = "0.4.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "const-oid", "ecdsa", @@ -2927,7 +2927,7 @@ dependencies = [ [[package]] name = "stackable-operator" version = "0.111.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "base64", "clap", @@ -2972,18 +2972,18 @@ dependencies = [ [[package]] name = "stackable-operator-derive" version = "0.3.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "stackable-shared" version = "0.1.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "jiff", "k8s-openapi", @@ -3000,7 +3000,7 @@ dependencies = [ [[package]] name = "stackable-telemetry" version = "0.6.4" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "axum", "clap", @@ -3024,7 +3024,7 @@ dependencies = [ [[package]] name = "stackable-versioned" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "kube", "schemars", @@ -3038,7 +3038,7 @@ dependencies = [ [[package]] name = "stackable-versioned-macros" version = "0.10.0" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "convert_case", "convert_case_extras", @@ -3050,13 +3050,13 @@ dependencies = [ "kube", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "stackable-webhook" version = "0.9.1" -source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c" +source = "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3" dependencies = [ "arc-swap", "async-trait", @@ -3109,7 +3109,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3137,9 +3137,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -3163,7 +3163,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3192,7 +3192,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3203,7 +3203,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3273,7 +3273,7 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3301,7 +3301,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3503,7 +3503,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3741,7 +3741,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -3795,7 +3795,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3806,7 +3806,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3975,7 +3975,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -3996,7 +3996,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4016,7 +4016,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -4037,7 +4037,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4070,7 +4070,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] diff --git a/Cargo.nix b/Cargo.nix index 9a82f154..14477171 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -439,7 +439,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "visit-mut" ]; } ]; @@ -466,7 +466,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } @@ -1161,7 +1161,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -1592,7 +1592,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "extra-traits" ]; } ]; @@ -1623,7 +1623,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -1649,7 +1649,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "visit-mut" ]; } ]; @@ -1726,7 +1726,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -1820,7 +1820,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; buildDependencies = [ @@ -1917,7 +1917,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -2083,13 +2083,13 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -2287,7 +2287,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -2775,7 +2775,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -4523,7 +4523,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -4794,8 +4794,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "k8s_version"; authors = [ @@ -5288,7 +5288,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -6782,7 +6782,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; features = { @@ -6851,7 +6851,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "printing" "clone-impls" "proc-macro" "full" "visit-mut" ]; } @@ -7218,7 +7218,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -7524,7 +7524,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -8155,7 +8155,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "parsing" "extra-traits" "visit" "visit-mut" ]; } { @@ -8497,13 +8497,13 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; @@ -8787,7 +8787,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -8819,7 +8819,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" ]; } @@ -9330,7 +9330,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -9366,7 +9366,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } @@ -9477,8 +9477,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_certs"; authors = [ @@ -9668,8 +9668,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_operator"; authors = [ @@ -9867,8 +9867,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; procMacro = true; libName = "stackable_operator_derive"; @@ -9890,7 +9890,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -9902,8 +9902,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_shared"; authors = [ @@ -9983,8 +9983,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_telemetry"; authors = [ @@ -10093,8 +10093,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_versioned"; authors = [ @@ -10143,8 +10143,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; procMacro = true; libName = "stackable_versioned_macros"; @@ -10199,7 +10199,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10211,8 +10211,8 @@ rec { workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/stackabletech//operator-rs.git"; - rev = "a9fbbc8d1f0dc0387f26ae443a8b7d5a6e24323c"; - sha256 = "0s5giiz1zhz644s2fy9bfvd1hybm4smz75ak33fkvh2flvjfqba3"; + rev = "bde231132cd604c12d69ffb9f3d16b6dc0e5c0c3"; + sha256 = "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp"; }; libName = "stackable_webhook"; authors = [ @@ -10394,7 +10394,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "parsing" ]; } ]; @@ -10458,11 +10458,11 @@ rec { }; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "full" "parsing" "printing" "proc-macro" "quote" ]; }; - "syn 2.0.117" = rec { + "syn 2.0.118" = rec { crateName = "syn"; - version = "2.0.117"; + version = "2.0.118"; edition = "2021"; - sha256 = "16cv7c0wbn8amxc54n4w15kxlx5ypdmla8s0gxr2l7bv7s0bhrg6"; + sha256 = "08hlbc32lqd5d67p26ck7chg0rkclsw9as6f96vfn4s2j1zyb6hv"; authors = [ "David Tolnay " ]; @@ -10534,7 +10534,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "derive" "parsing" "printing" "clone-impls" "visit" "extra-traits" ]; } @@ -10601,7 +10601,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10627,7 +10627,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; } ]; @@ -10839,7 +10839,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "parsing" ]; } ]; @@ -10991,7 +10991,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; @@ -11778,7 +11778,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ]; } @@ -12523,7 +12523,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "visit" "visit-mut" "full" "extra-traits" ]; } { @@ -13132,7 +13132,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -13159,7 +13159,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -14034,7 +14034,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "fold" ]; } { @@ -14102,14 +14102,14 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" ]; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "visit" ]; } ]; @@ -14158,7 +14158,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "fold" ]; } { @@ -14212,7 +14212,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "full" "extra-traits" "visit" ]; } ]; @@ -14325,7 +14325,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.117"; + packageId = "syn 2.0.118"; features = [ "extra-traits" ]; } ]; diff --git a/crate-hashes.json b/crate-hashes.json index a658a742..0ff88d85 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": "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//operator-rs.git?branch=smooth-operator#k8s-version@0.1.3": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-certs@0.4.0": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator-derive@0.3.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-operator@0.111.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-shared@0.1.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-telemetry@0.6.4": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned-macros@0.10.0": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-versioned@0.10.0": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", + "git+https://github.com/stackabletech//operator-rs.git?branch=smooth-operator#stackable-webhook@0.9.1": "1j0mx81r6ki3paw40gs69p4wbnfbzw1iykz8b45mmryxg8naxihp", "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 5925935f354ac0e03f602ccf17276781e007f658 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 16:29:39 +0200 Subject: [PATCH 61/66] refactor: use v2 RoleGroupConfig and optional replicas --- rust/operator-binary/src/controller.rs | 31 ++++++++---------- .../src/controller/build/jvm.rs | 7 +++- .../controller/build/resource/config_map.rs | 8 ++--- .../controller/build/resource/statefulset.rs | 32 +++++++++++++------ .../src/controller/validate.rs | 22 +++++++------ rust/operator-binary/src/crd/affinity.rs | 2 +- 6 files changed, 60 insertions(+), 42 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 3f723efb..9253a0e4 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -197,7 +197,9 @@ impl ValidatedCluster { let resource_names = self.resource_names(role, role_group_name); let role_group_statefulset_name = resource_names.stateful_set_name(); let role_group_service_name = resource_names.headless_service_name(); - for replica in 0..validated_rg.replicas { + // Pods must be predicted from a concrete count (e.g. for KRaft quorum + // voters), so an unset replica count falls back to 1. + for replica in 0..validated_rg.replicas.unwrap_or(1) { pod_descriptors.push(KafkaPodDescriptor { namespace: self.namespace.clone(), role_group_service_name: role_group_service_name.clone(), @@ -409,27 +411,21 @@ 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. 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. +/// The validated, merged per-role-group product config. #[derive(Clone, Debug, PartialEq)] -pub struct ValidatedRoleGroupConfig { - pub replicas: u16, +pub struct ValidatedKafkaConfig { 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, /// Validated logging configuration (derived from `config.logging` during validation). pub logging: validate::ValidatedLogging, } +/// A validated, merged Kafka role-group config. +pub type ValidatedRoleGroupConfig = stackable_operator::v2::role_utils::RoleGroupConfig< + ValidatedKafkaConfig, + stackable_operator::v2::role_utils::JavaCommonConfig, + AnyConfigOverrides, +>; + pub struct Ctx { pub client: stackable_operator::client::Client, pub operator_environment: OperatorEnvironmentOptions, @@ -621,6 +617,7 @@ pub async fn reconcile_kafka( // 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 + .config .logging .vector_container .is_some() @@ -672,7 +669,7 @@ pub async fn reconcile_kafka( .context(BuildStatefulsetSnafu)?, }; - if let AnyConfig::Broker(broker_config) = &validated_rg.config { + if let AnyConfig::Broker(broker_config) = &validated_rg.config.config { let rg_bootstrap_listener = build_broker_rolegroup_bootstrap_listener( &validated_cluster, kafka_role, diff --git a/rust/operator-binary/src/controller/build/jvm.rs b/rust/operator-binary/src/controller/build/jvm.rs index adcdefc1..0a26c38c 100644 --- a/rust/operator-binary/src/controller/build/jvm.rs +++ b/rust/operator-binary/src/controller/build/jvm.rs @@ -102,7 +102,12 @@ mod tests { .get(&KafkaRole::Broker) .and_then(|groups| groups.get(&"default".parse().unwrap())) .expect("broker default role group should exist"); - (rg.config.clone(), rg.jvm_argument_overrides.clone()) + ( + rg.config.config.clone(), + rg.product_specific_common_config + .jvm_argument_overrides + .clone(), + ) } #[test] 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 551db138..13301aef 100644 --- a/rust/operator-binary/src/controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -71,11 +71,11 @@ pub fn build_rolegroup_config_map( listener_config: &KafkaListenerConfig, vector_config: Option, ) -> Result { - let role = validated_rg.config.kafka_role(); + let role = validated_rg.config.config.kafka_role(); 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 = config_file_name(&validated_rg.config).to_string(); + let kafka_config_file_name = config_file_name(&validated_rg.config.config).to_string(); let config_overrides = validated_rg .config_overrides .config_file_overrides() @@ -90,7 +90,7 @@ pub fn build_rolegroup_config_map( return NoKraftControllersFoundSnafu.fail(); } - let kafka_config = match &validated_rg.config { + let kafka_config = match &validated_rg.config.config { AnyConfig::Broker(_) => crate::controller::build::properties::broker_properties::build( cluster_config, listener_config, @@ -177,7 +177,7 @@ pub fn build_rolegroup_config_map( let config_data = role_group_config_map_data( &resolved_product_image.product_version, - &validated_rg.config, + &validated_rg.config.config, ); for (file_name, data) in config_data { if let Some(data) = data { diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index d332e81a..1de9b723 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -146,7 +146,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.config; + let merged_config = &validated_rg.config.config; 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 @@ -261,7 +261,9 @@ pub fn build_broker_rolegroup_statefulset( add_common_kafka_env( &mut cb_kafka, merged_config, - &validated_rg.jvm_argument_overrides, + &validated_rg + .product_specific_common_config + .jvm_argument_overrides, resolved_product_image, kafka_role, role_group_name, @@ -334,7 +336,11 @@ pub fn build_broker_rolegroup_statefulset( ..Probe::default() }); - add_log_config_volume(&mut pod_builder, &validated_rg.logging, &resource_names)?; + add_log_config_volume( + &mut pod_builder, + &validated_rg.config.logging, + &resource_names, + )?; let metadata = ObjectMetaBuilder::new() .with_labels(recommended_labels.clone()) @@ -377,7 +383,7 @@ pub fn build_broker_rolegroup_statefulset( add_vector_container( &mut pod_builder, - &validated_rg.logging, + &validated_rg.config.logging, resolved_product_image, &resource_names, ); @@ -407,7 +413,7 @@ pub fn build_broker_rolegroup_statefulset( .build(), spec: Some(StatefulSetSpec { pod_management_policy: Some("Parallel".to_string()), - replicas: Some(i32::from(validated_rg.replicas)), + replicas: validated_rg.replicas.map(i32::from), selector: LabelSelector { match_labels: Some( validated_cluster @@ -435,7 +441,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.config; + let merged_config = &validated_rg.config.config; let resource_names = validated_cluster.resource_names(kafka_role, role_group_name); let recommended_labels = validated_cluster.recommended_labels(kafka_role, role_group_name); @@ -529,7 +535,9 @@ pub fn build_controller_rolegroup_statefulset( add_common_kafka_env( &mut cb_kafka, merged_config, - &validated_rg.jvm_argument_overrides, + &validated_rg + .product_specific_common_config + .jvm_argument_overrides, resolved_product_image, kafka_role, role_group_name, @@ -569,7 +577,11 @@ pub fn build_controller_rolegroup_statefulset( ..Probe::default() }); - add_log_config_volume(&mut pod_builder, &validated_rg.logging, &resource_names)?; + add_log_config_volume( + &mut pod_builder, + &validated_rg.config.logging, + &resource_names, + )?; let metadata = ObjectMetaBuilder::new() .with_labels(recommended_labels.clone()) @@ -600,7 +612,7 @@ pub fn build_controller_rolegroup_statefulset( add_vector_container( &mut pod_builder, - &validated_rg.logging, + &validated_rg.config.logging, resolved_product_image, &resource_names, ); @@ -630,7 +642,7 @@ pub fn build_controller_rolegroup_statefulset( type_: Some("RollingUpdate".to_string()), ..StatefulSetUpdateStrategy::default() }), - replicas: Some(i32::from(validated_rg.replicas)), + replicas: validated_rg.replicas.map(i32::from), selector: LabelSelector { match_labels: Some( validated_cluster diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 252923f9..ec2ee17a 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -29,8 +29,8 @@ use stackable_operator::{ use crate::{ controller::{ - RoleGroupName, ValidatedCluster, ValidatedClusterConfig, ValidatedRoleConfig, - ValidatedRoleGroupConfig, + RoleGroupName, ValidatedCluster, ValidatedClusterConfig, ValidatedKafkaConfig, + ValidatedRoleConfig, ValidatedRoleGroupConfig, dereference::DereferencedObjects, security::{self, ValidatedKafkaSecurity}, }, @@ -363,16 +363,20 @@ where 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), + // Passed through as-is (including `None`) so an unset replica count lets a + // horizontal autoscaler own the StatefulSet's `.spec.replicas`. + replicas: merged.replicas, + config: ValidatedKafkaConfig { + config: wrap_config(merged.config.config), + logging, + }, config_overrides: wrap_overrides(merged.config.config_overrides), env_overrides, + // Kafka does not use CLI overrides; the field is carried (and merged upstream) + // but unused. + cli_overrides: merged.config.cli_overrides, pod_overrides: merged.config.pod_overrides, - jvm_argument_overrides: merged - .config - .product_specific_common_config - .jvm_argument_overrides, - logging, + product_specific_common_config: merged.config.product_specific_common_config, }; let role_group_name = RoleGroupName::from_str(role_group_name).with_context(|_| { ParseRoleGroupNameSnafu { diff --git a/rust/operator-binary/src/crd/affinity.rs b/rust/operator-binary/src/crd/affinity.rs index ca7deebc..152c8e58 100644 --- a/rust/operator-binary/src/crd/affinity.rs +++ b/rust/operator-binary/src/crd/affinity.rs @@ -64,7 +64,7 @@ mod tests { .role_group_configs .get(&role) .and_then(|groups| groups.get(&"default".parse().unwrap())) - .map(|rg| &rg.config) + .map(|rg| &rg.config.config) .expect("role group should exist"); assert_eq!( From 678f2ecb44f65b58464b0e0bbfa8f8a163703f6c Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 22:52:46 +0200 Subject: [PATCH 62/66] fix: remove upstream references --- rust/operator-binary/src/controller/validate.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index ec2ee17a..03cd8119 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -316,7 +316,7 @@ 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 upstream +/// Each role group is merged and validated via /// [`with_validated_config`], which folds the config fragment (default <- role <- /// role group) plus the `configOverrides`, `envOverrides`, `podOverrides` and /// `jvmArgumentOverrides` (role group wins) into a single @@ -350,7 +350,7 @@ where >(role_group, role, &default_config) .context(ValidateRoleGroupConfigSnafu)?; - // The upstream merge returns env overrides as a HashMap. Convert to an + // The 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 { From 11ca379c32f8537e5275ddef3ac24645426b3e77 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 22:55:54 +0200 Subject: [PATCH 63/66] fix: user ListenerName type --- rust/operator-binary/src/controller.rs | 9 +++++---- .../src/controller/build/resource/statefulset.rs | 8 +++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 9253a0e4..3992cfa4 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -38,7 +38,7 @@ use stackable_operator::{ kvp::label::{recommended_labels, role_group_selector}, role_group_utils::ResourceNames, types::{ - kubernetes::{ConfigMapName, NamespaceName, Uid}, + kubernetes::{ConfigMapName, ListenerName, NamespaceName, Uid}, operator::{ClusterName, ControllerName, OperatorName, ProductName, ProductVersion}, }, }, @@ -242,12 +242,13 @@ impl ValidatedCluster { &self, role: &KafkaRole, role_group_name: &RoleGroupName, - ) -> String { - format!( + ) -> ListenerName { + ListenerName::from_str(&format!( "{}-bootstrap", self.resource_names(role, role_group_name) .stateful_set_name() - ) + )) + .expect("the bootstrap listener name is a valid Listener name") } /// Recommended labels for a role-group resource, using the given product version. diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 1de9b723..4c4b37ff 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -38,7 +38,7 @@ use stackable_operator::{ STACKABLE_LOG_DIR, ValidatedContainerLogConfigChoice, vector_container, }, role_group_utils::ResourceNames, - types::kubernetes::{ContainerName, ListenerName, PersistentVolumeClaimName, VolumeName}, + types::kubernetes::{ContainerName, PersistentVolumeClaimName, VolumeName}, }, }; @@ -185,10 +185,8 @@ pub fn build_broker_rolegroup_statefulset( // bootstrap listener should be persistent, // main broker listener is an ephemeral PVC instead - 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_listener_name = + validated_cluster.bootstrap_listener_name(kafka_role, role_group_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( From cea6ee485ad55098ee4c4b6a6c512afe5e54c032 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 23:09:17 +0200 Subject: [PATCH 64/66] fix: extract hardcoded constants --- .../controller/build/resource/statefulset.rs | 17 +++-- .../src/controller/build/security.rs | 68 ++++++++++++------- rust/operator-binary/src/crd/listener.rs | 17 +++-- rust/operator-binary/src/crd/mod.rs | 2 +- 4 files changed, 67 insertions(+), 37 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 4c4b37ff..a2827aa4 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -81,6 +81,11 @@ stackable_operator::constant!(VECTOR_CONTAINER_NAME: ContainerName = "vector"); stackable_operator::constant!(VECTOR_CONFIG_VOLUME_NAME: VolumeName = "config"); stackable_operator::constant!(VECTOR_LOG_VOLUME_NAME: VolumeName = "log"); +/// Name of both the env var and the ZooKeeper discovery ConfigMap key holding the +/// ZooKeeper connection string. +const ZOOKEEPER_ENV_VAR_NAME: &str = "ZOOKEEPER"; +const POD_MANAGEMENT_POLICY_PARALLEL: &str = "Parallel"; + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display("failed to add kerberos config"))] @@ -212,11 +217,11 @@ pub fn build_broker_rolegroup_statefulset( &validated_cluster.cluster_config.zookeeper_config_map_name { env.push(EnvVar { - name: "ZOOKEEPER".to_string(), + name: ZOOKEEPER_ENV_VAR_NAME.to_string(), value_from: Some(EnvVarSource { config_map_key_ref: Some(ConfigMapKeySelector { name: zookeeper_config_map_name.to_string(), - key: "ZOOKEEPER".to_string(), + key: ZOOKEEPER_ENV_VAR_NAME.to_string(), ..ConfigMapKeySelector::default() }), ..EnvVarSource::default() @@ -410,7 +415,7 @@ pub fn build_broker_rolegroup_statefulset( .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) .build(), spec: Some(StatefulSetSpec { - pod_management_policy: Some("Parallel".to_string()), + pod_management_policy: Some(POD_MANAGEMENT_POLICY_PARALLEL.to_string()), replicas: validated_rg.replicas.map(i32::from), selector: LabelSelector { match_labels: Some( @@ -500,11 +505,11 @@ pub fn build_controller_rolegroup_statefulset( &validated_cluster.cluster_config.zookeeper_config_map_name { env.push(EnvVar { - name: "ZOOKEEPER".to_string(), + name: ZOOKEEPER_ENV_VAR_NAME.to_string(), value_from: Some(EnvVarSource { config_map_key_ref: Some(ConfigMapKeySelector { name: zookeeper_config_map_name.to_string(), - key: "ZOOKEEPER".to_string(), + key: ZOOKEEPER_ENV_VAR_NAME.to_string(), ..ConfigMapKeySelector::default() }), ..EnvVarSource::default() @@ -635,7 +640,7 @@ pub fn build_controller_rolegroup_statefulset( .with_label(RESTART_CONTROLLER_ENABLED_LABEL.to_owned()) .build(), spec: Some(StatefulSetSpec { - pod_management_policy: Some("Parallel".to_string()), + pod_management_policy: Some(POD_MANAGEMENT_POLICY_PARALLEL.to_string()), update_strategy: Some(StatefulSetUpdateStrategy { type_: Some("RollingUpdate".to_string()), ..StatefulSetUpdateStrategy::default() diff --git a/rust/operator-binary/src/controller/build/security.rs b/rust/operator-binary/src/controller/build/security.rs index add15883..de8e4f20 100644 --- a/rust/operator-binary/src/controller/build/security.rs +++ b/rust/operator-binary/src/controller/build/security.rs @@ -42,6 +42,13 @@ const OPA_TLS_MOUNT_PATH: &str = "/stackable/tls-opa"; const OPA_TLS_VOLUME_NAME: &str = "tls-opa"; const SSL_STORE_PASSWORD: &str = ""; const SSL_STORE_TYPE_PKCS12: &str = "PKCS12"; +const SSL_CLIENT_AUTH_REQUIRED: &str = "required"; +const SASL_MECHANISM_GSSAPI: &str = "GSSAPI"; +const STACKABLE_KCAT_BINARY: &str = "/stackable/kcat"; +const PROPERTY_SECURITY_PROTOCOL: &str = "security.protocol"; +const PROPERTY_SASL_ENABLED_MECHANISMS: &str = "sasl.enabled.mechanisms"; +const PROPERTY_SASL_KERBEROS_SERVICE_NAME: &str = "sasl.kerberos.service.name"; +const PROPERTY_SASL_INTER_BROKER_MECHANISM: &str = "sasl.mechanism.inter.broker.protocol"; const STACKABLE_TLS_KAFKA_INTERNAL_DIR: &str = "/stackable/tls-kafka-internal"; const STACKABLE_TLS_KAFKA_INTERNAL_VOLUME_NAME: &str = "tls-kafka-internal"; const STACKABLE_TLS_KAFKA_SERVER_DIR: &str = "/stackable/tls-kafka-server"; @@ -91,7 +98,7 @@ pub fn kcat_prober_container_commands(security: &ValidatedKafkaSecurity) -> Vec< let port = security.client_port(); if security.tls_client_authentication_class().is_some() { - args.push("/stackable/kcat".to_string()); + args.push(STACKABLE_KCAT_BINARY.to_string()); args.push("-b".to_string()); args.push(format!("localhost:{}", port)); args.extend(kcat_client_auth_ssl(STACKABLE_TLS_KCAT_DIR)); @@ -130,7 +137,7 @@ pub fn kcat_prober_container_commands(security: &ValidatedKafkaSecurity) -> Vec< ) .to_string(), ); - bash_args.push("/stackable/kcat".to_string()); + bash_args.push(STACKABLE_KCAT_BINARY.to_string()); bash_args.push("-b".to_string()); bash_args.push("$POD_BROKER_LISTENER_ADDRESS:$POD_BROKER_LISTENER_PORT".to_string()); bash_args.extend(kcat_client_sasl_ssl(STACKABLE_TLS_KCAT_DIR, service_name)); @@ -138,13 +145,13 @@ pub fn kcat_prober_container_commands(security: &ValidatedKafkaSecurity) -> Vec< args.push(bash_args.join(" ")); } else if security.tls_server_secret_class().is_some() { - args.push("/stackable/kcat".to_string()); + args.push(STACKABLE_KCAT_BINARY.to_string()); args.push("-b".to_string()); args.push(format!("localhost:{}", port)); args.extend(kcat_client_ssl(STACKABLE_TLS_KCAT_DIR)); args.push("-L".to_string()); } else { - args.push("/stackable/kcat".to_string()); + args.push(STACKABLE_KCAT_BINARY.to_string()); args.push("-b".to_string()); args.push(format!("localhost:{}", port)); args.push("-L".to_string()); @@ -160,10 +167,13 @@ pub fn client_properties(security: &ValidatedKafkaSecurity) -> Vec<(String, Opti if security.tls_client_authentication_class().is_some() { props.push(( - "security.protocol".to_string(), + PROPERTY_SECURITY_PROTOCOL.to_string(), Some(KafkaListenerProtocol::Ssl.to_string()), )); - props.push(("ssl.client.auth".to_string(), Some("required".to_string()))); + props.push(( + "ssl.client.auth".to_string(), + Some(SSL_CLIENT_AUTH_REQUIRED.to_string()), + )); push_client_ssl_stores(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); } else if security.has_kerberos_enabled() { // TODO: to make this configuration file usable out of the box the operator needs to be @@ -172,21 +182,21 @@ pub fn client_properties(security: &ValidatedKafkaSecurity) -> Vec<(String, Opti // This will simplify the code and the command lines lot. // It will also make the jaas files reusable by the Kafka shell scripts. props.push(( - "security.protocol".to_string(), + PROPERTY_SECURITY_PROTOCOL.to_string(), Some(KafkaListenerProtocol::SaslSsl.to_string()), )); push_client_ssl_stores(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); props.push(( - "sasl.enabled.mechanisms".to_string(), - Some("GSSAPI".to_string()), + PROPERTY_SASL_ENABLED_MECHANISMS.to_string(), + Some(SASL_MECHANISM_GSSAPI.to_string()), )); props.push(( - "sasl.kerberos.service.name".to_string(), + PROPERTY_SASL_KERBEROS_SERVICE_NAME.to_string(), Some(KafkaRole::Broker.kerberos_service_name().to_string()), )); props.push(( - "sasl.mechanism.inter.broker.protocol".to_string(), - Some("GSSAPI".to_string()), + PROPERTY_SASL_INTER_BROKER_MECHANISM.to_string(), + Some(SASL_MECHANISM_GSSAPI.to_string()), )); props.push(( "sasl.jaas.config".to_string(), @@ -197,13 +207,13 @@ pub fn client_properties(security: &ValidatedKafkaSecurity) -> Vec<(String, Opti realm="$KERBEROS_REALM")))); } else if security.tls_server_secret_class().is_some() { props.push(( - "security.protocol".to_string(), + PROPERTY_SECURITY_PROTOCOL.to_string(), Some(KafkaListenerProtocol::Ssl.to_string()), )); push_client_ssl_truststore(&mut props, STACKABLE_TLS_KAFKA_SERVER_DIR); } else { props.push(( - "security.protocol".to_string(), + PROPERTY_SECURITY_PROTOCOL.to_string(), Some(KafkaListenerProtocol::Plaintext.to_string()), )); } @@ -417,7 +427,7 @@ pub fn broker_config_settings(security: &ValidatedKafkaSecurity) -> BTreeMap BTreeMap BTreeMap BTreeMap // client auth required config.insert( KafkaListenerName::Controller.listener_ssl_client_auth(), - "required".to_string(), + SSL_CLIENT_AUTH_REQUIRED.to_string(), ); } } // Kerberos if security.has_kerberos_enabled() { - config.insert("sasl.enabled.mechanisms".to_string(), "GSSAPI".to_string()); config.insert( - "sasl.kerberos.service.name".to_string(), + PROPERTY_SASL_ENABLED_MECHANISMS.to_string(), + SASL_MECHANISM_GSSAPI.to_string(), + ); + config.insert( + PROPERTY_SASL_KERBEROS_SERVICE_NAME.to_string(), KafkaRole::Controller.kerberos_service_name().to_string(), ); config.insert( - "sasl.mechanism.inter.broker.protocol".to_string(), - "GSSAPI".to_string(), + PROPERTY_SASL_INTER_BROKER_MECHANISM.to_string(), + SASL_MECHANISM_GSSAPI.to_string(), ); tracing::debug!("Kerberos configs added: [{:#?}]", config); } @@ -631,7 +647,7 @@ fn kcat_client_sasl_ssl(cert_directory: &str, service_name: &str) -> Vec "-X".to_string(), "sasl.kerberos.keytab=/stackable/kerberos/keytab".to_string(), "-X".to_string(), - "sasl.mechanism=GSSAPI".to_string(), + format!("sasl.mechanism={SASL_MECHANISM_GSSAPI}"), "-X".to_string(), format!("sasl.kerberos.service.name={service_name}"), "-X".to_string(), diff --git a/rust/operator-binary/src/crd/listener.rs b/rust/operator-binary/src/crd/listener.rs index bc815751..7aabadad 100644 --- a/rust/operator-binary/src/crd/listener.rs +++ b/rust/operator-binary/src/crd/listener.rs @@ -7,6 +7,13 @@ use strum::EnumString; pub(crate) const LISTENER_LOCAL_ADDRESS: &str = "0.0.0.0"; +// Layout of the listener volume mounted by the listener operator: the default address is +// exposed under `/default-address/address` and per-port values under +// `/default-address/ports/`. +const LISTENER_DEFAULT_ADDRESS_DIR: &str = "default-address"; +const LISTENER_ADDRESS_FILE: &str = "address"; +const LISTENER_PORTS_DIR: &str = "ports"; + #[derive(strum::Display, Debug, EnumString)] pub enum KafkaListenerProtocol { /// Unencrypted and unauthenticated HTTP connections @@ -178,17 +185,19 @@ impl Display for KafkaListener { } pub fn node_address_cmd_env(directory: &str) -> String { - format!("$(cat {directory}/default-address/address)") + format!("$(cat {directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_ADDRESS_FILE})") } pub fn node_port_cmd_env(directory: &str, port_name: &str) -> String { - format!("$(cat {directory}/default-address/ports/{port_name})") + format!("$(cat {directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_PORTS_DIR}/{port_name})") } pub fn node_address_cmd(directory: &str) -> String { - format!("${{file:UTF-8:{directory}/default-address/address}}") + format!("${{file:UTF-8:{directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_ADDRESS_FILE}}}") } pub fn node_port_cmd(directory: &str, port_name: &str) -> String { - format!("${{file:UTF-8:{directory}/default-address/ports/{port_name}}}") + format!( + "${{file:UTF-8:{directory}/{LISTENER_DEFAULT_ADDRESS_DIR}/{LISTENER_PORTS_DIR}/{port_name}}}" + ) } diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index edd553b4..26e7960e 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -280,7 +280,7 @@ impl HasStatusCondition for v1alpha1::KafkaCluster { impl v1alpha1::KafkaCluster { pub fn effective_metadata_manager(&self) -> Result { match &self.spec.cluster_config.metadata_manager { - Some(manager) => match manager.clone() { + Some(manager) => match manager { MetadataManager::ZooKeeper => { if !self.spec.image.product_version().starts_with("3.") { Err(Error::Kafka4RequiresKraftMetadataManager) From 2603f67d227122586cc87240de014b597ff3cea0 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 23:22:54 +0200 Subject: [PATCH 65/66] test: improve unit test coverage --- rust/operator-binary/src/controller.rs | 89 +++++ .../controller/build/resource/config_map.rs | 22 ++ .../src/controller/build/security.rs | 309 ++++++++++++++++++ 3 files changed, 420 insertions(+) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 3992cfa4..6e72ddf5 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -812,3 +812,92 @@ pub(crate) mod test_support { .expect("validate should succeed for the test fixture") } } + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::{ + PodDescriptorsError, + test_support::{minimal_kafka, validated_cluster}, + }; + use crate::crd::role::KafkaRole; + + /// Two broker role groups whose names hash to the same node-id offset must be + /// rejected: a collision would hand two pods the same Kafka `node.id`. `rg865` + /// and `rg1400` are a known colliding pair for the `broker` role (see + /// [`node_id_hash32_offset`](super::node_id_hasher::node_id_hash32_offset)). + #[test] + fn pod_descriptors_rejects_node_id_hash_collision() { + let kafka = minimal_kafka( + 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: + rg865: + replicas: 1 + rg1400: + replicas: 1 + "#, + ); + let validated = validated_cluster(&kafka); + + match validated.pod_descriptors(None) { + Err(PodDescriptorsError::KafkaNodeIdHashCollision { + role, + colliding_role, + .. + }) => { + assert_eq!(role, KafkaRole::Broker); + assert_eq!(colliding_role, KafkaRole::Broker); + } + other => panic!("expected a node-id hash collision error, got {other:?}"), + } + } + + /// Non-colliding role groups expand to one descriptor per replica, each with a + /// unique `node_id`. + #[test] + fn pod_descriptors_assigns_unique_node_ids() { + let kafka = minimal_kafka( + 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: 2 + other: + replicas: 1 + "#, + ); + let validated = validated_cluster(&kafka); + + let descriptors = validated + .pod_descriptors(None) + .expect("non-colliding role groups must not error"); + + assert_eq!(descriptors.len(), 3); + let node_ids: BTreeSet = descriptors.iter().map(|d| d.node_id).collect(); + assert_eq!(node_ids.len(), 3, "node ids must be unique: {node_ids:?}"); + } +} 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 13301aef..392e0961 100644 --- a/rust/operator-binary/src/controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -227,3 +227,25 @@ fn jaas_config_file(is_kerberos_enabled: bool) -> String { }, } } + +#[cfg(test)] +mod tests { + use super::jaas_config_file; + + #[test] + fn jaas_config_file_empty_without_kerberos() { + assert_eq!(jaas_config_file(false), ""); + } + + #[test] + fn jaas_config_file_renders_bootstrap_and_client_sections_with_kerberos() { + let jaas = jaas_config_file(true); + assert!(jaas.contains("bootstrap.KafkaServer")); + assert!(jaas.contains("client.KafkaServer")); + assert!(jaas.contains("Krb5LoginModule")); + assert!(jaas.contains("${env:KERBEROS_REALM}")); + // The bootstrap and client principals embed distinct listener addresses. + assert!(jaas.contains("/stackable/listener-bootstrap")); + assert!(jaas.contains("/stackable/listener-broker")); + } +} diff --git a/rust/operator-binary/src/controller/build/security.rs b/rust/operator-binary/src/controller/build/security.rs index de8e4f20..9bfcb732 100644 --- a/rust/operator-binary/src/controller/build/security.rs +++ b/rust/operator-binary/src/controller/build/security.rs @@ -656,3 +656,312 @@ fn kcat_client_sasl_ssl(cert_directory: &str, service_name: &str) -> Vec ), ] } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + crd::authentication::{core, kerberos, tls}, + }; + + use super::*; + use crate::crd::authentication::ResolvedAuthenticationClasses; + + fn tls_auth_class() -> core::v1alpha1::AuthenticationClass { + core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("tls-auth").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Tls( + tls::v1alpha1::AuthenticationProvider { + client_cert_secret_class: Some("client-auth-secret-class".to_string()), + }, + ), + }, + } + } + + fn kerberos_auth_class() -> core::v1alpha1::AuthenticationClass { + core::v1alpha1::AuthenticationClass { + metadata: ObjectMetaBuilder::new().name("kerberos-auth").build(), + spec: core::v1alpha1::AuthenticationClassSpec { + provider: core::v1alpha1::AuthenticationClassProvider::Kerberos( + kerberos::v1alpha1::AuthenticationProvider { + kerberos_secret_class: "kerberos-secret-class".to_string(), + }, + ), + }, + } + } + + fn no_auth() -> ResolvedAuthenticationClasses { + ResolvedAuthenticationClasses::new(vec![]) + } + + /// Plaintext: no TLS, no authentication, no OPA. + fn plaintext() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new(no_auth(), None, None, None) + } + + /// Server TLS only (encryption without client authentication). + fn server_tls() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new(no_auth(), None, Some("tls".parse().unwrap()), None) + } + + /// Mutual TLS (client-certificate authentication). + fn client_auth_tls() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![tls_auth_class()]), + None, + Some("tls".parse().unwrap()), + None, + ) + } + + /// Kerberos, which also requires server and internal TLS. + fn kerberos() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new( + ResolvedAuthenticationClasses::new(vec![kerberos_auth_class()]), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), + None, + ) + } + + /// Internal TLS only (broker/controller encryption without client TLS). + fn internal_tls() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new(no_auth(), Some("tls".parse().unwrap()), None, None) + } + + /// OPA authorization with a TLS truststore (internal + server TLS also enabled). + fn opa() -> ValidatedKafkaSecurity { + ValidatedKafkaSecurity::new( + no_auth(), + Some("tls".parse().unwrap()), + Some("tls".parse().unwrap()), + Some("opa-tls".parse().unwrap()), + ) + } + + fn as_str_vec(commands: &[String]) -> Vec<&str> { + commands.iter().map(String::as_str).collect() + } + + fn as_map(props: Vec<(String, Option)>) -> BTreeMap> { + props.into_iter().collect() + } + + // ---- kcat_prober_container_commands ---- + + #[test] + fn kcat_prober_plaintext_targets_insecure_client_port() { + let commands = kcat_prober_container_commands(&plaintext()); + assert_eq!( + as_str_vec(&commands), + vec!["/stackable/kcat", "-b", "localhost:9092", "-L"] + ); + } + + #[test] + fn kcat_prober_server_tls_trusts_ca_without_client_key() { + let commands = kcat_prober_container_commands(&server_tls()); + let commands = as_str_vec(&commands); + + assert_eq!(commands[0], "/stackable/kcat"); + assert_eq!(commands[2], "localhost:9093"); + assert_eq!(commands[commands.len() - 1], "-L"); + assert!( + commands + .iter() + .any(|a| a.contains("ssl.ca.location=/stackable/tls-kcat/ca.crt")) + ); + assert!(!commands.iter().any(|a| a.contains("ssl.key.location"))); + } + + #[test] + fn kcat_prober_client_auth_presents_client_key() { + let commands = kcat_prober_container_commands(&client_auth_tls()); + let commands = as_str_vec(&commands); + + assert_eq!(commands[0], "/stackable/kcat"); + assert_eq!(commands[2], "localhost:9093"); + assert!( + commands + .iter() + .any(|a| a.contains("ssl.key.location=/stackable/tls-kcat/tls.key")) + ); + } + + #[test] + fn kcat_prober_kerberos_wraps_command_in_bash() { + let commands = kcat_prober_container_commands(&kerberos()); + let commands = as_str_vec(&commands); + + assert_eq!(commands[0], "/bin/bash"); + assert_eq!(commands[4], "-c"); + assert_eq!(commands.len(), 6); + + let script = commands[5]; + assert!(script.contains("KERBEROS_REALM")); + assert!(script.contains("/stackable/kcat")); + assert!(script.contains("sasl.mechanism=GSSAPI")); + assert!(script.contains("sasl.kerberos.service.name=kafka")); + } + + // ---- copy_opa_tls_cert_command ---- + + #[test] + fn copy_opa_tls_cert_command_imports_ca_when_opa_enabled() { + let command = copy_opa_tls_cert_command(&opa()); + assert!(command.contains("keytool -importcert")); + assert!(command.contains("-alias opa-ca")); + assert!(command.contains("/stackable/tls-opa/ca.crt")); + } + + #[test] + fn copy_opa_tls_cert_command_empty_without_opa() { + assert_eq!(copy_opa_tls_cert_command(&plaintext()), ""); + } + + // ---- client_properties ---- + + #[test] + fn client_properties_plaintext() { + let props = as_map(client_properties(&plaintext())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("PLAINTEXT".to_string())) + ); + assert_eq!(props.len(), 1); + } + + #[test] + fn client_properties_server_tls_uses_truststore_only() { + let props = as_map(client_properties(&server_tls())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("SSL".to_string())) + ); + assert!(props.contains_key("ssl.truststore.location")); + assert!(!props.contains_key("ssl.keystore.location")); + } + + #[test] + fn client_properties_client_auth_requires_keystore_and_client_auth() { + let props = as_map(client_properties(&client_auth_tls())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("SSL".to_string())) + ); + assert_eq!( + props.get("ssl.client.auth"), + Some(&Some("required".to_string())) + ); + assert!(props.contains_key("ssl.keystore.location")); + } + + #[test] + fn client_properties_kerberos_uses_sasl_ssl() { + let props = as_map(client_properties(&kerberos())); + assert_eq!( + props.get("security.protocol"), + Some(&Some("SASL_SSL".to_string())) + ); + assert_eq!( + props.get("sasl.enabled.mechanisms"), + Some(&Some("GSSAPI".to_string())) + ); + assert_eq!( + props.get("sasl.kerberos.service.name"), + Some(&Some("kafka".to_string())) + ); + assert!(props.contains_key("sasl.jaas.config")); + } + + // ---- broker_config_settings ---- + + #[test] + fn broker_config_plaintext_only_sets_inter_broker_listener() { + let config = broker_config_settings(&plaintext()); + assert_eq!( + config.get("inter.broker.listener.name"), + Some(&"INTERNAL".to_string()) + ); + assert_eq!(config.len(), 1); + } + + #[test] + fn broker_config_client_auth_requires_client_auth() { + let config = broker_config_settings(&client_auth_tls()); + assert_eq!( + config.get("listener.name.client.ssl.client.auth"), + Some(&"required".to_string()) + ); + assert!(config.contains_key("listener.name.client.ssl.keystore.location")); + } + + #[test] + fn broker_config_kerberos_adds_sasl_and_bootstrap_stores() { + let config = broker_config_settings(&kerberos()); + assert_eq!( + config.get("sasl.enabled.mechanisms"), + Some(&"GSSAPI".to_string()) + ); + assert_eq!( + config.get("sasl.kerberos.service.name"), + Some(&"kafka".to_string()) + ); + assert_eq!( + config.get("sasl.mechanism.inter.broker.protocol"), + Some(&"GSSAPI".to_string()) + ); + assert!(config.contains_key("listener.name.bootstrap.ssl.keystore.location")); + } + + #[test] + fn broker_config_internal_tls_covers_internal_and_controller() { + let config = broker_config_settings(&internal_tls()); + assert!(config.contains_key("listener.name.internal.ssl.keystore.location")); + assert!(config.contains_key("listener.name.controller.ssl.keystore.location")); + assert_eq!( + config.get("listener.name.internal.ssl.client.auth"), + Some(&"required".to_string()) + ); + } + + #[test] + fn broker_config_opa_adds_authorizer_truststore() { + let config = broker_config_settings(&opa()); + assert!(config.contains_key("opa.authorizer.truststore.path")); + assert!(config.contains_key("opa.authorizer.truststore.password")); + assert!(config.contains_key("opa.authorizer.truststore.type")); + } + + // ---- controller_config_settings ---- + + #[test] + fn controller_config_plaintext_is_empty() { + assert!(controller_config_settings(&plaintext()).is_empty()); + } + + #[test] + fn controller_config_internal_tls_covers_controller_and_internal() { + let config = controller_config_settings(&internal_tls()); + assert!(config.contains_key("listener.name.controller.ssl.keystore.location")); + assert!(config.contains_key("listener.name.internal.ssl.keystore.location")); + } + + #[test] + fn controller_config_kerberos_adds_sasl() { + let config = controller_config_settings(&kerberos()); + assert_eq!( + config.get("sasl.enabled.mechanisms"), + Some(&"GSSAPI".to_string()) + ); + assert_eq!( + config.get("sasl.kerberos.service.name"), + Some(&"kafka".to_string()) + ); + } +} From 9d4cf1ffca4567ddeea0e4ae6d8d13b774ee6489 Mon Sep 17 00:00:00 2001 From: Malte Sander Date: Wed, 17 Jun 2026 23:46:21 +0200 Subject: [PATCH 66/66] revert: rolegroup length changes --- 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 8371ed91..e445cb01 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 + name: test-kafka-broker-automatic-log-config status: readyReplicas: 1 replicas: 1 @@ -14,7 +14,7 @@ status: apiVersion: apps/v1 kind: StatefulSet metadata: - name: test-kafka-broker-custom-log + name: test-kafka-broker-custom-log-config 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 8f3e6dab..42588924 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: + automatic-log-config: replicas: 1 config: logging: @@ -86,7 +86,7 @@ spec: - name: prepared-logs configMap: name: prepared-logs - custom-log: + custom-log-config: 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 ec77e308..c072e08c 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":{"replicas":0}, "custom-log":{"replicas":0}}}}}' + kubectl patch kafkacluster test-kafka -n $NAMESPACE --type merge -p '{"spec":{"brokers":{"roleGroups":{"automatic-log-config":{"replicas":0}, "custom-log-config":{"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 15a49f43..f30e142e 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-0" && + .pod == "test-kafka-broker-automatic-log-config-0" && .container == "kafka" filteredAutomaticLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-automatic-log-0" && + .pod == "test-kafka-broker-automatic-log-config-0" && .container == "vector" filteredCustomLogConfigBrokerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-0" && + .pod == "test-kafka-broker-custom-log-config-0" && .container == "kafka" filteredCustomLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-0" && + .pod == "test-kafka-broker-custom-log-config-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 14ef3091..c93294b3 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 + name: test-kafka-broker-automatic-log-config 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 + name: test-kafka-broker-custom-log-config 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 + name: test-kafka-controller-automatic-log-config 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 + name: test-kafka-controller-custom-log-config 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-headless + name: test-kafka-broker-automatic-log-config-headless spec: ports: - name: kafka-tls @@ -94,7 +94,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-broker-automatic-log-metrics + name: test-kafka-broker-automatic-log-config-metrics spec: ports: - name: metrics @@ -105,7 +105,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-broker-custom-log-headless + name: test-kafka-broker-custom-log-config-headless spec: ports: - name: kafka-tls @@ -116,7 +116,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-broker-custom-log-metrics + name: test-kafka-broker-custom-log-config-metrics spec: ports: - name: metrics @@ -127,7 +127,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-automatic-log-headless + name: test-kafka-controller-automatic-log-config-headless spec: ports: - name: kafka-tls @@ -138,7 +138,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-automatic-log-metrics + name: test-kafka-controller-automatic-log-config-metrics spec: ports: - name: metrics @@ -149,7 +149,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-custom-log-headless + name: test-kafka-controller-custom-log-config-headless spec: ports: - name: kafka-tls @@ -160,7 +160,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: test-kafka-controller-custom-log-metrics + name: test-kafka-controller-custom-log-config-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 0a1eeb06..95d85da6 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: + automatic-log-config: replicas: 1 config: logging: @@ -117,7 +117,7 @@ spec: - name: prepared-logs configMap: name: prepared-logs - custom-log: + custom-log-config: replicas: 1 config: logging: @@ -157,7 +157,7 @@ spec: cpu: 300m limits: cpu: 1100m - automatic-log: + automatic-log-config: replicas: 1 config: logging: @@ -190,7 +190,7 @@ spec: - name: prepared-logs configMap: name: prepared-logs - custom-log: + custom-log-config: 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 cda90a7b..67eed310 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-0" && + .pod == "test-kafka-broker-automatic-log-config-0" && .container == "kafka" filteredAutomaticLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-automatic-log-0" && + .pod == "test-kafka-broker-automatic-log-config-0" && .container == "vector" filteredCustomLogConfigBrokerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-0" && + .pod == "test-kafka-broker-custom-log-config-0" && .container == "kafka" filteredCustomLogConfigBrokerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-broker-custom-log-0" && + .pod == "test-kafka-broker-custom-log-config-0" && .container == "vector" filteredAutomaticLogConfigControllerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-automatic-log-0" && + .pod == "test-kafka-controller-automatic-log-config-0" && .container == "kafka" filteredAutomaticLogConfigControllerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-automatic-log-0" && + .pod == "test-kafka-controller-automatic-log-config-0" && .container == "vector" filteredCustomLogConfigControllerKafka: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-custom-log-0" && + .pod == "test-kafka-controller-custom-log-config-0" && .container == "kafka" filteredCustomLogConfigControllerVector: type: filter inputs: [validEvents] condition: >- - .pod == "test-kafka-controller-custom-log-0" && + .pod == "test-kafka-controller-custom-log-config-0" && .container == "vector" filteredInvalidEvents: type: filter