Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ All notable changes to this project will be documented in this file.
- Internal operator refactoring: introduce a build() step in the reconciler that
assembles all relevant Kubernetes resources before anything is applied ([#961]).
- Bump stackable-operator to 0.114.0 ([#970])
- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac`
functions and carry the full set of recommended labels ([#966]).

[#961]: https://github.com/stackabletech/nifi-operator/pull/961
[#966]: https://github.com/stackabletech/nifi-operator/pull/966
[#970]: https://github.com/stackabletech/nifi-operator/pull/970

## [26.7.0] - 2026-07-21
Expand Down
82 changes: 64 additions & 18 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
config_map::build_rolegroup_config_map,
listener::{build_group_listener, group_listener_name},
pdb::build_pdb,
rbac::{build_role_binding, build_service_account},
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
statefulset::build_node_rolegroup_statefulset,
},
Expand Down Expand Up @@ -66,13 +67,7 @@ pub enum Error {
/// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
/// dereferenced and validated by this point, so the errors returned here are resource-assembly
/// failures only.
///
/// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under
/// (RBAC resources are built and applied separately, in the reconcile step).
pub fn build(
cluster: &ValidatedCluster,
service_account_name: &str,
) -> Result<KubernetesResources, Error> {
pub fn build(cluster: &ValidatedCluster) -> Result<KubernetesResources, Error> {
let mut stateful_sets = vec![];
let mut services = vec![];
let mut listeners = vec![];
Expand Down Expand Up @@ -109,16 +104,10 @@ pub fn build(

let effective_replicas = rg.replicas.map(i32::from);
stateful_sets.push(
build_node_rolegroup_statefulset(
cluster,
role_group_name,
rg,
effective_replicas,
service_account_name,
)
.context(StatefulSetSnafu {
role_group: role_group_name.clone(),
})?,
build_node_rolegroup_statefulset(cluster, role_group_name, rg, effective_replicas)
.context(StatefulSetSnafu {
role_group: role_group_name.clone(),
})?,
);
}

Expand All @@ -128,11 +117,15 @@ pub fn build(
listeners,
config_maps,
pod_disruption_budgets,
service_accounts: vec![build_service_account(cluster)],
role_bindings: vec![build_role_binding(cluster)],
})
}

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use stackable_operator::kube::Resource;

use super::{build, properties::test_support::minimal_validated_cluster};
Expand All @@ -149,7 +142,7 @@ mod tests {
#[test]
fn build_produces_expected_resources() {
let cluster = minimal_validated_cluster();
let resources = build(&cluster, "simple-nifi-serviceaccount").expect("build succeeds");
let resources = build(&cluster).expect("build succeeds");

// The minimal fixture has a single `default` role group for the `node` role.
assert_eq!(
Expand All @@ -169,4 +162,57 @@ mod tests {
["simple-nifi-node"]
);
}

/// Locks the RBAC resource names, the roleRef, and the recommended label set against
/// accidental drift. The fixture's cluster name deliberately differs from the product name so
/// that swapped `name`/`instance` label values cannot pass unnoticed.
///
/// The version label is the bare `2.9.0` because the fixture hand-builds its
/// [`ResolvedProductImage`](stackable_operator::commons::product_image_selection::ResolvedProductImage)
/// instead of resolving it (which would append the `-stackable…` suffix).
#[test]
fn build_produces_rbac() {
let cluster = minimal_validated_cluster();
let resources = build(&cluster).expect("build succeeds");

assert_eq!(
sorted_names(&resources.service_accounts),
["simple-nifi-serviceaccount"]
);
assert_eq!(
sorted_names(&resources.role_bindings),
["simple-nifi-rolebinding"]
);

let expected_labels = BTreeMap::from(
[
("app.kubernetes.io/component", "none"),
("app.kubernetes.io/instance", "simple-nifi"),
(
"app.kubernetes.io/managed-by",
"nifi.stackable.tech_nificluster",
),
("app.kubernetes.io/name", "nifi"),
("app.kubernetes.io/role-group", "none"),
("app.kubernetes.io/version", "2.9.0"),
("stackable.tech/vendor", "Stackable"),
]
.map(|(key, value)| (key.to_string(), value.to_string())),
);
let service_account = resources
.service_accounts
.first()
.expect("a ServiceAccount is built");
assert_eq!(
service_account.metadata.labels,
Some(expected_labels.clone())
);

let role_binding = resources
.role_bindings
.first()
.expect("a RoleBinding is built");
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
assert_eq!(role_binding.role_ref.name, "nifi-clusterrole");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub fn build_rolegroup_config_map(
cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.role_group_config_map()
.to_string(),
role_group_name,
Expand Down
1 change: 1 addition & 0 deletions rust/operator-binary/src/controller/build/resource/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
pub mod config_map;
pub mod listener;
pub mod pdb;
pub mod rbac;
pub mod service;
pub mod statefulset;
2 changes: 1 addition & 1 deletion rust/operator-binary/src/controller/build/resource/pdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn build_pdb(
let pdb = pod_disruption_budget_builder_with_role(
cluster,
&product_name(),
&ValidatedCluster::role_name(),
&NifiRole::Node.into(),
&operator_name(),
&controller_name(),
)
Expand Down
42 changes: 42 additions & 0 deletions rust/operator-binary/src/controller/build/resource/rbac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Builds the RBAC resources (ServiceAccount + RoleBinding) shared by all role groups.

use std::str::FromStr;

use stackable_operator::{
k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding},
kvp::Labels,
v2::{
rbac,
types::operator::{RoleGroupName, RoleName},
},
};

use crate::controller::ValidatedCluster;

stackable_operator::constant!(NONE_ROLE_NAME: RoleName = "none");
stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none");

/// Builds the [`ServiceAccount`] that the role-group Pods run under.
pub fn build_service_account(cluster: &ValidatedCluster) -> ServiceAccount {
rbac::build_service_account(
cluster,
&cluster.cluster_resource_names(),
rbac_labels(cluster),
)
}

/// Builds the [`RoleBinding`] that binds the [`ServiceAccount`] from [`build_service_account`] to
/// the operator-deployed ClusterRole.
pub fn build_role_binding(cluster: &ValidatedCluster) -> RoleBinding {
rbac::build_role_binding(
cluster,
&cluster.cluster_resource_names(),
rbac_labels(cluster),
)
}

/// Both resources are shared by the whole cluster rather than tied to a role or role group, so
/// the recommended labels carry `none` for both values.
fn rbac_labels(cluster: &ValidatedCluster) -> Labels {
cluster.recommended_labels_for(&NONE_ROLE_NAME, &NONE_ROLE_GROUP_NAME)
}
4 changes: 2 additions & 2 deletions rust/operator-binary/src/controller/build/resource/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn build_rolegroup_headless_service(
metadata: cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.headless_service_name()
.to_string(),
role_group_name,
Expand Down Expand Up @@ -50,7 +50,7 @@ pub fn build_rolegroup_metrics_service(
metadata: cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.metrics_service_name()
.to_string(),
role_group_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ pub(crate) fn build_node_rolegroup_statefulset(
role_group_name: &RoleGroupName,
rg: &NifiRoleGroupConfig,
effective_replicas: Option<i32>,
service_account_name: &str,
) -> Result<StatefulSet> {
tracing::debug!("Building statefulset");

Expand All @@ -181,7 +180,7 @@ pub(crate) fn build_node_rolegroup_statefulset(
let git_sync_resources = &rg.config.git_sync_resources;

// Type-safe names for this role group's resources (StatefulSet, ConfigMap, headless Service).
let resource_names = cluster.resource_names(role_group_name);
let resource_names = cluster.role_group_resource_names(role_group_name);

// The validated, merged `NifiConfig` is the single source of truth; the ConfigMap builder
// sources the same `rg.config`.
Expand Down Expand Up @@ -604,7 +603,7 @@ pub(crate) fn build_node_rolegroup_statefulset(
&cluster.cluster_config.server_tls_secret_class,
&KEYSTORE_VOLUME_NAME,
[cluster
.resource_names(role_group_name)
.role_group_resource_names(role_group_name)
.metrics_service_name()
.to_string()],
SecretFormat::TlsPkcs12,
Expand Down Expand Up @@ -642,7 +641,12 @@ pub(crate) fn build_node_rolegroup_statefulset(
..Volume::default()
})
.context(AddVolumeSnafu)?
.service_account_name(service_account_name)
.service_account_name(
cluster
.cluster_resource_names()
.service_account_name()
.to_string(),
)
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());

let mut pod_template = pod_builder.build_template();
Expand Down Expand Up @@ -709,7 +713,7 @@ fn get_volume_claim_templates(

// Used for PVC templates that cannot be modified once they are deployed, so the version label
// is set to the placeholder `none` to keep the labels stable across version upgrades.
let unversioned_recommended_labels = cluster.recommended_labels_unversioned(role_group_name);
let unversioned_recommended_labels = cluster.unversioned_recommended_labels(role_group_name);

// listener endpoints will use persistent volumes
// so that load balancers can hard-code the target addresses and
Expand Down
Loading
Loading