Skip to content

Commit 0a68d92

Browse files
committed
refactor: replace 14-assert cm discovery checks
1 parent 590a4b2 commit 0a68d92

3 files changed

Lines changed: 237 additions & 40 deletions

File tree

rust/operator-binary/src/zk_controller/build/resource/discovery.rs

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,61 @@ mod tests {
225225
};
226226

227227
use super::*;
228+
use crate::{
229+
zk_controller::{
230+
ZK_CONTROLLER_NAME,
231+
test_support::{minimal_zk, validated_cluster},
232+
},
233+
znode_controller::{
234+
ZNODE_CONTROLLER_NAME,
235+
validate::test_support::{minimal_znode, validated_znode},
236+
},
237+
};
238+
239+
/// A `ZookeeperCluster` fixture. `test_support`'s auth-free `new_for_tests()` still enables
240+
/// server TLS, so this resolves to the secure client port (2282).
241+
const TLS_ZK: &str = r#"
242+
apiVersion: zookeeper.stackable.tech/v1alpha1
243+
kind: ZookeeperCluster
244+
metadata:
245+
name: test-zk
246+
spec:
247+
image:
248+
productVersion: "3.9.5"
249+
servers:
250+
roleGroups:
251+
default:
252+
replicas: 3
253+
"#;
254+
255+
/// Same as [`TLS_ZK`] but with `serverSecretClass: null`, disabling server TLS so the insecure
256+
/// client port (2181) is used.
257+
const NON_TLS_ZK: &str = r#"
258+
apiVersion: zookeeper.stackable.tech/v1alpha1
259+
kind: ZookeeperCluster
260+
metadata:
261+
name: test-zk
262+
spec:
263+
image:
264+
productVersion: "3.9.5"
265+
clusterConfig:
266+
tls:
267+
serverSecretClass: null
268+
servers:
269+
roleGroups:
270+
default:
271+
replicas: 3
272+
"#;
273+
274+
const ZNODE: &str = r#"
275+
apiVersion: zookeeper.stackable.tech/v1alpha1
276+
kind: ZookeeperZnode
277+
metadata:
278+
name: test-znode
279+
spec:
280+
clusterRef:
281+
name: test-zk
282+
"#;
228283

229284
fn listener(ingress_addresses: Option<Vec<ListenerIngress>>) -> Listener {
230285
Listener {
@@ -241,14 +296,26 @@ mod tests {
241296
}
242297
}
243298

244-
fn ingress(port: i32) -> ListenerIngress {
299+
fn ingress_at(address: &str, port: i32) -> ListenerIngress {
245300
ListenerIngress {
246-
address: "node-0".to_owned(),
301+
address: address.to_owned(),
247302
address_type: AddressType::Hostname,
248303
ports: BTreeMap::from([(ZOOKEEPER_SERVER_PORT_NAME.to_owned(), port)]),
249304
}
250305
}
251306

307+
fn ingress(port: i32) -> ListenerIngress {
308+
ingress_at("node-0", port)
309+
}
310+
311+
/// Pulls a `.data` key out of a built ConfigMap.
312+
fn data<'a>(cm: &'a ConfigMap, key: &str) -> &'a str {
313+
cm.data
314+
.as_ref()
315+
.and_then(|d| d.get(key))
316+
.unwrap_or_else(|| panic!("discovery ConfigMap missing key {key}"))
317+
}
318+
252319
#[test]
253320
fn listener_addresses_returns_host_port_pairs() {
254321
let listener = listener(Some(vec![ingress(2181)]));
@@ -285,4 +352,91 @@ mod tests {
285352
Err(Error::InvalidPort { .. })
286353
));
287354
}
355+
356+
#[test]
357+
fn cluster_discovery_cm_data_tls() {
358+
// TLS cluster -> secure client port 2282. The listener exposes the same port, as it does at
359+
// runtime. No chroot, so `ZOOKEEPER` equals `ZOOKEEPER_HOSTS` and `ZOOKEEPER_CHROOT` is `/`.
360+
let validated = validated_cluster(&minimal_zk(TLS_ZK));
361+
let cm = build_discovery_configmap(
362+
&validated,
363+
ZK_CONTROLLER_NAME,
364+
listener(Some(vec![ingress(2282)])),
365+
)
366+
.expect("discovery CM builds");
367+
368+
assert_eq!(data(&cm, "ZOOKEEPER"), "node-0:2282");
369+
assert_eq!(data(&cm, "ZOOKEEPER_HOSTS"), "node-0:2282");
370+
assert_eq!(data(&cm, "ZOOKEEPER_CLIENT_PORT"), "2282");
371+
assert_eq!(data(&cm, "ZOOKEEPER_CHROOT"), "/");
372+
}
373+
374+
#[test]
375+
fn cluster_discovery_cm_data_non_tls() {
376+
// Server TLS disabled -> insecure client port 2181.
377+
let validated = validated_cluster(&minimal_zk(NON_TLS_ZK));
378+
let cm = build_discovery_configmap(
379+
&validated,
380+
ZK_CONTROLLER_NAME,
381+
listener(Some(vec![ingress(2181)])),
382+
)
383+
.expect("discovery CM builds");
384+
385+
assert_eq!(data(&cm, "ZOOKEEPER"), "node-0:2181");
386+
assert_eq!(data(&cm, "ZOOKEEPER_HOSTS"), "node-0:2181");
387+
assert_eq!(data(&cm, "ZOOKEEPER_CLIENT_PORT"), "2181");
388+
assert_eq!(data(&cm, "ZOOKEEPER_CHROOT"), "/");
389+
}
390+
391+
#[test]
392+
fn cluster_discovery_cm_joins_multiple_hosts_sorted() {
393+
// Multiple listener ingress addresses are joined into one comma-separated connection string,
394+
// ordered by the `BTreeSet` in `listener_addresses` (i.e. sorted).
395+
let validated = validated_cluster(&minimal_zk(TLS_ZK));
396+
let cm = build_discovery_configmap(
397+
&validated,
398+
ZK_CONTROLLER_NAME,
399+
listener(Some(vec![
400+
ingress_at("node-1", 2282),
401+
ingress_at("node-0", 2282),
402+
])),
403+
)
404+
.expect("discovery CM builds");
405+
406+
assert_eq!(data(&cm, "ZOOKEEPER"), "node-0:2282,node-1:2282");
407+
assert_eq!(data(&cm, "ZOOKEEPER_HOSTS"), "node-0:2282,node-1:2282");
408+
}
409+
410+
#[test]
411+
fn znode_discovery_cm_appends_chroot_to_connection_string_only() {
412+
// The chroot is appended to `ZOOKEEPER` (the merged Java client string) but NOT to
413+
// `ZOOKEEPER_HOSTS` (kept separate for clients that don't understand the merged format).
414+
let validated = validated_znode(&minimal_znode(ZNODE), TLS_ZK);
415+
let cm = build_znode_discovery_configmap(
416+
&validated,
417+
ZNODE_CONTROLLER_NAME,
418+
listener(Some(vec![ingress(2282)])),
419+
"/znode-abc",
420+
)
421+
.expect("znode discovery CM builds");
422+
423+
assert_eq!(data(&cm, "ZOOKEEPER"), "node-0:2282/znode-abc");
424+
assert_eq!(data(&cm, "ZOOKEEPER_HOSTS"), "node-0:2282");
425+
assert_eq!(data(&cm, "ZOOKEEPER_CLIENT_PORT"), "2282");
426+
assert_eq!(data(&cm, "ZOOKEEPER_CHROOT"), "/znode-abc");
427+
}
428+
429+
#[test]
430+
fn znode_discovery_cm_rejects_relative_chroot() {
431+
let validated = validated_znode(&minimal_znode(ZNODE), TLS_ZK);
432+
assert!(matches!(
433+
build_znode_discovery_configmap(
434+
&validated,
435+
ZNODE_CONTROLLER_NAME,
436+
listener(Some(vec![ingress(2282)])),
437+
"znode-abc",
438+
),
439+
Err(Error::RelativeChroot { .. })
440+
));
441+
}
288442
}

rust/operator-binary/src/znode_controller/validate.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,58 @@ pub fn validate(
200200
object_overrides: znode.spec.object_overrides.clone(),
201201
})
202202
}
203+
204+
/// Shared helpers for building validated test znodes from minimal YAML fixtures.
205+
///
206+
/// Mirrors `zk_controller::test_support`: rather than hand-constructing a [`ValidatedZnode`]
207+
/// (whose `metadata` is private), tests run the real [`validate`] step so the wiring under test
208+
/// (product version, `zookeeper_security`, identity) matches production.
209+
#[cfg(test)]
210+
pub(crate) mod test_support {
211+
use stackable_operator::cli::OperatorEnvironmentOptions;
212+
213+
use super::{ValidatedZnode, validate};
214+
use crate::{
215+
crd::{authentication::DereferencedAuthenticationClasses, v1alpha1},
216+
zk_controller::test_support::minimal_zk,
217+
znode_controller::dereference::DereferencedObjects,
218+
};
219+
220+
/// Parses a minimal `ZookeeperZnode` test fixture, defaulting `namespace`/`uid` so the
221+
/// validate step can build a [`ValidatedZnode`].
222+
pub fn minimal_znode(yaml: &str) -> v1alpha1::ZookeeperZnode {
223+
let mut znode: v1alpha1::ZookeeperZnode =
224+
serde_yaml::from_str(yaml).expect("invalid test ZookeeperZnode YAML");
225+
znode
226+
.metadata
227+
.namespace
228+
.get_or_insert_with(|| "default".to_owned());
229+
znode
230+
.metadata
231+
.uid
232+
.get_or_insert_with(|| "5f1d9a2e-3c4b-4a1e-9b7d-2e6f8c0a1b3c".to_owned());
233+
znode
234+
}
235+
236+
fn operator_environment() -> OperatorEnvironmentOptions {
237+
OperatorEnvironmentOptions {
238+
operator_namespace: "stackable-operators".to_owned(),
239+
operator_service_name: "zookeeper-operator".to_owned(),
240+
image_repository: "oci.example.org".to_owned(),
241+
}
242+
}
243+
244+
/// Runs the real znode validate step against a minimal znode fixture and the given
245+
/// `ZookeeperCluster` YAML (which controls product version and `zookeeper_security`).
246+
pub fn validated_znode(znode: &v1alpha1::ZookeeperZnode, zk_yaml: &str) -> ValidatedZnode {
247+
validate(
248+
znode,
249+
&DereferencedObjects {
250+
zk: minimal_zk(zk_yaml),
251+
authentication_classes: DereferencedAuthenticationClasses::new_for_tests(),
252+
},
253+
&operator_environment(),
254+
)
255+
.expect("znode validate should succeed for the test fixture")
256+
}
257+
}

tests/templates/kuttl/smoke/14-assert.yaml.j2

Lines changed: 26 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,33 @@ commands:
2424
# The Vector sidecar's env wiring is set by the upstream `vector_container` helper and its
2525
# presence on the StatefulSet is unit-tested (statefulset.rs
2626
# `vector_agent_adds_vector_container_to_statefulset`), so no runtime env assertion is needed here.
27+
#
28+
# Discovery ConfigMap content (`ZOOKEEPER`/`ZOOKEEPER_HOSTS`/`ZOOKEEPER_CHROOT`/
29+
# `ZOOKEEPER_CLIENT_PORT`, incl. the znode chroot suffix and the secure-vs-insecure client port)
30+
# is unit-tested by `build_(znode_)discovery_configmap` (discovery.rs). The checks below only guard
31+
# what a unit test cannot: that the CMs actually reconciled with the real service-name/namespace
32+
# wiring and carry non-empty values.
2733
- script: |
28-
expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json
29-
ZOOKEEPER: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }}
30-
ZOOKEEPER_CHROOT: /
31-
ZOOKEEPER_CLIENT_PORT: "{{ zk_client_port }}"
32-
ZOOKEEPER_HOSTS: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }}
33-
YAMLEOF
34-
)
35-
actual=$(kubectl -n $NAMESPACE get cm test-zk -o yaml | yq -o=json '.data')
36-
expected_file=$(mktemp) && actual_file=$(mktemp)
37-
printf '%s\n' "$expected" > "$expected_file"
38-
printf '%s\n' "$actual" > "$actual_file"
39-
if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then
40-
echo "ERROR: ConfigMap test-zk data drifted from snapshot."
41-
printf '%s\n' "$diff_out"
42-
rm -f "$expected_file" "$actual_file"
43-
exit 1
44-
fi
45-
rm -f "$expected_file" "$actual_file"
46-
- script: |
47-
ZNODE_UID=$(kubectl -n $NAMESPACE get zookeeperznode test-znode -o jsonpath='{.metadata.uid}')
48-
expected=$(cat <<'YAMLEOF' | sed -e "s|__NAMESPACE__|$NAMESPACE|g" -e "s|__ZNODE_UID__|$ZNODE_UID|g" | yq -o=json
49-
ZOOKEEPER: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }}/znode-__ZNODE_UID__
50-
ZOOKEEPER_CHROOT: /znode-__ZNODE_UID__
51-
ZOOKEEPER_CLIENT_PORT: "{{ zk_client_port }}"
52-
ZOOKEEPER_HOSTS: test-zk-server.__NAMESPACE__.svc.cluster.local:{{ zk_client_port }}
53-
YAMLEOF
54-
)
55-
actual=$(kubectl -n $NAMESPACE get cm test-znode -o yaml | yq -o=json '.data')
56-
expected_file=$(mktemp) && actual_file=$(mktemp)
57-
printf '%s\n' "$expected" > "$expected_file"
58-
printf '%s\n' "$actual" > "$actual_file"
59-
if ! diff_out=$(diff -u "$expected_file" "$actual_file"); then
60-
echo "ERROR: ConfigMap test-znode data drifted from snapshot."
61-
printf '%s\n' "$diff_out"
62-
rm -f "$expected_file" "$actual_file"
63-
exit 1
64-
fi
65-
rm -f "$expected_file" "$actual_file"
34+
for cm in test-zk test-znode; do
35+
data=$(kubectl -n $NAMESPACE get cm "$cm" -o yaml | yq -o=json '.data')
36+
for key in ZOOKEEPER ZOOKEEPER_HOSTS ZOOKEEPER_CLIENT_PORT ZOOKEEPER_CHROOT; do
37+
value=$(printf '%s' "$data" | yq -r ".$key // \"\"")
38+
if [ -z "$value" ]; then
39+
echo "ERROR: discovery ConfigMap $cm is missing/empty key $key."
40+
printf '%s\n' "$data"
41+
exit 1
42+
fi
43+
done
44+
# The connection string must point at the cluster-internal Service in this namespace.
45+
host=$(printf '%s' "$data" | yq -r '.ZOOKEEPER_HOSTS')
46+
case "$host" in
47+
test-zk-server.$NAMESPACE.svc.cluster.local:*) ;;
48+
*)
49+
echo "ERROR: discovery ConfigMap $cm ZOOKEEPER_HOSTS=$host does not target the expected Service."
50+
exit 1
51+
;;
52+
esac
53+
done
6654
- script: |
6755
expected=$(cat <<'YAMLEOF' | sed "s|__NAMESPACE__|$NAMESPACE|g" | yq -o=json
6856
logback.xml: |

0 commit comments

Comments
 (0)