Skip to content

Commit f11c86a

Browse files
nightkrNickLarsenNZsbernauer
authored
Allow overriding ZNode path (#799)
* Add znodePath field to ZookeeperZnode status * Error handling * Docs, update CRD YAML * Changelog * Add znode override test * Documentation * Add missing doc start marker * fix: use module level error * chore: apply clippy suggestion * chore: apply markdown lint suggestion * Update CHANGELOG.md Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de> --------- Co-authored-by: Nick Larsen <nick.larsen@stackable.tech> Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de>
1 parent f6f416e commit f11c86a

File tree

8 files changed

+103
-11
lines changed

8 files changed

+103
-11
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Allow overriding ZNode path by setting `status.znodePath` ([#799]).
10+
711
### Changed
812

913
- Bump `built`, `clap`, `rstest`, `stackable-operator` and `strum`
@@ -14,6 +18,7 @@ All notable changes to this project will be documented in this file.
1418
- Processing of corrupted log events fixed; If errors occur, the error
1519
messages are added to the log event ([#821]).
1620

21+
[#799]: https://github.com/stackabletech/zookeeper-operator/pull/799
1722
[#812]: https://github.com/stackabletech/zookeeper-operator/pull/812
1823
[#821]: https://github.com/stackabletech/zookeeper-operator/pull/821
1924

deploy/helm/zookeeper-operator/crds/crds.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7561,10 +7561,22 @@ spec:
75617561
type: string
75627562
type: object
75637563
type: object
7564+
status:
7565+
nullable: true
7566+
properties:
7567+
znodePath:
7568+
description: |-
7569+
The absolute ZNode allocated to the ZookeeperZnode. This will typically be set by the operator.
7570+
7571+
This can be set explicitly by an administrator, such as when restoring from a backup.
7572+
nullable: true
7573+
type: string
7574+
type: object
75647575
required:
75657576
- spec
75667577
title: ZookeeperZnode
75677578
type: object
75687579
served: true
75697580
storage: true
7570-
subresources: {}
7581+
subresources:
7582+
status: {}

deploy/helm/zookeeper-operator/templates/roles.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ rules:
120120
- {{ include "operator.name" . }}.stackable.tech
121121
resources:
122122
- {{ include "operator.name" . }}clusters/status
123+
- {{ include "operator.name" . }}znodes/status
123124
verbs:
124125
- patch
125126
{{ if .Capabilities.APIVersions.Has "security.openshift.io/v1" }}

docs/modules/zookeeper/pages/usage_guide/isolating_clients_with_znodes.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,20 @@ The Stackable Operators for Kafka and Druid use the discovery ConfigMaps to conn
100100
== What's next
101101

102102
You can find out more about the discovery ConfigMap xref:discovery.adoc[] and the xref:znodes.adoc[] in the concepts documentation.
103+
104+
== Restoring from backups
105+
106+
For security reasons, a unique ZNode path is generated every time the same ZookeeperZnode object is recreated, even if it has the same name.
107+
108+
If a ZookeeperZnode needs to be associated with an existing ZNode path, the field `status.znodePath` can be set to
109+
the desired path. Note that since this is a subfield of `status`, it must explicitly be updated on the `status` subresource,
110+
and requires RBAC permissions to replace the `zookeeperznodes/status` resource. For example:
111+
112+
[source,bash]
113+
----
114+
kubectl get zookeeperznode/test-znode -o json -n $NAMESPACE \
115+
| jq '.status.znodePath = "/znode-override"' \
116+
| kubectl replace -f- --subresource=status
117+
----
118+
119+
NOTE: The auto-generated ZNode will still be kept, and should be cleaned up by an administrator.

rust/crd/src/lib.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,16 +466,14 @@ impl ZookeeperCluster {
466466
"1.", "2.", "3.0.", "3.1.", "3.2.", "3.3.", "3.4.", "3.5.", "3.6.", "3.7.",
467467
];
468468

469-
let framework = if zookeeper_versions_with_log4j
469+
if zookeeper_versions_with_log4j
470470
.into_iter()
471471
.any(|prefix| version.starts_with(prefix))
472472
{
473473
LoggingFramework::LOG4J
474474
} else {
475475
LoggingFramework::LOGBACK
476-
};
477-
478-
framework
476+
}
479477
}
480478

481479
/// The name of the role-level load-balanced Kubernetes `Service`
@@ -695,6 +693,7 @@ impl ZookeeperPodRef {
695693
plural = "zookeeperznodes",
696694
shortname = "zno",
697695
shortname = "znode",
696+
status = "ZookeeperZnodeStatus",
698697
namespaced,
699698
crates(
700699
kube_core = "stackable_operator::kube::core",
@@ -709,6 +708,15 @@ pub struct ZookeeperZnodeSpec {
709708
pub cluster_ref: ClusterRef<ZookeeperCluster>,
710709
}
711710

711+
#[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
712+
#[serde(rename_all = "camelCase")]
713+
pub struct ZookeeperZnodeStatus {
714+
/// The absolute ZNode allocated to the ZookeeperZnode. This will typically be set by the operator.
715+
///
716+
/// This can be set explicitly by an administrator, such as when restoring from a backup.
717+
pub znode_path: Option<String>,
718+
}
719+
712720
#[cfg(test)]
713721
mod tests {
714722
use super::*;

rust/operator-binary/src/znode_controller.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Reconciles state for ZooKeeper znodes between Kubernetes [`ZookeeperZnode`] objects and the ZooKeeper cluster
22
//!
33
//! See [`ZookeeperZnode`] for more details.
4-
use std::{convert::Infallible, sync::Arc};
4+
use std::{borrow::Cow, convert::Infallible, sync::Arc};
55

66
use snafu::{OptionExt, ResultExt, Snafu};
77
use stackable_operator::{
@@ -19,9 +19,11 @@ use stackable_operator::{
1919
time::Duration,
2020
};
2121
use stackable_zookeeper_crd::{
22-
security::ZookeeperSecurity, ZookeeperCluster, ZookeeperZnode, DOCKER_IMAGE_BASE_NAME,
22+
security::ZookeeperSecurity, ZookeeperCluster, ZookeeperZnode, ZookeeperZnodeStatus,
23+
DOCKER_IMAGE_BASE_NAME,
2324
};
2425
use strum::{EnumDiscriminants, IntoStaticStr};
26+
use tracing::{debug, info};
2527

2628
use crate::{
2729
discovery::{self, build_discovery_configmaps},
@@ -92,6 +94,11 @@ pub enum Error {
9294
cm: ObjectRef<ConfigMap>,
9395
},
9496

97+
#[snafu(display("failed to update status"))]
98+
ApplyStatus {
99+
source: stackable_operator::client::Error,
100+
},
101+
95102
#[snafu(display("error managing finalizer"))]
96103
Finalizer {
97104
source: finalizer::Error<Infallible>,
@@ -148,6 +155,7 @@ impl ReconcilerError for Error {
148155
Error::EnsureZnodeMissing { zk, .. } => Some(zk.clone().erase()),
149156
Error::BuildDiscoveryConfigMap { source: _ } => None,
150157
Error::ApplyDiscoveryConfigMap { cm, .. } => Some(cm.clone().erase()),
158+
Error::ApplyStatus { .. } => None,
151159
Error::Finalizer { source: _ } => None,
152160
Error::DeleteOrphans { source: _ } => None,
153161
Error::ObjectHasNoNamespace => None,
@@ -174,14 +182,40 @@ pub async fn reconcile_znode(
174182
let client = &ctx.client;
175183

176184
let zk = find_zk_of_znode(client, &znode).await;
177-
// Use the uid (managed by k8s itself) rather than the object name, to ensure that malicious users can't trick the controller
178-
// into letting them take over a znode owned by someone else
179-
let znode_path = format!("/znode-{}", uid);
185+
let mut default_status_updates: Option<ZookeeperZnodeStatus> = None;
186+
// Store the znode path in the status rather than the object itself, to ensure that only K8s administrators can override it
187+
let znode_path = match znode.status.as_ref().and_then(|s| s.znode_path.as_deref()) {
188+
Some(znode_path) => {
189+
debug!(znode.path = znode_path, "Using configured znode path");
190+
Cow::Borrowed(znode_path)
191+
}
192+
None => {
193+
// Default to the uid (managed by k8s itself) rather than the object name, to ensure that malicious users can't trick the controller
194+
// into letting them take over a znode owned by someone else
195+
let znode_path = format!("/znode-{}", uid);
196+
info!(
197+
znode.path = znode_path,
198+
"No znode path set, setting to default"
199+
);
200+
default_status_updates
201+
.get_or_insert_with(Default::default)
202+
.znode_path = Some(znode_path.clone());
203+
Cow::Owned(znode_path)
204+
}
205+
};
206+
207+
if let Some(status) = default_status_updates {
208+
info!("Writing default configuration to status");
209+
ctx.client
210+
.merge_patch_status(&*znode, &status)
211+
.await
212+
.context(ApplyStatusSnafu)?;
213+
}
180214

181215
finalizer(
182216
&client.get_api::<ZookeeperZnode>(&ns),
183217
&format!("{OPERATOR_NAME}/znode"),
184-
znode,
218+
znode.clone(),
185219
|ev| async {
186220
match ev {
187221
finalizer::Event::Apply(znode) => {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
apiVersion: kuttl.dev/v1beta1
3+
kind: TestAssert
4+
---
5+
apiVersion: v1
6+
kind: ConfigMap
7+
metadata:
8+
name: test-znode
9+
data:
10+
ZOOKEEPER_CHROOT: /znode-override
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
apiVersion: kuttl.dev/v1beta1
3+
kind: TestStep
4+
commands:
5+
- script: kubectl get zookeeperznode/test-znode -o json -n $NAMESPACE | jq '.status.znodePath = "/znode-override"' | kubectl replace -f- --subresource=status

0 commit comments

Comments
 (0)