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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ 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]).
- BREAKING: The `nodes` role is now required by the CRD; a NifiCluster without it was
previously accepted by the API server but failed reconciliation ([#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
2 changes: 1 addition & 1 deletion extra/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,6 @@ spec:
at role level, the `roleConfig`.
You can learn more about this in the
[Roles and role group concept documentation](https://docs.stackable.tech/home/nightly/concepts/roles-and-role-groups).
nullable: true
properties:
cliOverrides:
additionalProperties:
Expand Down Expand Up @@ -2209,6 +2208,7 @@ spec:
required:
- clusterConfig
- image
- nodes
type: object
status:
nullable: true
Expand Down
73 changes: 49 additions & 24 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@

use std::str::FromStr;

use snafu::{OptionExt, ResultExt, Snafu};
use stackable_operator::v2::types::{common::Port, operator::RoleGroupName};
use snafu::{ResultExt, Snafu};
use stackable_operator::{
builder::meta::ObjectMetaBuilder,
v2::{
builder::meta::ownerreference_from_resource,
types::{common::Port, operator::RoleGroupName},
},
};

use crate::{
controller::{
Expand All @@ -14,6 +20,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 @@ -45,9 +52,6 @@ pub const NIFI_PYTHON_WORKING_DIRECTORY: &str = "/nifi-python-working-directory"

#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("NifiCluster has no nodes role defined"))]
NoNodesDefined,

#[snafu(display("failed to build ConfigMap for role group {role_group}"))]
ConfigMap {
source: resource::config_map::Error,
Expand All @@ -66,13 +70,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 All @@ -84,7 +82,7 @@ pub fn build(
let node_role_group_configs = cluster
.role_group_configs
.get(&nifi_role)
.context(NoNodesDefinedSnafu)?;
.expect("the nodes role is required by the CRD and validate always inserts it");
Comment thread
adwk67 marked this conversation as resolved.

// Role-level resources (one per role): the PodDisruptionBudget and the group Listener.
let role_config = &cluster.role_config;
Expand All @@ -109,16 +107,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,9 +120,33 @@ pub fn build(
listeners,
config_maps,
pod_disruption_budgets,
service_accounts: vec![build_service_account(cluster)],
role_bindings: vec![build_role_binding(cluster)],
})
}

/// Returns an [`ObjectMetaBuilder`] pre-filled with the namespace, an owner reference back to
/// the cluster, and the recommended labels for a resource named `name` in `role_group_name`.
///
/// Consolidates the metadata chain repeated by the child-resource builders. Call sites that
/// need extra labels/annotations chain them onto the returned builder. Role-level resources
/// (e.g. the per-role [`Listener`](stackable_operator::crd::listener::v1alpha1::Listener)) pass
/// the placeholder role-group `none`, preserving the historical
/// `app.kubernetes.io/role-group: none` label.
pub(crate) fn object_meta(
cluster: &ValidatedCluster,
name: impl Into<String>,
role_group_name: &RoleGroupName,
) -> ObjectMetaBuilder {
let mut builder = ObjectMetaBuilder::new();
builder
.name_and_namespace(cluster)
.name(name)
.ownerreference(ownerreference_from_resource(cluster, None, Some(true)))
.with_labels(cluster.recommended_labels(role_group_name));
builder
}

#[cfg(test)]
mod tests {
use stackable_operator::kube::Resource;
Expand All @@ -149,7 +165,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 @@ -168,5 +184,14 @@ mod tests {
sorted_names(&resources.pod_disruption_budgets),
["simple-nifi-node"]
);
// The cluster-shared RBAC pair.
assert_eq!(
sorted_names(&resources.service_accounts),
["simple-nifi-serviceaccount"]
);
assert_eq!(
sorted_names(&resources.role_bindings),
["simple-nifi-rolebinding"]
);
}
}
33 changes: 24 additions & 9 deletions rust/operator-binary/src/controller/build/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,23 @@ pub(crate) mod test_support {
},
};

/// The expected `app.kubernetes.io/version` label value for the given product version.
///
/// The `-stackable` suffix carries the operator's own version, which is `0.0.0-dev` on main
/// but rewritten by the release process — so tests must derive it rather than hardcode it,
/// or they fail on release branches.
pub fn app_version_label(product_version: &str) -> String {
format!(
"{product_version}-stackable{}",
crate::built_info::PKG_VERSION
)
}

/// A minimal NiFi cluster YAML. Mirrors the fixture used by bootstrap_conf tests,
/// stripped down to the mandatory fields only (Kubernetes clustering backend, SingleUser auth).
///
/// The cluster name (`simple-nifi`) deliberately differs from the product name (`nifi`), so
/// tests asserting recommended labels catch swapped `name`/`instance` values.
pub const MINIMAL_NIFI_YAML: &str = r#"
apiVersion: nifi.stackable.tech/v1alpha1
kind: NifiCluster
Expand Down Expand Up @@ -140,24 +155,24 @@ pub(crate) mod test_support {
let nifi: v1alpha1::NifiCluster =
serde_yaml::from_str(MINIMAL_NIFI_YAML).expect("invalid test YAML");

// Mirrors what `image.resolve()` produces in production, so label-asserting tests see
// realistic values.
let image = ResolvedProductImage {
product_version: "2.9.0".to_string(),
app_version_label_value: "2.9.0".parse::<LabelValue>().unwrap(),
image: "oci.stackable.tech/sdp/nifi:2.9.0-stackable0.0.0-dev".to_string(),
app_version_label_value: app_version_label("2.9.0").parse::<LabelValue>().unwrap(),
image: format!("oci.stackable.tech/sdp/nifi:{}", app_version_label("2.9.0")),
image_pull_policy: "IfNotPresent".to_string(),
pull_secrets: None,
};

let role_group_configs = build_role_group_configs(&nifi, &image, &None)
.expect("role group configs should merge for minimal fixture");

let role_config = nifi
.role_config(&NifiRole::Node)
.map(|role_config| ValidatedRoleConfig {
pdb: role_config.common.pod_disruption_budget.clone(),
listener_class: role_config.listener_class.clone(),
})
.expect("the minimal fixture defines the nodes role");
let node_role_config = nifi.role_config(&NifiRole::Node);
let role_config = ValidatedRoleConfig {
pdb: node_role_config.common.pod_disruption_budget.clone(),
listener_class: node_role_config.listener_class.clone(),
};

let name = ClusterName::from_str("simple-nifi").expect("valid cluster name");
let namespace = NamespaceName::from_str("default").expect("valid namespace");
Expand Down
19 changes: 10 additions & 9 deletions rust/operator-binary/src/controller/build/resource/config_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use stackable_operator::{
use crate::controller::{
NifiRoleGroupConfig, ValidatedCluster,
build::{
object_meta,
properties::{
ConfigFileName, authorizers, bootstrap_conf, login_identity_providers, nifi_properties,
product_logging, security_properties, state_management_xml,
Expand Down Expand Up @@ -66,15 +67,15 @@ pub fn build_rolegroup_config_map(

cm_builder
.metadata(
cluster
.object_meta(
cluster
.resource_names(role_group_name)
.role_group_config_map()
.to_string(),
role_group_name,
)
.build(),
object_meta(
cluster,
cluster
.role_group_resource_names(role_group_name)
.role_group_config_map()
.to_string(),
role_group_name,
)
.build(),
)
.add_data(
ConfigFileName::BootstrapConf.to_string(),
Expand Down
14 changes: 7 additions & 7 deletions rust/operator-binary/src/controller/build/resource/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use stackable_operator::{

use crate::controller::{
ValidatedCluster,
build::{HTTPS_PORT, HTTPS_PORT_NAME, PLACEHOLDER_LISTENER_ROLE_GROUP},
build::{HTTPS_PORT, HTTPS_PORT_NAME, PLACEHOLDER_LISTENER_ROLE_GROUP, object_meta},
};

pub const LISTENER_VOLUME_NAME: &str = "listener";
Expand All @@ -29,12 +29,12 @@ pub fn build_group_listener(
listener_group_name: ListenerName,
) -> Listener {
Listener {
metadata: cluster
.object_meta(
listener_group_name.to_string(),
&PLACEHOLDER_LISTENER_ROLE_GROUP,
)
.build(),
metadata: object_meta(
cluster,
listener_group_name.to_string(),
&PLACEHOLDER_LISTENER_ROLE_GROUP,
)
.build(),
spec: ListenerSpec {
class_name: Some(listener_class.to_string()),
ports: Some(vec![ListenerPort {
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(),
&role.into(),
&operator_name(),
&controller_name(),
)
Expand Down
Loading
Loading