Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
571bf23
feat: add quorum-manager sidecar for dynamic KRaft voter membership
razvan Aug 18, 2026
059e7d9
feat: add startup/liveness/readiness probes for KRaft controllers
razvan Aug 18, 2026
f98c117
feat: use OrderedReady pod management for controller StatefulSets
razvan Aug 18, 2026
645061b
fix: reject and gracefully handle KRaft controllers scaled to zero
razvan Aug 18, 2026
930a497
test: cover KRaft controller scale-up/down/shutdown in kuttl operatio…
razvan Aug 18, 2026
3074345
docs: document KRaft dynamic voter membership and update CHANGELOG
razvan Aug 18, 2026
fc21e2e
Cleanup inline comments and documentation
razvan Aug 18, 2026
d02b920
Remove the custom bash trap functions
razvan Aug 19, 2026
62adbff
Comments abd documentation cleanups
razvan Aug 19, 2026
95839be
Merge remote-tracking branch 'origin/main' into feat/kraft-dynamic-vo…
razvan Aug 20, 2026
c4cb3a5
Cleanup CHANGELOG and KRaft docs.
razvan Aug 20, 2026
4b42def
Remove the kcat-prober container and consolidate container probes
razvan Aug 20, 2026
0596a0a
Cleanups
razvan Aug 20, 2026
ed3180b
Stop Kafka to prevent kuttl namespace deletion timeouts
razvan Aug 21, 2026
6276e1f
Merge branch 'main' into feat/kraft-dynamic-voter-membership
maltesander Sep 1, 2026
cfc9745
Apply batched suggestions from code review
razvan Sep 9, 2026
d9406f8
Apply batched suggestions from code review
razvan Sep 9, 2026
12c090f
fix pre commit hooks
razvan Sep 9, 2026
8b7391d
implement review feedback: more idiomatic rust
razvan Sep 9, 2026
c551a4d
review feedback: extract pre-stop command into separate script
razvan Sep 9, 2026
f52775e
fix the nightly test suite
razvan Sep 10, 2026
9f19d62
review feedback: in kraft mode, controllers are mandatory
razvan Sep 10, 2026
a0b4985
review feedback: add troublshooting docs and extract quorum manager c…
razvan Sep 10, 2026
ee8304b
Merge branch 'main' into feat/kraft-dynamic-voter-membership
razvan Sep 10, 2026
581e81b
review feedback: move controller scripts to scripts/ folder
razvan Sep 11, 2026
f7b8a93
review feedback: cleanup comments, remove troubleshooting guide
razvan Sep 11, 2026
dd5fbbe
Apply batched suggestions from code review
razvan Sep 14, 2026
e50c898
review feedback: add timestamps to quorum scrips
razvan Sep 14, 2026
8705fb0
review feedback: clean up unit tests
razvan Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ tilt_options.json
.envrc

.DS_Store

.worktrees/
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file.

### Changed

- The dynamic KRaft quorum created by the operator is now scaled automatically. Previously,
manual intervention was needed after every scale operation.
This change introduces a new side-car container (`quorum-manager`) to all controller pods
that adds the new controller to the voter list.
On termination, a new `preStop` hook on the controller container (`kafka`) removes the pod from
the voter list before shutdown.
The property `controller.quorum.bootstrap.servers` now contains the headless service names
of all controller role groups instead of individual peer host names. This prevents the
restart controller from restarting all pods in the quorum when a new one is added/deleted.
The controller `StatefulSet` is now scaled using `OrderedReady` instead of the `Parallel` strategy
to ensure only one voter is added/removed at a time and thus keep the quorum healthy ([#1010]).
- Internal operator refactoring: introduce a build() step in the reconciler that
assembles all relevant Kubernetes resources before anything is applied ([#985]).
- Bump stackable-operator to 0.116.0 ([#994], [#1011]).
Expand Down Expand Up @@ -34,12 +45,24 @@ All notable changes to this project will be documented in this file.
See [our internal issue](https://github.com/stackabletech/hdfs-operator/issues/626) and [the fix](https://github.com/kube-rs/kube/pull/2042) for details ([#998]).
- The operator now watches all resources that it creates and early-exits the reconcile action when the
cluster is marked for deletion ([#1014]).
- A KRaft cluster without a `controllers` role is now rejected during validation.
Previously, the operator would create a cluster consisting only of `brokers` which would never
become healthy ([#1010]).

### Removed

- BREAKING: The broker pod's separate `kcat-prober` sidecar container has been removed; its
`kcat`-based readiness probe now runs directly on the `kafka` container instead (`kcat` has
shipped in the same product image as `kafka` since #527, so the dedicated container/image was
no longer needed). The `kcat-prober` value is no longer accepted in a broker's
`logging.containers` CRD field ([#1010]).

[#985]: https://github.com/stackabletech/kafka-operator/pull/985
[#990]: https://github.com/stackabletech/kafka-operator/pull/990
[#994]: https://github.com/stackabletech/kafka-operator/pull/994
[#998]: https://github.com/stackabletech/kafka-operator/pull/998
[#1000]: https://github.com/stackabletech/kafka-operator/pull/1000
[#1010]: https://github.com/stackabletech/kafka-operator/pull/1010
[#1011]: https://github.com/stackabletech/kafka-operator/pull/1011
[#1014]: https://github.com/stackabletech/kafka-operator/pull/1014
[#1017]: https://github.com/stackabletech/kafka-operator/pull/1017
Expand Down
36 changes: 16 additions & 20 deletions docs/modules/kafka/pages/usage-guide/kraft-controller.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -85,33 +85,29 @@ KRaft mode requires major configuration changes compared to ZooKeeper:
* `cluster-id`: This is set to the `metadata.name` of the KafkaCluster resource during initial formatting
* `node.id`: This is a calculated integer, hashed from the `role` and `rolegroup` and added `replica` id.
* `process.roles`: Will always only be `broker` or `controller`. Mixed `broker,controller` servers are not supported.
* The operator configures a static voter list containing the controller pods. Controllers are not dynamically managed.
* Each controller pod runs an additional `quorum-manager` sidecar container that, on startup, admits the pod into
the KRaft voter set.
On pod restart or termination, the controller is removed from the voter set by the `preStop` hook on the `kafka` container.
* Exactly one controller (the one with the numerically lowest `node.id` among all controller pod descriptors) formats
with `kafka-storage.sh format --standalone`, bootstrapping a single-node quorum by itself.
Every other controller formats with `--no-initial-controllers` and joins purely through the sidecar's `add-controller` call.
Brokers always format with `--no-initial-controllers` too; they are never voters.
* `controller.quorum.bootstrap.servers` points at each controller role group's own headless Service DNS name, not
individual pod addresses.

== Known Issues

* Automatic migration from Apache ZooKeeper to KRaft is not supported.
* Scaling controller replicas might lead to unstable clusters.
* Kerberos is currently not supported for KRaft in all versions.
* The quorum is created once by the controller with the lowest `node.id` using `--standalone`. If this controller
loses it's PVC, a new conflicting quorum is created on restart.
* A Controller that loses its persistent volume is not re-admitted to the voter set automatically, because it
returns with a new directory ID while the quorum still lists the old one.

== Troubleshooting
==== Prevention

=== Cluster does not start

Check that at least a quorum (majority) of controllers are reachable.

=== Frequent leader elections

Likely caused by controller resource starvation or unstable Kubernetes scheduling.

=== Migration issues (ZooKeeper to KRaft)

Ensure Kafka version 3.9.x and higher and follow the official migration documentation.
The Stackable Kafka operator currently does not support the migration.

=== Scaling issues

The https://developers.redhat.com/articles/2024/11/27/dynamic-kafka-controller-quorum[Dynamic scaling] is only supported from Kafka version 3.9.0.
If you are using older versions, automatic scaling may not work properly (e.g. adding or removing controller replicas).
* Use a StorageClass backed by network-attached storage, so that a node failure does not imply volume loss.
* Do not delete Controller PVCs as part of routine maintenance.

== Kraft migration guide

Expand Down
5 changes: 1 addition & 4 deletions docs/modules/kafka/partials/supported-versions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
* 3.9.2 (LTS)
* 3.9.1 (deprecated)

Support for clusters running in Kraft mode (which includes Apache Kafka 4.x.x) is experimental because it has not been thoroughly tested in production environments yet.
Support for clusters running in Kraft mode (which includes Apache Kafka >= 4.x) is experimental due to the following known issues:

Also there are some known issues such as:

* Controller scaling is not reliable.
* Kerberos authentication is not tested yet.
* Service exposition is not definitive.
160 changes: 0 additions & 160 deletions extra/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,86 +181,6 @@ spec:
description: Configuration per logger
type: object
type: object
kcat-prober:
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
vector:
anyOf:
- required:
Expand Down Expand Up @@ -717,86 +637,6 @@ spec:
description: Configuration per logger
type: object
type: object
kcat-prober:
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
vector:
anyOf:
- required:
Expand Down
9 changes: 8 additions & 1 deletion rust/operator-binary/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,14 @@ pub(crate) mod test_support {

/// Runs the real validate step against a minimal (auth/OPA-free) fixture.
pub fn validated_cluster(kafka: &v1alpha1::KafkaCluster) -> ValidatedCluster {
validate_err(kafka).expect("validate should succeed for the test fixture")
}

/// Runs the real validate step against a minimal (auth/OPA-free) fixture, without unwrapping
/// the result. Used for tests asserting on a specific validation failure.
pub fn validate_err(
kafka: &v1alpha1::KafkaCluster,
) -> Result<ValidatedCluster, super::validate::Error> {
validate(
kafka,
DereferencedObjects {
Expand All @@ -621,7 +629,6 @@ pub(crate) mod test_support {
},
&operator_environment(),
)
.expect("validate should succeed for the test fixture")
}
}

Expand Down
Loading
Loading