Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ All notable changes to this project will be documented in this file.

- Support the annotation `secrets.stackable.tech/backend.autotls.cert.domain-components-in-subject-dn`
in the `SecretOperatorVolumeSourceBuilder` ([#1209]).
- Add `Role::fixed_replica_count` and `Role::estimated_replica_count` helper functions ([#1241]).

[#1209]: https://github.com/stackabletech/operator-rs/pull/1209
[#1241]: https://github.com/stackabletech/operator-rs/pull/1241

## [0.113.0] - 2026-06-22

Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/crds/AuthenticationClass.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ spec:
type: array
type: object
secretClass:
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) containing the LDAP bind credentials.'
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.'
type: string
required:
- secretClass
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/crds/DummyCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1932,7 +1932,7 @@ spec:
type: array
type: object
secretClass:
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) containing the LDAP bind credentials.'
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.'
type: string
required:
- secretClass
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/crds/S3Bucket.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ spec:
type: array
type: object
secretClass:
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) containing the LDAP bind credentials.'
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.'
type: string
required:
- secretClass
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/crds/S3Connection.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ spec:
type: array
type: object
secretClass:
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) containing the LDAP bind credentials.'
description: '[SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) providing the requested secrets.'
type: string
required:
- secretClass
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/builder/pdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ impl PodDisruptionBudgetBuilder<ObjectMeta, LabelSelector, ()> {
/// Mutually exclusive with [`PodDisruptionBudgetBuilder::with_max_unavailable`].
#[deprecated(
since = "0.51.0",
note = "It is strongly recommended to use [`max_unavailable`]. Please read the ADR on Pod disruptions before using this function."
note = "It is strongly recommended to use [`PodDisruptionBudgetBuilder::with_max_unavailable`]. Please read the ADR on Pod disruptions before using this function."
)]
pub fn with_min_available(
self,
Expand Down
2 changes: 1 addition & 1 deletion crates/stackable-operator/src/commons/secret_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub enum SecretClassVolumeError {
)]
#[serde(rename_all = "camelCase")]
pub struct SecretClassVolume {
/// [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) containing the LDAP bind credentials.
/// [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) providing the requested secrets.
pub secret_class: String,

/// [Scope](DOCS_BASE_URL_PLACEHOLDER/secret-operator/scope) of the
Expand Down
156 changes: 156 additions & 0 deletions crates/stackable-operator/src/role_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,62 @@ where
}
}

impl<Config, ConfigOverrides, RoleConfig, CommonConfig>
Role<Config, ConfigOverrides, RoleConfig, CommonConfig>
where
RoleConfig: Default + JsonSchema + Serialize,
CommonConfig: Default + JsonSchema + Serialize,
ConfigOverrides: Default + JsonSchema + Serialize,
{
/// Returns [`Some<u32>`] in case the number of replicas is hard-coded to a certain value.
///
/// This is the case when all `replicas` are set to [`Some<u16>`], in which case they are simply
/// summed.
Comment thread
Techassi marked this conversation as resolved.
///
/// The argument `zero_replicas_counting` is a safety mechanism, which allows the caller to
/// decide if an explicit replica count of `0` should be treated as [`None`]. It also means that
/// [`None`] is returned in case no roleGroups are configured at all.
pub fn fixed_replica_count(&self, zero_replicas_counting: ZeroReplicasCounting) -> Option<u32> {
// An empty role has no fixed replica count when zeros are treated as None.
if zero_replicas_counting == ZeroReplicasCounting::TreatAsNone
&& self.role_groups.is_empty()
{
return None;
}

self.role_groups
.values()
.map(|rg| match rg.replicas {
None => None,
Some(0) if zero_replicas_counting == ZeroReplicasCounting::TreatAsNone => None,
// The individual replicas are [`u16`]s, so a [`u32`] sum has plenty of space.
Some(replicas) => Some(u32::from(replicas)),
})
.sum()
Comment thread
Techassi marked this conversation as resolved.
}

/// Returns the estimated total number of replicas across all role groups.
///
/// Unlike [`Self::fixed_replica_count`], this always returns a value: a role group with an unset
/// (i.e. [`None`]) replica count is assumed to run a single replica. Use this when a best-effort
/// estimate is needed even though the exact number of replicas is not hard-coded.
pub fn estimated_replica_count(&self) -> u32 {
self.role_groups
.values()
.map(|rg| u32::from(rg.replicas.unwrap_or(1)))
.sum()
}
}

/// How explicit zero (`0`) replicas on a role group should be counted
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ZeroReplicasCounting {
/// Treat them as what they are: `Some(0)`.
TreatAsZero,
/// Treat them as if the user configured [`None`].
TreatAsNone,
}

impl<Config, ConfigOverrides, RoleConfig>
Role<Config, ConfigOverrides, RoleConfig, JavaCommonConfig>
where
Expand Down Expand Up @@ -654,4 +710,104 @@ mod tests {
]
);
}

#[test]
fn replica_counts_with_all_replicas_set() {
let role = construct_role_with_replicas([Some(3), Some(2), Some(5)]);

assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
Some(10)
);
assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
Some(10)
);
assert_eq!(role.estimated_replica_count(), 10);
}

#[test]
fn replica_counts_with_one_replica_unset() {
let role = construct_role_with_replicas([Some(3), None, Some(2)]);

assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
None
);
assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
None
);
assert_eq!(role.estimated_replica_count(), 6);
}

#[test]
fn replica_counts_with_a_zero_replica() {
let role = construct_role_with_replicas([Some(3), Some(0)]);

assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
Some(3)
);
// With treat_zero_as_none the zero turns the whole count into None.
assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
None
);
assert_eq!(role.estimated_replica_count(), 3);
}

#[test]
fn replica_counts_with_a_single_zero_role_group() {
let role = construct_role_with_replicas([Some(0)]);

assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
Some(0)
);
assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
None
);
assert_eq!(role.estimated_replica_count(), 0);
}

#[test]
fn replica_counts_without_role_groups() {
let role = construct_role_with_replicas(vec![]);

assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
Some(0)
);
assert_eq!(
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
None
);
assert_eq!(role.estimated_replica_count(), 0);
}

/// Builds a [`Role`] with one role group per passed `replicas` entry, so tests only need to
/// care about the replica counts that [`Role::fixed_replica_count`] operates on.
fn construct_role_with_replicas(
replicas: impl IntoIterator<Item = Option<u16>>,
) -> Role<(), EmptyConfigOverrides, GenericRoleConfig, GenericCommonConfig> {
Role {
config: CommonConfiguration::default(),
role_config: GenericRoleConfig::default(),
role_groups: replicas
.into_iter()
.enumerate()
.map(|(index, replicas)| {
(
format!("role-group-{index}"),
RoleGroup {
config: CommonConfiguration::default(),
replicas,
},
)
})
.collect(),
}
}
}