Skip to content

refactor: Extract build and apply steps in the zk- and znode-controller - #1069

Merged
maltesander merged 13 commits into
mainfrom
refactor/extract-apply-and-status-steps
Aug 12, 2026
Merged

refactor: Extract build and apply steps in the zk- and znode-controller#1069
maltesander merged 13 commits into
mainfrom
refactor/extract-apply-and-status-steps

Conversation

@maltesander

@maltesander maltesander commented Aug 5, 2026

Copy link
Copy Markdown
Member

Description

Brings the ZooKeeper operator in line with the apply and update status steps merged in airflow-operator#828 and hbase-operator#787, with the opensearch-operator as the target shape.

Both controllers now run an explicit pipeline, with each step in its own submodule:

  • ZookeeperCluster:apply, update_status
  • ZookeeperZnode: build, apply

Behavioural changes

  • The ZookeeperCluster controller watches Listeners, and the operator ClusterRole gains the watch verb on listeners.stackable.tech, which it did not have.
  • A Listener without ingress addresses is no longer an error for the cluster controller (the discovery ConfigMap is simply skipped until the next reconciliation). It remains an error for the znode controller.

Tests

--- PASS: kuttl (218.72s)
    --- PASS: kuttl/harness (0.00s)
        --- PASS: kuttl/harness/cluster-operation_zookeeper-latest-3.9.5_openshift-false (45.76s)
        --- PASS: kuttl/harness/smoke_zookeeper-3.9.5_use-server-tls-false_use-client-auth-tls-true_openshift-false (93.24s)
        --- PASS: kuttl/harness/logging_zookeeper-3.9.5_openshift-false (60.66s)
        --- PASS: kuttl/harness/smoke_zookeeper-3.9.5_use-server-tls-false_use-client-auth-tls-false_openshift-false (50.81s)
        --- PASS: kuttl/harness/smoke_zookeeper-3.9.5_use-server-tls-true_use-client-auth-tls-true_openshift-false (55.86s)
        --- PASS: kuttl/harness/znode_zookeeper-latest-3.9.5_openshift-false (22.54s)
        --- PASS: kuttl/harness/delete-rolegroup_zookeeper-3.9.5_openshift-false (31.13s)
        --- PASS: kuttl/harness/smoke_zookeeper-3.9.5_use-server-tls-true_use-client-auth-tls-false_openshift-false (56.42s)

Definition of Done Checklist

  • Not all of these items are applicable to all PRs, the author should update this template to only leave the boxes in that are relevant
  • Please make sure all these things are done and tick the boxes

Author

  • Changes are OpenShift compatible
  • CRD changes approved
  • CRD documentation for all fields, following the style guide.
  • Helm chart can be installed and deployed operator works
  • Integration tests passed (for non trivial changes)
  • Changes need to be "offline" compatible
  • Links to generated (nightly) docs added
  • Release note snippet added

Reviewer

  • Code contains useful comments
  • Code contains useful logging statements
  • (Integration-)Test cases added
  • Documentation added or updated. Follows the style guide.
  • Changelog updated
  • Cargo.toml only contains references to git tags (not specific commits or branches)

Acceptance

  • Feature Tracker has been updated
  • Proper release label has been added
  • Links to generated (nightly) docs added
  • Release note snippet added
  • Add type/deprecation label & add to the deprecation schedule
  • Add type/experimental label & add to the experimental features tracker

The discovery ConfigMap advertises the addresses that the listener
operator publishes on the ZooKeeper role Listener, so it used to be
built and applied inline in reconcile_zk, from the Listener that had
just been applied. That Listener carries no addresses yet on the first
reconciliation, so the controller relied on the reconciliation failing
and being requeued five seconds later.

Follow the pattern the OpenSearch operator already uses: dereference the
role Listener, validate its addresses into the ValidatedCluster, and let
build() emit an Option<ConfigMap>. The controller now watches Listeners,
so the reconciliation that writes the discovery ConfigMap is triggered as
soon as the addresses appear, and it also reruns when they change.

The listener address extraction moves into a shared listener_addresses
module, because the ZookeeperZnode controller needs it too.

The operator ClusterRole gains the watch verb on listeners, which is
required for the new watch.
reconcile_zk built the ClusterResources itself and applied every
collection with its own inline loop, so the driver carried the apply
order, the orphan deletion and the resource specific error variants.

Extract all of that into an Applier, following the airflow and hbase
operators. KubernetesResources is now marked as either Prepared or
Applied, which makes it impossible to derive the cluster status from
resources that were only built. apply() destructures the resource set
exhaustively, so a new field fails to compile here instead of silently
never being applied.

Unlike the sibling operators, the apply module is declared in
zk_controller.rs itself, so the Applier is imported without `self` to
avoid a name collision with the module declaration.
The cluster conditions and the discovery hash were computed inline at
the end of reconcile_zk, from resources that happened to be in scope.

Move both into an update_status step, following the airflow and hbase
operators. It takes KubernetesResources<Applied>, so the type system
proves the status is derived from resources that were actually applied
rather than merely built. The discovery hash, which the sibling
operators do not have, becomes a private helper next to it.

reconcile_zk is now the dereference, validate, build, apply and
update_status pipeline and nothing else.
reconcile_apply created the ClusterResources, talked to ZooKeeper,
fetched the role Listener, built the discovery ConfigMap and applied it,
all inline, so the ZookeeperZnode controller was the only one left
without a pipeline.

Give it the same dereference, validate, build and apply structure as the
ZookeeperCluster controller. The Listener fetch moves into the
dereference step, where a missing Listener stays a non error so it can
never block finalizer removal, and its addresses become a validated
field on ValidatedZnode. Creating the znode inside the ZooKeeper
ensemble lives in the apply step as a free function, next to the
Applier, because it is a client side effect that the client free build()
cannot perform.

The discovery ConfigMap builders move to a shared discovery module, so
the znode controller no longer reaches into the cluster controller's
build tree.

Unlike the cluster controller, the resources carry no Prepared or
Applied marker: the ZookeeperZnode has no cluster conditions, so there
is no status step the marker could protect.
@maltesander maltesander changed the title refactor: Extract build and apply steps in the znode controller refactor: Extract build and apply steps in the zk- and znode-controller Aug 5, 2026
@adwk67
adwk67 self-requested a review August 12, 2026 09:07

@adwk67 adwk67 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discovery.rs has not test module: we could at least add a test asserting that a Listener with addresses produces a discovery CM with the right
ZOOKEEPER/ZOOKEEPER_HOSTS/ZOOKEEPER_CLIENT_PORT/ZOOKEEPER_CHROOT content.

Comment thread rust/operator-binary/src/zk_controller/build.rs Outdated
Comment thread rust/operator-binary/src/discovery.rs Outdated
Comment thread rust/operator-binary/src/znode_controller/apply.rs Outdated
Comment thread rust/operator-binary/src/znode_controller/validate.rs
Comment thread rust/operator-binary/src/zk_controller/validate.rs
Comment thread rust/operator-binary/src/zk_controller/update_status.rs Outdated
Skipping the discovery ConfigMap while the role Listener publishes no
addresses let the apply step delete the already published one as an
orphan, breaking consumers that mount it. Before the pipeline
extraction, the missing addresses aborted the run before
delete_orphaned_resources() ran, so this only surfaced now.

Follow the kafka-operator pattern instead: always write the ConfigMap,
with an empty ZOOKEEPER value while no address is known. The Listener
watch triggers a new run that fills the value in.
The znode controller had no tests at all. Add fixtures for it and cover
both ways the role Listener can block validation: addresses published
under an unexpected port name (ReadRoleListenerAddresses), and no
addresses at all, either because the Listener does not exist yet or
because the listener operator has not published its status
(NoRoleListenerAddresses).

The Listener fixtures move into a shared test_support module in
listener_addresses, so the cluster controller's tests can use them too.
Add a try_validate variant that takes a role Listener, and cover the
three outcomes: addresses published under an unexpected port name fail
with ReadRoleListenerAddresses, published addresses reach the validated
cluster sorted, and a missing or unpublished Listener validates to no
addresses rather than an error, which is what lets the build step still
write the discovery ConfigMap.
Assert the ZOOKEEPER, ZOOKEEPER_HOSTS, ZOOKEEPER_CLIENT_PORT and
ZOOKEEPER_CHROOT content for both the cluster and the znode variant,
that only ZOOKEEPER carries the chroot, and that a relative chroot is
rejected. The empty-address case is covered too, pinning that the
ConfigMap is written even while the role Listener publishes nothing.

The Listener fixtures now publish the secure client port, which is what
the role Listener serves while the fixture keeps TLS enabled.
@maltesander
maltesander requested a review from adwk67 August 12, 2026 10:53
adwk67
adwk67 previously approved these changes Aug 12, 2026

@adwk67 adwk67 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - just made the user-facing change clear in the changelog.

Comment thread CHANGELOG.md
Co-authored-by: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com>
@maltesander
maltesander added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit c4be308 Aug 12, 2026
18 checks passed
@maltesander
maltesander deleted the refactor/extract-apply-and-status-steps branch August 12, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants