Skip to content

Commit cf7375f

Browse files
committed
test: add test coverage for beat replicas, superset config and vector path
1 parent a2782f0 commit cf7375f

3 files changed

Lines changed: 196 additions & 20 deletions

File tree

rust/operator-binary/src/controller/build/properties/superset_config.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,105 @@ fn rolegroup_properties(
270270

271271
properties
272272
}
273+
274+
#[cfg(test)]
275+
mod tests {
276+
use stackable_operator::{
277+
commons::{affinity::StackableAffinity, resources::Resources},
278+
product_logging::spec::AutomaticContainerLogConfig,
279+
v2::{
280+
config_overrides::KeyValueConfigOverrides,
281+
product_logging::framework::ValidatedContainerLogConfigChoice,
282+
},
283+
};
284+
285+
use super::{DEFAULT_ROW_LIMIT, DEFAULT_WEBSERVER_TIMEOUT, rolegroup_properties};
286+
use crate::{
287+
controller::{ValidatedLogging, ValidatedSupersetConfig},
288+
crd::{SupersetConfigOptions, SupersetRole, v1alpha1::SupersetConfigOverrides},
289+
};
290+
291+
/// Builds a [`ValidatedSupersetConfig`] with only the fields that affect `rolegroup_properties`
292+
/// set; everything else defaults.
293+
fn validated_config(
294+
row_limit: Option<i32>,
295+
webserver_timeout: Option<u32>,
296+
) -> ValidatedSupersetConfig {
297+
ValidatedSupersetConfig {
298+
affinity: StackableAffinity::default(),
299+
graceful_shutdown_timeout: None,
300+
logging: ValidatedLogging {
301+
superset_container: ValidatedContainerLogConfigChoice::Automatic(
302+
AutomaticContainerLogConfig::default(),
303+
),
304+
vector_container: None,
305+
enable_vector_agent: false,
306+
},
307+
resources: Resources::default(),
308+
row_limit,
309+
webserver_timeout,
310+
}
311+
}
312+
313+
fn row_limit_key() -> String {
314+
SupersetConfigOptions::RowLimit.to_string()
315+
}
316+
317+
fn webserver_timeout_key() -> String {
318+
SupersetConfigOptions::SupersetWebserverTimeout.to_string()
319+
}
320+
321+
/// The `ROW_LIMIT`/`SUPERSET_WEBSERVER_TIMEOUT` defaults are only emitted for the `Node` role.
322+
#[test]
323+
fn rolegroup_properties_defaults_are_node_only() {
324+
let worker = rolegroup_properties(
325+
&SupersetRole::Worker,
326+
&validated_config(None, None),
327+
&SupersetConfigOverrides::default(),
328+
);
329+
assert!(!worker.contains_key(&row_limit_key()));
330+
assert!(!worker.contains_key(&webserver_timeout_key()));
331+
332+
let node = rolegroup_properties(
333+
&SupersetRole::Node,
334+
&validated_config(None, None),
335+
&SupersetConfigOverrides::default(),
336+
);
337+
assert_eq!(
338+
node.get(&row_limit_key()),
339+
Some(&DEFAULT_ROW_LIMIT.to_string())
340+
);
341+
assert_eq!(
342+
node.get(&webserver_timeout_key()),
343+
Some(&DEFAULT_WEBSERVER_TIMEOUT.to_string())
344+
);
345+
}
346+
347+
/// A typed `row_limit`/`webserver_timeout` overrides the Node default.
348+
#[test]
349+
fn rolegroup_properties_typed_fields_override_defaults() {
350+
let node = rolegroup_properties(
351+
&SupersetRole::Node,
352+
&validated_config(Some(10), Some(600)),
353+
&SupersetConfigOverrides::default(),
354+
);
355+
assert_eq!(node.get(&row_limit_key()), Some(&"10".to_string()));
356+
assert_eq!(node.get(&webserver_timeout_key()), Some(&"600".to_string()));
357+
}
358+
359+
/// `configOverrides` are applied last and win over both the default and the typed field.
360+
#[test]
361+
fn rolegroup_properties_config_overrides_win() {
362+
let mut superset_config_py = KeyValueConfigOverrides::default();
363+
superset_config_py
364+
.overrides
365+
.insert(row_limit_key(), "99".to_string());
366+
367+
let node = rolegroup_properties(
368+
&SupersetRole::Node,
369+
&validated_config(Some(10), None),
370+
&SupersetConfigOverrides { superset_config_py },
371+
);
372+
assert_eq!(node.get(&row_limit_key()), Some(&"99".to_string()));
373+
}
374+
}

rust/operator-binary/src/controller/build/resource/deployment.rs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,24 +85,11 @@ pub fn build_rolegroup_deployment(
8585
worker_liveness_probe(),
8686
rolegroup_config.replicas,
8787
),
88-
SupersetRole::Beat => {
89-
// Beat must only ever run a single instance, so the replica count is never left
90-
// unset (no HorizontalPodAutoscaler): an explicit `0` is honoured (to stop Beat),
91-
// any value `> 1` is clamped to `1`, and an unset value defaults to `1`.
92-
let replicas = match rolegroup_config.replicas {
93-
Some(0) => Some(0),
94-
Some(replicas) if replicas > 1 => {
95-
tracing::warn! {"replicas for role `beat` set to greater `1`. Multiple beat instances are not allowed. Setting to `1` replica."}
96-
Some(1)
97-
}
98-
_ => Some(1),
99-
};
100-
(
101-
format!("{CELERY_APP_INVOCATION} beat --pidfile {CELERY_BEAT_PIDFILE}"),
102-
beat_liveness_probe(),
103-
replicas,
104-
)
105-
}
88+
SupersetRole::Beat => (
89+
format!("{CELERY_APP_INVOCATION} beat --pidfile {CELERY_BEAT_PIDFILE}"),
90+
beat_liveness_probe(),
91+
beat_replicas(rolegroup_config.replicas),
92+
),
10693
SupersetRole::Node => {
10794
unreachable!("the `Node` role is deployed as a StatefulSet, not a Deployment")
10895
}
@@ -232,3 +219,39 @@ fn beat_liveness_probe() -> Probe {
232219
..Default::default()
233220
}
234221
}
222+
223+
/// Computes the replica count for the Celery `beat` role.
224+
///
225+
/// Beat is a singleton scheduler, so it must never run more than one instance: an explicit `0` is
226+
/// honoured (to stop Beat), any value `> 1` is clamped to `1` (with a warning), and an unset value
227+
/// defaults to `1`. The result is always `Some`, so no HorizontalPodAutoscaler can own the count.
228+
fn beat_replicas(requested: Option<u16>) -> Option<u16> {
229+
match requested {
230+
Some(0) => Some(0),
231+
Some(replicas) if replicas > 1 => {
232+
tracing::warn!(
233+
"replicas for role `beat` set to greater `1`. Multiple beat instances are not allowed. Setting to `1` replica."
234+
);
235+
Some(1)
236+
}
237+
_ => Some(1),
238+
}
239+
}
240+
241+
#[cfg(test)]
242+
mod tests {
243+
use super::beat_replicas;
244+
245+
#[test]
246+
fn beat_replicas_clamps_to_a_single_instance() {
247+
// An unset replica count defaults to a single instance.
248+
assert_eq!(beat_replicas(None), Some(1));
249+
// An explicit `0` is honoured so Beat can be stopped.
250+
assert_eq!(beat_replicas(Some(0)), Some(0));
251+
// A single instance is kept as-is.
252+
assert_eq!(beat_replicas(Some(1)), Some(1));
253+
// Anything greater than one is clamped down to a single instance.
254+
assert_eq!(beat_replicas(Some(2)), Some(1));
255+
assert_eq!(beat_replicas(Some(100)), Some(1));
256+
}
257+
}

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

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,17 @@ pub fn validate_cluster(
255255
#[cfg(test)]
256256
mod tests {
257257

258-
use stackable_operator::utils::yaml_from_str_singleton_map;
258+
use std::str::FromStr;
259259

260-
use super::validate_cluster;
260+
use stackable_operator::{
261+
product_logging::spec::{
262+
AutomaticContainerLogConfig, ContainerLogConfig, ContainerLogConfigChoice, Logging,
263+
},
264+
utils::yaml_from_str_singleton_map,
265+
v2::types::kubernetes::ConfigMapName,
266+
};
267+
268+
use super::{Error, validate_cluster, validate_logging};
261269
use crate::{
262270
controller::dereference::DereferencedObjects,
263271
crd::{
@@ -281,6 +289,49 @@ mod tests {
281289
}
282290
}
283291

292+
/// Builds a [`Logging`] with automatic log configuration for the Superset and Vector containers.
293+
fn automatic_logging(enable_vector_agent: bool) -> Logging<v1alpha1::Container> {
294+
let automatic = || ContainerLogConfig {
295+
choice: Some(ContainerLogConfigChoice::Automatic(
296+
AutomaticContainerLogConfig::default(),
297+
)),
298+
};
299+
Logging {
300+
enable_vector_agent,
301+
containers: [
302+
(v1alpha1::Container::Superset, automatic()),
303+
(v1alpha1::Container::Vector, automatic()),
304+
]
305+
.into(),
306+
}
307+
}
308+
309+
/// The Vector aggregator discovery ConfigMap name is required exactly when the Vector agent is
310+
/// enabled, and a Vector container is configured only in that case.
311+
#[test]
312+
fn validate_logging_requires_vector_aggregator_only_when_vector_enabled() {
313+
// Vector enabled without an aggregator ConfigMap name fails validation up-front.
314+
assert!(matches!(
315+
validate_logging(&automatic_logging(true), &None),
316+
Err(Error::MissingVectorAggregatorConfigMapName)
317+
));
318+
319+
// Vector enabled with an aggregator name configures a Vector container.
320+
let aggregator = Some(
321+
ConfigMapName::from_str("vector-aggregator-discovery").expect("valid ConfigMap name"),
322+
);
323+
let validated = validate_logging(&automatic_logging(true), &aggregator)
324+
.expect("logging should validate");
325+
assert!(validated.enable_vector_agent);
326+
assert!(validated.vector_container.is_some());
327+
328+
// Vector disabled needs no aggregator name and configures no Vector container.
329+
let validated =
330+
validate_logging(&automatic_logging(false), &None).expect("logging should validate");
331+
assert!(!validated.enable_vector_agent);
332+
assert!(validated.vector_container.is_none());
333+
}
334+
284335
/// Characterises the `superset_config.py` override resolution: role-level overrides are merged
285336
/// with role-group overrides, the role group winning on shared keys, and unique keys from both
286337
/// levels surviving.

0 commit comments

Comments
 (0)